diff --git a/changelog.md b/changelog.md
index 99b9f3eb..ac3e5ad9 100644
--- a/changelog.md
+++ b/changelog.md
@@ -2,6 +2,38 @@
All notable changes to this project will be documented in this file.
+## [2.49.1] - 2026-06-08
+
+Positioning-shift patch on top of 2.49.0 — the hosted trading mode shipped in 2.49.0 but the docs, READMEs, and OpenAPI schemas still defaulted to the self-hosted sidecar path. This release flips the default everywhere the SDK + docs surface a customer hits: hosted PMXT is the primary experience; self-hosting becomes the advanced escape hatch. No SDK runtime behavior changes — pure documentation, schema, and copy work. Marketing-site changes ship separately in a sibling pmxt-website PR.
+
+### Added
+
+- **Docs**: 11 new MDX pages on the Mintlify site covering the hosted trading mode end-to-end — `trading-quickstart` (60-second walkthrough), `concepts/hosted-trading` (feature landing), `concepts/hosted-vs-self-hosted` (one-pager comparison), `concepts/catalog-uuid-vs-venue-id` (the UUID/venue-id gotcha), `guides/escrow-lifecycle` (PreFundedEscrow walkthrough), `guides/signing` (EthAccountSigner / EthersSigner + EIP-712), `guides/hosted-errors` (the 5 most-common subclasses with `try/except` cookbook), `guides/migrate-to-hosted-trading` (ported `MIGRATION.md` content with language tabs), `guides/self-hosted` (consolidated local-sidecar story), `api-reference/errors` (full `HostedTradingError` tree with dual-parent semantic-map), `api-reference/configuration` (`ExchangeOptions` + env vars + base-URL resolution).
+- **Docs**: New "Hosted Trading" and "Self-host" sidebar groups in `docs.json`, plus a "Reference" group at the top of the API Reference tab. `sdk/server` moved out of the previous "SDK" group into "Self-host" (without slug rename — link-stability preserved for this release).
+- **Core**: New `ExchangeOptions` component schema in `core/src/server/openapi.yaml` documenting constructor-level options (`pmxtApiKey`, `walletAddress`, `signer`, `privateKey`, `baseUrl`, etc.) — previously only `ExchangeCredentials` (per-request body credentials) existed at the schema level.
+- **Core**: `BuiltOrder.expiry` field added to the OpenAPI schema (the TTL that triggers `BuiltOrderExpired` at submit time was implicit in the SDK and undocumented at the spec level).
+
+### Changed
+
+- **Docs**: `introduction.mdx` "It runs two ways" bullet order inverted — hosted listed first as the default, self-hosted second as the advanced path. First code block swapped from a dual-variant local/hosted snippet to a single hosted-default `pmxt.Polymarket(pmxt_api_key=...)` constructor.
+- **Docs**: `authentication.mdx` venue-credentials section reframed — "Hosted writes (recommended)" subsection added on top showing the `pmxt_api_key + wallet_address + private_key` constructor with a one-line `client.escrow.deposit()` example. The raw-private-key prose was preserved but relabeled as "Self-hosted / direct venue credentials (advanced)". Status/body/meaning error table picked up a fourth "SDK exception" column cross-linking to the new `/api-reference/errors` page.
+- **Docs**: `security.mdx` "Run pmxt locally" callout downgraded from `` to `` and reworded — self-hosting is positioned as one option among several rather than the implicit "safer choice". PreFundedEscrow custody surfaced as the hosted alternative.
+- **Docs**: `sdk/server.mdx` got a top `` banner clarifying that the page applies to self-hosted mode only and hosted-mode users can skip it. (File location and slug intentionally not renamed in this release to preserve external links.)
+- **Docs**: `concepts/venues.mdx` gained a third table at the bottom — "Hosted-trading venues" — listing Polymarket and Opinion with custody type, cross-chain support, and minimum-order-size columns.
+- **READMEs (root, Python, TypeScript)**: All three flipped to hosted-default. Subtitles, "Why pmxt?" bullets, Quick Start, and Trading sections now lead with `pmxt.Polymarket(pmxt_api_key=...)`. Per-venue raw-credentials blocks preserved verbatim but moved into "Self-hosted trading (advanced)" subsections. Root README's "No API key required" bullet (actively anti-hosted-positioning) replaced with a "Hosted API" lead bullet. Net +176 lines across the three files.
+
+### Fixed
+
+- **Core**: `Order` schema in `core/src/server/openapi.yaml` (and the generated `docs/api-reference/openapi.json`) now includes the nullable `txHash`, `chain`, and `blockNumber` fields the SDK has been returning in hosted mode since 2.49.0. Previous spec was silent on these and downstream codegen consumers missed them.
+- **Core**: `UserTrade` schema gained the same `txHash` / `chain` / `blockNumber` nullable trio.
+- **Core**: `Position` schema — `required` list trimmed from `[marketId, outcomeId, outcomeLabel, size, entryPrice, currentPrice, unrealizedPnL]` to `[marketId, outcomeId, size]`. The other four became optional in 2.49.0 when the SDK stopped fabricating mark-to-market defaults for positions without a known current price; the schema kept claiming they were required, so generated clients with strict-null checking were rejecting valid responses. New `currentValue` field added (`size * currentPrice` when available). `txHash` / `chain` / `blockNumber` enrichment added.
+- **Core**: `Balance` schema gained the optional `venue` field that hosted-mode responses already carry on multi-venue queries.
+- **Core**: `ErrorDetail` schema expanded from `{ message: string }` to the full envelope shipping in production responses — `code` (with a populated enum covering all `HostedTradingError` codes plus the pre-existing tree), `retryable: boolean`, optional `exchange`, optional free-form `detail` object. Downstream codegen can now branch on `code`.
+
+### Docs
+
+- **`docs.json`**: First-time `redirects` array added (empty for this release; reserves the structure for future slug renames).
+
## [2.49.0] - 2026-06-08
### Added
diff --git a/docs/api-reference/configuration.mdx b/docs/api-reference/configuration.mdx
new file mode 100644
index 00000000..d41aa107
--- /dev/null
+++ b/docs/api-reference/configuration.mdx
@@ -0,0 +1,168 @@
+---
+title: Configuration
+description: "Exchange constructor arguments, environment variables, and base-URL resolution rules."
+---
+
+The PMXT SDKs share one configuration surface across hosted and self-hosted modes. The same `ExchangeOptions` object selects mode, supplies credentials, and overrides defaults. Hosted is the default; self-hosted is the explicit fallback when no `pmxt_api_key` is present.
+
+## ExchangeOptions
+
+Constructor arguments, Python kwargs alongside TypeScript option keys.
+
+### Hosted (the default)
+
+| Python kwarg | TypeScript key | Type | Required? | Description |
+| ------------ | -------------- | ---- | --------- | ----------- |
+| `pmxt_api_key` | `pmxtApiKey` | `str` | Required for hosted | PMXT API key from [pmxt.dev/dashboard](https://pmxt.dev/dashboard). Triggers hosted mode. |
+| `wallet_address` | `walletAddress` | `str` | Required for hosted writes + escrow | Address that owns the escrow balance and signs orders. |
+| `private_key` | `privateKey` | `str` | Required for hosted writes | EVM private key used to sign EIP-712 typed-data locally. Auto-wrapped into a signer. |
+| `signer` | `signer` | `Signer` | Optional (alt to `private_key`) | Custom signer implementing `sign_typed_data` / `signTypedData`. For hardware wallets, MPC, remote signing. |
+| `base_url` | `baseUrl` | `str` | Optional | Override the default `https://api.pmxt.dev`. Rarely needed. |
+| `trade_base_url` | `tradeBaseUrl` | `str` | Optional | Override the default `https://trade.pmxt.dev`. Rarely needed. |
+| `timeout` | `timeout` | `float` (s) | Optional | Per-request timeout in seconds. Default 30. |
+
+
+```python Python
+import pmxt
+
+# Read-only: no private_key
+read_only = pmxt.Polymarket(
+ pmxt_api_key="pmxt_live_...",
+ wallet_address="0xYourWallet...",
+)
+
+# Trading-capable: private_key auto-wraps into EthAccountSigner
+trader = pmxt.Polymarket(
+ pmxt_api_key="pmxt_live_...",
+ wallet_address="0xYourWallet...",
+ private_key="0xYourPrivateKey...",
+)
+
+# Custom signer (hardware wallet, MPC, etc.)
+trader = pmxt.Polymarket(
+ pmxt_api_key="pmxt_live_...",
+ wallet_address="0xYourWallet...",
+ signer=my_ledger_signer,
+)
+```
+
+```typescript TypeScript
+import { Polymarket } from "pmxtjs";
+
+// Read-only
+const readOnly = new Polymarket({
+ pmxtApiKey: "pmxt_live_...",
+ walletAddress: "0xYourWallet...",
+});
+
+// Trading-capable
+const trader = new Polymarket({
+ pmxtApiKey: "pmxt_live_...",
+ walletAddress: "0xYourWallet...",
+ privateKey: "0xYourPrivateKey...",
+});
+
+// Custom signer
+const traderCustom = new Polymarket({
+ pmxtApiKey: "pmxt_live_...",
+ walletAddress: "0xYourWallet...",
+ signer: myLedgerSigner,
+});
+```
+
+
+### Self-hosted (fallback)
+
+When `pmxt_api_key` is absent, the SDK enters self-hosted mode and routes through the local `pmxt-core` server. Venue-native credentials apply here.
+
+| Python kwarg | TypeScript key | Type | Description |
+| ------------ | -------------- | ---- | ----------- |
+| `private_key` | `privateKey` | `str` | EVM private key, for Polymarket / Limitless / Probable / Opinion / etc. |
+| `api_key_id`, `private_key_pem` | `apiKeyId`, `privateKeyPem` | `str` | Kalshi RSA credentials. |
+| `email`, `password` | `email`, `password` | `str` | Smarkets session credentials. |
+| `local_port` | `localPort` | `int` | Override the default pmxt-core port (`3847`). |
+
+See [Self-hosted](/guides/self-hosted) for per-venue credential examples.
+
+## Environment variables
+
+| Variable | Effect |
+| -------- | ------ |
+| `PMXT_API_KEY` | Default `pmxt_api_key` for all SDK clients. Explicit constructor arg wins. |
+| `PMXT_BASE_URL` | Override `api.pmxt.dev`. Useful for staging environments. |
+| `PMXT_TRADE_BASE_URL` | Override `trade.pmxt.dev`. Useful for staging hosted-trading environments. |
+| `PMXT_LOCAL_PORT` | Override the local pmxt-core port (default `3847`). Self-hosted only. |
+| `PMXT_ALWAYS_RESTART=1` | Force-restart the local pmxt-core on every `ensure_server_running` call. Useful during local SDK development. |
+| `PMXT_TIMEOUT` | Default per-request timeout in seconds. |
+| `PMXT_LOG_LEVEL` | Log verbosity for SDK + local server: `debug`, `info`, `warn`, `error`. |
+
+
+Constructor arguments always take precedence over environment variables. The env-var path is for ergonomics in deploy environments where you set `PMXT_API_KEY` once.
+
+
+## Base URL resolution
+
+When the SDK makes a request, it picks one of three base URLs based on the operation and configuration:
+
+1. **`trade.pmxt.dev` (hosted trading)** — used for `/v0/trade/*`, `/v0/user/*`, and `/v0/escrow/*` whenever `pmxt_api_key` is set. Override with `trade_base_url` or `PMXT_TRADE_BASE_URL`.
+2. **`api.pmxt.dev` (hosted catalog + reads)** — used for `/v0/markets`, `/v0/events`, `/api/{venue}/*` whenever `pmxt_api_key` is set and the operation is not a hosted-trading write. Override with `base_url` or `PMXT_BASE_URL`.
+3. **`http://localhost:3847` (self-hosted)** — used for **every** operation when `pmxt_api_key` is absent. Override port with `local_port` or `PMXT_LOCAL_PORT`.
+
+### Precedence (highest to lowest)
+
+1. Explicit constructor `base_url` / `trade_base_url`.
+2. `PMXT_BASE_URL` / `PMXT_TRADE_BASE_URL` env var.
+3. Default (`https://api.pmxt.dev` / `https://trade.pmxt.dev` / `http://localhost:3847`).
+
+### Worked example
+
+```python
+import os
+os.environ["PMXT_BASE_URL"] = "https://api-staging.pmxt.dev"
+
+# Hosted mode (because pmxt_api_key is set); base_url comes from env
+client = pmxt.Polymarket(pmxt_api_key="pmxt_test_...")
+# → reads go to https://api-staging.pmxt.dev
+# → trade writes still go to https://trade.pmxt.dev (PMXT_TRADE_BASE_URL not set)
+
+# Self-hosted mode (no pmxt_api_key); env var ignored
+local = pmxt.Polymarket(private_key="0x...")
+# → reads + writes go to http://localhost:3847
+```
+
+## Hosted as default
+
+The configuration philosophy:
+
+- Setting `pmxt_api_key` (or `PMXT_API_KEY`) is the **single switch** that opts in to hosted mode. No other flag is required.
+- All hosted-specific URLs default to PMXT's production endpoints — no setup beyond the API key.
+- Self-hosted is the explicit fallback path. The SDK only attempts to spawn `pmxt-core` when no API key is configured.
+
+The result: the **shortest correct config is hosted**. A single env var (`PMXT_API_KEY`) plus a constructor with a wallet and private key is everything you need to trade.
+
+```python
+# Minimum hosted trading config:
+import os
+os.environ["PMXT_API_KEY"] = "pmxt_live_..."
+
+client = pmxt.Polymarket(
+ wallet_address="0x...",
+ private_key="0x...",
+)
+```
+
+## Self-hosted fallback (advanced)
+
+If you have specific reasons to run local — sub-100ms latency, raw venue credentials, regulatory custody — drop the API key. See [Self-hosted](/guides/self-hosted) for the full guide and [Server Management](/sdk/server) for lifecycle controls.
+
+```python
+# Self-hosted: no PMXT_API_KEY, no pmxt_api_key arg
+client = pmxt.Polymarket(private_key="0x...")
+```
+
+## See also
+
+- [Authentication](/authentication) — API keys and venue credentials.
+- [Self-hosted](/guides/self-hosted) — running pmxt-core locally.
+- [Server Management](/sdk/server) — local server lifecycle.
+- [Hosted vs self-hosted](/concepts/hosted-vs-self-hosted) — when each mode applies.
diff --git a/docs/api-reference/errors.mdx b/docs/api-reference/errors.mdx
new file mode 100644
index 00000000..7b7dade3
--- /dev/null
+++ b/docs/api-reference/errors.mdx
@@ -0,0 +1,211 @@
+---
+title: Error Reference
+description: "Every error class the SDK can raise, with status codes, detail shapes, parent classes, and catch examples."
+---
+
+PMXT's error hierarchy is designed so the **same `except`/`catch` clause works in both hosted and self-hosted modes**. Hosted errors descend from `HostedTradingError` *and* from a semantic parent (`InsufficientFunds`, `InvalidOrder`, `AuthenticationError`, etc.). Self-hosted errors raise the semantic parent directly. Catch the parent and you cover both paths.
+
+For recovery patterns and the five most-common errors with code, see [Handling hosted errors](/guides/hosted-errors).
+
+## Hierarchy
+
+```
+PmxtError (root for the whole SDK)
+├── ValidationError (local-only — bad arguments)
+│ └── MissingWalletAddress (escrow call without wallet_address)
+├── AuthenticationError
+│ ├── InvalidApiKey ◄── hosted
+│ └── InvalidSignature ◄── hosted
+├── InsufficientFunds
+│ └── InsufficientEscrowBalance ◄── hosted
+├── InvalidOrder
+│ ├── OrderSizeTooSmall ◄── hosted
+│ ├── BuiltOrderExpired ◄── hosted
+│ └── NoLiquidity ◄── hosted
+├── NotFoundError
+│ └── OutcomeNotFound ◄── hosted
+├── ExchangeNotAvailable
+│ └── CatalogUnavailable ◄── hosted (retryable)
+├── NotSupported
+└── HostedTradingError (any 4xx/5xx from trade.pmxt.dev)
+ │
+ └── (all classes marked ◄── hosted above are also HostedTradingError)
+```
+
+In Python this is implemented via multi-inheritance: `class InsufficientEscrowBalance(InsufficientFunds, HostedTradingError)`. In TypeScript, JS single-inheritance means each hosted leaf extends only its semantic parent, and the `HostedTradingError` membership is carried by a `static isHostedError = true` flag. Use the `isHostedError(e)` helper to test for membership.
+
+## `HostedTradingError`
+
+**Root of all errors returned by `trade.pmxt.dev`.**
+
+| | |
+| - | - |
+| Status code | Any 4xx or 5xx from the hosted trading API |
+| Detail body | `{ "error": "..." }` or `{ "detail": "..." }` or `{ "success": false, "error": "..." }` |
+| Parents (Python) | `PmxtError` |
+| TS membership | `e instanceof HostedTradingError` or `isHostedError(e) === true` |
+| Retryable | 5xx → yes; 4xx → no (unless a leaf overrides) |
+| Fields | `e.status` (int), `e.detail` (str) |
+
+
+```python Python
+from pmxt._hosted_errors import HostedTradingError
+
+try:
+ client.create_order(...)
+except HostedTradingError as e:
+ print(e.status, e.detail)
+```
+
+```typescript TypeScript
+import { isHostedError, HostedTradingError } from "pmxtjs";
+
+try {
+ await client.createOrder({ ... });
+} catch (e) {
+ if (e instanceof HostedTradingError) {
+ console.log((e as any).status, (e as any).detail);
+ } else if (isHostedError(e)) {
+ // Subclass without the direct extends-chain — still hosted
+ }
+}
+```
+
+
+## `InsufficientEscrowBalance`
+
+**Escrow free balance is below the order's USDC requirement.**
+
+| | |
+| - | - |
+| Status code | `400` |
+| Detail starts with | `Insufficient escrow balance` |
+| Parents (Python) | `InsufficientFunds`, `HostedTradingError` |
+| TS extends | `InsufficientFunds`; `static isHostedError = true` |
+| Retryable | No (deposit and retry) |
+
+Recovery: top up via `client.escrow.deposit_tx(...)`.
+
+## `OrderSizeTooSmall`
+
+**Resolved share count is below the venue's minimum (Polymarket: 5 shares).**
+
+| | |
+| - | - |
+| Status code | `400` |
+| Detail contains | `below the minimum` |
+| Parents (Python) | `InvalidOrder`, `HostedTradingError` |
+| TS extends | `InvalidOrder`; `static isHostedError = true` |
+| Retryable | No (resize and retry) |
+
+## `InvalidApiKey`
+
+**`pmxt_api_key` is missing, malformed, revoked, or expired.**
+
+| | |
+| - | - |
+| Status code | `401` (always) |
+| Detail | `invalid api key` or `missing api key` |
+| Parents (Python) | `AuthenticationError`, `HostedTradingError` |
+| TS extends | `AuthenticationError`; `static isHostedError = true` |
+| Retryable | No |
+
+Recovery: rotate the key in the [dashboard](https://pmxt.dev/dashboard). Do not retry with the same key.
+
+## `OutcomeNotFound`
+
+**Catalog could not resolve the requested `outcome_id` (or `market_id`).**
+
+| | |
+| - | - |
+| Status code | `404` |
+| Detail contains | `catalog: no outcome` |
+| Parents (Python) | `NotFoundError`, `HostedTradingError` |
+| TS extends | `NotFoundError`; `static isHostedError = true` |
+| Retryable | No |
+
+Most often: you passed a **venue-native ID** to a hosted endpoint that expects a **catalog UUID**. See [Catalog UUID vs venue ID](/concepts/catalog-uuid-vs-venue-id).
+
+## `CatalogUnavailable`
+
+**The hosted catalog is temporarily unavailable.**
+
+| | |
+| - | - |
+| Status code | `502` / `503` |
+| Detail starts with | `catalog:` |
+| Parents (Python) | `ExchangeNotAvailable`, `HostedTradingError` |
+| TS extends | `ExchangeNotAvailable`; `static isHostedError = true` |
+| Retryable | **Yes** (override on `DEFAULT_RETRYABLE`) |
+
+Recovery: retry with exponential backoff.
+
+## `BuiltOrderExpired`
+
+**The `built_order_id` or `cancel_id` TTL elapsed before submit/cancel.**
+
+| | |
+| - | - |
+| Status code | `400` |
+| Detail contains | `built_order_id expired` or `cancel_id expired` |
+| Parents (Python) | `InvalidOrder`, `HostedTradingError` |
+| TS extends | `InvalidOrder`; `static isHostedError = true` |
+| Retryable | No (re-build and re-sign) |
+
+Re-call `build_order`, re-sign, re-submit. Common cause: slow hardware-wallet confirmations.
+
+## `InvalidSignature`
+
+**The hosted trading API rejected the EIP-712 signature or typed-data shape.**
+
+| | |
+| - | - |
+| Status code | `400` / `401` |
+| Detail contains | `Invalid signature` |
+| Parents (Python) | `AuthenticationError`, `HostedTradingError` |
+| TS extends | `AuthenticationError`; `static isHostedError = true` |
+| Retryable | No |
+
+Most often: signing the wrong domain (chain ID, verifying contract). See [Signing](/guides/signing).
+
+## `NoLiquidity`
+
+**Empty book on the side you're crossing.**
+
+| | |
+| - | - |
+| Status code | `400` |
+| Detail contains | `book has no resting asks` or `book has no resting bids` |
+| Parents (Python) | `InvalidOrder`, `HostedTradingError` |
+| TS extends | `InvalidOrder`; `static isHostedError = true` |
+| Retryable | No (post a limit instead, or wait) |
+
+## `MissingWalletAddress`
+
+**Local validation — `client.escrow.*` was called without a `wallet_address`.**
+
+| | |
+| - | - |
+| Status code | n/a (local) |
+| Parents (Python) | `ValidationError` |
+| TS extends | `ValidationError` |
+| Hosted member? | **No** — this is a local pre-flight check, not a hosted-API response |
+
+Pass `wallet_address` to the exchange constructor.
+
+## Catching by intent
+
+| Intent | Catch (Python) | Catch (TypeScript) |
+| ------ | -------------- | ------------------ |
+| Any hosted error | `except HostedTradingError` | `if (isHostedError(e))` |
+| Any auth problem | `except AuthenticationError` | `e instanceof AuthenticationError` |
+| Any insufficient-funds | `except InsufficientFunds` | `e instanceof InsufficientFunds` |
+| Any invalid order | `except InvalidOrder` | `e instanceof InvalidOrder` |
+| Anything PMXT | `except PmxtError` | `e instanceof PmxtError` |
+
+## See also
+
+- [Handling hosted errors](/guides/hosted-errors) — recovery cookbook with code for the five most-common errors.
+- [Hosted trading concept](/concepts/hosted-trading) — where these errors come from.
+- Python source: [`sdks/python/pmxt/_hosted_errors.py`](https://github.com/pmxt-dev/pmxt/blob/main/sdks/python/pmxt/_hosted_errors.py)
+- TypeScript source: [`sdks/typescript/pmxt/hosted-errors.ts`](https://github.com/pmxt-dev/pmxt/blob/main/sdks/typescript/pmxt/hosted-errors.ts)
diff --git a/docs/authentication.mdx b/docs/authentication.mdx
index 043c4ce9..a3195adf 100644
--- a/docs/authentication.mdx
+++ b/docs/authentication.mdx
@@ -56,9 +56,38 @@ Explicit argument takes precedence over the environment variable.
## Venue credentials
-Your PMXT API key authenticates you to **PMXT**. Venue credentials
-(Polymarket private key, Kalshi API key, etc.) authenticate you to the
-**venue** and are passed in the request body for calls that need them:
+### Hosted writes (recommended)
+
+In hosted mode, trade writes use **PMXT's PreFundedEscrow custody** plus a
+locally-signed EIP-712 payload. You pass your `pmxt_api_key`, the wallet
+address you trade from, and a private key used only for local signing —
+the private key never leaves your machine, and PMXT custodies USDC in
+the escrow contract on your behalf.
+
+```python
+import pmxt
+
+client = pmxt.Polymarket(
+ pmxt_api_key="pmxt_live_...",
+ wallet_address="0xYourWallet...",
+ private_key="0xYourPrivateKey...",
+)
+
+# One-time: approve + deposit USDC into PreFundedEscrow
+deposit_tx = client.escrow.deposit_tx(amount=10.0)
+# (sign + broadcast deposit_tx with your wallet library)
+```
+
+See [Trading quickstart](/trading-quickstart) for the full 60-second flow
+and [Escrow lifecycle](/guides/escrow-lifecycle) for the approve / deposit /
+withdraw mechanics. Hosted writes today: Polymarket and Opinion.
+
+### Self-hosted / direct venue credentials (advanced)
+
+When you run [pmxt-core locally](/guides/self-hosted), or when you need a
+venue that hosted writes don't yet support, you pass venue-native
+credentials (Polymarket private key, Kalshi RSA, Smarkets session, etc.)
+directly:
```python
order = poly.create_order(
@@ -70,14 +99,14 @@ order = poly.create_order(
)
```
-PMXT never stores venue credentials. They are forwarded to the venue
-and dropped when the call completes.
+PMXT never stores venue credentials. They are forwarded to the venue and
+dropped when the call completes.
Some venues (Polymarket, Limitless, Probable, Opinion, Baozi) require
raw wallet private keys with full fund control. **Use a dedicated
trading wallet** and read [Security & Credential Handling](/security)
-before passing private keys to the hosted API.
+before passing private keys to PMXT — hosted or self-hosted.
## Rotating keys
@@ -96,8 +125,11 @@ All revocations are logged under **Settings > Audit Log**.
## Errors
-| Status | Body | Meaning |
-| ------ | ---------------------------------------- | ----------------------------------------- |
-| `401` | `{"error": "missing api key"}` | No `Authorization` header on the request. |
-| `401` | `{"error": "invalid api key"}` | Key unknown, revoked, or expired. |
-| `429` | `{"error": "rate_limit_exceeded", ... }` | See [Plans & Limits](/rate-limits). |
+| Status | Body | Meaning | SDK exception |
+| ------ | ---------------------------------------- | ----------------------------------------- | ------------- |
+| `401` | `{"error": "missing api key"}` | No `Authorization` header on the request. | [`InvalidApiKey`](/api-reference/errors#invalidapikey) |
+| `401` | `{"error": "invalid api key"}` | Key unknown, revoked, or expired. | [`InvalidApiKey`](/api-reference/errors#invalidapikey) |
+| `429` | `{"error": "rate_limit_exceeded", ... }` | See [Plans & Limits](/rate-limits). | `RateLimitExceeded` |
+
+See [API Reference / Errors](/api-reference/errors) for the full error
+class hierarchy.
diff --git a/docs/concepts/catalog-uuid-vs-venue-id.mdx b/docs/concepts/catalog-uuid-vs-venue-id.mdx
new file mode 100644
index 00000000..486a4547
--- /dev/null
+++ b/docs/concepts/catalog-uuid-vs-venue-id.mdx
@@ -0,0 +1,90 @@
+---
+title: Catalog UUID vs Venue ID
+description: "Two market identifier spaces coexist in PMXT — when each is used, and how to convert."
+---
+
+PMXT speaks two market identifier languages. The **catalog UUID** is PMXT's own stable identifier — a UUID assigned to every row in `prediction_markets.markets` and `prediction_markets.outcomes`. The **venue-native ID** is whatever the underlying venue uses — a Polymarket condition ID hex string, a Kalshi ticker, an Opinion market hash, a Limitless market address.
+
+Both exist. They are not interchangeable. Picking the wrong one is the single most common cause of `OutcomeNotFound` errors.
+
+## When each is used
+
+| Endpoint family | Identifier expected |
+| --------------- | ------------------- |
+| `trade.pmxt.dev/v0/trade/*` (hosted writes) | **Catalog UUID** |
+| `trade.pmxt.dev/v0/user/*` (hosted reads) | Wallet address (UUIDs not required) |
+| `api.pmxt.dev/v0/markets`, `/v0/events` (Router) | **Catalog UUID** in responses |
+| `api.pmxt.dev/api/{venue}/fetchMarkets` (per-venue reads) | Returns **venue-native ID** |
+| `api.pmxt.dev/api/{venue}/fetchOrderBook` (per-venue reads) | Accepts **venue-native ID** |
+| Direct venue API (self-hosted) | **Venue-native ID** only |
+
+The hosted trading API was designed to be venue-agnostic. To do that, every market reference needs a stable PMXT-owned identifier rather than a venue-native one that may collide across venues or change shape.
+
+## Examples
+
+A single Polymarket binary market has both:
+
+- Catalog `market_id`: `2eeb03dc-404b-41d5-bc57-6aeb37927ae6`
+- Polymarket `conditionId`: `0xc704f74e2f9dfae70f770cb253ffadde10768eeab41233098bf5ac67995a94b5`
+
+Each outcome has both:
+
+- Catalog `outcome_id`: `a114f052-1fd1-4bcd-b9cf-de019db81b67`
+- Polymarket `tokenId`: `104932610032177696635191871147557737718087870958469629338467406422339967452218`
+
+`client.create_order(market_id="2eeb03dc-...", outcome_id="a114f052-...")` works against `trade.pmxt.dev`. `client.create_order(market_id="0xc704...", outcome_id="10493...")` will fail with `OutcomeNotFound`.
+
+## The `fetch_markets` quirk
+
+
+**`fetch_markets` against a venue exchange returns venue-native IDs, not catalog UUIDs.** This is the quirk you will hit first.
+
+If you call `pmxt.Polymarket(...).fetch_markets(query="election")`, the `market_id` and `outcome_id` fields on every returned row are Polymarket-native. Feeding those into hosted-mode `create_order` will fail because `trade.pmxt.dev` resolves only catalog UUIDs.
+
+
+This is a known gap. Three workarounds, ordered by ergonomics:
+
+### Workaround 1 — Use the Router
+
+The [Router](/router/overview) endpoints (`fetch_markets`, `fetch_events`) speak catalog UUIDs natively. The same query against the Router gives you UUIDs you can feed directly into `create_order`.
+
+```python
+router = pmxt.Router(pmxt_api_key="pmxt_live_...")
+markets = router.fetch_markets(query="knicks 2026 nba champion", venue="polymarket")
+# markets[0].market_id is already a catalog UUID
+```
+
+This is the path you should use for hosted trading. The Router is the hosted-mode address-book.
+
+### Workaround 2 — Reverse-resolve through the catalog
+
+If you already have a venue-native ID and need the catalog UUID, query the catalog via the hosted SQL endpoint or fetch the matched cluster:
+
+```python
+# Look up by venue-native condition_id
+clusters = router.fetch_matched_market_clusters(
+ venue="polymarket",
+ venue_market_id="0xc704f74e2f9dfae70f770cb253ffadde10768eeab41233098bf5ac67995a94b5",
+)
+catalog_uuid = clusters[0].market_id
+```
+
+### Workaround 3 — Catalog DB lookup (Enterprise)
+
+For batch workflows, query the `prediction_markets.markets` and `prediction_markets.outcomes` tables directly via the [SQL endpoint](/sql). Each row has the canonical UUID alongside the venue-native fields (`venue_market_id`, `venue_outcome_id` / `token_id`).
+
+```sql
+SELECT id AS market_id, venue_market_id, venue
+FROM prediction_markets.markets
+WHERE venue = 'polymarket'
+ AND venue_market_id = '0xc704...';
+```
+
+## A simple rule of thumb
+
+- Discovering markets? **Use the Router** — UUIDs everywhere.
+- Trading via hosted? **UUIDs only.**
+- Reading order books / OHLCV? **Either works** at the per-venue endpoints, but venue-native is canonical there.
+- Self-hosted trading? **Venue-native** only — there's no PMXT catalog in the path.
+
+When in doubt: if the URL contains `trade.pmxt.dev`, it's UUIDs. If the URL contains `/api/{venue}/`, it's venue-native.
diff --git a/docs/concepts/hosted-trading.mdx b/docs/concepts/hosted-trading.mdx
new file mode 100644
index 00000000..062834e2
--- /dev/null
+++ b/docs/concepts/hosted-trading.mdx
@@ -0,0 +1,105 @@
+---
+title: Hosted Trading
+description: "The hosted-default execution path — how PMXT routes orders, why custody lives in PreFundedEscrow, and when to choose self-host instead."
+---
+
+Hosted trading is PMXT's default execution path. You provide an API key and a wallet; PMXT custodies USDC in a `PreFundedEscrow` contract; orders are built server-side, signed in your browser/server with your private key, and submitted by PMXT to the underlying venue. You never run a local server, never integrate with venue-specific signature schemes, and never expose your private key over the wire.
+
+This page explains what hosted mode actually is, what flows through where, and when it's the right choice.
+
+## When to use hosted mode
+
+Hosted is the right default for:
+
+- **Web and mobile apps** — your backend holds the `pmxt_api_key`; users keep their own private keys on their devices.
+- **Trading bots that don't need sub-100ms latency** — typical hosted round-trip is ~150–300ms including the venue submit.
+- **Multi-venue strategies** — you stay on one HTTP surface even when you're trading across Polymarket and Opinion.
+- **Anyone who doesn't want to operate infrastructure.**
+
+Choose [self-hosted](/concepts/hosted-vs-self-hosted) instead when you need sub-100ms latency, want to use raw venue credentials (Polymarket L2 API keys, Kalshi RSA, etc.), or have regulatory custody constraints.
+
+## The mental model
+
+A hosted trade has three actors:
+
+1. **Your client** — your application code, holding the `pmxt_api_key` and (for writes) the user's `private_key`.
+2. **`trade.pmxt.dev`** — PMXT's hosted trading API. Routes orders, owns the escrow contract, talks to the venue.
+3. **The venue** — Polymarket's CLOB, Opinion's matching engine, etc. Sees PMXT as the submitter.
+
+Reads (`fetch_balance`, `fetch_positions`, `fetch_my_trades`, etc.) only need the API key and a wallet address. Writes (`create_order`, `cancel_order`) require the user to sign an EIP-712 typed-data payload locally.
+
+## The trade flow
+
+```
+┌──────────┐ 1. POST /v0/trade/build-order ┌──────────────────┐
+│ client │ ───────────────────────────────► │ trade.pmxt.dev │
+│ │ │ │
+│ │ 2. unsigned typed-data + id │ │
+│ │ ◄─────────────────────────────── │ │
+│ │ │ │
+│ signs │ ─── EIP-712 (local only) ────► │ │
+│ │ │ │
+│ │ 3. POST /v0/trade/submit-order │ │
+│ │ { built_order_id, signature }│ │
+│ │ ───────────────────────────────► │ │
+│ │ │ 4. Venue submit │
+│ │ 5. order id + status │ ───────────────► │
+│ │ ◄─────────────────────────────── │ (Polygon / │
+└──────────┘ │ BSC chain) │
+ └──────────────────┘
+```
+
+Step-by-step:
+
+1. **Build.** The SDK calls `POST /v0/trade/build-order` with the catalog UUIDs (`market_id`, `outcome_id`), side, and amount. The server resolves the venue-native fields (token IDs, salt, expiry, fees), packages them into the venue's EIP-712 typed-data shape, and returns a `built_order_id` plus the payload to sign.
+2. **Sign.** The SDK signs the typed-data payload locally with your `private_key`. This step never leaves your process. See [Signing](/guides/signing) for the exact shape.
+3. **Submit.** The SDK calls `POST /v0/trade/submit-order` with the `built_order_id` and the signature. The server attaches the signature to the prepared order and submits to the venue.
+4. **Settle.** The venue matches the order. On Polymarket, fills come from the CLOB and are settled on Polygon via the CTF exchange. On Opinion, settlement uses a dual-signature cross-chain flow.
+
+`create_order` is a convenience wrapper that chains build → sign → submit in one call. `build_order` and `submit_order` are the lower-level primitives if you want to inspect or modify the typed-data before signing.
+
+## Catalog UUIDs are the address space
+
+Every hosted endpoint speaks in **catalog UUIDs**, not venue-native IDs. A Polymarket condition ID, a Kalshi ticker, an Opinion market hash — none of those work directly against `trade.pmxt.dev`. Instead, PMXT's catalog assigns a stable UUID to every `prediction_markets.markets` row and every `prediction_markets.outcomes` row, and the trading API uses those UUIDs as the addressable identifiers.
+
+The catalog UUIDs are the same ones returned by the Router (`/v0/markets`, `/v0/events`). They are stable across venue API changes and survive venue re-listings.
+
+
+`fetch_markets` against a venue exchange (`pmxt.Polymarket(...).fetch_markets(...)`) currently returns **venue-native IDs**, not catalog UUIDs. To trade against those rows, you need to reverse-resolve to the UUID. See [Catalog UUID vs venue ID](/concepts/catalog-uuid-vs-venue-id) for the workaround.
+
+
+## Custody: PreFundedEscrow
+
+PMXT does not custody USDC in a hot wallet, an exchange-style omnibus account, or a multisig. It custodies in a `PreFundedEscrow` smart contract:
+
+- **Polymarket** uses the escrow contract on **Polygon**. The escrow holds USDC and acts as the operator that submits orders on behalf of the user.
+- **Opinion** uses the escrow contract on **BSC** as the settlement leg of a cross-chain flow.
+
+The user's wallet retains beneficial ownership at all times. Funds enter via `client.escrow.deposit()`, exit via `client.escrow.withdraw()`. Both are unsigned-tx builders — your wallet signs and submits them. See [Escrow Lifecycle](/guides/escrow-lifecycle).
+
+## Trust model
+
+The `pmxt_api_key` is a **service-role credential**:
+
+- The key holder can read any user's escrow data and forward signed orders for any wallet.
+- Writes are still gated by the user's EIP-712 signature against their own wallet. **The key alone cannot move funds.**
+- Reads (`fetch_balance`, `fetch_positions`) are NOT gated by a signature — anyone holding the key can read any associated wallet's hosted positions.
+
+Keep `pmxt_api_key` on a server. Never ship it to a browser bundle. Never log it.
+
+## What's supported today
+
+| Venue | Hosted writes | Hosted reads | Notes |
+| ----- | ------------- | ------------ | ----- |
+| Polymarket | Yes | Yes | Polygon escrow, CLOB exchange |
+| Opinion | Yes | Yes | Cross-chain (BSC settlement, dual-signature) |
+| Kalshi, Limitless, Smarkets, Probable, Myriad, Metaculus, etc. | No | Read-only via catalog | Use [self-hosted](/guides/self-hosted) for writes |
+
+If a venue you need isn't here, run pmxt-core locally and pass raw venue credentials — see [Self-hosted](/guides/self-hosted).
+
+## Next
+
+- [Trading quickstart](/trading-quickstart) — 60-second guided path.
+- [Escrow lifecycle](/guides/escrow-lifecycle) — deposits, withdrawals, the timelock.
+- [Hosted errors](/guides/hosted-errors) — every error class and how to recover.
+- [Hosted vs self-hosted](/concepts/hosted-vs-self-hosted) — one-page comparison.
diff --git a/docs/concepts/hosted-vs-self-hosted.mdx b/docs/concepts/hosted-vs-self-hosted.mdx
new file mode 100644
index 00000000..818b28bd
--- /dev/null
+++ b/docs/concepts/hosted-vs-self-hosted.mdx
@@ -0,0 +1,77 @@
+---
+title: Hosted vs Self-hosted
+description: "One-page comparison of the two PMXT execution paths."
+---
+
+PMXT runs two ways. **Hosted** is the default — it's what `pmxt.Polymarket(pmxt_api_key=...)` does when an API key is set. **Self-hosted** is the advanced escape hatch — you run `pmxt-core` on your own machine and the SDK talks to `localhost`. Both expose the same SDK surface; the difference is where execution happens and who holds keys.
+
+## At a glance
+
+| | Hosted (`pmxt_api_key` set) | Self-hosted (no API key) |
+| - | --------------------------- | ------------------------ |
+| **Install footprint** | `pip install pmxt` / `npm i pmxtjs` | SDK + pmxt-core local server |
+| **Who holds the API key?** | You (backend secret) | Not used |
+| **Who holds venue credentials?** | PMXT escrow contract (USDC custody only) | You, on your machine |
+| **Trading auth (writes)** | Your EIP-712 signature, signed locally | Raw venue credential (private key, Kalshi RSA, etc.) |
+| **Reads** | `trade.pmxt.dev/v0/user/...` | Direct to the venue API |
+| **Writes** | `trade.pmxt.dev/v0/trade/{build,submit}-order` | Direct to the venue API |
+| **Custody** | USDC in PMXT `PreFundedEscrow` | You retain venue-native custody |
+| **Latency** | ~150–300ms round-trip | Limited by venue + your network |
+| **Trading venues** | Polymarket, Opinion | Every venue PMXT supports |
+| **Read-only venues** | All catalog venues via Router | All catalog venues via Router |
+| **Infra to run** | None | One local process |
+| **Regulatory custody** | PMXT escrow as counterparty | You as direct counterparty to the venue |
+
+## When hosted is the right choice
+
+- You're building a **web or mobile app** and want to keep `pmxt_api_key` server-side while end users keep their private keys client-side.
+- You want **one HTTP surface** across Polymarket and Opinion without learning each venue's order schema.
+- You want **PreFundedEscrow custody** instead of managing venue-native accounts.
+- You don't want to operate the pmxt-core process, manage upgrades, or run a sidecar.
+
+## When self-hosted is the right choice
+
+- You need **sub-100ms latency** — colocated arbitrage, market-making, latency-sensitive HFT-style strategies.
+- You **prefer raw venue credentials** — Polymarket L2 API keys, Kalshi RSA keys, Smarkets sessions — and don't want PMXT in the custody path.
+- You have **regulatory custody requirements** that mandate direct counterparty status with the venue.
+- You want to **trade on venues not yet supported by hosted** (Kalshi, Limitless, Smarkets, Probable, Myriad, etc.).
+- You're an **OSS contributor** developing or debugging pmxt-core itself.
+
+See [Self-hosted](/guides/self-hosted) for setup.
+
+## What's identical either way
+
+The SDK surface is the same. Code written against hosted will run against self-hosted with no changes except dropping the `pmxt_api_key` argument:
+
+```python
+# Hosted (default)
+client = pmxt.Polymarket(pmxt_api_key="pmxt_live_...", wallet_address="0x...", private_key="0x...")
+
+# Self-hosted (drop the API key — talks to http://localhost:3847)
+client = pmxt.Polymarket(private_key="0x...")
+```
+
+`fetch_markets`, `fetch_order_book`, `create_order`, `fetch_positions` — same signatures, same response shapes.
+
+## What differs subtly
+
+- **`fetch_balance`**: hosted returns escrow USDC; self-hosted returns the venue-native balance (e.g. Polymarket's CLOB-proxy USDC). Same shape, different source. See the [migration guide](/guides/migrate-to-hosted-trading) for the implications.
+- **`Position` fields**: hosted may surface `None`/`undefined` on `entry_price`, `current_price`, `unrealized_pnl`, `outcome_label` when the server doesn't yet have the data. Self-hosted always populates them.
+- **Catalog UUIDs vs venue IDs**: hosted-mode `create_order` requires catalog UUIDs (`market_id`, `outcome_id`). Self-hosted accepts the venue-native ID directly. See [Catalog UUID vs venue ID](/concepts/catalog-uuid-vs-venue-id).
+- **Error classes**: hosted produces `HostedTradingError` subclasses with semantic parents (`InsufficientEscrowBalance` is also `InsufficientFunds`). Self-hosted produces the parent classes directly. Catch the parent and you handle both paths. See [Hosted errors](/guides/hosted-errors).
+
+## Mixing modes
+
+You can run **hosted reads with self-hosted writes** (or vice versa) — they're independent SDK instances. A common pattern is:
+
+```python
+# Hosted client for cross-venue research
+hosted = pmxt.Polymarket(pmxt_api_key="pmxt_live_...")
+markets = hosted.fetch_markets(query="election")
+
+# Self-hosted client for trading with raw venue creds
+local = pmxt.Polymarket(private_key="0x...")
+order = local.create_order(...)
+```
+
+This is useful if you trust PMXT enough for catalog and price data but want custody to stay with the venue for execution.
diff --git a/docs/concepts/venues.mdx b/docs/concepts/venues.mdx
index b57a63bb..ac022a32 100644
--- a/docs/concepts/venues.mdx
+++ b/docs/concepts/venues.mdx
@@ -69,3 +69,19 @@ Not every venue supports every method. Broadly:
See the [API Reference](/api-reference/overview) for the per-method
matrix (inferred from the OpenAPI `operationId`s).
+
+## Hosted-trading venues
+
+A subset of venues support **hosted trading** — writes routed through
+`trade.pmxt.dev` with PMXT's PreFundedEscrow custody and locally-signed
+EIP-712 orders. Every other venue is read-only via the catalog or
+requires [self-hosted](/guides/self-hosted) for writes.
+
+| Venue | Custody | Cross-chain | Min order size | Hosted writes supported |
+| ----- | ------- | ----------- | -------------- | ----------------------- |
+| Polymarket | `PreFundedEscrow` on Polygon | No (Polygon-only) | 5 shares | Yes |
+| Opinion | `PreFundedEscrow` on BSC | Yes (dual-signature settlement) | Venue minimum | Yes |
+
+See [Hosted trading](/concepts/hosted-trading) for the build → sign →
+submit flow, and [Trading quickstart](/trading-quickstart) for the
+end-to-end path.
diff --git a/docs/docs.json b/docs/docs.json
index f8d57d9d..06d86f36 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -45,6 +45,7 @@
"href": "https://pmxt.dev/dashboard"
}
},
+ "redirects": [],
"navigation": {
"tabs": [
{
@@ -55,6 +56,7 @@
"pages": [
"introduction",
"quickstart",
+ "trading-quickstart",
"authentication",
"mcp",
"security",
@@ -67,12 +69,25 @@
"pages": [
"concepts/unified-schema",
"concepts/catalog-vs-live",
- "concepts/venues"
+ "concepts/venues",
+ "concepts/hosted-trading",
+ "concepts/hosted-vs-self-hosted",
+ "concepts/catalog-uuid-vs-venue-id"
]
},
{
- "group": "SDK",
+ "group": "Hosted Trading",
"pages": [
+ "guides/escrow-lifecycle",
+ "guides/signing",
+ "guides/hosted-errors",
+ "guides/migrate-to-hosted-trading"
+ ]
+ },
+ {
+ "group": "Self-host",
+ "pages": [
+ "guides/self-hosted",
"sdk/server"
]
}
@@ -107,6 +122,13 @@
{
"tab": "API Reference",
"groups": [
+ {
+ "group": "Reference",
+ "pages": [
+ "api-reference/errors",
+ "api-reference/configuration"
+ ]
+ },
{
"group": "Overview",
"pages": [
diff --git a/docs/guides/escrow-lifecycle.mdx b/docs/guides/escrow-lifecycle.mdx
new file mode 100644
index 00000000..85461357
--- /dev/null
+++ b/docs/guides/escrow-lifecycle.mdx
@@ -0,0 +1,209 @@
+---
+title: Escrow Lifecycle
+description: "Deposit, trade, withdraw — how USDC moves through PMXT's PreFundedEscrow custody."
+---
+
+Hosted trading on PMXT settles through a `PreFundedEscrow` smart contract. Your wallet retains beneficial ownership; PMXT operates the contract and submits orders to the venue on your behalf. This page walks through the full lifecycle: approve, deposit, trade, withdraw.
+
+The escrow contract lives on **Polygon** for Polymarket trades and on **BSC** for Opinion's cross-chain settlement.
+
+## Why escrow at all?
+
+Polymarket's CLOB exchange expects the submitter to be the operator of the user's CLOB proxy wallet. That proxy is created by Polymarket's USDC.e adapter and is non-trivial to operate from a third-party context. PMXT's `PreFundedEscrow` solves this by acting as a pre-funded operator: the user deposits USDC once, PMXT routes orders against that balance, and the user can withdraw at any time. The user signs every order with EIP-712; the escrow contract can only spend USDC against signed orders, never unilaterally.
+
+For Opinion, the escrow plays a different role — it's the on-chain settlement leg of a dual-signature cross-chain flow. Same custody story, different mechanics.
+
+## The `client.escrow` namespace
+
+Hosted exchange clients (Polymarket, Opinion) expose an `escrow` namespace. Every method **builds an unsigned transaction**. Your wallet — MetaMask, ethers `Wallet`, viem, web3.py, etc. — is responsible for signing and broadcasting it. PMXT never holds your private key.
+
+| Method (Python / TypeScript) | Description |
+| ---------------------------- | ----------- |
+| `escrow.approve_tx(token, amount_wei=None)` / `escrow.approveTx(token, amountWei?)` | Build an unsigned ERC-20 approval for USDC or CTF. |
+| `escrow.deposit_tx(amount)` / `escrow.depositTx(amount)` | Build an unsigned USDC deposit into PreFundedEscrow. |
+| `escrow.withdraw_tx(action, amount=None)` / `escrow.withdrawTx(action, amount?)` | Build an unsigned `request` / `claim` / `cancel` withdrawal. |
+| `escrow.withdrawals(include="pending,events")` / `escrow.withdrawals({ include })` | Read pending withdrawal state and historical events. |
+
+See the source: [`sdks/python/pmxt/escrow.py`](https://github.com/pmxt-dev/pmxt/blob/main/sdks/python/pmxt/escrow.py) and [`sdks/typescript/pmxt/escrow.ts`](https://github.com/pmxt-dev/pmxt/blob/main/sdks/typescript/pmxt/escrow.ts).
+
+## 1. Approve
+
+Before the first deposit, the escrow contract needs permission to pull USDC (and, for some flows, the Polymarket CTF token) from your wallet. This is a standard ERC-20 approval.
+
+
+```python Python
+# Unlimited approval for USDC (most common)
+tx = client.escrow.approve_tx("usdc")
+# tx is a dict like:
+# { "to": "0x...", "data": "0x...", "value": "0", "chain_id": 137 }
+# Sign and broadcast it with your preferred wallet library.
+
+# Or scope the approval to a specific wei amount
+tx = client.escrow.approve_tx("usdc", amount_wei=1_000_000_000) # 1,000 USDC
+
+# Approve the Polymarket CTF token (only needed for direct CTF transfers)
+tx = client.escrow.approve_tx("ctf")
+```
+
+```typescript TypeScript
+// Unlimited approval for USDC (most common)
+const tx = await client.escrow.approveTx("usdc");
+// tx is an object like:
+// { to: "0x...", data: "0x...", value: "0", chainId: 137 }
+
+// Or scope the approval to a specific wei amount (bigint)
+const txScoped = await client.escrow.approveTx("usdc", 1_000_000_000n);
+
+// Approve the CTF token
+const ctfTx = await client.escrow.approveTx("ctf");
+```
+
+
+
+Approval is one-time per token + spender. If you ever rotate the escrow contract (rare, gated on a PMXT migration announcement), you'll need to re-approve.
+
+
+## 2. Deposit
+
+Once approval is in place, deposit USDC. Amounts are in **whole USDC** (6 decimals); the SDK validates precision and rejects values like `0.0000001`.
+
+
+```python Python
+# Deposit 10 USDC
+tx = client.escrow.deposit_tx(amount=10.0)
+# Sign and send with your wallet library.
+
+# Decimals up to 6 places are supported
+tx = client.escrow.deposit_tx(amount=10.5)
+tx = client.escrow.deposit_tx(amount=10.123456)
+```
+
+```typescript TypeScript
+// Deposit 10 USDC — number, decimal string, or wei BigInt all accepted
+const tx = await client.escrow.depositTx(10);
+const tx2 = await client.escrow.depositTx("10.5");
+const tx3 = await client.escrow.depositTx(10_500_000n); // wei micro-USDC
+```
+
+
+
+USDC precision is 6 decimals. The SDK rejects `0.0000001` with `ValidationError`. Pre-round before passing the value in.
+
+
+## 3. Confirm the deposit
+
+After the deposit transaction confirms on-chain, the balance shows up in escrow. `fetch_balance` is the canonical check.
+
+
+```python Python
+balance = client.fetch_balance()
+print(f"Free: {balance.free}, Used: {balance.used}, Total: {balance.total}")
+```
+
+```typescript TypeScript
+const balance = await client.fetchBalance();
+console.log(`Free: ${balance.free}, Used: ${balance.used}, Total: ${balance.total}`);
+```
+
+
+
+On-chain confirmation usually takes 2–5 seconds on Polygon. If `fetch_balance` still reads zero 30 seconds after broadcast, check the tx on Polygonscan — a failed deposit (e.g. due to missing approval) will not update escrow state.
+
+
+## 4. Trade
+
+With escrow funded, you can trade. `create_order` and `submit_order` debit the escrow balance; `cancel_order` releases the reservation. No additional escrow calls are needed during normal trading — the balance just gets spent.
+
+```python
+order = client.create_order(
+ market_id="2eeb03dc-404b-41d5-bc57-6aeb37927ae6",
+ outcome_id="a114f052-1fd1-4bcd-b9cf-de019db81b67",
+ side="buy",
+ order_type="market",
+ amount=5.0,
+ denom="usdc",
+ slippage_pct=30.0,
+)
+```
+
+## 5. Withdraw
+
+Withdrawals are a **two-step timelock**. You first `request` a withdrawal; after a contract-enforced delay (~1 hour in production), you `claim` it. You can also `cancel` a pending request.
+
+The timelock is a security feature — it gives the user a window to detect and abort an unauthorized withdrawal even if the operator key were compromised.
+
+### Request
+
+
+```python Python
+tx = client.escrow.withdraw_tx("request", amount=10.0)
+# Sign and send. After the timelock, the funds become claimable.
+```
+
+```typescript TypeScript
+const tx = await client.escrow.withdrawTx("request", 10);
+```
+
+
+### Inspect pending withdrawals
+
+
+```python Python
+state = client.escrow.withdrawals(include="pending,events")
+# state.pending is a list of pending requests with their `claimable_at` timestamps.
+for req in state["pending"]:
+ print(req["amount"], "claimable at", req["claimable_at"])
+```
+
+```typescript TypeScript
+const state = await client.escrow.withdrawals({ include: "pending,events" });
+// state.pending lists requests with claimable_at timestamps
+for (const req of state.pending) {
+ console.log(req.amount, "claimable at", req.claimable_at);
+}
+```
+
+
+### Claim
+
+Once `claimable_at` has passed, claim the funds — they move from escrow to your wallet.
+
+
+```python Python
+tx = client.escrow.withdraw_tx("claim")
+```
+
+```typescript TypeScript
+const tx = await client.escrow.withdrawTx("claim");
+```
+
+
+
+`claim` does not take an `amount`. It claims all matured requests at once. The escrow tracks individual request maturities; only matured ones settle.
+
+
+### Cancel
+
+If you change your mind during the timelock window, cancel a pending request and the funds remain in escrow as free balance.
+
+
+```python Python
+tx = client.escrow.withdraw_tx("cancel")
+```
+
+```typescript TypeScript
+const tx = await client.escrow.withdrawTx("cancel");
+```
+
+
+## Errors you might hit
+
+- `MissingWalletAddress` — `client.escrow.*` requires `wallet_address` on the exchange constructor. Pass it explicitly.
+- `ValidationError: amount precision exceeds 6 decimals` — round before passing.
+- `InsufficientEscrowBalance` (during a trade) — deposit more before retrying, or wait for matured withdrawals to clear pending positions.
+- `HostedTradingError` (5xx) — transient server error; retry with backoff. See [Hosted errors](/guides/hosted-errors).
+
+## Source references
+
+- Python: [`sdks/python/pmxt/escrow.py`](https://github.com/pmxt-dev/pmxt/blob/main/sdks/python/pmxt/escrow.py)
+- TypeScript: [`sdks/typescript/pmxt/escrow.ts`](https://github.com/pmxt-dev/pmxt/blob/main/sdks/typescript/pmxt/escrow.ts)
diff --git a/docs/guides/hosted-errors.mdx b/docs/guides/hosted-errors.mdx
new file mode 100644
index 00000000..414eb9b8
--- /dev/null
+++ b/docs/guides/hosted-errors.mdx
@@ -0,0 +1,269 @@
+---
+title: Handling Hosted Errors
+description: "Every common hosted-trading error, when it fires, and how to recover."
+---
+
+Hosted trading errors all descend from `HostedTradingError`. Each subclass also inherits from a **semantic parent** — `InsufficientEscrowBalance` is also an `InsufficientFunds`, `OrderSizeTooSmall` is also an `InvalidOrder`. Catch the parent and you handle both hosted and self-hosted paths with the same code. Catch the leaf and you can branch on the specific recovery action.
+
+In **Python**, this is true multi-inheritance — `isinstance(e, InsufficientFunds)` and `isinstance(e, HostedTradingError)` both work. In **TypeScript**, the same effect is achieved with a `static isHostedError = true` flag and the `isHostedError()` helper, since JS only allows single-extends.
+
+For the full class reference, see [API Reference / Errors](/api-reference/errors).
+
+This page covers the five errors you'll hit most often.
+
+## InsufficientEscrowBalance
+
+**When it fires:** the order would draw more USDC than your escrow free balance. PMXT debits escrow when an order is submitted; if the requested `amount` exceeds `balance.free`, the build phase rejects the order.
+
+**Detail string:** `Insufficient escrow balance: requested 50.0 USDC, available 12.34 USDC`.
+
+**Parent classes:** `InsufficientFunds`, `HostedTradingError`.
+
+**Recovery:** deposit more, or shrink the order. See [Escrow lifecycle](/guides/escrow-lifecycle).
+
+
+```python Python
+from pmxt.errors import InsufficientFunds
+from pmxt._hosted_errors import InsufficientEscrowBalance
+
+try:
+ client.create_order(...)
+except InsufficientEscrowBalance as e:
+ print(f"Need to deposit more. {e.detail}")
+ # Build a deposit tx for the shortfall
+ tx = client.escrow.deposit_tx(amount=20.0)
+ # ... sign and broadcast, then retry the order
+except InsufficientFunds:
+ # Self-hosted path also lands here
+ ...
+```
+
+```typescript TypeScript
+import { InsufficientEscrowBalance, isHostedError } from "pmxtjs";
+
+try {
+ await client.createOrder({ ... });
+} catch (e) {
+ if (e instanceof InsufficientEscrowBalance) {
+ console.log(`Need to deposit. ${(e as any).detail}`);
+ const tx = await client.escrow.depositTx(20);
+ // sign and broadcast, retry
+ } else if (isHostedError(e)) {
+ // any other hosted-error parent
+ } else {
+ throw e;
+ }
+}
+```
+
+
+## OrderSizeTooSmall
+
+**When it fires:** the resolved share count is below the venue's minimum. Polymarket's minimum is **5 shares per order**. A $2 buy at $0.78/share is only 2.5 shares — rejected.
+
+**Detail string:** `Order size 2.564 below the minimum 5 shares for venue polymarket`.
+
+**Parent classes:** `InvalidOrder`, `HostedTradingError`.
+
+**Recovery:** size up the order or pick a cheaper outcome.
+
+
+The 5-share minimum is enforced **after** PMXT resolves your USDC amount into shares using the current price. If the price moves between price-check and submit, a borderline-sized order may flip from accepted to rejected. Add a buffer for marginal sizes.
+
+
+
+```python Python
+from pmxt._hosted_errors import OrderSizeTooSmall
+
+try:
+ client.create_order(amount=2.0, ...)
+except OrderSizeTooSmall as e:
+ # Resize: at $0.78/share, 5 shares ≈ $3.90. Round up with buffer.
+ client.create_order(amount=5.0, ...)
+```
+
+```typescript TypeScript
+import { OrderSizeTooSmall } from "pmxtjs";
+
+try {
+ await client.createOrder({ amount: 2, ... });
+} catch (e) {
+ if (e instanceof OrderSizeTooSmall) {
+ await client.createOrder({ amount: 5, ... });
+ } else {
+ throw e;
+ }
+}
+```
+
+
+## InvalidApiKey
+
+**When it fires:** the `pmxt_api_key` is missing, malformed, revoked, or expired. Surface is `HTTP 401` from `trade.pmxt.dev`.
+
+**Detail string:** `invalid api key` or `missing api key`.
+
+**Parent classes:** `AuthenticationError`, `HostedTradingError`.
+
+**Recovery:** rotate the key from [pmxt.dev/dashboard](https://pmxt.dev/dashboard). Update your deployed config. **Do not retry** with the same key.
+
+
+```python Python
+from pmxt.errors import AuthenticationError
+from pmxt._hosted_errors import InvalidApiKey
+
+try:
+ client.fetch_balance()
+except InvalidApiKey:
+ # Rotate the key; don't retry with the same one
+ raise SystemExit("PMXT_API_KEY invalid — rotate from dashboard")
+```
+
+```typescript TypeScript
+import { InvalidApiKey } from "pmxtjs";
+
+try {
+ await client.fetchBalance();
+} catch (e) {
+ if (e instanceof InvalidApiKey) {
+ throw new Error("PMXT_API_KEY invalid — rotate from dashboard");
+ }
+ throw e;
+}
+```
+
+
+## BuiltOrderExpired
+
+**When it fires:** between `build_order` and `submit_order`, the built-order TTL elapsed (typically 30 seconds). Also fires for `cancel_id expired` in the cancel flow.
+
+**Detail string:** `built_order_id expired` or `cancel_id expired`.
+
+**Parent classes:** `InvalidOrder`, `HostedTradingError`.
+
+**Recovery:** re-build, then re-sign, then submit. Don't reuse the old `built_order_id`.
+
+
+Hardware-wallet signing is the most common cause — Ledger confirmations can take 10–60 seconds, blowing past the TTL. If you sign with a hardware wallet, expect to retry once on `BuiltOrderExpired`.
+
+
+
+```python Python
+from pmxt._hosted_errors import BuiltOrderExpired
+
+def submit_with_retry(client, *, market_id, outcome_id, **kwargs):
+ for attempt in range(2):
+ built = client.build_order(market_id=market_id, outcome_id=outcome_id, **kwargs)
+ sig = signer.sign_typed_data(built.typed_data)
+ try:
+ return client.submit_order(
+ built_order_id=built.built_order_id,
+ signature=sig,
+ )
+ except BuiltOrderExpired:
+ if attempt == 1:
+ raise
+ continue
+```
+
+```typescript TypeScript
+import { BuiltOrderExpired } from "pmxtjs";
+
+async function submitWithRetry(client, params) {
+ for (let attempt = 0; attempt < 2; attempt++) {
+ const built = await client.buildOrder(params);
+ const sig = await signer.signTypedData(
+ built.typedData.domain,
+ built.typedData.types,
+ built.typedData.message,
+ );
+ try {
+ return await client.submitOrder({
+ builtOrderId: built.builtOrderId,
+ signature: sig,
+ });
+ } catch (e) {
+ if (e instanceof BuiltOrderExpired && attempt === 0) continue;
+ throw e;
+ }
+ }
+}
+```
+
+
+## NoLiquidity
+
+**When it fires:** the side of the book you're crossing is empty — there are no resting asks for a market buy, or no resting bids for a market sell.
+
+**Detail string:** `book has no resting asks` or `book has no resting bids`.
+
+**Parent classes:** `InvalidOrder`, `HostedTradingError`.
+
+**Recovery:** wait for liquidity, post a limit order instead of a market order, or pick a different outcome.
+
+
+```python Python
+from pmxt._hosted_errors import NoLiquidity
+
+try:
+ client.create_order(order_type="market", ...)
+except NoLiquidity:
+ # Fall back to a limit order at a price you'd accept
+ client.create_order(order_type="limit", price=0.50, ...)
+```
+
+```typescript TypeScript
+import { NoLiquidity } from "pmxtjs";
+
+try {
+ await client.createOrder({ orderType: "market", ... });
+} catch (e) {
+ if (e instanceof NoLiquidity) {
+ await client.createOrder({ orderType: "limit", price: 0.5, ... });
+ } else {
+ throw e;
+ }
+}
+```
+
+
+## Workaround warnings
+
+
+**Use aggressive `slippage_pct` until the upstream economic validator tightens its `worst_price` checks.** Pragmatic defaults: `slippage_pct=30` for buys, `slippage_pct=99.9` for sells. Lower values frequently trip a precision check that has nothing to do with actual slippage. This will tighten once the validator ships its fix.
+
+
+
+**`fetch_markets` returns venue-native IDs**, but `create_order` needs catalog UUIDs. If you see `OutcomeNotFound` despite having a valid-looking ID, you are almost certainly passing a venue-native ID to a hosted endpoint. See [Catalog UUID vs venue ID](/concepts/catalog-uuid-vs-venue-id).
+
+
+## Catching everything hosted
+
+If you only want to know "did the hosted layer reject this?", catch `HostedTradingError` (Python) or use `isHostedError(e)` (TS):
+
+
+```python Python
+from pmxt._hosted_errors import HostedTradingError
+
+try:
+ client.create_order(...)
+except HostedTradingError as e:
+ log.error("hosted trade failed", status=e.status, detail=e.detail)
+```
+
+```typescript TypeScript
+import { isHostedError } from "pmxtjs";
+
+try {
+ await client.createOrder({ ... });
+} catch (e) {
+ if (isHostedError(e)) {
+ log.error("hosted trade failed", e);
+ } else {
+ throw e;
+ }
+}
+```
+
+
+For the full error class reference, parent classes, and status codes, see [API Reference / Errors](/api-reference/errors).
diff --git a/docs/guides/migrate-to-hosted-trading.mdx b/docs/guides/migrate-to-hosted-trading.mdx
new file mode 100644
index 00000000..48d52e99
--- /dev/null
+++ b/docs/guides/migrate-to-hosted-trading.mdx
@@ -0,0 +1,197 @@
+---
+title: Migrate to Hosted Trading
+description: "Move existing pmxt / pmxtjs code from venue-direct to hosted-mode trading."
+---
+
+If you've been using PMXT in **venue-direct** mode — passing raw venue credentials to a local `pmxt-core` server — moving to hosted mode is mostly additive. The SDK surface is unchanged; the constructor takes new arguments; a few read methods change their data source. This guide enumerates every observable difference so you can audit your code before flipping the switch.
+
+This is a port of `sdks/python/MIGRATION.md` and `sdks/typescript/MIGRATION.md` with extra context. The original notes are tagged with the SDK version that introduced each change (`2.18.0` and later).
+
+## TL;DR
+
+1. Get a `pmxt_api_key` from [pmxt.dev/dashboard](https://pmxt.dev/dashboard).
+2. Pass `pmxt_api_key`, `wallet_address`, `private_key` to the exchange constructor.
+3. Approve + deposit USDC into PreFundedEscrow once (`client.escrow.approve_tx`, `client.escrow.deposit_tx`).
+4. Audit `fetch_balance()` consumers — the source changed.
+5. Audit `Position` field consumers — four fields are now Optional.
+6. Catch the new `HostedTradingError` tree where you used to catch broad `Exception`.
+
+## 1. Constructor — new arguments
+
+In venue-direct mode you'd construct a Polymarket client with a raw private key only. In hosted mode you also pass `pmxt_api_key` and an explicit `wallet_address`.
+
+
+```python Python
+# Before (venue-direct)
+client = pmxt.Polymarket(private_key="0xYourPrivateKey...")
+
+# After (hosted)
+client = pmxt.Polymarket(
+ pmxt_api_key="pmxt_live_...",
+ wallet_address="0xYourWallet...",
+ private_key="0xYourPrivateKey...",
+)
+```
+
+```typescript TypeScript
+// Before (venue-direct)
+const client = new Polymarket({ privateKey: "0xYourPrivateKey..." });
+
+// After (hosted)
+const client = new Polymarket({
+ pmxtApiKey: "pmxt_live_...",
+ walletAddress: "0xYourWallet...",
+ privateKey: "0xYourPrivateKey...",
+});
+```
+
+
+The `wallet_address` argument is new in hosted mode and is **required** for any `client.escrow.*` call — the SDK uses it to scope all escrow operations to a single user. If you've been deriving the address from the private key implicitly, make it explicit.
+
+## 2. Trust model — `pmxt_api_key` is a service-role credential
+
+`pmxt_api_key` is not app-scoped per-user auth. The key holder can:
+
+- Read any user's escrow data (balance, positions, trades) by passing a different `wallet_address`.
+- Forward signed orders for any wallet (the signature still gates the write).
+
+Writes are still gated by the user's EIP-712 signature against their own wallet — the key alone cannot move funds. Reads (`fetch_balance`, `fetch_positions`, etc.) are NOT gated by signature.
+
+**Treat `pmxt_api_key` as a backend secret.** Keep it on a server. Never ship it to a browser bundle. Never log it.
+
+If your previous architecture had end-user private keys on the server, hosted mode lets you flip that — keep `pmxt_api_key` on the server and push private keys to the client, where they're signed locally and never leave the device.
+
+## 3. `fetch_balance` — semantic change
+
+The shape of the `Balance` object is unchanged, but the source is different.
+
+| Mode | Source |
+| ---- | ------ |
+| Venue-direct (`pmxt_api_key=None`) | Wallet's CLOB-proxy USDC balance at the venue |
+| Hosted (`pmxt_api_key` set) | Wallet's USDC balance in PMXT's PreFundedEscrow on Polygon |
+
+If your code relied on venue-direct semantics — for example, comparing the balance against the raw on-chain USDC.e balance — audit it. The hosted balance reflects **available trading capital in escrow**, not the wallet's total USDC.
+
+
+```python Python
+balance = client.fetch_balance()
+# Hosted: `free` is the deposit-side escrow balance available for trading
+# Venue-direct: `free` is the wallet's CLOB-proxy USDC balance
+```
+
+```typescript TypeScript
+const balance = await client.fetchBalance();
+// Hosted: `free` is the escrow balance
+// Venue-direct: `free` is the CLOB-proxy USDC balance
+```
+
+
+## 4. `Position` fields now Optional
+
+Four fields on `Position` became Optional in 2.18.0. Venue-direct mode still populates them; hosted mode returns `None` / `undefined` when the server doesn't have the data yet.
+
+
+```python Python
+# Python: these are now Optional[...]
+position.outcome_label # Optional[str]
+position.entry_price # Optional[float]
+position.current_price # Optional[float]
+position.unrealized_pnl # Optional[float]
+
+# If your code does math on these, guard the None
+if position.current_price is not None:
+ pnl = position.current_price * position.quantity
+```
+
+```typescript TypeScript
+// TypeScript: these are now `| undefined`
+position.outcomeLabel // string | undefined
+position.entryPrice // number | undefined
+position.currentPrice // number | undefined
+position.unrealizedPnL // number | undefined
+
+if (position.currentPrice !== undefined) {
+ const pnl = position.currentPrice * position.quantity;
+}
+```
+
+
+`market_id`, `outcome_id`, `quantity`, `side`, `notional` are still required and always populated.
+
+## 5. New error tree
+
+Hosted mode introduces `HostedTradingError` and a suite of semantic-parent subclasses. The parents are the same classes used in venue-direct mode (`InsufficientFunds`, `InvalidOrder`, `AuthenticationError`, etc.), so existing `except`/`catch` clauses keep working.
+
+```python
+# Before: catch the venue-direct error
+try:
+ client.create_order(...)
+except InsufficientFunds:
+ ...
+
+# After: same catch still works for hosted (InsufficientEscrowBalance extends InsufficientFunds)
+try:
+ client.create_order(...)
+except InsufficientFunds:
+ ...
+```
+
+If you want hosted-specific handling, catch the leaf:
+
+```python
+from pmxt._hosted_errors import InsufficientEscrowBalance
+try:
+ client.create_order(...)
+except InsufficientEscrowBalance as e:
+ # Specifically the hosted-escrow shortfall
+ ...
+```
+
+See [Hosted errors](/guides/hosted-errors) for the full cookbook.
+
+## 6. Escrow setup — new one-time step
+
+Before your first hosted trade, USDC has to be in the PreFundedEscrow contract.
+
+
+```python Python
+# Once per wallet, once per token
+tx = client.escrow.approve_tx("usdc")
+# sign and broadcast via your wallet library
+
+# Once per top-up
+tx = client.escrow.deposit_tx(amount=100.0)
+# sign and broadcast
+```
+
+```typescript TypeScript
+const approve = await client.escrow.approveTx("usdc");
+// sign + broadcast
+
+const deposit = await client.escrow.depositTx(100);
+// sign + broadcast
+```
+
+
+There is no equivalent in venue-direct mode — direct venue trading uses the wallet's native balance. See [Escrow lifecycle](/guides/escrow-lifecycle) for details.
+
+## 7. Catalog UUIDs replace venue-native IDs in trading calls
+
+Venue-direct trading lets you pass venue-native IDs (Polymarket condition IDs, token IDs). Hosted trading requires **catalog UUIDs**. The simplest path: discover markets via the [Router](/router/overview) instead of via `client.fetch_markets`, since the Router returns UUIDs natively.
+
+See [Catalog UUID vs venue ID](/concepts/catalog-uuid-vs-venue-id) for the reverse-resolution workaround if you already have venue-native IDs in your database.
+
+## 8. Network surface
+
+Venue-direct mode talks to a local `pmxt-core` server, which talks to venue APIs directly. Hosted mode talks to `trade.pmxt.dev` and `api.pmxt.dev`. Make sure both are reachable from your runtime if you flip — corporate firewalls that allow only `api.pmxt.dev` will need `trade.pmxt.dev` added.
+
+## Rollback plan
+
+If you migrate and need to roll back, drop the `pmxt_api_key` argument. The SDK falls back to venue-direct mode and routes to the local pmxt-core server. No other changes needed. You'll lose the escrow funds until you withdraw them via `client.escrow.withdraw_tx`, but the venue-native trading flow is unchanged.
+
+## See also
+
+- [Trading quickstart](/trading-quickstart) — 60-second guided path.
+- [Escrow lifecycle](/guides/escrow-lifecycle) — deposit, withdraw, claim.
+- [Hosted errors](/guides/hosted-errors) — error class reference and recovery.
+- [Self-hosted](/guides/self-hosted) — when not to migrate.
diff --git a/docs/guides/self-hosted.mdx b/docs/guides/self-hosted.mdx
new file mode 100644
index 00000000..492a72fe
--- /dev/null
+++ b/docs/guides/self-hosted.mdx
@@ -0,0 +1,168 @@
+---
+title: Self-hosted (Advanced)
+description: "Run pmxt-core on your own machine when hosted mode isn't the right fit."
+---
+
+Self-hosted PMXT runs the open-source `pmxt-core` server on your machine. The SDK detects the absence of a `pmxt_api_key` and routes every request through `http://localhost:3847` instead of `trade.pmxt.dev`. Venue credentials stay on your machine; you talk to venue APIs directly.
+
+This is the **advanced escape hatch**. Most users should use [hosted mode](/concepts/hosted-trading). Read on if you have specific reasons to run local.
+
+## When to choose self-hosted
+
+- **Sub-100ms latency.** Hosted trading adds a network hop. If you're running latency-sensitive arbitrage or market-making, the local server colocates the venue submission step.
+- **Raw venue credentials.** You want to use Polymarket L2 API keys, Kalshi RSA keys, Smarkets session cookies — credentials that hosted mode doesn't accept.
+- **Regulatory custody requirements.** You need to be the direct counterparty to the venue with no third-party intermediary in the custody path.
+- **Venues hosted mode doesn't trade yet.** Hosted writes are Polymarket + Opinion. Self-hosted writes work on every venue PMXT supports (Kalshi, Limitless, Smarkets, Probable, Myriad, Metaculus, etc.).
+- **OSS contribution.** You're developing or debugging `pmxt-core` itself.
+
+If none of these apply, [hosted](/concepts/hosted-trading) is simpler.
+
+## 1. Install pmxt-core
+
+The SDK installs and supervises `pmxt-core` for you. You don't run a separate binary; the SDK spawns it as a child process on first use.
+
+
+```bash Python
+pip install pmxt
+```
+
+```bash TypeScript
+npm install pmxtjs
+```
+
+
+That's it — `pmxt-core` is bundled.
+
+## 2. Construct without an API key
+
+Drop the `pmxt_api_key` argument. The SDK detects the absence and starts the local server.
+
+
+```python Python
+import pmxt
+
+# No pmxt_api_key → SDK spawns and supervises pmxt-core locally
+poly = pmxt.Polymarket()
+
+markets = poly.fetch_markets(query="election", limit=3)
+```
+
+```typescript TypeScript
+import { Polymarket } from "pmxtjs";
+
+// No pmxtApiKey → SDK spawns and supervises pmxt-core locally
+const poly = new Polymarket({});
+
+const markets = await poly.fetchMarkets({ query: "election", limit: 3 });
+```
+
+
+The first call blocks briefly while the local server warms up; subsequent calls reuse the process.
+
+## 3. Manage the local server
+
+For lifecycle control (start, stop, restart, status, logs), use the `server` namespace. See [Server Management](/sdk/server) for the full reference.
+
+```python
+pmxt.server.start() # idempotent
+pmxt.server.health() # bool
+pmxt.server.status() # structured snapshot
+pmxt.server.stop()
+pmxt.server.restart()
+pmxt.server.logs(20)
+```
+
+Most users never call these — creating an exchange instance auto-starts the server.
+
+## 4. Per-venue raw credentials
+
+Self-hosted unlocks the full venue-credential surface. Pass credentials directly to the exchange constructor.
+
+### Polymarket — EVM private key
+
+```python
+import pmxt
+
+poly = pmxt.Polymarket(private_key="0xYourPrivateKey...")
+
+order = poly.create_order(
+ outcome=market.yes,
+ side="buy",
+ type="limit",
+ price=0.42,
+ amount=100,
+)
+```
+
+### Kalshi — RSA key pair
+
+```python
+import pmxt
+
+kalshi = pmxt.Kalshi(
+ api_key_id="...",
+ private_key_pem="-----BEGIN RSA PRIVATE KEY-----\n...",
+)
+```
+
+### Limitless / Probable / Opinion (self-hosted) — EVM private key
+
+```python
+limitless = pmxt.Limitless(private_key="0x...")
+probable = pmxt.Probable(private_key="0x...")
+opinion = pmxt.Opinion(private_key="0x...")
+```
+
+### Smarkets — email + password
+
+```python
+smarkets = pmxt.Smarkets(email="you@example.com", password="...")
+```
+
+### Baozi — Solana keypair
+
+```python
+baozi = pmxt.Baozi(private_key="")
+```
+
+See [Supported Venues](/concepts/venues) for the full credential matrix.
+
+
+**Use a dedicated trading wallet** for any venue that requires a private key with full fund control (Polymarket, Limitless, Probable, Opinion, Baozi). Self-hosted keeps the key on your machine — but a compromised host still compromises the wallet. Limit balances accordingly.
+
+
+## 5. Environment overrides
+
+| Variable | Effect |
+| -------- | ------ |
+| `PMXT_LOCAL_PORT=3847` | Override the local server port (default `3847`). |
+| `PMXT_ALWAYS_RESTART=1` | Force-restart the local server on every `ensure_server_running` call. Useful during development. |
+| `PMXT_BASE_URL` | Point the SDK at a custom server URL (advanced — defaults to `http://localhost:3847`). |
+
+See [API Reference / Configuration](/api-reference/configuration) for the full env-var matrix.
+
+## 6. Running multiple exchanges
+
+A single pmxt-core process can serve every venue. Construct multiple exchange clients against the same server — they all share the local process.
+
+```python
+poly = pmxt.Polymarket(private_key="0x...")
+kalshi = pmxt.Kalshi(api_key_id="...", private_key_pem="...")
+# Both clients reuse the same pmxt-core process at http://localhost:3847
+```
+
+## 7. Production deployment
+
+For production self-hosted, the SDK + bundled server is sufficient — there's no separate daemon to manage. If you want to run `pmxt-core` as a long-lived process with systemd or similar, see the [pmxt-core README](https://github.com/pmxt-dev/pmxt) for standalone server commands.
+
+## What you give up vs hosted
+
+- **Cross-venue search via Router** — still works, but requires the hosted `pmxt_api_key` for the catalog. If you want Router without trading-hosted, construct a separate `Router` client with the API key alongside your local trading clients. See [Hosted vs self-hosted / mixing modes](/concepts/hosted-vs-self-hosted#mixing-modes).
+- **Hosted error tree** — you'll get the parent classes (`InvalidOrder`, `InsufficientFunds`) but not the hosted leaves. Catching parents keeps your code portable.
+- **PreFundedEscrow custody** — irrelevant in self-hosted; you custody at the venue.
+
+## Next
+
+- [Server Management](/sdk/server) — start/stop/status/logs.
+- [Hosted vs self-hosted](/concepts/hosted-vs-self-hosted) — full comparison.
+- [Supported Venues](/concepts/venues) — what each venue accepts.
diff --git a/docs/guides/signing.mdx b/docs/guides/signing.mdx
new file mode 100644
index 00000000..557a0ad7
--- /dev/null
+++ b/docs/guides/signing.mdx
@@ -0,0 +1,242 @@
+---
+title: Signing Orders
+description: "EIP-712 typed-data signing for hosted trading — what the SDK does, what the protocol looks like, and how to bring your own signer."
+---
+
+Hosted trading writes are authenticated by an **EIP-712 typed-data signature** produced by the user's wallet. The signature is computed locally on your machine; PMXT never sees your private key. The `pmxt_api_key` authenticates you to PMXT; the signature authenticates the order to the venue's on-chain settlement contract.
+
+This page covers (1) what the SDK does automatically, (2) the EIP-712 payload shape for each venue, and (3) how to bring your own signer (hardware wallet, remote signer, MPC, etc.).
+
+## What the SDK does for you
+
+If you pass `private_key` to the exchange constructor, the SDK auto-wraps it into an internal signer and signs every order for you. You never see the typed-data shape.
+
+
+```python Python
+import pmxt
+
+# Python: private_key is wrapped into an EthAccountSigner internally
+client = pmxt.Polymarket(
+ pmxt_api_key="pmxt_live_...",
+ wallet_address="0xYourWallet...",
+ private_key="0xYourPrivateKey...",
+)
+
+# create_order signs locally with EthAccountSigner before submitting
+order = client.create_order(
+ market_id="2eeb03dc-404b-41d5-bc57-6aeb37927ae6",
+ outcome_id="a114f052-1fd1-4bcd-b9cf-de019db81b67",
+ side="buy",
+ order_type="market",
+ amount=5.0,
+ denom="usdc",
+ slippage_pct=30.0,
+)
+```
+
+```typescript TypeScript
+import { Polymarket } from "pmxtjs";
+
+// TypeScript: privateKey is wrapped into an EthersSigner internally
+const client = new Polymarket({
+ pmxtApiKey: "pmxt_live_...",
+ walletAddress: "0xYourWallet...",
+ privateKey: "0xYourPrivateKey...",
+});
+
+// createOrder signs with EthersSigner before submitting
+const order = await client.createOrder({
+ marketId: "2eeb03dc-404b-41d5-bc57-6aeb37927ae6",
+ outcomeId: "a114f052-1fd1-4bcd-b9cf-de019db81b67",
+ side: "buy",
+ orderType: "market",
+ amount: 5,
+ denom: "usdc",
+ slippagePct: 30,
+});
+```
+
+
+In Python, the wrapper is `EthAccountSigner` (uses `eth_account` under the hood). In TypeScript, it's `EthersSigner` (uses ethers v6 `Wallet._signTypedData`). Both implement a tiny protocol: a single async `signTypedData(domain, types, message) -> hex` method.
+
+## Why reads only need a wallet address
+
+For reads (`fetch_balance`, `fetch_positions`, `fetch_my_trades`), no signature is required — the `pmxt_api_key` is enough. You can construct a hosted client with only `pmxt_api_key` and `wallet_address` and read all hosted state for that wallet:
+
+```python
+read_only = pmxt.Polymarket(
+ pmxt_api_key="pmxt_live_...",
+ wallet_address="0xSomeOtherWallet...",
+ # no private_key
+)
+balance = read_only.fetch_balance() # works
+```
+
+
+Because reads don't require a signature, anyone with the `pmxt_api_key` can read any wallet's hosted state. Keep the key on the server. See the [trust model](/concepts/hosted-trading#trust-model).
+
+
+## The EIP-712 payload — Polymarket
+
+Polymarket orders use the `Order` primary type on the Polygon CTF exchange domain.
+
+**Domain**
+
+```json
+{
+ "name": "Polymarket CTF Exchange",
+ "version": "1",
+ "chainId": 137,
+ "verifyingContract": "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E"
+}
+```
+
+**Types**
+
+```json
+{
+ "Order": [
+ { "name": "salt", "type": "uint256" },
+ { "name": "maker", "type": "address" },
+ { "name": "signer", "type": "address" },
+ { "name": "taker", "type": "address" },
+ { "name": "tokenId", "type": "uint256" },
+ { "name": "makerAmount", "type": "uint256" },
+ { "name": "takerAmount", "type": "uint256" },
+ { "name": "expiration", "type": "uint256" },
+ { "name": "nonce", "type": "uint256" },
+ { "name": "feeRateBps", "type": "uint256" },
+ { "name": "side", "type": "uint8" },
+ { "name": "signatureType", "type": "uint8" }
+ ]
+}
+```
+
+The `Order.maker` is the user's wallet. `signer` is also the user's wallet (no operator-on-behalf-of for hosted PMXT — the operator role is played by the escrow contract). `taker` is `0x000...` (open order).
+
+The full `message` is what `build_order` returns; you sign it and pass the result back to `submit_order`.
+
+## The EIP-712 payload — Opinion
+
+Opinion uses a **dual-signature cross-chain** flow. Two typed-data payloads must be signed:
+
+1. **`typed_data`** — the Opinion order itself, signed against the Opinion settlement contract on BSC.
+2. **`pull_typed_data`** — a USDC pull authorization, signed against the BSC USDC contract for the cross-chain settlement leg.
+
+Both are returned by `build_order` and both must be signed before `submit_order` is called. The SDK signs both with the same private key by default.
+
+```python
+built = client.build_order(...)
+# built has two typed-data payloads
+sig_order = signer.sign_typed_data(built.typed_data)
+sig_pull = signer.sign_typed_data(built.pull_typed_data)
+client.submit_order(
+ built_order_id=built.built_order_id,
+ signature=sig_order,
+ pull_signature=sig_pull,
+)
+```
+
+## Bringing your own signer
+
+If your key lives in a hardware wallet, an HSM, an MPC service, or anything else that isn't a raw hex private key string, skip `private_key` and use the lower-level `build_order` / `submit_order` flow.
+
+
+```python Python
+import pmxt
+
+client = pmxt.Polymarket(
+ pmxt_api_key="pmxt_live_...",
+ wallet_address="0xYourWallet...",
+ # NO private_key — we'll sign with a custom signer
+)
+
+# 1. Build the order. Returns the typed-data payload plus a built_order_id.
+built = client.build_order(
+ market_id="2eeb03dc-...",
+ outcome_id="a114f052-...",
+ side="buy",
+ order_type="market",
+ amount=5.0,
+ denom="usdc",
+ slippage_pct=30.0,
+)
+
+# 2. Sign with your own signer (Ledger, HSM, MPC, remote, etc.)
+signature = my_custom_signer.sign_typed_data(
+ domain=built.typed_data["domain"],
+ types=built.typed_data["types"],
+ message=built.typed_data["message"],
+)
+
+# 3. Submit
+order = client.submit_order(
+ built_order_id=built.built_order_id,
+ signature=signature,
+)
+```
+
+```typescript TypeScript
+import { Polymarket } from "pmxtjs";
+
+const client = new Polymarket({
+ pmxtApiKey: "pmxt_live_...",
+ walletAddress: "0xYourWallet...",
+ // NO privateKey
+});
+
+const built = await client.buildOrder({
+ marketId: "2eeb03dc-...",
+ outcomeId: "a114f052-...",
+ side: "buy",
+ orderType: "market",
+ amount: 5,
+ denom: "usdc",
+ slippagePct: 30,
+});
+
+// Sign with your own signer
+const signature = await myCustomSigner.signTypedData(
+ built.typedData.domain,
+ built.typedData.types,
+ built.typedData.message,
+);
+
+const order = await client.submitOrder({
+ builtOrderId: built.builtOrderId,
+ signature,
+});
+```
+
+
+## The signer protocol
+
+Any signer that satisfies this interface works:
+
+```python
+class Signer(Protocol):
+ def sign_typed_data(self, domain: dict, types: dict, message: dict) -> str:
+ """Return a 0x-prefixed hex signature."""
+```
+
+```typescript
+interface Signer {
+ signTypedData(
+ domain: Eip712Domain,
+ types: Record,
+ message: Record,
+ ): Promise;
+}
+```
+
+The SDK's `EthAccountSigner` / `EthersSigner` are reference implementations of this protocol. For hardware wallets, ethers v6 supports Ledger out of the box. For MPC, see Fireblocks / Privy / Turnkey docs.
+
+## Common pitfalls
+
+
+**Don't sign the wrong domain.** Each venue (Polymarket / Opinion) has its own EIP-712 domain with a specific `chainId` and `verifyingContract`. The server constructs these in `build_order`. Signing against the wrong domain produces `InvalidSignature`.
+
+
+
+**Built orders expire.** A `built_order_id` is single-use and time-bound (typically 30 seconds). If you sign slowly — e.g. waiting on a hardware wallet confirmation — you may hit `BuiltOrderExpired` on submit. Re-build to get a fresh payload.
+
diff --git a/docs/introduction.mdx b/docs/introduction.mdx
index 90cea464..52578b6c 100644
--- a/docs/introduction.mdx
+++ b/docs/introduction.mdx
@@ -10,22 +10,21 @@ talking to.
It runs two ways:
-- **Locally** — the open-source [pmxt](https://github.com/pmxt-dev/pmxt)
- local server runs on your machine. No API key, no external dependency. Your
- requests go directly to the venues.
-- **Hosted** — `api.pmxt.dev` adds a shared catalog, cross-venue search,
- and you skip running infrastructure. Set an API key and the SDK
- switches automatically.
+- **Hosted (default)** — `api.pmxt.dev` and `trade.pmxt.dev` give you a
+ shared catalog, cross-venue search, and end-to-end [hosted
+ trading](/concepts/hosted-trading) with PreFundedEscrow custody. Set
+ an API key and the SDK is fully operational.
+- **Self-hosted (advanced)** — for users who run
+ [pmxt-core](https://github.com/pmxt-dev/pmxt) on their own machine.
+ No API key, no external dependency. Your requests go directly to the
+ venues. See [self-hosted](/guides/self-hosted).
The code is identical either way:
```python
import pmxt
-# Local: talks directly to Polymarket
-poly = pmxt.Polymarket()
-
-# Hosted: talks to api.pmxt.dev (add an API key and that's it)
+# Hosted (default): talks to api.pmxt.dev / trade.pmxt.dev
poly = pmxt.Polymarket(pmxt_api_key="pmxt_live_...")
markets = poly.fetch_markets(query="fed rate cut", limit=5)
@@ -33,6 +32,15 @@ for m in markets:
print(m.title, m.outcomes[0].price)
```
+If you'd rather run pmxt-core yourself, drop the API key:
+
+```python
+# Self-hosted: SDK spawns pmxt-core on localhost
+poly = pmxt.Polymarket()
+```
+
+See [self-hosted](/guides/self-hosted) for when that's the right choice.
+
Swap the venue class — `pmxt.Kalshi(...)`, `pmxt.Limitless(...)` — and
the methods and response shapes stay the same.
diff --git a/docs/sdk/server.mdx b/docs/sdk/server.mdx
index eca9490a..00b96cba 100644
--- a/docs/sdk/server.mdx
+++ b/docs/sdk/server.mdx
@@ -3,6 +3,15 @@ title: Server Management
description: "Start, stop, restart, and inspect the PMXT local server from your SDK code."
---
+
+This page covers the **self-hosted** local server. If you're using a
+`pmxt_api_key`, you can ignore this page — the SDK talks to
+`api.pmxt.dev` / `trade.pmxt.dev` directly and never spawns a local
+process. See [Hosted trading](/concepts/hosted-trading) for the default
+path, or [Self-hosted](/guides/self-hosted) for when running local is
+the right choice.
+
+
The PMXT SDKs manage a local server automatically. When you
create an exchange instance without a hosted API key, the SDK spawns a
local server process, waits for it to become healthy, and routes every
diff --git a/docs/security.mdx b/docs/security.mdx
index e1973ab8..f6c02529 100644
--- a/docs/security.mdx
+++ b/docs/security.mdx
@@ -51,20 +51,20 @@ wallet, fund it with only what you need, and use that wallet's private key
with PMXT.
-
-**For maximum security, run pmxt locally.** The hosted API exists for
-convenience. If you are not comfortable sending your private key to a
-third-party server — even over HTTPS — you can run pmxt-core locally and
-retain full custody of your credentials. They will never leave your machine.
+
+**Self-host pmxt-core if you want to keep venue credentials entirely on
+your own machine.** Most users should use hosted with PMXT's
+PreFundedEscrow custody — see [Hosted trading](/concepts/hosted-trading).
+Self-hosting is the right fit when you need sub-100ms latency, want raw
+venue credentials, or have regulatory custody requirements. See
+[Self-hosted](/guides/self-hosted) for setup.
```bash
-npm install pmxt-core
+npm install pmxtjs
# or
pip install pmxt
```
-
-See the [quickstart](/quickstart) for local setup.
-
+
### Best practices
diff --git a/docs/trading-quickstart.mdx b/docs/trading-quickstart.mdx
new file mode 100644
index 00000000..38d09d30
--- /dev/null
+++ b/docs/trading-quickstart.mdx
@@ -0,0 +1,177 @@
+---
+title: Trading Quickstart
+description: "Place your first hosted trade in 60 seconds — API key, escrow deposit, market order."
+---
+
+This page walks you from a fresh `pmxt_api_key` to a confirmed position on Polymarket in under a minute. Hosted mode is the default: PMXT custodies USDC in [PreFundedEscrow](/guides/escrow-lifecycle) and your wallet only ever signs EIP-712 typed-data payloads — no private key leaves your machine when you trade.
+
+
+Hosted trading currently supports **Polymarket** (Polygon) and **Opinion** (cross-chain). Other venues are read-only via the hosted catalog. To trade on those, run the [self-hosted server](/guides/self-hosted) with raw venue credentials.
+
+
+## 1. Get an API key
+
+Go to [pmxt.dev/dashboard](https://pmxt.dev/dashboard), create a key, and copy it. It looks like `pmxt_live_...` and works immediately.
+
+```bash
+export PMXT_API_KEY="pmxt_live_..."
+```
+
+## 2. Install the SDK
+
+
+```bash Python
+pip install pmxt
+```
+
+```bash TypeScript
+npm install pmxtjs
+```
+
+
+## 3. Construct a hosted client
+
+A hosted trading client takes three things: your PMXT API key, the wallet address you'll trade from, and the private key for that wallet (used only to sign EIP-712 messages locally — it is never sent to PMXT). The SDK auto-wraps `private_key` into an `EthAccountSigner` / `EthersSigner` for you.
+
+
+```python Python
+import pmxt
+
+client = pmxt.Polymarket(
+ pmxt_api_key="pmxt_live_...",
+ wallet_address="0xYourWallet...",
+ private_key="0xYourPrivateKey...",
+)
+```
+
+```typescript TypeScript
+import { Polymarket } from "pmxtjs";
+
+const client = new Polymarket({
+ pmxtApiKey: "pmxt_live_...",
+ walletAddress: "0xYourWallet...",
+ privateKey: "0xYourPrivateKey...",
+});
+```
+
+```bash curl
+# Trading via curl is possible but tedious — you have to build the
+# EIP-712 payload, sign it locally, and POST it yourself. The SDK
+# does all of this for you. See /guides/signing for the protocol.
+echo "Use an SDK for the quickstart."
+```
+
+
+
+Use a **dedicated trading wallet**. The private key only stays on your machine for local signing, but treat the wallet as compromised if the host is ever compromised. Read [Security](/security) before funding production wallets.
+
+
+## 4. Approve and deposit USDC into escrow
+
+Hosted trades execute against your balance in PMXT's `PreFundedEscrow` contract. The first time you trade, you need to (a) approve the escrow to pull USDC from your wallet, and (b) deposit. Both methods on `client.escrow` build **unsigned** transactions — you submit them with your wallet (web3, ethers, viem, MetaMask, etc.).
+
+
+```python Python
+# 1. Build an unsigned ERC-20 approval tx for USDC
+approve_tx = client.escrow.approve_tx("usdc")
+# Sign and send approve_tx with your wallet library...
+
+# 2. Build an unsigned deposit tx for 10 USDC
+deposit_tx = client.escrow.deposit_tx(amount=10.0)
+# Sign and send deposit_tx with your wallet library...
+
+# 3. Confirm the deposit landed
+balance = client.fetch_balance()
+print(f"Escrow USDC: {balance.free}")
+```
+
+```typescript TypeScript
+// 1. Build an unsigned ERC-20 approval tx for USDC
+const approveTx = await client.escrow.approveTx("usdc");
+// Sign and send approveTx with your wallet library...
+
+// 2. Build an unsigned deposit tx for 10 USDC
+const depositTx = await client.escrow.depositTx(10);
+// Sign and send depositTx with your wallet library...
+
+// 3. Confirm the deposit landed
+const balance = await client.fetchBalance();
+console.log(`Escrow USDC: ${balance.free}`);
+```
+
+
+
+Approval and deposit are one-time setup. Subsequent trades just spend from escrow until you withdraw. See [Escrow Lifecycle](/guides/escrow-lifecycle) for the full deposit / withdraw flow.
+
+
+## 5. Place your first market order
+
+Let's buy 5 USDC of the YES outcome on **"Will the Knicks win the 2026 NBA Championship?"**. These UUIDs come straight from the live hosted catalog.
+
+
+```python Python
+order = client.create_order(
+ market_id="2eeb03dc-404b-41d5-bc57-6aeb37927ae6",
+ outcome_id="a114f052-1fd1-4bcd-b9cf-de019db81b67",
+ side="buy",
+ order_type="market",
+ amount=5.0,
+ denom="usdc",
+ slippage_pct=30.0,
+)
+print(f"Order {order.id}: {order.status}")
+```
+
+```typescript TypeScript
+const order = await client.createOrder({
+ marketId: "2eeb03dc-404b-41d5-bc57-6aeb37927ae6",
+ outcomeId: "a114f052-1fd1-4bcd-b9cf-de019db81b67",
+ side: "buy",
+ orderType: "market",
+ amount: 5,
+ denom: "usdc",
+ slippagePct: 30,
+});
+console.log(`Order ${order.id}: ${order.status}`);
+```
+
+```bash curl
+# create_order is a convenience wrapper. The hosted API exposes a
+# build → sign → submit flow. See /guides/signing for the raw protocol.
+echo "Use an SDK for create_order."
+```
+
+
+
+**Polymarket has a 5-share minimum per order.** At ~$0.78/share, a 5 USDC buy is ~6.4 shares — fine. But $2 at the same price is only 2.5 shares and will be rejected with `OrderSizeTooSmall`. Size up or switch to a cheaper outcome.
+
+
+
+**Use aggressive `slippage_pct`** until the upstream economic validator tightens its `worst_price` checks. Pragmatic defaults: `slippage_pct=30` for buys, `slippage_pct=99.9` for sells. Lower values frequently trip a precision check that has nothing to do with actual slippage.
+
+
+## 6. Verify the fill
+
+Hosted positions appear immediately on `fetch_positions`. The position's `quantity` and `notional` reflect the escrow-side accounting.
+
+
+```python Python
+positions = client.fetch_positions()
+for p in positions:
+ print(f"{p.market_id} qty={p.quantity} notional={p.notional}")
+```
+
+```typescript TypeScript
+const positions = await client.fetchPositions();
+for (const p of positions) {
+ console.log(`${p.marketId} qty=${p.quantity} notional=${p.notional}`);
+}
+```
+
+
+That's it — you placed a real trade through hosted PMXT. From here:
+
+- [Escrow lifecycle](/guides/escrow-lifecycle) — deposits, withdrawals, the request/claim timelock.
+- [Hosted errors](/guides/hosted-errors) — what each error means and how to recover.
+- [Signing](/guides/signing) — the EIP-712 protocol underneath `create_order`.
+- [Self-hosted](/guides/self-hosted) — when to skip hosted and run pmxt-core yourself.
diff --git a/readme.md b/readme.md
index cefe0617..abffa973 100644
--- a/readme.md
+++ b/readme.md
@@ -1,7 +1,7 @@
# pmxt [](https://twitter.com/intent/tweet?text=The%20ccxt%20for%20prediction%20markets.&url=https://github.com/pmxt-dev/pmxt&hashtags=predictionmarkets,trading) [](https://doi.org/10.5281/zenodo.19111315)
-**The [ccxt](https://github.com/ccxt/ccxt) for prediction markets.** A unified, language-agnostic API for accessing prediction market data across multiple exchanges — works from Python, TypeScript, or any HTTP client.
+**The [ccxt](https://github.com/ccxt/ccxt) for prediction markets.** Hosted unified API for prediction markets — trade Polymarket, Kalshi, Opinion, and more from one API key. Open-source SDK and self-host option included.
@@ -79,8 +79,9 @@
Different prediction market platforms have different APIs, data formats, and conventions. pmxt provides a single, consistent interface to work with all of them.
+- **Hosted API.** Get a key at [pmxt.dev/dashboard](https://pmxt.dev/dashboard), construct a client, trade. PMXT handles custody, signing infrastructure, and on-chain settlement.
+- **Open source (MIT).** Self-host the local server for full control — your keys, your machine, no PMXT in the loop. See [Self-hosted](#self-hosted).
- **Language-agnostic.** Python and TypeScript SDKs today, with HTTP access for any other language. No lock-in to a single ecosystem.
-- **Open source (MIT).** No API key required, no vendor dependency, no rate-limit business model. Self-host and run locally, or hit our hosted api.
- **Drop-in Dome API replacement.** Automatic codemod (`dome-to-pmxt`) for teams migrating after the Polymarket acquisition.
- **Unified trading, not just data.** Place orders across Polymarket, Kalshi, and Limitless with a single interface.
- **[MCP-native](https://pmxt.dev/mcp).** Use pmxt directly from Claude, Cursor, and other AI agents.
@@ -135,52 +136,119 @@ npx dome-to-pmxt ./src
## Quickstart
-Prediction markets are structured in a hierarchy to group related information.
-
-* **Event**: The broad topic (e.g., *"Who will Trump nominate as Fed Chair?"*)
-* **Market**: A specific tradeable question (e.g., *"Will Trump nominate Kevin Warsh as the next Fed Chair?"*)
-* **Outcome**: The actual share you buy (e.g., *"Yes"* or *"No"*)
+Get your API key at [pmxt.dev/dashboard](https://pmxt.dev/dashboard). For reads, only `pmxt_api_key` and `wallet_address` are required. For trading, also pass `private_key` — the SDK auto-wraps it into an EIP-712 signer.
### Python
```python
import pmxt
-api = pmxt.Exchange()
-
-# 1. Search for the broad Event
-events = api.fetch_events(query='Who will Trump nominate as Fed Chair?')
-fed_event = events[0]
+# Reads — pmxt_api_key + wallet_address only
+client = pmxt.Polymarket(
+ pmxt_api_key="pmxt_live_...",
+ wallet_address="0xYourWalletAddress",
+)
-# 2. Find the specific Market within that event
-warsh = fed_event.markets.match('Kevin Warsh')
+positions = client.fetch_positions()
+balance = client.fetch_balance()
+markets = client.fetch_markets(query="nba")
-print(f"Price: {warsh.yes.price}")
+# Trading — also pass private_key
+trader = pmxt.Polymarket(
+ pmxt_api_key="pmxt_live_...",
+ wallet_address="0xYourWalletAddress",
+ private_key="0xYourPrivateKey",
+)
+order = trader.create_order(
+ market_id="market-uuid",
+ outcome_id="outcome-uuid",
+ side="buy",
+ order_type="market",
+ amount=5.0,
+ denom="usdc",
+ slippage_pct=30.0,
+)
```
### TypeScript
-> **Note:** Named imports do not work in ESM. Use `import pmxt from 'pmxtjs'` (default import), not `import { Polymarket } from 'pmxtjs'`.
+> **Note:** Named imports do not work in ESM. Use `import pmxt from 'pmxtjs'` (default import) for the namespaced form, or import `Polymarket` from `pmxtjs` only via the CJS build.
```typescript
-import pmxt from 'pmxtjs';
+import { Polymarket } from "pmxtjs";
+
+// Reads — pmxtApiKey + walletAddress only
+const client = new Polymarket({
+ pmxtApiKey: "pmxt_live_...",
+ walletAddress: "0xYourWalletAddress",
+});
+
+const positions = await client.fetchPositions();
+const balance = await client.fetchBalance();
+
+// Trading — also pass privateKey
+const trader = new Polymarket({
+ pmxtApiKey: "pmxt_live_...",
+ walletAddress: "0xYourWalletAddress",
+ privateKey: "0xYourPrivateKey",
+});
+const order = await trader.createOrder({
+ marketId: "market-uuid",
+ outcomeId: "outcome-uuid",
+ side: "buy",
+ type: "market",
+ amount: 5.0,
+ denom: "usdc",
+ slippage_pct: 30.0,
+} as any);
+```
+
+### Prediction market hierarchy
+
+Prediction markets are structured in a hierarchy to group related information.
+
+* **Event**: The broad topic (e.g., *"Who will Trump nominate as Fed Chair?"*)
+* **Market**: A specific tradeable question (e.g., *"Will Trump nominate Kevin Warsh as the next Fed Chair?"*)
+* **Outcome**: The actual share you buy (e.g., *"Yes"* or *"No"*)
+
+## Trading
+pmxt supports unified trading across exchanges. The hosted API is the default — see Quickstart above for the basic flow.
+
+### Hosted trading (recommended)
+
+With a PMXT API key, you only need your wallet address and a private key to sign orders. PMXT handles custody, signer infrastructure, and on-chain settlement.
+
+```python
+import pmxt
-const api = new pmxt.Exchange();
+trader = pmxt.Polymarket(
+ pmxt_api_key="pmxt_live_...",
+ wallet_address="0xYourWalletAddress",
+ private_key="0xYourPrivateKey",
+)
-// 1. Search for the broad Event
-const events = await api.fetchEvents({ query: 'Who will Trump nominate as Fed Chair?' });
-const fedEvent = events[0];
+# 1. Check balance
+balance = trader.fetch_balance()
+print(f"Available balance: {balance[0].available}")
-// 2. Find the specific Market within that event
-const warsh = fedEvent.markets.match('Kevin Warsh');
+# 2. Fetch markets
+markets = trader.fetch_markets(query='Trump')
-console.log(`Price: ${warsh.yes?.price}`);
+# 3. Place an order
+order = trader.create_order(
+ market_id=markets[0].market_id,
+ outcome_id=markets[0].yes.outcome_id,
+ side='buy',
+ order_type='market',
+ amount=5.0,
+ denom='usdc',
+ slippage_pct=30.0,
+)
+print(f"Order status: {order.status}")
```
-## Trading
-pmxt supports unified trading across exchanges.
+### Self-hosted trading (advanced)
-### Setup
-To trade, you must provide your private credentials during initialization. For detailed credential setup instructions, see the exchange-specific guides: [Polymarket](core/docs/SETUP_POLYMARKET.md), [Kalshi](core/docs/SETUP_KALSHI.md), [Limitless](core/docs/SETUP_LIMITLESS.md).
+Use this when you self-host the local server. See [Self-hosted](#self-hosted) for setup. You provide venue credentials directly — no `pmxt_api_key` required. For detailed credential setup instructions, see the exchange-specific guides: [Polymarket](core/docs/SETUP_POLYMARKET.md), [Kalshi](core/docs/SETUP_KALSHI.md), [Limitless](core/docs/SETUP_LIMITLESS.md).
#### Polymarket
```python
@@ -207,35 +275,9 @@ exchange = pmxt.Limitless(
)
```
-### Trading Example (Python)
-
-```python
-import pmxt
-import os
-
-# Initialize with credentials (e.g., Polymarket)
-exchange = pmxt.Polymarket(
- private_key=os.getenv('POLYMARKET_PRIVATE_KEY'),
- proxy_address=os.getenv('POLYMARKET_PROXY_ADDRESS')
-)
-
-# 1. Check Balance
-balance = exchange.fetch_balance()
-print(f"Available balance: {balance[0].available}")
+## Self-hosted
-# 2. Fetch markets
-markets = exchange.fetch_markets(query='Trump')
-
-# 3. Place an Order (using outcome shorthand)
-order = exchange.create_order(
- outcome=markets[0].yes,
- side='buy',
- type='limit',
- price=0.33,
- amount=100
-)
-print(f"Order Status: {order.status}")
-```
+To self-host pmxt-core on your own machine: `pip install pmxt-core` (Python) or `npm install pmxt-core` (Node.js), then construct any venue client without `pmxt_api_key`. The SDK spawns a local sidecar process; you supply venue credentials directly. See the [self-hosted guide](https://pmxt.dev/docs/guides/self-hosted) for details.
## Documentation
diff --git a/sdks/python/README.md b/sdks/python/README.md
index b298d58f..94a0716a 100644
--- a/sdks/python/README.md
+++ b/sdks/python/README.md
@@ -1,8 +1,8 @@
# PMXT Python SDK
-A unified Python interface for interacting with multiple prediction market exchanges (Kalshi, Polymarket).
+A unified Python interface for prediction market exchanges (Polymarket, Kalshi, Limitless, Opinion, and more).
-> **Note**: This SDK requires the PMXT sidecar server to be running. See [Installation](#installation) below.
+> **Note**: Use with a PMXT API key (hosted, recommended) or self-host the sidecar locally. Get a key at [pmxt.dev/dashboard](https://pmxt.dev/dashboard).
## Installation
@@ -10,19 +10,23 @@ A unified Python interface for interacting with multiple prediction market excha
pip install pmxt
```
-**Requirements**: Python >= 3.8. The sidecar server is bundled automatically via the `pmxt-core` dependency -- no separate install needed.
+**Requirements**: Python >= 3.8. The sidecar server is bundled automatically via the `pmxt-core` dependency — only needed when self-hosting.
## Quick Start
+Get your API key at [pmxt.dev/dashboard](https://pmxt.dev/dashboard). For reads, only `pmxt_api_key` and `wallet_address` are required.
+
```python
import pmxt
-# Initialize exchanges (server starts automatically!)
-poly = pmxt.Polymarket()
-kalshi = pmxt.Kalshi()
+# Reads — pmxt_api_key + wallet_address only
+client = pmxt.Polymarket(
+ pmxt_api_key="pmxt_live_...",
+ wallet_address="0xYourWalletAddress",
+)
# Search for markets
-markets = poly.fetch_markets(query="Trump")
+markets = client.fetch_markets(query="Trump")
print(markets[0].title)
# Get outcome details
@@ -30,28 +34,41 @@ outcome = markets[0].outcomes[0]
print(f"{outcome.label}: {outcome.price * 100:.1f}%")
# Fetch historical data (use outcome.outcome_id!)
-candles = poly.fetch_ohlcv(
+candles = client.fetch_ohlcv(
outcome.outcome_id,
resolution="1d",
- limit=30
+ limit=30,
)
# Get current order book
-order_book = poly.fetch_order_book(outcome.outcome_id)
+order_book = client.fetch_order_book(outcome.outcome_id)
spread = order_book.asks[0].price - order_book.bids[0].price
print(f"Spread: {spread * 100:.2f}%")
+
+# Account reads
+positions = client.fetch_positions()
+balance = client.fetch_balance()
```
-### How It Works
+### How it works (hosted)
-The Python SDK automatically manages the PMXT sidecar server:
+When you pass `pmxt_api_key`, the SDK talks to the PMXT hosted services:
+
+1. Catalog requests go to `api.pmxt.dev` (markets, events, order books, OHLCV, trades).
+2. Trading requests go to `trade.pmxt.dev` (orders, positions, balances).
+3. The SDK does **not** spawn a local process.
+4. For Polymarket and Opinion, PMXT's PreFundedEscrow handles custody — you sign orders with your own key, PMXT settles on-chain.
+
+### How it works (self-hosted)
+
+When you omit `pmxt_api_key`, the Python SDK manages the PMXT sidecar server for you:
1. **First API call**: Checks if server is running
2. **Auto-start**: Starts server if needed (takes ~1-2 seconds)
3. **Reuse**: Multiple Python processes share the same server
4. **Zero config**: Just import and use!
-### Manual Server Control (Optional)
+#### Manual server control (optional)
If you prefer to manage the server yourself:
@@ -63,9 +80,57 @@ poly = pmxt.Polymarket(auto_start_server=False)
# $ pmxt-server
```
-## Authentication (for Trading)
+## Trading
+
+### Hosted trading (recommended)
+
+With a PMXT API key, pass `pmxt_api_key`, `wallet_address`, and `private_key`. The SDK auto-wraps your key into an EIP-712 signer and PMXT settles the order on-chain.
+
+#### Polymarket
+
+```python
+import pmxt
+
+trader = pmxt.Polymarket(
+ pmxt_api_key="pmxt_live_...",
+ wallet_address="0xYourWalletAddress",
+ private_key="0xYourPrivateKey",
+)
+
+balance = trader.fetch_balance()
+print(f"Available: ${balance[0].available}")
+
+order = trader.create_order(
+ market_id="market-uuid",
+ outcome_id="outcome-uuid",
+ side="buy",
+ order_type="market",
+ amount=5.0,
+ denom="usdc",
+ slippage_pct=30.0,
+)
+print(f"Order status: {order.status}")
+```
+
+#### Opinion
+
+```python
+import pmxt
+
+trader = pmxt.Opinion(
+ pmxt_api_key="pmxt_live_...",
+ wallet_address="0xYourWalletAddress",
+ private_key="0xYourPrivateKey",
+)
+```
+
+See the full [hosted trading guide](https://pmxt.dev/docs/concepts/hosted-trading) for venue support, custody model, and limits.
+
+### Self-hosted trading (advanced)
+
+When self-hosting, you supply venue credentials directly — no `pmxt_api_key`. The SDK spawns a local sidecar process.
-### Polymarket
+#### Polymarket
Requires your **Polygon Private Key**:
@@ -94,7 +159,7 @@ order = poly.create_order(
)
```
-### Kalshi
+#### Kalshi
Requires **API Key** and **Private Key**:
@@ -113,7 +178,7 @@ for pos in positions:
print(f"{pos.outcome_label}: ${pos.unrealized_pnl:.2f}")
```
-### Limitless
+#### Limitless
Requires **Private Key**:
diff --git a/sdks/typescript/README.md b/sdks/typescript/README.md
index bebcabdc..9311d3c9 100644
--- a/sdks/typescript/README.md
+++ b/sdks/typescript/README.md
@@ -1,6 +1,8 @@
# pmxtjs
-A unified TypeScript/Node.js SDK for prediction markets - The ccxt for prediction markets.
+A unified TypeScript/Node.js SDK for prediction markets — The ccxt for prediction markets.
+
+> **Note**: Use with a PMXT API key (hosted, recommended) or self-host the sidecar locally. Get a key at [pmxt.dev/dashboard](https://pmxt.dev/dashboard).
## Installation
@@ -10,15 +12,19 @@ npm install pmxtjs
## Quick Start
+Get your API key at [pmxt.dev/dashboard](https://pmxt.dev/dashboard). For reads, only `pmxtApiKey` and `walletAddress` are required.
+
```typescript
-import pmxt from 'pmxtjs';
+import { Polymarket } from "pmxtjs";
-// Initialize exchanges
-const poly = new pmxt.Polymarket();
-const kalshi = new pmxt.Kalshi();
+// Reads — pmxtApiKey + walletAddress only
+const client = new Polymarket({
+ pmxtApiKey: "pmxt_live_...",
+ walletAddress: "0xYourWalletAddress",
+});
// Search for markets
-const markets = await poly.fetchMarkets({ query: 'Trump' });
+const markets = await client.fetchMarkets({ query: "Trump" });
console.log(markets[0].title);
// Get outcome details
@@ -26,17 +32,29 @@ const outcome = markets[0].outcomes[0];
console.log(`${outcome.label}: ${(outcome.price * 100).toFixed(1)}%`);
// Fetch historical data (use outcome.outcomeId!)
-const candles = await poly.fetchOHLCV(outcome.outcomeId, {
+const candles = await client.fetchOHLCV(outcome.outcomeId, {
resolution: '1d',
- limit: 30
+ limit: 30,
});
// Get current order book
-const orderBook = await poly.fetchOrderBook(outcome.outcomeId);
+const orderBook = await client.fetchOrderBook(outcome.outcomeId);
const spread = orderBook.asks[0].price - orderBook.bids[0].price;
console.log(`Spread: ${(spread * 100).toFixed(2)}%`);
+
+// Account reads
+const positions = await client.fetchPositions();
+const balance = await client.fetchBalance();
```
+### How it works (hosted)
+
+When you pass `pmxtApiKey`, the SDK talks to PMXT's hosted services: catalog requests go to `api.pmxt.dev`, trading requests go to `trade.pmxt.dev`. The SDK does **not** spawn a local process. For Polymarket and Opinion, PMXT's PreFundedEscrow handles custody — you sign orders with your own key, PMXT settles on-chain.
+
+### How it works (self-hosted)
+
+Omit `pmxtApiKey` to use the local sidecar. Install `pmxt-core` from npm and supply venue credentials directly. See [Self-hosted trading (advanced)](#self-hosted-trading-advanced) below.
+
## Core Methods
### Market Data
@@ -77,7 +95,51 @@ console.log(`Spread: ${(spread * 100).toFixed(2)}%`);
## Trading
-### Authentication
+### Hosted trading (recommended)
+
+With a PMXT API key, pass `pmxtApiKey`, `walletAddress`, and `privateKey`. The SDK auto-wraps your key into an `EthersSigner` and PMXT settles the order on-chain.
+
+**Polymarket:**
+```typescript
+import { Polymarket } from "pmxtjs";
+
+const trader = new Polymarket({
+ pmxtApiKey: "pmxt_live_...",
+ walletAddress: "0xYourWalletAddress",
+ privateKey: "0xYourPrivateKey",
+});
+
+const balance = await trader.fetchBalance();
+console.log(`Available: $${balance[0].available}`);
+
+const order = await trader.createOrder({
+ marketId: "market-uuid",
+ outcomeId: "outcome-uuid",
+ side: "buy",
+ type: "market",
+ amount: 5.0,
+ denom: "usdc",
+ slippage_pct: 30.0,
+} as any);
+console.log(`Order status: ${order.status}`);
+```
+
+**Opinion:**
+```typescript
+import { Opinion } from "pmxtjs";
+
+const trader = new Opinion({
+ pmxtApiKey: "pmxt_live_...",
+ walletAddress: "0xYourWalletAddress",
+ privateKey: "0xYourPrivateKey",
+});
+```
+
+See the full [hosted trading guide](https://pmxt.dev/docs/concepts/hosted-trading) for venue support, custody model, and limits.
+
+### Self-hosted trading (advanced)
+
+When self-hosting, supply venue credentials directly — no `pmxtApiKey`. The SDK spawns a local sidecar process.
**Polymarket:**
```typescript