# PMXT — Full Documentation > One API for supported prediction markets. Unified data and hosted catalog search across Polymarket, Kalshi, Limitless, Smarkets, and 12 more venues. Trading where venues expose writes. Docs: https://pmxt.dev/docs API Base: https://api.pmxt.dev --- # Documentation ## Get Started ### Introduction Source: https://pmxt.dev/docs/introduction > **Note:** New here? Start with [Quickstart](https://pmxt.dev/docs/quickstart) (read-only, 30 seconds) or [Trading Quickstart](https://pmxt.dev/docs/trading-quickstart) (hosted writes, 5 minutes). PMXT is a unified API for supported prediction-market venues — Polymarket, Kalshi, Limitless, Smarkets, and [12 more venues](https://pmxt.dev/docs/concepts/venues). It uses shared SDK method names and unified response shapes where each venue supports the underlying capability. > **Note:** New to prediction markets? Start at the [101](https://pmxt.dev/docs/concepts/prediction-markets-101) page for the vocabulary — outcome, price-as-probability, EIP-712, escrow, USDC on Polygon, and Group A methods. It runs two ways: - **Hosted (default)** — PMXT's [hosts](https://pmxt.dev/docs/authentication#hosts) give you a shared catalog, cross-venue search, and end-to-end [hosted trading](https://pmxt.dev/docs/concepts/hosted-trading) with [PreFundedEscrow](https://pmxt.dev/docs/concepts/prediction-markets-101#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](https://pmxt.dev/docs/guides/self-hosted). The SDK call shape is shared, but the runtime target changes: ```python import pmxt # Hosted (default): talks to PMXT's hosts — see /authentication#hosts poly = pmxt.Polymarket(pmxt_api_key="pmxt_live_...") markets = poly.fetch_markets(query="fed rate cut", limit=5) for m in markets: print(m.title, m.outcomes[0].price) ``` If you'd rather run pmxt-core yourself, drop the API key: ```python import pmxt # Self-hosted: SDK spawns pmxt-core on localhost poly = pmxt.Polymarket() ``` Hosted mode uses a PMXT API key and hosted services. Self-hosted mode talks to local `pmxt-core` and uses venue-native credentials where a capability requires them. See [self-hosted](https://pmxt.dev/docs/guides/self-hosted) for when that's the right choice. Swap the venue class — `pmxt.Kalshi(...)`, `pmxt.Limitless(...)` — and use the same method names and response shapes where that venue implements the capability. #### The Router — cross-venue intelligence The **[Router](https://pmxt.dev/docs/router/overview)** is PMXT's cross-venue intelligence layer. Search, match, and compare prices across the hosted catalog — all from a single PMXT API key: ```python import pmxt router = pmxt.Router(pmxt_api_key="pmxt_live_...") # Search catalog venues at once markets = router.fetch_markets(query="fed rate cut") # Find the cluster of matching markets on other venues clusters = router.fetch_matched_market_clusters(markets[0]) # Browse high-volume matched clusters across venues top_clusters = router.fetch_matched_market_clusters(sort="volume", min_venues=2, limit=10) ``` - **[Cross-venue search](https://pmxt.dev/docs/router/search)** — One query searches the hosted catalog. Results in ~10ms from a shared index. - **[Market matching](https://pmxt.dev/docs/router/matching)** — Find clusters of the same or related market across venues with relation types and confidence scores. - **[Price comparison](https://pmxt.dev/docs/router/prices)** — Compare bid/ask across venues and find related markets. - **[Compose with venues](https://pmxt.dev/docs/router/prices#composing-with-venue-exchanges)** — Discover with the Router, then trade with venue clients. Same schema; catalog IDs for Router and hosted flows, venue-native IDs for direct self-hosted writes. #### Unified venue interface Same method names, same response shapes, with support varying by venue. `fetch_markets`, `create_order`, and `fetch_positions` use the unified schema where the venue implements them. - **[Unified schema](https://pmxt.dev/docs/concepts/unified-schema)** — `Event` / `Market` / `Outcome` — the shape that works everywhere. - **[Same SDK, local or hosted](https://pmxt.dev/docs/quickstart)** — The `pmxt` (Python) and `pmxtjs` (TypeScript) SDKs share high-level calls across hosted and self-hosted modes. Credentials and supported writes vary by mode. #### What you'd build yourself Without PMXT, getting cross-venue prediction market data means: - **15+ venue integrations** — each with its own auth, pagination, field names, and rate limits. Polymarket uses CLOB token IDs. Kalshi uses tickers. Smarkets uses contract IDs. You normalize all of it. - **A data pipeline** — to ingest, deduplicate, and keep fresh as markets open, resolve, and re-price across supported venues. - **A search layer** — because querying venue APIs sequentially is slow. - **Ongoing maintenance** — every time a venue changes a field name or ships a new endpoint, your integration breaks. PMXT handles all of that. You write `fetch_markets(query="...")` and get back clean, unified data. #### Get started - **[Quickstart](https://pmxt.dev/docs/quickstart)** — API key to working code in 30 seconds. - **[Router](https://pmxt.dev/docs/router/search)** — Cross-venue search and the beginning of smart order routing. - **[Unified schema](https://pmxt.dev/docs/concepts/unified-schema)** — The data shape that works everywhere. - **[API reference](https://pmxt.dev/docs/api-reference/overview)** — Every method, with interactive try-it-out. ### Quickstart Source: https://pmxt.dev/docs/quickstart #### 1. Get an API key Go to [pmxt.dev/dashboard](https://pmxt.dev/dashboard) and create a key. It starts with `pmxt_live_` and works immediately. #### 2. Query a venue Pick a venue and start pulling data. The SDK gives you a unified interface — same method names and response shapes where the venue supports them. **Python:** ```bash pip install pmxt ``` ```python import pmxt poly = pmxt.Polymarket(pmxt_api_key="pmxt_live_...") markets = poly.fetch_markets(query="election", limit=3) for m in markets: print(m.title, m.outcomes[0].price) ``` **TypeScript:** ```bash npm install pmxtjs ``` ```typescript import pmxt from "pmxtjs"; const poly = new pmxt.Polymarket({ pmxtApiKey: "pmxt_live_..." }); const markets = await poly.fetchMarkets({ query: "election", limit: 3 }); markets.forEach((m) => console.log(m.title, m.outcomes[0].price)); ``` Switch venues by swapping the class — `pmxt.Kalshi(...)`, `pmxt.Limitless(...)`, etc. PMXT keeps the schema consistent, with per-venue capabilities called out in [Supported venues](https://pmxt.dev/docs/concepts/venues). #### 3. Search the hosted catalog The **Router** is PMXT's cross-venue search layer — and the foundation for a full smart order router for prediction markets. One query searches across the hosted catalog and returns ranked, unified results. ```bash curl "https://api.pmxt.dev/v0/markets?query=election&limit=3" \ -H "Authorization: Bearer pmxt_live_..." ``` ```json { "data": [ { "marketId": "d35bc8c6-5602-477d-af21-e9513af9d696", "title": "US forces enter Iran by April 30?", "volume24h": 89757605.08, "outcomes": [ { "outcomeId": "...", "label": "April 30", "price": 0.9985 }, { "outcomeId": "...", "label": "Not April 30", "price": 0.0015 } ] } ], "meta": { "count": 3, "limit": 3, "offset": 0 } } ``` The same `marketId`, `title`, and `outcomes` shape applies whether the row came from Polymarket, Kalshi, Limitless, or another hosted catalog venue. Filter by venue with `&exchange=kalshi`, by category with `&category=Politics`, or omit the venue filter to search the hosted catalog. > **Note:** The Router is also available as an SDK class — `pmxt.Router(...)` in Python and `new pmxt.Router(...)` in TypeScript. Beyond search, it supports [cross-venue matching](https://pmxt.dev/docs/router/matching) and [price comparison](https://pmxt.dev/docs/router/prices). #### 4. Go further - **[Router](https://pmxt.dev/docs/router/search)** — `/v0/events` and `/v0/markets` — cross-venue search and the beginning of a smart order router for prediction markets. - **[Full venue API](https://pmxt.dev/docs/api-reference/overview)** — Order books, trades, OHLCV, positions, balances, and trading, supported where each venue implements them via `POST /api/:exchange/:method`. - **[Unified schema](https://pmxt.dev/docs/concepts/unified-schema)** — `Event` / `Market` / `Outcome` — the shape that works everywhere. - **[Supported venues](https://pmxt.dev/docs/concepts/venues)** — Every venue PMXT speaks, and what each supports. ### Trading Quickstart Source: https://pmxt.dev/docs/trading-quickstart From a fresh `pmxt_api_key` to a confirmed position on Polymarket in under a minute. For the full hosted model, see [Hosted trading](https://pmxt.dev/docs/concepts/hosted-trading). > **Note:** Hosted writes today: **Polymarket**, **Opinion**, and **Limitless**. Other venues are read-only via the hosted catalog; use [self-hosted](https://pmxt.dev/docs/guides/self-hosted) for venue-native trading where the venue exposes writes. Check [Feature Support & Compliance](https://github.com/pmxt-dev/pmxt/blob/main/core/COMPLIANCE.md) for per-venue support. #### 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." ``` > **Note:** PMXT is non-custodial by construction: the SDK signs locally and your key never leaves your machine. For production, a practical habit is to keep only the float you're actively trading in this wallet. See [Security](https://pmxt.dev/docs/security). #### 4. Approve and deposit USDC into escrow Hosted trades execute against your balance in the non-custodial `PreFundedEscrow` contract. The first time you trade, you need to approve the escrow to pull USDC from your wallet and make an initial deposit. The fastest way is the dashboard. ##### Recommended: deposit from the dashboard Open [pmxt.dev/dashboard](https://pmxt.dev/dashboard), connect the wallet that matches `0xYourWallet`, and use the Deposit flow. Your wallet (MetaMask, Rabby, WalletConnect, etc.) prompts you to sign each transaction — PMXT never sees your private key. 1. Go to [pmxt.dev/dashboard](https://pmxt.dev/dashboard) and connect the same wallet address you passed to the SDK. 2. Click **Deposit** and enter the USDC amount. 3. Approve the USDC allowance in your wallet (one-time per token). 4. Confirm the deposit transaction in your wallet. > **Note:** Approval and deposit are one-time setup. Subsequent trades just spend from escrow until you withdraw. See [Escrow Lifecycle](https://pmxt.dev/docs/guides/escrow-lifecycle) for the full deposit / withdraw flow. If you're scripting deposits (CI, treasury automation, custom wallet integrations), `client.escrow` builds **unsigned** transactions you sign and broadcast with your own wallet library (web3, ethers, viem, etc.). ```python Python # 1. Build an unsigned ERC-20 approval tx for USDC approve_tx = client.escrow.approve_tx("usdc")["tx"] # Sign and send approve_tx with your wallet library... # (escrow builders return {"tx": {...}}; unwrap with ["tx"]) # 2. Build an unsigned deposit tx for 10 USDC deposit_tx = client.escrow.deposit_tx(amount=10.0)["tx"] # Sign and send deposit_tx with your wallet library... # 3. Confirm the deposit landed balances = client.fetch_balance() print(f"Escrow USDC: {balances[0].available}") ``` ```typescript TypeScript // 1. Build an unsigned ERC-20 approval tx for USDC const { tx: approveTx } = await client.escrow.approveTx("usdc"); // Sign and send approveTx with your wallet library... // (escrow builders return { tx: {...} }; destructure as shown) // 2. Build an unsigned deposit tx for 10 USDC const { tx: depositTx } = await client.escrow.depositTx(10); // Sign and send depositTx with your wallet library... // 3. Confirm the deposit landed const balances = await client.fetchBalance(); console.log(`Escrow USDC: ${balances[0].available}`); ``` #### 5. Find a market and place your first order `client.fetch_markets` queries the venue's live catalog and returns markets you can trade directly. Pass any returned outcome straight to `create_order` — no UUID lookup required. ```python Python markets = client.fetch_markets({"query": "knicks 2026 nba champion"}) market = markets[0] yes = next(o for o in market.outcomes if o.label.lower() == "yes") order = client.create_order( outcome=yes, side="buy", order_type="market", amount=5.0, # market buys are denominated in USDC: spend exactly $5 ) print(f"Order {order.id}: {order.status} filled={order.filled}") ``` ```typescript TypeScript const markets = await client.fetchMarkets({ query: "knicks 2026 nba champion" }); const market = markets[0]; const yes = market.outcomes.find((o) => o.label.toLowerCase() === "yes")!; const order = await client.createOrder({ outcome: yes, side: "buy", type: "market", amount: 5, // market buys are denominated in USDC: spend exactly $5 }); console.log(`Order ${order.id}: ${order.status} filled=${order.filled}`); ``` ```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." ``` > **Note:** The hosted trading API accepts either the venue-native identifier (returned by `client.fetch_markets`) or a catalog UUID (returned by [Router](https://pmxt.dev/docs/router/overview)) — whichever you have. The SDK picks the right wire field automatically. See [Catalog UUID vs Venue ID](https://pmxt.dev/docs/concepts/catalog-uuid-vs-venue-id) when cross-venue identity matters. > **Note:** **`outcome=` requires a `MarketOutcome` instance**, not a bare dict — passing a raw dict raises `AttributeError`. Use the object you got from `client.fetch_markets()[i].outcomes[j]` (or `router.fetch_markets()[i].outcomes[j]`) as shown above. If you only have string ids, pass them explicitly instead: `client.create_order(market_id="...", outcome_id="...", side="buy", ...)`. > **Note:** **What PMXT abstracts vs what leaks through from the venue.** PMXT unifies the order shape across venues, but a few Polymarket rules pass through as-is. Polymarket enforces **two independent minimums on marketable BUY orders** — a **5-share minimum** AND a **$1 notional minimum** — and the higher of the two binds. At $0.138/share the 5-share rule alone would only require $0.69, but the $1 rule raises the effective minimum to ~8 shares (~$1.10); a 5-share buy at that price is rejected by the venue with `invalid amount for a marketable BUY order ($0.69), min size: 1` and PMXT surfaces it as `OrderSizeTooSmall`. Market orders are **budget-capped** (a buy spends exactly `amount` USDC, a sell sells exactly `amount` shares — `slippage_pct` is ignored). Hosted **limit** orders are supported via `client.create_order(order_type="limit", price=..., amount=...)` and the same 5-share and $1 marketable-BUY minimums apply; limit BUY and SELL are supported, and both use `denom="shares"` in hosted SDK requests. #### 6. Verify the fill Hosted positions appear immediately on `fetch_positions`. The position's `size` is your share count; the remaining USDC sits in `fetch_balance`. > **Note:** The `id` returned by hosted `create_order` is a **PMXT internal task id**, not a Polymarket (or other venue) order id. Calling Polymarket's own order-lookup with it will return nothing. Use `client.fetch_order(id)` — the hosted SDK knows how to resolve the task and reports the live status (`queued`, `fulfilling`, `fulfilled`, `failed`, `no_fill`). A fresh `create_order` returns `status="queued"` because the venue submit happens asynchronously; poll `fetch_order` (or pass `wait=true`) to get the venue-confirmed outcome. ```python Python positions = client.fetch_positions() for p in positions: print(f"{p.market_id} {p.outcome_label} size={p.size}") balances = client.fetch_balance() print(f"Escrow USDC left: {balances[0].available}") ``` ```typescript TypeScript const positions = await client.fetchPositions(); for (const p of positions) { console.log(`${p.marketId} ${p.outcomeLabel} size=${p.size}`); } const balances = await client.fetchBalance(); console.log(`Escrow USDC left: ${balances[0].available}`); ``` That's it — you placed a real trade through hosted PMXT. From here: - [Escrow lifecycle](https://pmxt.dev/docs/guides/escrow-lifecycle) — deposits, withdrawals, the request/claim timelock. - [Hosted errors](https://pmxt.dev/docs/guides/hosted-errors) — what each error means and how to recover. - [Signing](https://pmxt.dev/docs/guides/signing) — the EIP-712 protocol underneath `create_order`. - [Self-hosted](https://pmxt.dev/docs/guides/self-hosted) — when to skip hosted and run pmxt-core yourself. ### Authentication Source: https://pmxt.dev/docs/authentication #### Hosts PMXT serves two hostnames. - `api.pmxt.dev` — reads, catalog search via Router, MCP server, venue passthrough (`/api/*`). - `trade.pmxt.dev` — hosted writes (build / submit / cancel) and hosted account state (orders, positions, balances, trades) via `/v0/trade/*`, `/v0/user/*`, and `/v0/orders/*`. The same `pmxt_api_key` authenticates against both — no separate credentials. Reads and writes have different latency and availability profiles; the trading host is the hot path for signed-order submission and is operated independently. #### API key Every request to `https://api.pmxt.dev` needs a Bearer token. Get one from [pmxt.dev/dashboard](https://pmxt.dev/dashboard) — it works immediately. ```http Authorization: Bearer pmxt_live_... ``` `X-Api-Key: pmxt_live_...` also works as a fallback, but Bearer is canonical and what every SDK sends. #### SDK setup Set `PMXT_API_KEY` and the SDK automatically routes to the hosted endpoint. No code changes versus local development. **Python:** ```python import os, pmxt os.environ["PMXT_API_KEY"] = "pmxt_live_..." poly = pmxt.Polymarket() # -> https://api.pmxt.dev ``` Or pass it directly: ```python import pmxt poly = pmxt.Polymarket(pmxt_api_key="pmxt_live_...") ``` **TypeScript:** ```typescript import pmxt from "pmxtjs"; process.env.PMXT_API_KEY = "pmxt_live_..."; const poly = new pmxt.Polymarket({}); // -> https://api.pmxt.dev ``` Or pass it directly: ```typescript import pmxt from "pmxtjs"; const poly = new pmxt.Polymarket({ pmxtApiKey: "pmxt_live_..." }); ``` Explicit argument takes precedence over the environment variable. #### Venue credentials ##### 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](https://pmxt.dev/docs/trading-quickstart) for the full 60-second flow and [Escrow lifecycle](https://pmxt.dev/docs/guides/escrow-lifecycle) for the approve / deposit / withdraw mechanics. Hosted writes today: Polymarket, Opinion, and Limitless. ##### Self-hosted / direct venue credentials (advanced) When you run [pmxt-core locally](https://pmxt.dev/docs/guides/self-hosted), or when you need venue-native trading where the venue exposes writes but hosted mode does not, you pass venue-native credentials (Polymarket private key, Kalshi RSA, Smarkets session, etc.) directly: ```python order = poly.create_order( outcome=market.yes, side="buy", order_type="limit", price=0.42, amount=100, ) ``` PMXT never stores venue credentials. They are forwarded to the venue and dropped when the call completes. > **Note:** 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](https://pmxt.dev/docs/security) before passing private keys to PMXT — hosted or self-hosted. #### Rotating keys > **Warning:** Rotate immediately if you suspect a key is exposed. Old keys are revoked atomically — in-flight requests with the old key are rejected within 60 seconds. 1. **Create** a new key from the [dashboard](https://pmxt.dev/dashboard). 2. **Deploy** the new key to your application. 3. **Revoke** the old key once traffic has drained. All revocations are logged under **Settings > Audit Log**. #### Errors | Status | Body | Meaning | SDK exception | | ------ | ---------------------------------------- | ----------------------------------------- | ------------- | | `401` | `{"error": "missing api key"}` | No `Authorization` header on the request. | `PmxtError` (message: `missing api key`) | | `401` | `{"error": "invalid api key"}` | Key unknown, revoked, or expired. | `PmxtError` (message: `invalid api key`) | | `429` | `{"error": "rate_limit_exceeded", ... }` | See [Plans & Limits](https://pmxt.dev/docs/rate-limits). | `RateLimitExceeded` | > **Note:** Today the SDKs raise the base `pmxt.errors.PmxtError` for both 401 cases — match on the message string. A dedicated `InvalidApiKey` subclass may be added in a future SDK release; this doc will be updated when that ships. See [API Reference / Errors](https://pmxt.dev/docs/api-reference/errors) for the full error class hierarchy. ### MCP Server Source: https://pmxt.dev/docs/mcp PMXT hosts a Streamable HTTP MCP server at `https://api.pmxt.dev/mcp`. Any MCP-compatible client — Claude Code, Cursor, or your own agent — can connect and use the full PMXT tool surface. No install required. #### Claude Code Add to `~/.claude.json` (global) or `.mcp.json` (per-project): ```json { "mcpServers": { "pmxt": { "type": "streamableHttp", "url": "https://api.pmxt.dev/mcp", "headers": { "Authorization": "Bearer pmxt_live_..." } } } } ``` #### Other clients Cursor uses the same config shape in its MCP settings. For custom clients, connect to `https://api.pmxt.dev/mcp` with any MCP SDK and pass your key as a `Bearer` token in the `Authorization` header. Source and local-process install at [`@pmxt/mcp`](https://www.npmjs.com/package/@pmxt/mcp). ### Security & Credential Handling Source: https://pmxt.dev/docs/security PMXT has two execution modes, and they have **different credential surfaces**. Most of the confusion about "does my key touch PMXT's server" comes from mixing them up. This page is scoped explicitly to each mode. | Mode | What you give PMXT | What touches PMXT's server | Custody | | ---- | ------------------ | -------------------------- | ------- | | [**Hosted**](https://pmxt.dev/docs/concepts/hosted-trading) (default) | An EIP-712 signature per order — **never your private key** | Outcome target, signatures, public wallet address | USDC in the on-chain [`PreFundedEscrow`](https://pmxt.dev/docs/concepts/hosted-trading) you control via the timelock | | [**Self-hosted**](https://pmxt.dev/docs/guides/self-hosted) | Raw venue credentials — given to a PMXT process **you run** on your machine | Nothing — PMXT's cloud is not in the loop | Your venue account / wallet, directly | If you're picking one, hosted is the default for almost everyone. Self-hosted is for users who need raw venue credentials (Polymarket L2 API keys, Kalshi RSA, etc.), sub-100ms latency, or regulatory custody constraints. #### Hosted mode: how credentials flow In hosted mode, **your private key never leaves your process.** PMXT's server sees signatures, never the key that produced them. ``` Your app ──HTTPS──> trade.pmxt.dev ──submits──> venue API │ └── signs EIP-712 locally with your private key (key never sent) ``` 1. The SDK calls `POST /v0/trade/build-order` with the outcome target and order parameters. The target is either a catalog UUID or a `venue` + `venue_outcome_id` pair. No key, no signature yet. 2. The server returns an unsigned EIP-712 typed-data payload bound to the Polygon [`PreFundedEscrow`](https://pmxt.dev/docs/concepts/hosted-trading) domain. 3. The SDK signs the payload **locally**, in your process, with your private key. 4. The SDK calls `POST /v0/trade/submit-order` with the signature. The server forwards the signed order to the venue. What PMXT's server receives on the wire: the outcome target, your public wallet address, and the signature. **Not the private key.** Funds custody: USDC sits in the on-chain `PreFundedEscrow`. Withdrawals are timelocked and user-only — PMXT's operator key cannot move USDC without your EIP-712 signature. See [Hosted trading](https://pmxt.dev/docs/concepts/hosted-trading) for addresses and the raw exit path. The `pmxt_api_key` authorizes calls to `trade.pmxt.dev`. The key alone cannot move funds. Keep it server-side. #### Self-hosted mode: how credentials flow In self-hosted mode, you run `pmxt-core` as a local sidecar on your own machine. The SDK talks to your local sidecar; the sidecar talks directly to the venue. **PMXT's cloud is not in the request path.** ``` Your app ──localhost──> pmxt-core (your machine) ──HTTPS──> venue API ▲ └── holds venue credentials in process memory ``` Venue credentials passed to the sidecar live in your process's RAM for the duration of the request and are garbage-collected after; the sidecar does not persist, log, or cache them. "In process memory" here means **your process, on your machine** — not PMXT's cloud. For on-chain venues (Polymarket, Limitless, Probable, Opinion, Baozi), "venue credential" means a raw private key — there is no scoped API-key alternative at the protocol layer. #### What each venue requires Credential type is a property of the venue, not of PMXT. See [Supported Venues](https://pmxt.dev/docs/concepts/venues) for the full credential matrix. The difference between modes is **where the credential lives**: - **Hosted (Polymarket, Opinion, Limitless):** PMXT never sees the venue private key. You sign EIP-712 locally against the `PreFundedEscrow` domain; PMXT's operator submits with its own key for gas. See [Hosted trading](https://pmxt.dev/docs/concepts/hosted-trading). - **Self-hosted (any venue):** the venue credential lives on your machine, in your `pmxt-core` process. #### Recommendations > **Note:** **Use a separate trading wallet** holding only the float you're actively trading. PMXT is non-custodial, but limiting wallet balances limits blast radius from any host compromise on your end. > **Note:** **Pick hosted unless you have a specific reason not to.** Self-host when you need raw venue credentials, sub-100ms latency, or regulatory custody requirements. ##### Best practices - **Rotate credentials regularly.** If a venue supports key rotation (Kalshi, Polymarket L2 API keys), rotate on a schedule. - **Monitor your positions.** Set up alerts on your venue accounts for unexpected trades or withdrawals. - **Limit wallet balances.** Keep only active trading capital in the wallet you use with PMXT. - **Revoke unused PMXT API keys.** If you stop using hosted, revoke your key from the [dashboard](https://pmxt.dev/dashboard) so it cannot be used to make calls on your behalf. (Self-hosted does not need a `pmxt_api_key`.) #### Disclaimer **PMXT is provided "as is" without warranty of any kind.** By using PMXT in either mode, you acknowledge and accept the following: - **You are responsible for the security of your credentials.** In hosted mode, your private key never reaches PMXT, but your `pmxt_api_key` and your signing wallet are still yours to protect. In self-hosted mode, the entire venue credential surface is on your machine and on you. - **PMXT is not responsible for any lost, stolen, or mismanaged funds.** This includes but is not limited to losses from unauthorized trades, venue API changes, software bugs, service outages, smart-contract bugs in the unaudited `PreFundedEscrow` (see [Hosted trading](https://pmxt.dev/docs/concepts/hosted-trading)), or security incidents. - **On-chain transactions are irreversible.** Blockchain transactions cannot be reversed or charged back. Funds sent or traded through on-chain venues are final. - **You should only risk what you can afford to lose.** Prediction markets carry inherent financial risk independent of the software used to access them. For users who require full custody and zero third-party trust, [self-hosted](https://pmxt.dev/docs/guides/self-hosted) is the right mode. ### Rate Limits Source: https://pmxt.dev/docs/rate-limits Every API key includes the full surface — cross-venue search, unified schema, and the supported venue pass-through. Limits are **per API key** and apply across both hosts (`api.pmxt.dev` and `trade.pmxt.dev`) — see [hosts](https://pmxt.dev/docs/authentication#hosts). #### Limits | Plan | Monthly credits | Per-minute | WebSocket streams | Price | | ---- | --------------- | ---------- | ----------------- | ----- | | Free | 25,000 | 60 | 5 | $0/mo | | Starter | 250,000 | 300 | 20 | $29.99/mo | | Pro | 1,000,000 | 1,000 | 100 | $99.99/mo | | Enterprise | Custom | Custom | Custom | [Contact us](https://pmxt.dev/pricing) | Limits apply per API key across both hosts (`api.pmxt.dev` and `trade.pmxt.dev`). Credit accounting: **1 REST call = 1 credit**, **1 WebSocket message = 0.1 credits**. Health checks are free. #### Rate limit responses When you hit a limit, the API returns `429 Too Many Requests` with the window and retry timing. The SDKs handle short bursts automatically with exponential backoff. **Per-minute:** ```json { "error": "rate_limit_exceeded", "plan": "free", "limit": 60, "used": 60, "window": "1 minute" } ``` **Per-month:** ```json { "error": "monthly_quota_exceeded", "plan": "free", "limit": 25000, "used": 25000 } ``` #### Usage dashboard Every request shows up under **Usage > Request Log** with endpoint, status, and latency. The same counts power the quotas — what you see is what's being counted. #### Need more? - **[Pricing](https://pmxt.dev/pricing)** — See plans and limits, or reach out for custom quotas. ## Concepts ### Prediction Markets 101 Source: https://pmxt.dev/docs/concepts/prediction-markets-101 A glossary for engineers landing in prediction markets for the first time. Skim it once; come back when an unfamiliar term shows up. #### Core concepts ##### Prediction market A market where the traded asset is a share in a possible future outcome. The share pays $1 if the outcome resolves true and $0 if it resolves false. Example: a "Yes" share on *"Will the Fed cut rates in March?"* pays $1 if the cut happens. ##### Event, Market, Outcome PMXT's catalog is three levels deep. An **Event** is a real-world question ("2024 US Presidential Election"). A **Market** is a specific tradable framing of that event ("Will Trump win the 2024 election?"). An **Outcome** is a single side of the market ("Yes" or "No"). One event can have many markets; one market always has two or more outcomes. See [Unified schema](https://pmxt.dev/docs/concepts/unified-schema) for the schema-level detail. ##### Price as probability Outcome prices are in the closed interval [0, 1] and are read as the market's implied probability that the outcome resolves true. A "Yes" share trading at 0.31 pays $1 if Yes resolves true, so the market is implying a roughly 31% chance. The "Yes" price plus the "No" price for a binary market sum to approximately 1 (minus venue spread). ##### Catalog UUID vs venue ID PMXT assigns its own stable UUID to every market and outcome. The underlying venue (Polymarket, Kalshi, etc.) also has its own native ID — a hex condition ID, a ticker, a hash. The hosted trading API accepts either — pass whatever your code already has; the SDK picks the right wire field. Cross-venue identity (Router results, portfolio analytics) is the place where the catalog UUID matters. See [Catalog UUID vs Venue ID](https://pmxt.dev/docs/concepts/catalog-uuid-vs-venue-id). #### Crypto / EVM terms (for the non-crypto reader) ##### Wallet An on-chain account, identified by a public address (e.g. `0xAbc...`). It can hold tokens and sign transactions. A wallet is controlled by a **private key**. ##### EVM private key A 32-byte secret that controls a wallet address on any Ethereum-compatible (EVM) chain. The same key works across Ethereum, Polygon, BSC, Arbitrum, and other EVM chains — the address derives from the key, not the chain. PMXT only uses your private key for local signing; it never leaves your machine. ##### Polygon A specific EVM-compatible chain (chainId 137). PMXT's `PreFundedEscrow` contract and Polymarket's CLOB exchange both live on Polygon. This is the only chain you fund. ##### USDC A US-dollar-pegged stablecoin (1 USDC ≈ $1) issued on many chains. PMXT hosted trading settles in **USDC.e on Polygon** (`0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174`) — the bridged token Polymarket's CTF exchange uses. Native Polygon USDC and bridged USDC on other chains are not interchangeable. ##### EIP-712 The typed-data signing standard PMXT uses for hosted writes. Instead of signing an opaque hex blob, your wallet signs a structured payload with named fields — making the signature human-auditable and replay-resistant. Every hosted order, cancel, and escrow action is an EIP-712 signature. See [Signing](https://pmxt.dev/docs/guides/signing). ##### PreFundedEscrow The non-custodial smart contract on Polygon that holds USDC for hosted trading. You deposit USDC once; subsequent trades spend from the escrow balance until you withdraw. Funds can only move against an EIP-712 signature from your wallet, so your wallet retains beneficial ownership the entire time. See [Hosted Trading](https://pmxt.dev/docs/concepts/hosted-trading) and [Escrow Lifecycle](https://pmxt.dev/docs/guides/escrow-lifecycle). #### Trading terms ##### CLOB Central Limit Order Book. The matching-engine model where bids and asks are stored in a sorted book and orders match against the best available price. Most prediction-market venues (Polymarket, Kalshi) run a CLOB. ##### Market order An order that executes immediately at the best available price, walking the book until filled. You specify size, not price. Set a generous `slippage_pct` to bound the worst price you'll accept. ##### Limit order An order that rests on the book at a specified price and only executes when a counterparty crosses it. You specify both price and size. ##### Group A methods In PMXT, **Group A** refers to the shared trading and account method surface for venues that implement those capabilities — `create_order`, `fetch_balance`, `fetch_positions`, `fetch_my_trades`, and friends. Method names and response schemas are unified where supported; hosted write support is venue-specific. See [Hosted Trading](https://pmxt.dev/docs/concepts/hosted-trading#whats-supported-today), [Supported Venues](https://pmxt.dev/docs/concepts/venues#feature-support), and [API Reference](https://pmxt.dev/docs/api-reference/overview). #### Hosted vs self-hosted ##### Hosted (default) PMXT operates the catalog, the escrow contract, and the venue-submit path. You hold a `pmxt_api_key` and a wallet private key; PMXT does everything else. See [Hosted Trading](https://pmxt.dev/docs/concepts/hosted-trading). ##### Self-hosted You run `pmxt-core` on your own machine, supply raw venue credentials, and the SDK talks directly to the venues. No API key, no escrow, no PMXT in the trade path. See [Hosted Trading](https://pmxt.dev/docs/concepts/hosted-trading#when-to-switch-to-self-hosted) for the comparison. ### Unified Schema Source: https://pmxt.dev/docs/concepts/unified-schema PMXT normalizes every supported venue to the same three-level shape: ``` Event (a topic, like "2028 US Presidential Election") └─ Market (a concrete question, like "Will Gavin Newsom win?") └─ Outcome (a tradable side, like "Yes" @ 0.31) ``` Every response — from the local server, from the hosted Router, or from a hosted venue pass-through — uses the same field names. Write your client once, switch venues with a config change. > **Note:** PMXT serves reads from a cached Postgres catalog for speed (~10 ms), or passes through live to the venue for freshness. The SDK chooses per-method based on whether the catalog can safely answer; the response envelope is identical either way. #### UnifiedMarket | Field | Type | Notes | | ----------------- | ----------------- | --------------------------------------------------------- | | `marketId` | `string` | Source-scoped market id; hosted catalog rows use PMXT UUIDs, while venue clients may use venue-native ids. | | `eventId` | `string \| null` | Parent event, if any. | | `title` | `string` | Human-readable market question. | | `slug` | `string \| null` | Venue-native slug, when available. | | `description` | `string \| null` | Long-form resolution criteria. | | `url` | `string \| null` | Canonical venue URL for the market. | | `image` | `string \| null` | Venue-hosted image. | | `category` | `string \| null` | Normalized category (e.g. `Sports`, `Politics`). | | `tags` | `string[] \| null`| Free-form tags from the venue. | | `volume` | `number` | All-time volume, in USD (or venue base unit). | | `volume24h` | `number` | Trailing-24h volume. | | `liquidity` | `number` | Depth metric — venue-specific, compared like-for-like. | | `resolutionDate` | `string \| null` | ISO 8601, UTC. | | `tickSize` | `number \| null` | Minimum price increment, when the venue exposes one. | | `status` | `string \| null` | Venue status — `active`, `closed`, `resolved`, ... | | `contractAddress` | `string \| null` | On-chain address, when the venue is on-chain. | | `outcomes` | `UnifiedOutcome[]`| See below. | #### UnifiedOutcome | Field | Type | Notes | | ----------------- | ------------------ | --------------------------------------------------------- | | `outcomeId` | `string` | Source-scoped outcome id; hosted catalog rows use PMXT UUIDs, while venue clients use venue-native trading ids. | | `marketId` | `string` | Back-reference to the parent market. | | `label` | `string` | Display label (e.g. `Yes`, `No`, `Donald Trump`). | | `price` | `number` | Last price, 0–1 for binary venues. | | `priceChange24h` | `number` | Absolute change vs 24h ago. | | `metadata` | `object` | Venue-specific fields (e.g. `clobTokenId`). | > **Note:** Identifier semantics are source-aware: Router and hosted catalog rows use stable PMXT UUIDs for `marketId` and `outcomeId`. Venue clients and local pass-throughs may use venue-native identifiers. Do not assume every `marketId` / `outcomeId` is a UUID or every outcome id is venue-native. See [Catalog UUID vs Venue ID](https://pmxt.dev/docs/concepts/catalog-uuid-vs-venue-id) when crossing between Router results, hosted trading, and venue-direct clients. #### UnifiedEvent | Field | Type | Notes | | -------------- | ----------------- | ------------------------------------------------------------ | | `id` | `string` | Stable PMXT event id. | | `title` | `string` | Event title. | | `slug` | `string \| null` | | | `description` | `string \| null` | | | `category` | `string \| null` | | | `tags` | `string[] \| null`| | | `volume` | `number` | | | `volume24h` | `number` | | | `url` | `string \| null` | | | `image` | `string \| null` | | | `markets` | `UnifiedMarket[]` | Child markets. Nested on every event response — no second fetch needed. | > **Note:** Fields that a specific venue doesn't expose come back as `null`. That's intentional — `null` always means "venue has no value for this field", never "PMXT failed to populate it". #### Where the shape is defined The canonical definition lives in [`core/src/types.ts`](https://github.com/pmxt-dev/pmxt/blob/main/core/src/types.ts) in the `pmxt` repo, and every SDK client (TypeScript and Python) mirrors it. The [OpenAPI reference](https://pmxt.dev/docs/api-reference/overview) is generated from the same source on every `pmxt-core` release. ### Supported Venues Source: https://pmxt.dev/docs/concepts/venues PMXT currently supports the following venue targets. The **wire key** is the value you pass in the URL — e.g. `POST /api/polymarket/fetchMarkets` or `new pmxt.Polymarket({})` from the SDKs. | Venue | Wire Key | Pass-Through Base | | ----- | -------- | ----------------- | | Polymarket | `polymarket` | `POST /api/polymarket/:method` | | Kalshi | `kalshi` | `POST /api/kalshi/:method` | | Kalshi (Demo) | `kalshi-demo` | `POST /api/kalshi-demo/:method` | | Limitless | `limitless` | `POST /api/limitless/:method` | | Probable | `probable` | `POST /api/probable/:method` | | Baozi | `baozi` | `POST /api/baozi/:method` | | Myriad | `myriad` | `POST /api/myriad/:method` | | Opinion | `opinion` | `POST /api/opinion/:method` | | Metaculus | `metaculus` | `POST /api/metaculus/:method` | | Smarkets | `smarkets` | `POST /api/smarkets/:method` | | Polymarket US | `polymarket_us` | `POST /api/polymarket_us/:method` | | Gemini Titan | `gemini-titan` | `POST /api/gemini-titan/:method` | | Hyperliquid | `hyperliquid` | `POST /api/hyperliquid/:method` | | SuiBets | `suibets` | `POST /api/suibets/:method` | | Rain | `rain` | `POST /api/rain/:method` | | Hunch | `hunch` | `POST /api/hunch/:method` | > **Note:** This list is regenerated automatically from the `ExchangeParam` enum in pmxt-core's OpenAPI spec on every `pmxt-core` upgrade. If a venue is missing here, it's not yet wired through pmxt-core. #### Catalog Venues The hosted catalog currently ingests the following venues: | Venue | Wire Key | Ingestion | | ----- | -------- | --------- | | Polymarket | `polymarket` | polling | | Kalshi | `kalshi` | polling | | Opinion | `opinion` | polling | | Limitless | `limitless` | polling | | Smarkets | `smarkets` | polling | | Probable | `probable` | polling | | Myriad | `myriad` | polling | #### Feature support Not every venue supports every method. Broadly: - **Catalog reads** (`fetchMarkets`, `fetchEvents`, `fetchMarket`, `fetchEvent`) — supported on every venue that the catalog ingests. - **Live reads** (`fetchOrderBook`, `fetchOHLCV`, `fetchTrades`) — supported where the venue exposes the data. - **Writes** (`createOrder`, `cancelOrder`, `fetchBalance`, `fetchPositions`) — supported where the venue has a trading API. See the [API Reference](https://pmxt.dev/docs/api-reference/overview) for the per-method matrix (inferred from the OpenAPI `operationId`s). ### Hosted Trading Source: https://pmxt.dev/docs/concepts/hosted-trading Hosted trading is PMXT's default execution path. You provide an API key and a wallet; your [USDC](https://pmxt.dev/docs/concepts/prediction-markets-101#usdc) sits in a non-custodial [`PreFundedEscrow`](https://pmxt.dev/docs/concepts/prediction-markets-101#prefundedescrow) smart contract on [Polygon](https://pmxt.dev/docs/concepts/prediction-markets-101#polygon) that can only move funds against an EIP-712 signature from your wallet; 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. > **Note:** **One chain to fund: Polygon.** Hosted trading for Polymarket, Opinion, and Limitless uses a single Polygon-based `PreFundedEscrow` contract. Fund USDC on Polygon once; the same EVM wallet/key can trade the currently hosted venues. For non-Polygon hosted venues like Opinion (on BSC) and Limitless (on Base), PMXT handles cross-chain settlement transparently — you never sign anything on a chain other than Polygon. #### When hosted mode is right 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, Opinion, and Limitless. - **Anyone who doesn't want to operate infrastructure.** A hosted trade has three actors: **your client** (holding the `pmxt_api_key` and, for writes, the user's `private_key`), **`trade.pmxt.dev`** (PMXT's hosted trading API — routes orders, operates against the escrow contract under user-signed authorization, talks to the venue), and **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](https://pmxt.dev/docs/concepts/prediction-markets-101#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 outcome target, side, and amount. Catalog UUIDs go on the wire as `outcome_id`; venue-native outcomes go on the wire as `venue` + `venue_outcome_id`. 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](https://pmxt.dev/docs/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. Settlement model varies by venue: - **Polymarket** — same-chain on Polygon. The user signs one EIP-712 `OrderParams` against the Polygon `PreFundedEscrow` domain. Settlement is direct against the Polymarket CTF exchange. No oracle, no cross-chain hop. - **Opinion BUY** — cross-chain Polygon → BSC, **single signature + oracle-attested delivery-versus-payment (DvP)**. The user signs one `CrossChainOrderParams` payload on the Polygon `PreFundedEscrow` domain, authorizing a capped spend of their USDC. The operator fronts BSC working capital, buys on Opinion's CLOB, and calls `depositForUser` on the BSC `VenueEscrow` to credit the user with the real outcome tokens. An independent **settlement oracle** observes the BSC delivery and signs a `DeliveryAttestation`. The operator submits the attestation to `settleCrossChainBuy` on Polygon, which verifies the user signature, the attestation binding (order, user, token, destination escrow), the oracle signature, a non-zero `tokensDelivered`, and a per-token worst-price check before releasing the user's USDC. The operator cannot pull USDC until it has provably delivered tokens. Trust assumption: honest oracle. - **Opinion SELL** — cross-chain BSC → Polygon, **dual-signed parallel, no oracle**. The user signs two EIP-712 payloads at build time: a Polygon pay leg (`CrossChainSellPayParams` on the `PreFundedEscrow` domain) and a BSC pull leg (`CrossChainSellPullParams` on the `VenueEscrow` domain). The operator fires both in parallel: `settleCrossChainSellUSDC` credits the user's USDC on Polygon, `settleCrossChainSellTokens` pulls the outcome tokens from the user's BSC escrow. Time-to-tokens is roughly one block. The 2×2 outcome matrix's row 3 (pay failed, pull succeeded) is the operator-trusted gap today; a future bond backstop will close it. - **Limitless** — cross-chain Polygon → Base, **v1 signature-gated front-and-reimburse** (ERC-7683). The user signs one ERC-7683 order via EIP-712 on the Polygon escrow domain: "buy ≥Y tokens, debit ≤X USDC, worstPrice, deadline, nonce". The operator fronts Base working capital, buys on Limitless' CLOB, delivers the real tokens to the user's Base escrow, then pulls the capped USDC on Polygon against the signature. **v1 trust asterisk:** the pull is gated by the signature alone, so reimbursement carries a delivery-performance trust point on the operator. v2 will gate the pull on a Base → Polygon delivery proof (UMA optimistic or ZK). `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 shared address space Catalog UUIDs are the most portable identifiers for hosted trading. PMXT's catalog assigns a stable UUID to every `prediction_markets.markets` row and every `prediction_markets.outcomes` row, so those IDs are the right choice for Router results, cross-venue analytics, and records you want to survive venue API changes. Raw REST can use catalog UUIDs or an explicit `venue` + `venue_outcome_id` pair. Bare Polymarket `conditionId` / `tokenId` strings are not enough by themselves; include the venue tuple so the backend can reverse-resolve the catalog row. 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. > **Note:** The SDK accepts either form. Outcomes returned by `pmxt.Polymarket(...).fetch_markets(...)` carry a venue-native ID; outcomes returned by `pmxt.Router(...)` carry a catalog UUID. Pass either straight to `create_order` / `build_order` — the SDK forwards it as `outcome_id` (UUID) or `(venue, venue_outcome_id)` (venue-native) on the wire. Raw REST against `trade.pmxt.dev` follows the same shape. See [Catalog UUID vs venue ID](https://pmxt.dev/docs/concepts/catalog-uuid-vs-venue-id). #### Custody: PreFundedEscrow **Non-custodial by construction** — escrow contracts enforce that funds can only move against an EIP-712 signature from the user's wallet (plus, for cross-chain buys, an oracle attestation). PMXT does not hold USDC in a hot wallet, an exchange-style omnibus account, or a multisig. The user's wallet retains beneficial ownership at all times. USDC custody lives on **Polygon** in the `PreFundedEscrow` contract (chainId 137, sometimes called `HomeEscrow`). Outcome-token custody on non-Polygon venues lives on the venue's native chain: - **Polygon `PreFundedEscrow`** holds the user's USDC (USDC.e, `0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174`) and serves the payment legs for Polymarket, Opinion, and Limitless. Polymarket CTF tokens are custodied here too — same-chain. - **BSC `VenueEscrow`** (chainId 56) holds Opinion [outcome](https://pmxt.dev/docs/concepts/prediction-markets-101#event-market-outcome) tokens. For Opinion BUYs the operator deposits tokens here on the user's behalf (no user signature on BSC). For Opinion SELLs the user signs a BSC-domain pull leg authorizing the operator to debit this escrow. - **Base escrow** holds Limitless outcome tokens. The operator delivers tokens here after buying on Limitless' CLOB; the user funds and signs only on Polygon. USDC enters via `client.escrow.deposit_tx(amount)` on Polygon, exits via `client.escrow.withdraw_tx("request" | "claim" | "cancel", ...)`. Both build unsigned txs — your wallet signs and submits them. See [Escrow Lifecycle](https://pmxt.dev/docs/guides/escrow-lifecycle). ##### Deployed contracts Both contracts are non-upgradeable, non-pausable, with no Ownable role — only an immutable `operator` set at deploy time. | Contract | Chain | ChainId | Mainnet address | Explorer | | -------- | ----- | ------- | --------------- | -------- | | `PreFundedEscrow` (HomeEscrow) | Polygon | 137 | `0x3ad326f78b1390b9a5dc5f00e7f62f8632de23e2` | [Polygonscan](https://polygonscan.com/address/0x3ad326f78b1390b9a5dc5f00e7f62f8632de23e2) | | `VenueEscrow` | BSC | 56 | `0x6a273643d84edbb603b808d8a724fb963c7a298a` | [BscScan](https://bscscan.com/address/0x6a273643d84edbb603b808d8a724fb963c7a298a) | Current operator addresses (immutable on the deployed contracts above): - Polygon `PreFundedEscrow.operator`: `0x84194eae9c63C1e3A769976bb762506d3443156d` - BSC `VenueEscrow.operator`: `0x84194eae9c63C1e3A769976bb762506d3443156d` - Polygon `PreFundedEscrow.settlementOracle`: `0x8aD93c0D15bC655b460dD700B1E25C86D1a728EE` The operator address is an EOA controlled by PMXT. Because `operator` is `immutable` on the deployed contracts, switching to a multisig would mean deploying new escrow contracts and migrating user balances — not an in-place rotation. ##### Trust model There is no admin. The contracts have **no `Ownable`, no `Pausable`, no upgrade proxy, no upgrade timelock, no fee setter**. The privileged surface is the `onlyOperator` modifier in the shared `OutcomeCustody` base contract, and the `operator` address is `immutable` — set once in the constructor and unchangeable for the life of the contract. What this means concretely: - **No one can pause withdrawals.** Even if PMXT's operator key is compromised, `withdraw` / `claimWithdrawal` keep working — unilateral withdrawal is always available, even if PMXT's server is down. - **No one can change the operator.** A compromised PMXT cannot rotate to a new operator and skip user signatures. - **No one can upgrade the contract.** What's deployed is what runs forever. Every value-moving operator call requires either the user's EIP-712 signature, the user as `msg.sender`, or (for cross-chain buys) an additional delivery attestation signed by an independent settlement oracle. The `pmxt_api_key` is a **service-role credential**: the key holder can read any associated wallet's hosted data and forward signed orders, but **the key alone cannot move funds**. Keep `pmxt_api_key` on a server. Never ship it to a browser bundle. Never log it. ##### Audit status **Unaudited as of 2026-06-09.** The contracts have not undergone a third-party security review. The contract is non-pausable and unilateral withdrawal is always available — if you want to exit, the timelock is your kill-switch and PMXT cannot block it. An audit report will be linked here when one is completed. #### When to switch to self-hosted Self-hosted runs `pmxt-core` on your own machine; the SDK talks to `localhost`. Both modes use the same SDK classes and method names where the capability is supported. The differences are where execution happens, who holds keys, and which write venues each mode supports. | | Hosted (`pmxt_api_key` set) | Self-hosted (no API key) | | - | --------------------------- | ------------------------ | | **Install footprint** | `pip install pmxt` / `npm i pmxtjs` | SDK + pmxt-core local server | | **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** | Non-custodial: USDC in `PreFundedEscrow`, movable only against your EIP-712 signature | You retain venue-native custody | | **Latency** | ~150–300ms round-trip | Limited by venue + your network | | **Trading venues** | Polymarket, Opinion, Limitless | Venues with venue-native write support | | **Read-only venues** | All catalog venues via Router | All catalog venues via Router | | **Infra to run** | None | One local process | | **Regulatory posture** | Non-custodial escrow; user retains beneficial ownership and unilateral withdrawal | You as direct counterparty to the venue | Choose self-hosted when you need **sub-100ms latency**, want to use **raw venue credentials** (Polymarket L2 API keys, Kalshi RSA, Smarkets sessions), have **regulatory requirements** that mandate direct counterparty status with the venue, or want venue-native trading where the venue exposes writes but hosted mode does not. See [Self-hosted](https://pmxt.dev/docs/guides/self-hosted) for setup. #### 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) | | Limitless | Yes | Yes | Polygon buy leg, Base pull-sell leg | | Kalshi, Smarkets, Probable, Myriad, Metaculus, etc. | No | Read-only via catalog | Use [self-hosted](https://pmxt.dev/docs/guides/self-hosted) for venue-native writes where supported; check [Feature Support & Compliance](https://github.com/pmxt-dev/pmxt/blob/main/core/COMPLIANCE.md) | If a venue you need isn't here and it exposes writes, run pmxt-core locally and pass raw venue credentials — see [Self-hosted](https://pmxt.dev/docs/guides/self-hosted). #### Next - [Trading quickstart](https://pmxt.dev/docs/trading-quickstart) — 60-second guided path. - [Escrow lifecycle](https://pmxt.dev/docs/guides/escrow-lifecycle) — deposits, withdrawals, the timelock. - [Hosted errors](https://pmxt.dev/docs/guides/hosted-errors) — every error class and how to recover. ### Catalog UUID vs Venue ID Source: https://pmxt.dev/docs/concepts/catalog-uuid-vs-venue-id PMXT speaks two market identifier languages: - The **catalog UUID** is PMXT's 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 `conditionId` hex string + `tokenId` integer, a Kalshi ticker, an Opinion market hash, etc. **The hosted trading API accepts either.** Whichever you have in hand, you can pass it straight to `create_order` / `build_order` — the SDK picks the right wire field and the backend resolves it. No conversion step required. ```python # Both of these work against trade.pmxt.dev without a conversion step: # Via the venue client — outcome carries a venue-native id from fetch_markets. client = pmxt.Polymarket(pmxt_api_key=..., wallet_address=..., private_key=...) market = client.fetch_markets({"query": "trump 2028"})[0] order = client.create_order(outcome=market.yes, side="buy", amount=10, ...) # Via the Router — outcome carries a catalog UUID. router = pmxt.Router(pmxt_api_key=...) events = router.fetch_events(query="trump 2028") yes = events[0].markets[0].outcomes[0] order = client.create_order(outcome=yes, side="buy", amount=10, ...) ``` #### When the distinction matters It matters in exactly two places. ##### 1. Cross-venue identity A single real-world outcome (e.g. "Trump wins 2028") can exist on multiple venues. Each venue has its own native id — they don't agree. The **catalog UUID** is the only identifier stable across venues. Use it when: - You're consuming `fetch_matched_market_clusters` / `fetch_matched_event_clusters` to find the same outcome on multiple venues. - You're building portfolio analytics that span venues. - You're storing references that need to survive a venue re-listing (venue-native ids can change; the catalog UUID stays). ##### 2. Self-hosted trading In [self-hosted mode](https://pmxt.dev/docs/guides/self-hosted) — `POST /api/{exchange}/createOrder` — there is no PMXT catalog in the path. You're talking to the venue directly. **Venue-native ids only.** Catalog UUIDs won't resolve. #### Reverse-resolving a venue id to a catalog UUID If you already have a venue-native id (e.g. a saved database of Polymarket `conditionId`s) and want the catalog UUID for cross-venue work: The SDK does not expose a single-shot `venue_market_id` -> catalog UUID kwarg today. For batch or one-off reverse lookups, query the catalog directly via `POST /v0/sql`: ```sql SELECT market_id FROM prediction_markets.markets WHERE venue = 'polymarket' AND venue_market_id = '0xc704f74e2f9dfae70f770cb253ffadde10768eeab41233098bf5ac67995a94b5'; ``` `prediction_markets.markets` and `prediction_markets.outcomes` carry the canonical UUID alongside the venue-native fields (`venue_market_id`, `venue_outcome_id` / `token_id`), so the same query shape covers outcomes too. #### Examples (the two ids for one market) A single Polymarket binary market carries: - Catalog `market_id`: `2eeb03dc-404b-41d5-bc57-6aeb37927ae6` - Polymarket `conditionId`: `0xc704f74e2f9dfae70f770cb253ffadde10768eeab41233098bf5ac67995a94b5` Each outcome carries: - Catalog `outcome_id`: `a114f052-1fd1-4bcd-b9cf-de019db81b67` - Polymarket `tokenId`: `104932610032177696635191871147557737718087870958469629338467406422339967452218` Both forms work against `trade.pmxt.dev/v0/trade/*`. Pick whichever your code already has. ## Hosted Trading ### Escrow Lifecycle Source: https://pmxt.dev/docs/guides/escrow-lifecycle Hosted trading on PMXT settles through a non-custodial `PreFundedEscrow` smart contract on **Polygon** (chainId 137). USDC sits in the escrow under your wallet's beneficial ownership; PMXT cannot move funds without an EIP-712 signature from your wallet. This page walks through the full lifecycle: approve, deposit, trade, withdraw. > **Note:** **You fund Polygon only.** USDC always sits in the Polygon `PreFundedEscrow` for the currently hosted trading venues (Polymarket, Opinion, and Limitless). For hosted venues that don't settle on Polygon (Opinion on BSC, Limitless on Base), PMXT operates an internal cross-chain leg that holds outcome tokens on the venue's chain; you neither fund nor sign anything on that side. Pass any EVM-compatible private key; the same EVM wallet/key can trade the currently hosted venues. #### 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 against the Polygon escrow's domain; the escrow contract can only spend USDC against signed orders, never unilaterally. For Opinion, the same Polygon escrow balance funds a cross-chain settlement leg into a BSC-side `VenueEscrow` that holds Opinion outcome tokens — you still only deposit on Polygon, and your EIP-712 signature still targets the Polygon escrow domain. The BSC hop is PMXT's plumbing. #### The `client.escrow` namespace Hosted exchange clients (Polymarket, Opinion, Limitless) 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 and deposit Before the first deposit, the escrow contract needs permission to pull USDC from your wallet (a standard ERC-20 approval). Then you deposit. For most users, the dashboard handles both in a single connected-wallet flow. ##### Recommended: use the dashboard Open [pmxt.dev/dashboard](https://pmxt.dev/dashboard), connect the wallet whose address you passed to the SDK, and use the Deposit flow. Your wallet (MetaMask, Rabby, WalletConnect, etc.) prompts you to sign — PMXT never sees your private key. 1. Go to [pmxt.dev/dashboard](https://pmxt.dev/dashboard) and connect the matching wallet. 2. Click **Deposit** and enter the USDC amount. 3. Approve the USDC allowance in your wallet (one-time per token). 4. Confirm the deposit transaction in your wallet. > **Note:** Approval is one-time per token + spender; the deposit itself is one-time setup before your first trade. If you ever rotate the escrow contract (rare, gated on a PMXT migration announcement), you'll need to re-approve. If you're scripting deposits (CI, treasury automation, custom wallet integrations), `client.escrow` builds **unsigned** transactions you sign and broadcast with your own wallet library. **Approve** ```python Python # Unlimited approval for USDC (most common) result = client.escrow.approve_tx("usdc") # result is a dict wrapping the unsigned tx, e.g.: # { "tx": { "to": "0x...", "data": "0x...", "value": "0", # "chainId": 137, "gas": "...", "maxFeePerGas": "...", # "maxPriorityFeePerGas": "...", "nonce": "..." } } tx = result["tx"] # Sign and broadcast `tx` with your preferred wallet library. # Or scope the approval to a specific wei amount result = client.escrow.approve_tx("usdc", amount_wei=1_000_000_000) # 1,000 USDC tx = result["tx"] # Approve the Polymarket CTF token (only needed for direct CTF transfers) result = client.escrow.approve_tx("ctf") tx = result["tx"] ``` ```typescript TypeScript // Unlimited approval for USDC (most common) const { tx } = await client.escrow.approveTx("usdc"); // The returned object wraps the unsigned tx, e.g.: // { tx: { to: "0x...", data: "0x...", value: "0", chainId: 137, // gas: "...", maxFeePerGas: "...", // maxPriorityFeePerGas: "...", nonce: "..." } } // Or scope the approval to a specific wei amount (bigint) const { tx: txScoped } = await client.escrow.approveTx("usdc", 1_000_000_000n); // Approve the CTF token const { tx: ctfTx } = await client.escrow.approveTx("ctf"); ``` **Deposit** Amounts are in **whole USDC** (6 decimals); the SDK validates precision and rejects values like `0.0000001`. ```python Python # Deposit 10 USDC result = client.escrow.deposit_tx(amount=10.0) tx = result["tx"] # Sign and send `tx` with your wallet library. # Decimals up to 6 places are supported tx = client.escrow.deposit_tx(amount=10.5)["tx"] tx = client.escrow.deposit_tx(amount=10.123456)["tx"] ``` ```typescript TypeScript // Deposit 10 USDC — number, decimal string, or wei BigInt all accepted const { tx } = await client.escrow.depositTx(10); const { tx: tx2 } = await client.escrow.depositTx("10.5"); const { tx: tx3 } = await client.escrow.depositTx(10_500_000n); // wei micro-USDC ``` > **Warning:** USDC precision is 6 decimals. The SDK rejects `0.0000001` with `ValidationError`. Pre-round before passing the value in. #### 2. Confirm the deposit After the deposit transaction confirms on-chain, the balance shows up in escrow. `fetch_balance` is the canonical check. ```python Python balances = client.fetch_balance() usdc = balances[0] print(f"Available: {usdc.available}, Locked: {usdc.locked}, Total: {usdc.total}") ``` ```typescript TypeScript const balances = await client.fetchBalance(); const usdc = balances[0]; console.log(`Available: ${usdc.available}, Locked: ${usdc.locked}, Total: ${usdc.total}`); ``` > **Note:** 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. #### 3. 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, ) ``` #### 4. 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 you a window to detect and abort an unauthorized withdrawal even if the operator key were compromised. Because the escrow is non-custodial, every withdrawal step requires a signature from your wallet. ##### Recommended: withdraw from the dashboard The dashboard handles the full request → wait → claim journey as one user flow. 1. Open [pmxt.dev/dashboard](https://pmxt.dev/dashboard) and connect your wallet. 2. Click **Withdraw**, enter the USDC amount, and confirm the request transaction in your wallet. 3. Wait for the timelock to elapse (~1 hour in production). The dashboard shows a countdown for each pending request. 4. Come back, click **Claim** on the matured request, and confirm the claim transaction in your wallet. Funds move from escrow to your wallet. If you change your mind during the timelock window, the dashboard exposes a **Cancel** action on the pending request — the funds stay in escrow as free balance. For scripted withdrawals, `client.escrow.withdraw_tx` builds **unsigned** request / claim / cancel transactions you sign and broadcast with your own wallet library. **Request** ```python Python tx = client.escrow.withdraw_tx("request", amount=10.0)["tx"] # Sign and send `tx`. 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")["tx"] ``` ```typescript TypeScript const { tx } = await client.escrow.withdrawTx("claim"); ``` > **Note:** `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")["tx"] ``` ```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](https://pmxt.dev/docs/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) ### Signing Orders Source: https://pmxt.dev/docs/guides/signing Hosted trading writes are authenticated by an **EIP-712 typed-data signature** 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. #### What the SDK does for you Pass `private_key` to the exchange constructor and you're done. The SDK auto-wraps it into a local signer (`EthAccountSigner` in Python, `EthersSigner` in TypeScript), builds the typed-data payload for the right venue, signs locally, and submits the signature. You never see the typed-data shape. ```python Python import pmxt client = pmxt.Polymarket( pmxt_api_key="pmxt_live_...", wallet_address="0xYourWallet...", private_key="0xYourPrivateKey...", ) 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"; const client = new Polymarket({ pmxtApiKey: "pmxt_live_...", walletAddress: "0xYourWallet...", privateKey: "0xYourPrivateKey...", }); const order = await client.createOrder({ marketId: "2eeb03dc-404b-41d5-bc57-6aeb37927ae6", outcomeId: "a114f052-1fd1-4bcd-b9cf-de019db81b67", side: "buy", type: "market", amount: 5, denom: "usdc", slippage_pct: 30, }); ``` ##### Single-signature vs dual-signature flows Most hosted writes are **single-signature** — one EIP-712 payload, surfaced as `built.raw["typed_data"]`. This covers: - Polymarket buys and sells — `OrderParams` on the Polygon `PreFundedEscrow` domain. - Opinion BUY — `CrossChainOrderParams` on the Polygon `PreFundedEscrow` domain (the BSC delivery leg needs no user signature; the user is the recipient). - Limitless buys and sells — ERC-7683 order on the Polygon escrow domain. **Opinion SELL is the one dual-signature case.** The user gives up tokens custodied on BSC, so the SDK builds two payloads at once: - `built.raw["typed_data"]` — `CrossChainSellPayParams` on the Polygon `PreFundedEscrow` domain (the operator's pay leg into the user's Polygon balance). - `built.raw["pull_typed_data"]` — `CrossChainSellPullParams` on the BSC `VenueEscrow` domain (the operator's pull leg against the user's BSC outcome tokens). The SDK signs both with the same EVM private key automatically — no extra wiring. The two domains are distinct (`PreFundedEscrow` on chainId 137, `VenueEscrow` on chainId 56) and each signature only authorizes its own chain's leg. #### 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 hosted state for that wallet: ```python import pmxt read_only = pmxt.Polymarket( pmxt_api_key="pmxt_live_...", wallet_address="0xSomeOtherWallet...", # no private_key ) balance = read_only.fetch_balance() # works ``` > **Warning:** 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](https://pmxt.dev/docs/concepts/hosted-trading#trust-model). #### Advanced: bring your own signer If your key lives in a hardware wallet, HSM, MPC service, or anything that isn't a raw hex private key, skip `private_key` and inject a custom signer at construction. The SDK uses it transparently for every hosted write — you keep calling `create_order` as usual. The signer protocol is one method: `sign_typed_data(typed_data: dict) -> hex` (Python) or `signTypedData(typedData): Promise` plus a readonly `address` (TypeScript). ```python Python import pmxt client = pmxt.Polymarket( pmxt_api_key="pmxt_live_...", wallet_address="0xYourWallet...", signer=my_custom_signer, # exposes sign_typed_data(typed_data) -> hex ) order = client.create_order( market_id="2eeb03dc-...", outcome_id="a114f052-...", side="buy", order_type="market", amount=5.0, denom="usdc", slippage_pct=30.0, ) ``` ```typescript TypeScript import { Polymarket } from "pmxtjs"; const client = new Polymarket({ pmxtApiKey: "pmxt_live_...", walletAddress: "0xYourWallet...", signer: myCustomSigner, // { address, signTypedData(typedData) -> Promise } }); const order = await client.createOrder({ marketId: "2eeb03dc-...", outcomeId: "a114f052-...", side: "buy", type: "market", amount: 5, denom: "usdc", slippage_pct: 30, }); ``` For Opinion SELL's dual-signature flow, the same injected signer is invoked twice (once for the Polygon pay leg, once for the BSC pull leg) — no extra wiring. The SDK's `EthAccountSigner` (Python) / `EthersSigner` (TypeScript) are reference implementations; ethers v6 supports Ledger out of the box, and for MPC see Fireblocks / Privy / Turnkey docs. ### Handling Hosted Errors Source: https://pmxt.dev/docs/guides/hosted-errors 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](https://pmxt.dev/docs/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](https://pmxt.dev/docs/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.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 order is below one of Polymarket's two independent venue-side minimums on marketable BUY orders. Both rules are enforced by the venue itself (not PMXT), and the **higher of the two binds**: - **5-share minimum.** A $2 buy at $0.78/share is only 2.5 shares — rejected. - **$1 notional minimum (marketable BUY).** A 5-share buy at $0.138/share is $0.69 — passes the 5-share rule but is rejected by the venue with the literal error `invalid amount for a marketable BUY order ($0.69), min size: 1`. The effective minimum at that price is ~8 shares (~$1.10). **Detail string (5-share rule):** `Order size 2.564 below the minimum 5 shares for venue polymarket`. **Detail string ($1 rule, passed through from venue):** `invalid amount for a marketable BUY order ($X), min size: 1`. **Parent classes:** `InvalidOrder`, `HostedTradingError`. **Recovery:** size up the order or pick a cheaper outcome. > **Warning:** 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`. > **Warning:** 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, **kwargs): # In hosted mode, create_order handles build -> sign -> submit atomically. # On BuiltOrderExpired the internal submit raced the 30s TTL; just call again. for attempt in range(2): try: return client.create_order(**kwargs) except BuiltOrderExpired: if attempt == 1: raise continue ``` ```typescript TypeScript import { BuiltOrderExpired } from "pmxtjs"; import type { CreateOrderInput, Order } from "pmxtjs"; async function submitWithRetry(client, params: CreateOrderInput): Promise { // In hosted mode, createOrder handles build -> sign -> submit atomically. // On BuiltOrderExpired the internal submit raced the 30s TTL; just call again. for (let attempt = 0; attempt < 2; attempt++) { try { return await client.createOrder(params); } catch (e) { if (e instanceof BuiltOrderExpired && attempt === 0) continue; throw e; } } throw new Error("unreachable"); } ``` #### 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, pick a different outcome, or post a resting limit order via `client.create_order(order_type="limit", price=..., amount=...)`. Limit BUY and SELL are supported, and both use `denom="shares"` in hosted SDK requests. ```python Python from pmxt._hosted_errors import NoLiquidity try: client.create_order(order_type="market", ...) except NoLiquidity: # Wait, retry against a different outcome, or post a hosted limit order. pass ``` ```typescript TypeScript import { NoLiquidity } from "pmxtjs"; try { await client.createOrder({ type: "market", ... }); } catch (e) { if (e instanceof NoLiquidity) { // Wait, retry against a different outcome, or post a hosted limit order. } else { throw e; } } ``` #### Workaround warnings > **Warning:** **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. > **Warning:** **Marketable limit price selection.** The SDK's `_validate_worst_price` gate applies a slippage buffer on top of the book — it is NOT simply "must cross top-of-book". For an immediately-executable price: - **Marketable limit BUY:** use `price = best_ask` (a small +1-tick buffer works too). - **Marketable limit SELL:** use `price = best_ask` (i.e. at or above the ask), NOT `best_bid` or `best_bid - 0.01`. Posting at `best_bid` looks marketable in book terms, but the validator's slippage floor (currently `worst_price ≥ best_bid × 0.8 + 0.029`, see `_hosted_typeddata.py:519`) will reject it for thin books or low-price outcomes. Pricing at or above `best_ask` is the reliable rule. > **Warning:** If you see `OutcomeNotFound` despite having a valid-looking ID, the SDK could not resolve it on either the catalog UUID or `(venue, venue_outcome_id)` path. Most commonly: the outcome was resolved against a different venue than the client's `exchange_name`, or the outcome has since been removed from the catalog. See [Catalog UUID vs venue ID](https://pmxt.dev/docs/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](https://pmxt.dev/docs/api-reference/errors). ### Coming from Polymarket's official SDK Source: https://pmxt.dev/docs/guides/migrate-to-hosted-trading > **Note:** If you're not already using Polymarket's SDK (`@polymarket/clob-client` / `py-clob-client`), skip this page. Start at [Trading quickstart](https://pmxt.dev/docs/trading-quickstart). The migration is **not** a 1:1 credential swap. PMXT's hosted trading replaces Polymarket's L2 auth and CTF-proxy custody with a service-role API key plus an EIP-712 escrow contract on Polygon. The two tables below are the load-bearing part. #### Credential mapping Polymarket's L2 auth — `apiKey` / `apiSecret` / `passphrase` derived from a one-time signature and used to HMAC every order — does not translate. Hosted PMXT collapses authentication to two things: a service-role `pmxt_api_key` and the same EOA private key you already use for EIP-712 signing. | Polymarket | PMXT hosted | | ---------- | ----------- | | `apiKey` + `apiSecret` + `passphrase` (L2) | `pmxt_api_key` (server secret) | | `signer` private key (EOA) | `private_key` (same EOA) | | `funder` address (proxy or EOA) | `wallet_address` (EOA, not the proxy) | **Discard:** the L2 key derivation, the HMAC code path, any per-user persistence of `apiSecret` / `passphrase`. **Keep:** the EVM signer key. #### signatureType mapping Polymarket's `signatureType` flag tells the CLOB which custody shape to expect: - `0` — EOA signs, EOA holds funds. - `1` — EOA signs, USDC proxy ("magic wallet") holds funds. - `2` — EOA signs, Gnosis Safe proxy holds funds. Hosted PMXT does not speak any of these. The hosted exchange signs EIP-712 against `PreFundedEscrow` on Polygon (chainId 137); settlement is `PreFundedEscrow.settleBuy` / `.settleSell`, not Polymarket's `Exchange.fillOrder`. **Delete any order-construction code that branches on `signatureType`.** There is one shape. Your EOA still signs. Any EVM key (raw, hardware, AA) works. #### USDC.e is the same, allowances are not Both stacks settle in Polygon **USDC.e** (`0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174`), not native USDC (`0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359`). No swap needed. But an existing approval to your Polymarket CTF proxy does **not** carry over — that allowance is to the proxy, not to `PreFundedEscrow`. Approve + deposit once: ```python Python tx = client.escrow.approve_tx("usdc") # once per EOA tx = client.escrow.deposit_tx(amount=500.0) # per top-up ``` ```typescript TypeScript const approve = await client.escrow.approveTx("usdc"); const deposit = await client.escrow.depositTx(500); ``` Your Polymarket-proxy balance is untouched — withdraw it via Polymarket's UI, or leave it parked and fund PMXT with fresh capital. Both stacks coexist on the same EOA. #### Identifier migration: avoid bare conditionId / tokenId `trade.pmxt.dev` does not accept bare Polymarket `conditionId` / `tokenId` strings. Use catalog UUIDs when you have them, or send an explicit `venue` + `venue_outcome_id` pair; the SDK does this automatically when you pass outcomes returned by `client.fetch_markets`. That venue tuple gives PMXT enough context to resolve the catalog row. Reverse-resolve a saved universe via the Router or the SQL endpoint — see [Catalog UUID vs venue ID](https://pmxt.dev/docs/concepts/catalog-uuid-vs-venue-id). #### Rollback Drop `pmxt_api_key` from the constructor and the SDK falls back to venue-direct mode against a local `pmxt-core`. Withdraw any escrow balance via `client.escrow.withdraw_tx`. #### See also - [Trading quickstart](https://pmxt.dev/docs/trading-quickstart) - [Escrow lifecycle](https://pmxt.dev/docs/guides/escrow-lifecycle) - [Hosted errors](https://pmxt.dev/docs/guides/hosted-errors) ## Self-host ### Self-hosted (Advanced) Source: https://pmxt.dev/docs/guides/self-hosted 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 through `http://localhost:3847` instead of `trade.pmxt.dev`. Venue credentials stay on your machine; you talk to venue APIs directly. #### When self-hosted is the right call Pick self-hosted if **any** of these apply: - **Sub-100ms latency** — local submission removes a network hop. Matters for latency-sensitive arb / market-making. - **Raw venue credentials** — Polymarket L2 API keys, Kalshi RSA, Smarkets session cookies. Hosted mode doesn't accept these. - **Regulatory custody** — you must be the direct counterparty with no intermediary in the custody path. - **Venues hosted mode can't write to yet** — hosted writes are Polymarket, Opinion, and Limitless. Self-hosted can use venue-native credentials where the venue exposes writes (Kalshi, Smarkets, Probable, Myriad, Metaculus, etc.); check [Feature Support & Compliance](https://github.com/pmxt-dev/pmxt/blob/main/core/COMPLIANCE.md) for per-venue support. - **OSS contribution** — you're hacking on `pmxt-core` itself. #### When hosted is still the better choice Everyone else. [Hosted](https://pmxt.dev/docs/concepts/hosted-trading) is simpler to deploy (no local process), gives you on-chain custody you control via the timelock, and PMXT's server never sees a private key. If you're building a consumer app, a research notebook, or any non-latency-critical service against Polymarket, Opinion, or Limitless, stop here and use hosted. #### 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](https://pmxt.dev/docs/sdk/server) for the full reference. ```python import pmxt 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", order_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 import pmxt limitless = pmxt.Limitless(private_key="0x...") probable = pmxt.Probable(private_key="0x...") opinion = pmxt.Opinion(private_key="0x...") ``` ##### Smarkets — email + password ```python import pmxt smarkets = pmxt.Smarkets(email="you@example.com", password="...") ``` ##### Baozi — Solana keypair ```python import pmxt baozi = pmxt.Baozi(private_key="") ``` See [Supported Venues](https://pmxt.dev/docs/concepts/venues) for the full credential matrix. > **Note:** For venues that use a fund-controlling private key (Polymarket, Limitless, Probable, Opinion, Baozi), a practical habit is to keep only the float you're actively trading in this wallet. #### 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](https://pmxt.dev/docs/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 import pmxt 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 trading](https://pmxt.dev/docs/concepts/hosted-trading). - **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](https://pmxt.dev/docs/sdk/server) — start/stop/status/logs. - [Hosted trading](https://pmxt.dev/docs/concepts/hosted-trading) — full comparison. - [Supported Venues](https://pmxt.dev/docs/concepts/venues) — what each venue accepts. ### Server Management Source: https://pmxt.dev/docs/sdk/server > **Note:** 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 PMXT's [hosts](https://pmxt.dev/docs/authentication#hosts) directly and never spawns a local process. See [Hosted trading](https://pmxt.dev/docs/concepts/hosted-trading). When you create an exchange instance without a hosted API key, the SDK spawns a local `pmxt-core` server, waits for it to become healthy, and routes every request through it. You rarely need to touch the server lifecycle yourself. For setup, see [Self-hosted](https://pmxt.dev/docs/guides/self-hosted). For the full server lifecycle API (`start`, `stop`, `restart`, `status`, `health`, `logs`) and environment variables, see the [pmxt GitHub README](https://github.com/pmxt-dev/pmxt). --- # Router ## Overview ### What is the Router? Source: https://pmxt.dev/docs/router/overview The Router is PMXT's cross-venue intelligence layer. One API key gives you a unified view of the hosted catalog venues through a single interface. **Python:** ```python import pmxt router = pmxt.Router(pmxt_api_key="pmxt_live_...") # Search catalog venues at once markets = router.fetch_markets(query="fed rate cut", limit=5) # Find the same market on other venues clusters = router.fetch_matched_market_clusters(markets[0]) # Compare prices across venues prices = router.compare_market_prices(market_id=markets[0].market_id) ``` **TypeScript:** ```typescript import pmxt from "pmxtjs"; const router = new pmxt.Router({ pmxtApiKey: "pmxt_live_..." }); // Search catalog venues at once const markets = await router.fetchMarkets({ query: "fed rate cut", limit: 5 }); // Find the same market on other venues const clusters = await router.fetchMatchedMarketClusters(markets[0]); // Compare prices across venues const prices = await router.compareMarketPrices({ marketId: markets[0].marketId }); ``` The Router only needs a PMXT API key — venue credentials are not required for search, matching, or price analysis. #### What you can do - **[Cross-venue search](https://pmxt.dev/docs/router/search)** — Search markets and events across the hosted catalog in a single query. Results come from a shared catalog — ~10ms, not one call per venue. - **[Market matching](https://pmxt.dev/docs/router/matching)** — Given a market on any venue, find the cluster of matching markets across other venues. Each cluster has relation types and confidence scores. - **[Price comparison](https://pmxt.dev/docs/router/prices#comparemarketprices)** — Side-by-side bid/ask for identity matches in the hosted catalog. See where it's cheapest to buy and where it pays the most to sell. - **[Related markets](https://pmxt.dev/docs/router/prices#fetchrelatedmarkets)** — Find markets whose resolution conditions are subsets or supersets of a target market — narrower or broader markets on other venues. - **[Matched markets](https://pmxt.dev/docs/router/prices#fetchmatchedmarketclusters)** — Browse matched market clusters across venues, then inspect venue prices and order books. - **[Compose with venues](https://pmxt.dev/docs/router/prices#composing-with-venue-exchanges)** — Use the Router for data, venue exchanges for trading. Same SDK and schema; catalog IDs for Router and hosted flows, venue-native IDs for direct self-hosted writes. #### How it works The Router is backed by a continuously-updated Postgres catalog that ingests markets, events, and outcomes from the hosted catalog venues. When you search or match, you're querying this catalog — not fanning out to venue APIs. Matching uses embedding-based similarity and LLM-verified relation classification to find semantically equivalent markets across venues, even when venues use different titles, different bracket ranges, or different framing. | Venue A (Polymarket) | Venue B (Kalshi) | Relation | | --------------------------------- | ----------------------------------- | ---------- | | "Will BTC exceed $100k by Dec?" | "Bitcoin above $100,000 on Dec 31" | identity | | "Democrats win presidency" | "Democratic nominee wins popular vote" | subset | | "Fed cuts rates in 2025" | "Fed cuts rates in June" | superset | #### Get started 1. **Get an API key** at [pmxt.dev/dashboard](https://pmxt.dev/dashboard). It starts with `pmxt_live_` and works immediately. 2. **Install the SDK:** **Python:** ```bash pip install pmxt ``` **TypeScript:** ```bash npm install pmxtjs ``` 3. **Create a Router and start querying:** **Python:** ```python import pmxt router = pmxt.Router(pmxt_api_key="pmxt_live_...") markets = router.fetch_markets(query="election") ``` **TypeScript:** ```typescript import pmxt from "pmxtjs"; const router = new pmxt.Router({ pmxtApiKey: "pmxt_live_..." }); const markets = await router.fetchMarkets({ query: "election" }); ``` That's it. Everything the Router returns uses the same [unified schema](https://pmxt.dev/docs/concepts/unified-schema) as every other PMXT exchange, but identifiers keep their address space: Router/catalog IDs work for Router and hosted flows, while direct self-hosted venue calls need IDs returned by that venue client. > **Note:** **Order routing is coming.** Today the Router is read-only. Smart order routing (best-execution across venues) is on the roadmap. ## Search ### Search Source: https://pmxt.dev/docs/router/search The **Router** is a catalog-backed, cross-venue search surface. It searches across the hosted catalog and returns a unified list, sorted by 24h volume. Unlike the venue pass-through (`POST /api/:exchange/:method`), the Router is `GET`-based, browser-friendly, and never falls through to a live venue call. It is always served from the Postgres catalog. Pick the shape by the endpoint you hit: - `GET /v0/events` — events with their child markets and outcomes nested inline. - `GET /v0/markets` — flat per-market rows with their outcomes inline. #### Endpoints ```http GET https://api.pmxt.dev/v0/events GET https://api.pmxt.dev/v0/markets Authorization: Bearer pmxt_live_... ``` #### Query parameters | Parameter | Type | Default | Notes | | ------------- | --------- | ------- | -------------------------------------------------------- | | `query` | `string` | — | Full-text ILIKE over title and slug. | | `limit` | `integer` | `50` | Max rows, capped at `500`. | | `offset` | `integer` | `0` | Standard pagination offset. | | `closed` | `boolean` | `false` | Include closed / resolved rows. | | `category` | `string` | — | Exact match on normalized category. | | `exchange` | `string` | — | Filter to a single venue (`polymarket`, `kalshi`, ...). | #### Events shape ```bash curl "https://api.pmxt.dev/v0/events?query=election&limit=2" \ -H "Authorization: Bearer pmxt_live_..." ``` ```json { "data": [ { "id": "e344d660-5e22-411f-b516-ea37a0d9c3cb", "title": "2028 US Presidential Election", "volume24h": 12345678.9, "markets": [ { "marketId": "…", "title": "Will Gavin Newsom win?", "outcomes": [ { "label": "Yes", "price": 0.31, "priceChange24h": 0.02 }, { "label": "No", "price": 0.69, "priceChange24h": -0.02 } ] } ] } ], "meta": { "count": 2, "limit": 2, "offset": 0 } } ``` Exactly the same shape as [`UnifiedEvent`](https://pmxt.dev/docs/concepts/unified-schema#unifiedevent). #### Markets shape ```bash curl "https://api.pmxt.dev/v0/markets?query=election&limit=3" \ -H "Authorization: Bearer pmxt_live_..." ``` ```json { "data": [ { "marketId": "d35bc8c6-5602-477d-af21-e9513af9d696", "eventId": "3b6432b6-c956-446e-a7a6-abda14e97aba", "title": "US forces enter Iran by April 30?", "volume24h": 89757605.08, "outcomes": [ { "outcomeId": "…", "label": "April 30", "price": 0.9985, "priceChange24h": 0.002 }, { "outcomeId": "…", "label": "Not April 30", "price": 0.0015, "priceChange24h": 0.000 } ] } ], "meta": { "count": 3, "limit": 3, "offset": 0 } } ``` #### Events vs markets | You want… | Use | | ----------------------------------------------- | ---------------------------- | | A single answer for a topic with many questions | `GET /v0/events` | | A flat list ranked across all events | `GET /v0/markets` | | The full venue surface (writes, OHLCV, ...) | [`POST /api/:exchange/:method`](https://pmxt.dev/docs/api-reference/overview) | #### Latency Typical p95 on a warm catalog: **~10 ms per request**, regardless of venue. Cross-venue fan-out that would take 2–5 seconds live is served by a single indexed SQL query against `prediction_markets.events` + `prediction_markets.markets` + `prediction_markets.outcomes`. See [Unified Schema](https://pmxt.dev/docs/concepts/unified-schema) for the full rationale. ## Analyze ### Find Similar Events Source: https://pmxt.dev/docs/router/event-matching Use event clusters when the thing you care about has multiple child markets. PMXT groups matched events across venues, then returns the markets attached to each event so you can inspect the full cross-venue structure in one response. You can anchor the lookup to an event you already have, or omit the anchor fields to browse event clusters across the catalog. #### fetchMatchedEventClusters **Python:** ```python import pmxt router = pmxt.Router(pmxt_api_key="pmxt_live_...") clusters = router.fetch_matched_event_clusters( slug="2026-nba-champion", relation="identity", min_confidence=0.8, venues=["polymarket", "kalshi"], include_raw_matches=True, limit=5, ) for cluster in clusters: print(cluster.cluster_id, cluster.confidence, cluster.canonical_title) for event in cluster.events: print(f" {event.source_exchange}: {event.title}") for market in event.markets[:3]: print(f" - {market.title}") ``` **TypeScript:** ```typescript import pmxt from "pmxtjs"; const router = new pmxt.Router({ pmxtApiKey: "pmxt_live_..." }); const clusters = await router.fetchMatchedEventClusters({ slug: "2026-nba-champion", relation: "identity", minConfidence: 0.8, venues: ["polymarket", "kalshi"], includeRawMatches: true, limit: 5, }); for (const cluster of clusters) { console.log(cluster.clusterId, cluster.confidence, cluster.canonicalTitle); for (const event of cluster.events) { console.log(` ${event.sourceExchange}: ${event.title}`); for (const market of event.markets.slice(0, 3)) { console.log(` - ${market.title}`); } } } ``` **curl:** ```bash curl -G "https://api.pmxt.dev/v0/matched-event-clusters" \ -H "Authorization: Bearer pmxt_live_..." \ --data-urlencode "slug=2026-nba-champion" \ --data-urlencode "relation=identity" \ --data-urlencode "minConfidence=0.8" \ --data-urlencode "venues=polymarket,kalshi" \ --data-urlencode "includeRawMatches=true" \ --data-urlencode "limit=5" ``` ##### Passing an existing event If the event came from `fetchEvents`, pass it directly. **Python:** ```python events = router.fetch_events(query="2026 NBA Champion", limit=1) clusters = router.fetch_matched_event_clusters(events[0]) ``` **TypeScript:** ```typescript const events = await router.fetchEvents({ query: "2026 NBA Champion", limit: 1, }); const clusters = await router.fetchMatchedEventClusters(events[0]); ``` ##### Browsing clusters Omit `eventId`, `slug`, and `url` to browse the highest-volume matched event clusters. **Python:** ```python clusters = router.fetch_matched_event_clusters( sort="volume", min_venues=2, limit=25, ) ``` **TypeScript:** ```typescript const clusters = await router.fetchMatchedEventClusters({ sort: "volume", minVenues: 2, limit: 25, }); ``` ##### Parameters | Parameter | Type | Default | Notes | | --------- | ---- | ------- | ----- | | `eventId` | `string` | - | Anchor to a PMXT event ID. | | `slug` | `string` | - | Anchor to an event slug. | | `url` | `string` | - | Anchor to a venue event URL. | | `relation` | `string` | - | Filter to one relation type. | | `relations` | `string` or `string[]` | - | Filter to multiple relation types. | | `minConfidence` / `min_confidence` | `number` | `0` | Minimum cluster edge confidence, from `0` to `1`. | | `venues` | `string` or `string[]` | - | Only include clusters touching these venues. | | `excludeVenues` / `exclude_venues` | `string` or `string[]` | - | Exclude clusters touching these venues. | | `minVenues` / `min_venues` | `integer` | - | Require at least this many venues in a cluster. | | `withOrderbook` / `with_orderbook` | `boolean` | `false` | Require live orderbook coverage on matched edges. | | `includeRawMatches` / `include_raw_matches` | `boolean` | `false` | Include pairwise edges used to build the cluster. | | `updatedSince` / `updated_since` | `datetime` | - | Only include matches updated after this time. | | `sort` | `"volume"` or `"confidence"` | `"volume"` | Cluster sort order. | | `limit` | `integer` | `50` | Maximum clusters to return. | | `offset` | `integer` | `0` | Pagination offset. | | `edgeLimit` / `edge_limit` | `integer` | - | Maximum pairwise edges to scan before clustering. | ##### Response shape ```json { "clusterId": "ecl_3e7b1534e6235f9e", "canonicalTitle": "2026 NBA Champion", "category": "Sports", "relations": ["identity"], "confidence": 0.97, "volume24h": 1487365.42, "events": [ { "id": "poly_event_...", "sourceExchange": "polymarket", "title": "2026 NBA Champion", "markets": [ { "marketId": "pm_knicks_...", "title": "2026 NBA Champion - Will the New York Knicks win the 2026 NBA Finals?", "outcomes": [ { "outcomeId": "pm_knicks_yes", "label": "Yes", "price": 0.23 }, { "outcomeId": "pm_knicks_no", "label": "No", "price": 0.77 } ] } ] }, { "id": "kalshi_event_...", "sourceExchange": "kalshi", "title": "2026 Pro Basketball Finals", "markets": [ { "marketId": "kalshi_knicks_...", "title": "Will New York win the 2026 Pro Basketball Finals?", "outcomes": [ { "outcomeId": "KXNBA-26-NYK", "label": "Yes", "price": 0.22 }, { "outcomeId": "KXNBA-26-NYK-NO", "label": "No", "price": 0.78 } ] } ] } ], "rawMatches": [ { "eventAId": "poly_event_...", "eventBId": "kalshi_event_...", "marketAId": "pm_knicks_...", "marketBId": "kalshi_knicks_...", "relation": "identity", "confidence": 0.97 } ] } ``` #### When To Use Event Clusters | You have | Use | | -------- | --- | | A single tradeable question | [Find Similar Markets](https://pmxt.dev/docs/router/matching) | | A parent event with many child markets | **Find Similar Events** | Event clusters are the better default when venues structure the same topic differently. For example, one venue may expose a single event with many team markets while another exposes each team as its own market under a differently named event. ### Find Similar Markets Source: https://pmxt.dev/docs/router/matching Use market clusters when you want the same tradeable question across venues. A cluster contains every matched market PMXT currently knows about for that question, plus the pairwise relations used to connect the cluster. You can anchor the lookup to a market you already have, or omit the anchor fields to browse clusters across the catalog. #### fetchMatchedMarketClusters **Python:** ```python import pmxt router = pmxt.Router(pmxt_api_key="pmxt_live_...") clusters = router.fetch_matched_market_clusters( slug="will-satoshi-move-any-bitcoin-in-2026", relation="identity", min_confidence=0.8, venues=["polymarket", "kalshi", "probable"], include_raw_matches=True, limit=5, ) for cluster in clusters: print(cluster.cluster_id, cluster.confidence, cluster.canonical_title) for market in cluster.markets: print(f" {market.source_exchange}: {market.title}") print(f" market_id={market.market_id}") for edge in cluster.raw_matches or []: print(f" edge: {edge['relation']} confidence={edge['confidence']}") ``` **TypeScript:** ```typescript import pmxt from "pmxtjs"; const router = new pmxt.Router({ pmxtApiKey: "pmxt_live_..." }); const clusters = await router.fetchMatchedMarketClusters({ slug: "will-satoshi-move-any-bitcoin-in-2026", relation: "identity", minConfidence: 0.8, venues: ["polymarket", "kalshi", "probable"], includeRawMatches: true, limit: 5, }); for (const cluster of clusters) { console.log(cluster.clusterId, cluster.confidence, cluster.canonicalTitle); for (const market of cluster.markets) { console.log(` ${market.sourceExchange}: ${market.title}`); console.log(` marketId=${market.marketId}`); } for (const edge of cluster.rawMatches ?? []) { console.log(` edge: ${edge.relation} confidence=${edge.confidence}`); } } ``` **curl:** ```bash curl -G "https://api.pmxt.dev/v0/matched-market-clusters" \ -H "Authorization: Bearer pmxt_live_..." \ --data-urlencode "slug=will-satoshi-move-any-bitcoin-in-2026" \ --data-urlencode "relation=identity" \ --data-urlencode "minConfidence=0.8" \ --data-urlencode "venues=polymarket,kalshi,probable" \ --data-urlencode "includeRawMatches=true" \ --data-urlencode "limit=5" ``` ##### Passing an existing market If the market came from `fetchMarkets`, pass it directly. **Python:** ```python markets = router.fetch_markets(query="Satoshi Bitcoin 2026", limit=1) clusters = router.fetch_matched_market_clusters(markets[0]) ``` **TypeScript:** ```typescript const markets = await router.fetchMarkets({ query: "Satoshi Bitcoin 2026", limit: 1, }); const clusters = await router.fetchMatchedMarketClusters(markets[0]); ``` ##### Browsing clusters Omit `marketId`, `slug`, and `url` to browse the highest-volume matched clusters. **Python:** ```python clusters = router.fetch_matched_market_clusters( sort="volume", min_venues=2, limit=25, ) ``` **TypeScript:** ```typescript const clusters = await router.fetchMatchedMarketClusters({ sort: "volume", minVenues: 2, limit: 25, }); ``` ##### Parameters | Parameter | Type | Default | Notes | | --------- | ---- | ------- | ----- | | `marketId` | `string` | - | Anchor to a PMXT market ID. | | `slug` | `string` | - | Anchor to a market slug. | | `url` | `string` | - | Anchor to a venue market URL. | | `relation` | `string` | - | Filter to one relation type. | | `relations` | `string` or `string[]` | - | Filter to multiple relation types. | | `minConfidence` / `min_confidence` | `number` | `0` | Minimum cluster edge confidence, from `0` to `1`. | | `venues` | `string` or `string[]` | - | Only include clusters touching these venues. | | `excludeVenues` / `exclude_venues` | `string` or `string[]` | - | Exclude clusters touching these venues. | | `minVenues` / `min_venues` | `integer` | - | Require at least this many venues in a cluster. | | `withOrderbook` / `with_orderbook` | `boolean` | `false` | Require live orderbook coverage on matched edges. | | `includeRawMatches` / `include_raw_matches` | `boolean` | `false` | Include pairwise edges used to build the cluster. | | `updatedSince` / `updated_since` | `datetime` | - | Only include matches updated after this time. | | `sort` | `"volume"` or `"confidence"` | `"volume"` | Cluster sort order. | | `limit` | `integer` | `50` | Maximum clusters to return. | | `offset` | `integer` | `0` | Pagination offset. | | `edgeLimit` / `edge_limit` | `integer` | - | Maximum pairwise edges to scan before clustering. | ##### Response shape ```json { "clusterId": "mcl_706533a07496a4eb", "canonicalTitle": "Will Satoshi move any Bitcoin in 2026?", "category": "Crypto", "relations": ["identity"], "confidence": 1, "volume24h": 42310.15, "markets": [ { "marketId": "pm_...", "sourceExchange": "polymarket", "title": "Will Satoshi move any Bitcoin in 2026?", "slug": "will-satoshi-move-any-bitcoin-in-2026", "outcomes": [ { "outcomeId": "pm_yes", "label": "Yes", "price": 0.24 }, { "outcomeId": "pm_no", "label": "No", "price": 0.76 } ] }, { "marketId": "kalshi_...", "sourceExchange": "kalshi", "title": "Will Satoshi move Bitcoin in 2026?", "outcomes": [ { "outcomeId": "KXSATOSHI-26", "label": "Yes", "price": 0.25 }, { "outcomeId": "KXSATOSHI-26-NO", "label": "No", "price": 0.75 } ] } ], "rawMatches": [ { "marketAId": "pm_...", "marketBId": "kalshi_...", "relation": "identity", "confidence": 1 } ] } ``` #### Relation Types Every edge in a cluster is classified into one of five set-theoretic relations describing how two markets' resolution conditions relate. | Relation | Meaning | Example | | -------- | ------- | ------- | | `identity` | Same resolution condition. | "BTC > $100k by Dec 31" on Polymarket and Kalshi. | | `subset` | A resolving YES implies the other resolves YES, not vice versa. | "Democrats win presidency" is narrower than "Democrats win popular vote". | | `superset` | The reverse of `subset`. | "Democrats win popular vote" is broader than "Democrats win presidency". | | `overlap` | Related, but neither condition implies the other. | "Fed cuts in June" and "Fed cuts twice in 2026". | | `disjoint` | Cannot both resolve YES. | Two mutually exclusive candidates winning the same office. | > **Note:** To match an entire event and see the corresponding child markets, use [Find Similar Events](https://pmxt.dev/docs/router/event-matching). ### Compare Prices Across Venues Source: https://pmxt.dev/docs/router/prices Use [matching](https://pmxt.dev/docs/router/matching) to find cross-venue clusters, then use price helpers or venue exchanges to inspect executable bid/ask data. #### compareMarketPrices Side-by-side bid/ask for identity matches in the hosted catalog. This returns a flat side-by-side view for one anchored market. For cluster-first discovery across the whole catalog, use [`fetchMatchedMarketClusters`](https://pmxt.dev/docs/router/matching#fetchmatchedmarketclusters). **Python:** ```python import pmxt router = pmxt.Router(pmxt_api_key="pmxt_live_...") prices = router.compare_market_prices(market_id="d35bc8c6-...") for p in prices: print(f"{p.venue:12s} bid {p.best_bid:.2f} ask {p.best_ask:.2f}") ``` ``` polymarket bid 0.60 ask 0.65 kalshi bid 0.58 ask 0.63 limitless bid 0.61 ask 0.66 ``` **TypeScript:** ```typescript import pmxt from "pmxtjs"; const router = new pmxt.Router({ pmxtApiKey: "pmxt_live_..." }); const prices = await router.compareMarketPrices({ marketId: "d35bc8c6-...", }); for (const p of prices) { console.log(`${p.venue.padEnd(12)} bid ${p.bestBid} ask ${p.bestAsk}`); } ``` ##### Response shape ```json { "market": { "marketId": "...", "title": "..." }, "relation": "identity", "confidence": 0.95, "reasoning": "Same resolution condition.", "bestBid": 0.60, "bestAsk": 0.65, "venue": "kalshi" } ``` #### fetchRelatedMarkets Find related markets across venues — markets whose resolution condition is a subset or superset of the target market. **Python:** ```python # Positional dict, camelCase keys. Returns a list of plain dicts # (not typed dataclasses like compare_market_prices). related = router.fetch_related_markets({"marketId": "d35bc8c6-..."}) for r in related: print(f"{r['relation']:10s} {r['venue']:12s} {r['market']['title']}") print(f" confidence {r['confidence']:.0%} bid {r['bestBid']} ask {r['bestAsk']}") print(f" {r['reasoning']}") ``` ``` subset kalshi Will the nominee be from California? confidence 85% bid 0.40 ask 0.45 Narrower market — California origin implies Democratic candidacy. superset polymarket Will a Democrat win the popular vote? confidence 72% bid 0.70 ask 0.73 Broader — popular vote does not guarantee election win. ``` **TypeScript:** ```typescript const related = await router.fetchRelatedMarkets({ marketId: "d35bc8c6-..." }); for (const r of related) { console.log(r.relation, r.venue, r.market.title); console.log(` confidence: ${r.confidence} reasoning: ${r.reasoning}`); } ``` Subset means A=YES implies B=YES but not vice versa. Superset is the reverse. See [relation types](https://pmxt.dev/docs/router/matching#relation-types) for the full taxonomy. > **Note:** `fetchRelatedMarkets` filters to `subset` and `superset` relations. If you want all relation types, use [`fetchMatchedMarketClusters`](https://pmxt.dev/docs/router/matching#fetchmatchedmarketclusters) with `relation` or `relations` filters. #### fetchMatchedMarketClusters Returns matched market clusters across venues. Use this when you want the full group of markets first, then inspect prices and outcomes inside each venue market. **Python:** ```python clusters = router.fetch_matched_market_clusters( relation="identity", sort="volume", min_venues=2, limit=20, ) for cluster in clusters: print(cluster.canonical_title) for market in cluster.markets: price = market.yes.price if market.yes else None print(f" {market.source_exchange:12s} @ {price}") ``` ``` Will BTC hit $100k by Dec 31? polymarket @ 0.58 kalshi @ 0.63 ``` **TypeScript:** ```typescript const clusters = await router.fetchMatchedMarketClusters({ relation: "identity", sort: "volume", minVenues: 2, limit: 20, }); for (const cluster of clusters) { console.log(cluster.canonicalTitle); for (const market of cluster.markets) { console.log(` ${market.sourceExchange} @ ${market.yes?.price}`); } } ``` ##### Parameters | Parameter | Type | Default | Notes | | --------------- | --------- | ------- | ---------------------------------------------------------- | | `relation` | `string` | — | Filter to a relation type such as `identity`. | | `minVenues` | `integer` | — | Require at least this many venues in a cluster. | | `sort` | `string` | `volume` | Sort by `volume` or `confidence`. | | `limit` | `integer` | `50` | Maximum number of clusters to return. | Results are cluster-first. PMXT does not pick a best opportunity for you; inspect the returned markets, outcomes, and order books before deciding how to trade. ##### Response shape ```json { "clusterId": "mcl_...", "canonicalTitle": "Will BTC hit $100k by Dec 31?", "relations": ["identity"], "confidence": 0.98, "markets": [ { "marketId": "pm_...", "sourceExchange": "polymarket", "title": "Will BTC hit $100k by Dec 31?" }, { "marketId": "kalshi_...", "sourceExchange": "kalshi", "title": "Bitcoin above $100,000 on Dec 31" } ] } ``` #### Composing with venue exchanges The Router is read-only. To trade, use venue-specific exchange classes with the local SDK. A typical workflow: ```python import pmxt router = pmxt.Router(pmxt_api_key="pmxt_live_...") poly = pmxt.Polymarket(private_key="0x...") # 1. Browse matched market clusters across venues clusters = router.fetch_matched_market_clusters(relation="identity", limit=20) # 2. Direct self-hosted calls need IDs returned by the venue client. # Re-fetch or reverse-resolve the matched venue market, then verify the outcome. polymarket_match = next(m for m in clusters[0].markets if m.source_exchange == "polymarket") venue_markets = poly.fetch_markets({"query": polymarket_match.title, "limit": 1}) venue_outcome = venue_markets[0].outcomes[0] # verify label/slug before trading # 3. Inspect the order book on that venue with its native outcome ID book = poly.fetch_order_book(outcome_id=venue_outcome.outcome_id) # 4. Place an order locally (never proxied through PMXT) # poly.create_order(...) ``` Router returns the same [unified schema](https://pmxt.dev/docs/concepts/unified-schema) as every other exchange, but identifiers keep their address space. Catalog IDs work for Router and hosted flows; direct self-hosted venue calls need venue-native IDs returned by that venue client. --- # API Reference ## Overview ### Overview Source: https://pmxt.dev/docs/api-reference/overview Every endpoint in the **Endpoints** section of this sidebar is generated automatically from the `pmxt-core` package's OpenAPI specification. This page is the hand-written preface; everything else under **API Reference → Endpoints** is rendered directly from the spec. #### How this works PMXT Hosted mounts `pmxt-core`'s HTTP surface under `POST /api/:exchange/:method`. That surface is the same one the local server exposes, and it's described by a single OpenAPI 3 document ([`openapi.yaml`](https://github.com/pmxt-dev/pmxt/blob/main/core/src/server/openapi.yaml)) generated from the TypeScript source of [`BaseExchange.ts`](https://github.com/pmxt-dev/pmxt/blob/main/core/src/exchanges/BaseExchange.ts) on every `pmxt-core` build. The spec ships inside the `pmxt-core` npm package at `node_modules/pmxt-core/dist/server/openapi.yaml`. On every `npm install`, hosted-pmxt's `postinstall` hook runs `scripts/sync-docs.js`, which: 1. Copies the spec into `docs/api-reference/openapi.yaml`. 2. Rewrites `servers` to point at `https://api.pmxt.dev`. 3. Adds a `bearerAuth` security scheme so every endpoint in this reference shows the correct `Authorization: Bearer …` requirement. 4. Regenerates [`concepts/venues`](https://pmxt.dev/docs/concepts/venues) from the `ExchangeParam` enum. When `pmxt-core` publishes a new version, the immediate watcher in hosted-pmxt opens a PR that bumps the dependency — `npm install` inside that PR re-runs the sync, and the refreshed spec lands in the same commit. **There is no separate docs build, no drift, and no manual step.** If you see a method documented here, it exists in the hosted surface at exactly that path. #### Request shape Every venue pass-through call has the same wire shape: ```http POST /api/:exchange/:method Authorization: Bearer pmxt_live_... Content-Type: application/json { "args": [ /* positional arguments to the SDK method, in order */ ], "credentials": { /* optional — venue-scoped credentials */ } } ``` The `args` array is the positional signature of the underlying SDK method, which is why each generated endpoint page lists its arguments in order. For example, `fetchMarkets({ query, limit })` becomes: ```json { "args": [{ "query": "election", "limit": 5 }] } ``` When the SDK supports keyword arguments, pass an object as the first element of `args`. When it supports positional arguments, pass them inline. #### Response envelope Every pass-through response is wrapped in the same envelope: ```json { "success": true, "data": /* method-specific payload */ } ``` On error: ```json { "success": false, "error": { "message": "…", "code": "AUTHENTICATION_ERROR", "retryable": false, "exchange": "Polymarket" } } ``` The envelope is identical whether a call was served from the [catalog](https://pmxt.dev/docs/concepts/unified-schema) or forwarded live to the venue — clients cannot tell the difference. #### Interactive try-it-out Every generated endpoint page in this reference has a **"Try It"** panel on the right — paste your API key once at the top of the page and every request runs against `https://api.pmxt.dev` with Bearer auth attached. ## System ### Server Health Check `GET /health` ### Load Markets `POST /api/{exchange}/loadMarkets` Load and cache all markets from the exchange into `this.markets` and `this.marketsBySlug`. Subsequent calls return the cached result without hitting the API again. This is the correct way to paginate or iterate over markets without drift. Because `fetchMarkets()` always hits the API, repeated calls with different `offset` values may return inconsistent results if the exchange reorders or adds markets between requests. Use `loadMarkets()` once to get a stable snapshot, then paginate over `Object.values(exchange.markets)` locally. **Response:** `{ success: true, data: ... }` **Python:** ```python import pmxt # API key optional — enables faster catalog-backed lookups exchange = pmxt.Kalshi( pmxt_api_key="YOUR_PMXT_API_KEY", ) result = exchange.load_markets() ``` **TypeScript:** ```typescript import { Kalshi } from "pmxtjs"; // API key optional — enables faster catalog-backed lookups const exchange = new Kalshi({ pmxtApiKey: "YOUR_PMXT_API_KEY", }); const result = await exchange.loadMarkets(); ``` ### Close `POST /api/{exchange}/close` Close all WebSocket connections and clean up resources. Call this when you're done streaming to properly release connections. **Python:** ```python import pmxt # API key optional — enables faster catalog-backed lookups exchange = pmxt.Kalshi( pmxt_api_key="YOUR_PMXT_API_KEY", ) result = exchange.close() ``` **TypeScript:** ```typescript import { Kalshi } from "pmxtjs"; // API key optional — enables faster catalog-backed lookups const exchange = new Kalshi({ pmxtApiKey: "YOUR_PMXT_API_KEY", }); const result = await exchange.close(); ``` ## Events & Markets ### Fetch Events Source: https://pmxt.dev/docs/api-reference/fetch-events > **Note:** **Faster with an API key** — With a PMXT API key this endpoint is served from an indexed catalog (~10 ms) instead of proxying to the venue (~500 ms). No code changes required — same request, same response, just faster. [Learn more](https://pmxt.dev/docs/concepts/unified-schema). #### Use cases ##### Search by keyword Find all events mentioning "Trump" on Polymarket: ```python Python import pmxt # Optional: pass pmxt_api_key for ~100x faster catalog-backed lookups api = pmxt.Polymarket() events = api.fetch_events(query="trump", limit=5) for event in events: print(event.title, event.url) ``` ```javascript JavaScript import { Polymarket } from "pmxtjs"; // Optional: pass pmxtApiKey for ~100x faster catalog-backed lookups const api = new Polymarket(); const events = await api.fetchEvents({ query: "trump", limit: 5 }); events.forEach((e) => console.log(e.title, e.url)); ``` ```bash curl curl "https://api.pmxt.dev/api/polymarket/fetchEvents?query=trump&limit=5" \ -H "Authorization: Bearer $PMXT_API_KEY" ``` ##### Filter by category Get all crypto-related events on Kalshi: ```python Python import pmxt api = pmxt.Limitless() events = api.fetch_events(category="Crypto", limit=5) ``` ```javascript JavaScript import { Limitless } from "pmxtjs"; const api = new Limitless(); const events = await api.fetchEvents({ category: "Crypto", limit: 5 }); ``` ```bash curl curl "https://api.pmxt.dev/api/limitless/fetchEvents?category=Crypto&limit=5" \ -H "Authorization: Bearer $PMXT_API_KEY" ``` ##### Filter by tags Get events tagged with geopolitics: ```python Python import pmxt api = pmxt.Polymarket() events = api.fetch_events(tags=["Geopolitics", "Middle East"], limit=5) ``` ```javascript JavaScript import { Polymarket } from "pmxtjs"; const api = new Polymarket(); const events = await api.fetchEvents({ tags: ["Geopolitics", "Middle East"], limit: 5, }); ``` ```bash curl curl "https://api.pmxt.dev/api/polymarket/fetchEvents?tags=Geopolitics&tags=Middle+East&limit=5" \ -H "Authorization: Bearer $PMXT_API_KEY" ``` ##### Filter active by category Find active sports events: ```python Python import pmxt api = pmxt.Polymarket() events = api.fetch_events( category="Sports", status="active", limit=5, ) ``` ```javascript JavaScript import { Polymarket } from "pmxtjs"; const api = new Polymarket(); const events = await api.fetchEvents({ category: "Sports", status: "active", limit: 5, }); ``` ```bash curl curl "https://api.pmxt.dev/api/polymarket/fetchEvents?category=Sports&status=active&limit=5" \ -H "Authorization: Bearer $PMXT_API_KEY" ``` ### Fetch Events Paginated `GET /api/{exchange}/fetchEventsPaginated` Paginated variant of {@link fetchEvents}. On the first call (no `cursor`), all events are fetched from the exchange and cached in an in-memory snapshot. A cursor is returned along with the first page. Subsequent calls with that cursor serve additional pages directly from the cached snapshot -- no additional API calls are made. The snapshot is invalidated after `snapshotTTL` ms (configured via `ExchangeOptions` in the constructor). A request using a cursor from an expired snapshot throws `'Cursor has expired'`. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `limit` | number | No | | | `cursor` | string | No | | | `filter` | string | No | | **Response:** `{ success: true, data: ... }` **Python:** ```python import pmxt # API key optional — enables faster catalog-backed lookups exchange = pmxt.Kalshi( pmxt_api_key="YOUR_PMXT_API_KEY", ) result = exchange.fetch_events_paginated(limit=10, cursor="abc123") ``` **TypeScript:** ```typescript import { Kalshi } from "pmxtjs"; // API key optional — enables faster catalog-backed lookups const exchange = new Kalshi({ pmxtApiKey: "YOUR_PMXT_API_KEY", }); const result = await exchange.fetchEventsPaginated({ limit: 10, cursor: "abc123" }); ``` ### Fetch Event Source: https://pmxt.dev/docs/api-reference/fetch-event > **Note:** **Faster with an API key** — With a PMXT API key this endpoint is served from an indexed catalog (~10 ms) instead of proxying to the venue (~500 ms). No code changes required — same request, same response, just faster. [Learn more](https://pmxt.dev/docs/concepts/unified-schema). #### Use cases ##### Look up by slug Slugs are the most reliable way to look up a specific event — they are human-readable, stable, and easy to share or store in config: ```python Python import pmxt # Optional: pass pmxt_api_key for ~100x faster catalog-backed lookups api = pmxt.Polymarket() event = api.fetch_event(slug="presidential-election-winner-2028") print(event.title, event.volume_24h) ``` ```javascript JavaScript import { Polymarket } from "pmxtjs"; // Optional: pass pmxtApiKey for ~100x faster catalog-backed lookups const api = new Polymarket(); const event = await api.fetchEvent({ slug: "presidential-election-winner-2028" }); console.log(event.title, event.volume24h); ``` ```bash curl curl "https://api.pmxt.dev/api/polymarket/fetchEvent?slug=presidential-election-winner-2028" \ -H "Authorization: Bearer $PMXT_API_KEY" ``` ##### Look up by event ID When you already have an event ID from a previous API call or webhook, use `eventId` for a direct lookup: ```python Python import pmxt api = pmxt.Polymarket() event = api.fetch_event(event_id="a49db286-39ca-4119-9de5-1335a92ef92a") print(event.title, event.url) ``` ```javascript JavaScript import { Polymarket } from "pmxtjs"; const api = new Polymarket(); const event = await api.fetchEvent({ eventId: "a49db286-39ca-4119-9de5-1335a92ef92a" }); console.log(event.title, event.url); ``` ```bash curl curl "https://api.pmxt.dev/api/polymarket/fetchEvent?eventId=a49db286-39ca-4119-9de5-1335a92ef92a" \ -H "Authorization: Bearer $PMXT_API_KEY" ``` ##### Access markets within an event Each event contains a `markets` array with full market details including outcomes and prices. This is useful for building dashboards or comparing outcomes across an event's markets: ```python Python import pmxt api = pmxt.Limitless() event = api.fetch_event(slug="presidential-election-winner-2028-1769010522121") for market in event.markets: print(f"{market.title}: {market.outcomes[1].price:.0%}") ``` ```javascript JavaScript import { Limitless } from "pmxtjs"; const api = new Limitless(); const event = await api.fetchEvent({ slug: "presidential-election-winner-2028-1769010522121" }); for (const market of event.markets) { console.log(`${market.title}: ${(market.outcomes[1].price * 100).toFixed(0)}%`); } ``` ```bash curl curl "https://api.pmxt.dev/api/limitless/fetchEvent?slug=presidential-election-winner-2028-1769010522121" \ -H "Authorization: Bearer $PMXT_API_KEY" ``` ### Fetch Markets Source: https://pmxt.dev/docs/api-reference/fetch-markets > **Note:** **Faster with an API key** — With a PMXT API key this endpoint is served from an indexed catalog (~10 ms) instead of proxying to the venue (~500 ms). No code changes required — same request, same response, just faster. [Learn more](https://pmxt.dev/docs/concepts/unified-schema). #### Use cases ##### Search by keyword Find all markets mentioning "bitcoin" on Polymarket: ```python Python import pmxt # Optional: pass pmxt_api_key for ~100x faster catalog-backed lookups api = pmxt.Polymarket() markets = api.fetch_markets(query="bitcoin", limit=5) for market in markets: print(market.title, market.yes.price) ``` ```javascript JavaScript import { Polymarket } from "pmxtjs"; // Optional: pass pmxtApiKey for ~100x faster catalog-backed lookups const api = new Polymarket(); const markets = await api.fetchMarkets({ query: "bitcoin", limit: 5 }); markets.forEach((m) => console.log(m.title, m.yes.price)); ``` ```bash curl curl "https://api.pmxt.dev/api/polymarket/fetchMarkets?query=bitcoin&limit=5" \ -H "Authorization: Bearer $PMXT_API_KEY" ``` ##### Filter by category Get all sports markets on Kalshi: ```python Python import pmxt api = pmxt.Kalshi() markets = api.fetch_markets(category="Sports", limit=5) ``` ```javascript JavaScript import { Kalshi } from "pmxtjs"; const api = new Kalshi(); const markets = await api.fetchMarkets({ category: "Sports", limit: 5 }); ``` ```bash curl curl "https://api.pmxt.dev/api/kalshi/fetchMarkets?category=Sports&limit=5" \ -H "Authorization: Bearer $PMXT_API_KEY" ``` ##### Filter by tags Get markets tagged with elections and Trump: ```python Python import pmxt api = pmxt.Polymarket() markets = api.fetch_markets(tags=["Elections", "Trump"], limit=5) ``` ```javascript JavaScript import { Polymarket } from "pmxtjs"; const api = new Polymarket(); const markets = await api.fetchMarkets({ tags: ["Elections", "Trump"], limit: 5, }); ``` ```bash curl curl "https://api.pmxt.dev/api/polymarket/fetchMarkets?tags=Elections&tags=Trump&limit=5" \ -H "Authorization: Bearer $PMXT_API_KEY" ``` ##### Filter by status Find active markets on Limitless: ```python Python import pmxt api = pmxt.Limitless() markets = api.fetch_markets( status="active", limit=5, ) ``` ```javascript JavaScript import { Limitless } from "pmxtjs"; const api = new Limitless(); const markets = await api.fetchMarkets({ status: "active", limit: 5, }); ``` ```bash curl curl "https://api.pmxt.dev/api/limitless/fetchMarkets?status=active&limit=5" \ -H "Authorization: Bearer $PMXT_API_KEY" ``` ### Fetch Markets Paginated `GET /api/{exchange}/fetchMarketsPaginated` Fetch markets with cursor-based pagination backed by a stable in-memory snapshot. On the first call (or when no cursor is supplied), fetches all markets once and caches them. Subsequent calls with a cursor returned from a previous call slice directly from the cached snapshot — no additional API calls are made. The snapshot is invalidated after `snapshotTTL` ms (configured via `ExchangeOptions` in the constructor). A request using a cursor from an expired snapshot throws `'Cursor has expired'`. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `limit` | number | No | | | `cursor` | string | No | | | `filter` | string | No | | **Response:** `{ success: true, data: ... }` **Python:** ```python import pmxt # API key optional — enables faster catalog-backed lookups exchange = pmxt.Kalshi( pmxt_api_key="YOUR_PMXT_API_KEY", ) result = exchange.fetch_markets_paginated(limit=10, cursor="abc123") ``` **TypeScript:** ```typescript import { Kalshi } from "pmxtjs"; // API key optional — enables faster catalog-backed lookups const exchange = new Kalshi({ pmxtApiKey: "YOUR_PMXT_API_KEY", }); const result = await exchange.fetchMarketsPaginated({ limit: 10, cursor: "abc123" }); ``` ### Fetch Market Source: https://pmxt.dev/docs/api-reference/fetch-market > **Note:** **Faster with an API key** — With a PMXT API key this endpoint is served from an indexed catalog (~10 ms) instead of proxying to the venue (~500 ms). No code changes required — same request, same response, just faster. [Learn more](https://pmxt.dev/docs/concepts/unified-schema). #### Use cases ##### Look up by slug Slugs are stable, human-readable identifiers that make your code self-documenting and resilient to ID changes. Use them whenever possible. ```python Python import pmxt # Optional: pass pmxt_api_key for ~100x faster catalog-backed lookups api = pmxt.Polymarket() market = api.fetch_market(slug="will-gavin-newsom-win-the-2028-us-presidential-election") print(market.title, market.status) ``` ```javascript JavaScript import { Polymarket } from "pmxtjs"; // Optional: pass pmxtApiKey for ~100x faster catalog-backed lookups const api = new Polymarket(); const market = await api.fetchMarket({ slug: "will-gavin-newsom-win-the-2028-us-presidential-election" }); console.log(market.title, market.status); ``` ```bash curl curl "https://api.pmxt.dev/api/polymarket/fetchMarket?slug=will-gavin-newsom-win-the-2028-us-presidential-election" \ -H "Authorization: Bearer $PMXT_API_KEY" ``` ##### Look up by market ID When you already have a venue-native market ID (e.g. from a trade confirmation or webhook), pass it directly: ```python Python import pmxt api = pmxt.Polymarket() market = api.fetch_market(market_id="46ac6f9c-c66a-48a5-8f12-beefd6e06221") print(market.title, market.volume) ``` ```javascript JavaScript import { Polymarket } from "pmxtjs"; const api = new Polymarket(); const market = await api.fetchMarket({ marketId: "46ac6f9c-c66a-48a5-8f12-beefd6e06221" }); console.log(market.title, market.volume); ``` ```bash curl curl "https://api.pmxt.dev/api/polymarket/fetchMarket?marketId=46ac6f9c-c66a-48a5-8f12-beefd6e06221" \ -H "Authorization: Bearer $PMXT_API_KEY" ``` ##### Read outcomes and prices Every market carries an `outcomes` array and, for binary markets, `yes` / `no` convenience accessors with live prices: ```python Python import pmxt api = pmxt.Polymarket() market = api.fetch_market(slug="will-gavin-newsom-win-the-2028-us-presidential-election") # Binary shorthand print(f"Yes {market.yes.price} No {market.no.price}") # Full outcomes list (works for multi-outcome markets too) for outcome in market.outcomes: print(f" {outcome.label}: {outcome.price}") ``` ```javascript JavaScript import { Polymarket } from "pmxtjs"; const api = new Polymarket(); const market = await api.fetchMarket({ slug: "will-gavin-newsom-win-the-2028-us-presidential-election" }); // Binary shorthand console.log(`Yes ${market.yes.price} No ${market.no.price}`); // Full outcomes list (works for multi-outcome markets too) market.outcomes.forEach((o) => console.log(` ${o.label}: ${o.price}`)); ``` ```bash curl curl "https://api.pmxt.dev/api/polymarket/fetchMarket?slug=will-gavin-newsom-win-the-2028-us-presidential-election" \ -H "Authorization: Bearer $PMXT_API_KEY" ``` ## Trading ### Create Order `POST /v0/trade/create-order` Place a buy or sell order on Polymarket, Opinion, or Limitless. Returns the resulting order with id, fill status, average price, fees, and the on-chain tx hash once it settles. Most callers pass an outcome straight from `client.fetch_markets()`: ```python market = client.fetch_markets({"query": "trump 2028"})[0] order = client.create_order(outcome=market.yes, side="buy", amount=10, order_type="limit", price=0.55) ``` Need to show the order to a user before they sign it (custom approval UX)? Use `buildOrderHosted` + `submitOrderHosted` instead — together they do the same thing. **Python:** ```python # Current hosted venues are funded once on Polygon — see /guides/escrow-lifecycle. # Hosted writes need: pmxt_api_key + wallet_address + private_key (signs EIP-712 locally). # Pass an outcome straight from client.fetch_markets() — no UUID lookup needed. import pmxt client = pmxt.Polymarket( pmxt_api_key="YOUR_PMXT_API_KEY", wallet_address="0xYourWallet", # EVM address — its Polygon USDC funds the escrow private_key="0x...", # any EVM key controlling that address ) market = client.fetch_markets({"query": "trump 2028"})[0] yes = next(o for o in market.outcomes if o.label.lower() == "yes") order = client.create_order( outcome=yes, side="buy", type="limit", amount=10, price=0.55, ) print(order.id, order.status) ``` **TypeScript:** ```typescript // Current hosted venues are funded once on Polygon — see /guides/escrow-lifecycle. // Hosted writes need: pmxtApiKey + walletAddress + privateKey (signs EIP-712 locally). // Pass an outcome straight from client.fetchMarkets() — no UUID lookup needed. import { Polymarket } from "pmxtjs"; const client = new Polymarket({ pmxtApiKey: "YOUR_PMXT_API_KEY", walletAddress: "0xYourWallet", // EVM address — its Polygon USDC funds the escrow privateKey: "0x...", // any EVM key controlling that address }); const markets = await client.fetchMarkets({ query: "trump 2028" }); const market = markets[0]; const yes = market.outcomes.find((o) => o.label.toLowerCase() === "yes")!; const order = await client.createOrder({ outcome: yes, side: "buy", type: "limit", amount: 10, price: 0.55, }); console.log(order.id, order.status); ``` ### Build Order `POST /v0/trade/build-order` Build an order *without* placing it. Returns the order's typed-data payload (for the wallet to sign) plus a `built_order_id` to pass to `submitOrderHosted` once it's signed. Use this when you want a human to approve each trade before it submits — for example, surfacing the order details in a wallet popup. For everything else, use `createOrderHosted`, which builds + signs + submits in one call. **Python:** ```python # Current hosted venues are funded once on Polygon — see /guides/escrow-lifecycle. # Hosted writes need: pmxt_api_key + wallet_address + private_key (signs EIP-712 locally). # Pass an outcome straight from client.fetch_markets() — no UUID lookup needed. import pmxt client = pmxt.Polymarket( pmxt_api_key="YOUR_PMXT_API_KEY", wallet_address="0xYourWallet", # EVM address — its Polygon USDC funds the escrow private_key="0x...", # any EVM key controlling that address ) market = client.fetch_markets({"query": "trump 2028"})[0] yes = next(o for o in market.outcomes if o.label.lower() == "yes") order = client.build_order( outcome=yes, side="buy", type="limit", amount=10, price=0.55, ) print(order.id, order.status) ``` **TypeScript:** ```typescript // Current hosted venues are funded once on Polygon — see /guides/escrow-lifecycle. // Hosted writes need: pmxtApiKey + walletAddress + privateKey (signs EIP-712 locally). // Pass an outcome straight from client.fetchMarkets() — no UUID lookup needed. import { Polymarket } from "pmxtjs"; const client = new Polymarket({ pmxtApiKey: "YOUR_PMXT_API_KEY", walletAddress: "0xYourWallet", // EVM address — its Polygon USDC funds the escrow privateKey: "0x...", // any EVM key controlling that address }); const markets = await client.fetchMarkets({ query: "trump 2028" }); const market = markets[0]; const yes = market.outcomes.find((o) => o.label.toLowerCase() === "yes")!; const order = await client.buildOrder({ outcome: yes, side: "buy", type: "limit", amount: 10, price: 0.55, }); console.log(order.id, order.status); ``` ### Submit Order `POST /v0/trade/submit-order` Submit a signed order returned by `buildOrderHosted` and get back the resulting order — id, fill status, average price, and the on-chain tx hash once it settles. `built_order_id` must come from a recent `buildOrderHosted` call and be submitted before its expiry. For one-shot order placement, use `createOrderHosted` instead. **Python:** ```python # Current hosted venues are funded once on Polygon — see /guides/escrow-lifecycle. # Hosted writes need: pmxt_api_key + wallet_address + private_key (signs EIP-712 locally). # Pass an outcome straight from client.fetch_markets() — no UUID lookup needed. import pmxt client = pmxt.Polymarket( pmxt_api_key="YOUR_PMXT_API_KEY", wallet_address="0xYourWallet", # EVM address — its Polygon USDC funds the escrow private_key="0x...", # any EVM key controlling that address ) market = client.fetch_markets({"query": "trump 2028"})[0] yes = next(o for o in market.outcomes if o.label.lower() == "yes") built = client.build_order( outcome=yes, side="buy", type="limit", amount=10, price=0.55, ) order = client.submit_order(built) print(order.id, order.status) ``` **TypeScript:** ```typescript // Current hosted venues are funded once on Polygon — see /guides/escrow-lifecycle. // Hosted writes need: pmxtApiKey + walletAddress + privateKey (signs EIP-712 locally). // Pass an outcome straight from client.fetchMarkets() — no UUID lookup needed. import { Polymarket } from "pmxtjs"; const client = new Polymarket({ pmxtApiKey: "YOUR_PMXT_API_KEY", walletAddress: "0xYourWallet", // EVM address — its Polygon USDC funds the escrow privateKey: "0x...", // any EVM key controlling that address }); const [market] = await client.fetchMarkets({ query: "trump 2028" }); const yes = market.outcomes.find((o) => o.label.toLowerCase() === "yes")!; const built = await client.buildOrder({ outcome: yes, side: "buy", type: "limit", amount: 10, price: 0.55, }); const order = await client.submitOrder(built); console.log(order.id, order.status); ``` ### Cancel Order `POST /v0/orders/cancel/build` Start a cancel on an existing open order. Returns the cancel payload for the wallet to sign — you then submit the signature to `POST /v0/orders/cancel`. The SDK's `cancelOrder()` chains both steps. Call this endpoint directly only if you need to show the cancel to a user before signing. **Python:** ```python # Current hosted venues are funded once on Polygon — see /guides/escrow-lifecycle. # Hosted writes need: pmxt_api_key + wallet_address + private_key (signs EIP-712 locally). import pmxt client = pmxt.Polymarket( pmxt_api_key="YOUR_PMXT_API_KEY", wallet_address="0xYourWallet", # EVM address — its Polygon USDC funds the escrow private_key="0x...", # any EVM key controlling that address ) cancelled = client.cancel_order("order_abc123") print(cancelled.id, cancelled.status) ``` **TypeScript:** ```typescript // Current hosted venues are funded once on Polygon — see /guides/escrow-lifecycle. // Hosted writes need: pmxtApiKey + walletAddress + privateKey (signs EIP-712 locally). import { Polymarket } from "pmxtjs"; const client = new Polymarket({ pmxtApiKey: "YOUR_PMXT_API_KEY", walletAddress: "0xYourWallet", // EVM address — its Polygon USDC funds the escrow privateKey: "0x...", // any EVM key controlling that address }); const cancelled = await client.cancelOrder("order_abc123"); console.log(cancelled.id, cancelled.status); ``` ## Orders & Positions ### Fetch Order `GET /v0/orders/{order_id}` Look up a single order by id. Returns the order's current status, filled / remaining size, average fill price, and — once settled — the on-chain tx hash. `order_id` is what `createOrderHosted` (or `fetchOpenOrdersHosted`) returns. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `order_id` | string | Yes | Unified order id. | **Python:** ```python # Hosted reads need: pmxt_api_key + wallet_address (private_key is only required for writes). import pmxt client = pmxt.Polymarket( pmxt_api_key="YOUR_PMXT_API_KEY", wallet_address="0xYourWallet", ) order = client.fetch_order("order_abc123") print(order.id, order.status, order.filled, order.remaining) ``` **TypeScript:** ```typescript // Hosted reads need: pmxtApiKey + walletAddress (privateKey is only required for writes). import { Polymarket } from "pmxtjs"; const client = new Polymarket({ pmxtApiKey: "YOUR_PMXT_API_KEY", walletAddress: "0xYourWallet", }); const order = await client.fetchOrder("order_abc123"); console.log(order.id, order.status, order.filled, order.remaining); ``` ### Fetch Open Orders `GET /v0/orders/open` List the wallet's resting orders that haven't filled or been cancelled yet. Each entry has the market, outcome, side, size, price, and time-in-force. Filter by `venue` to scope to Polymarket, Opinion, or Limitless. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `address` | string | Yes | EVM wallet address. | | `venue` | string | No | Restrict to a single hosted venue. | **Python:** ```python # Hosted reads need: pmxt_api_key + wallet_address (private_key is only required for writes). import pmxt client = pmxt.Polymarket( pmxt_api_key="YOUR_PMXT_API_KEY", wallet_address="0xYourWallet", ) for order in client.fetch_open_orders(): print(order.id, order.side, order.price, order.remaining) ``` **TypeScript:** ```typescript // Hosted reads need: pmxtApiKey + walletAddress (privateKey is only required for writes). import { Polymarket } from "pmxtjs"; const client = new Polymarket({ pmxtApiKey: "YOUR_PMXT_API_KEY", walletAddress: "0xYourWallet", }); for (const order of await client.fetchOpenOrders()) { console.log(order.id, order.side, order.price, order.remaining); } ``` ### Fetch My Trades `GET /v0/user/{address}/trades` List the wallet's historical fills on Polymarket, Opinion, and Limitless. Each trade carries the executed price (net of venue fees), size, and on-chain settlement tx hash. Closed orders are modelled as trades in hosted mode — use this endpoint instead of `fetchClosedOrders` / `fetchAllOrders` (which raise `NotSupported`). | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `address` | string | Yes | EVM wallet address. | **Python:** ```python # Hosted reads need: pmxt_api_key + wallet_address (private_key is only required for writes). import pmxt client = pmxt.Polymarket( pmxt_api_key="YOUR_PMXT_API_KEY", wallet_address="0xYourWallet", ) for trade in client.fetch_my_trades(): print(trade.id, trade.side, trade.amount, trade.price, trade.tx_hash) ``` **TypeScript:** ```typescript // Hosted reads need: pmxtApiKey + walletAddress (privateKey is only required for writes). import { Polymarket } from "pmxtjs"; const client = new Polymarket({ pmxtApiKey: "YOUR_PMXT_API_KEY", walletAddress: "0xYourWallet", }); for (const trade of await client.fetchMyTrades()) { console.log(trade.id, trade.side, trade.amount, trade.price, trade.txHash); } ``` ### Fetch Positions `GET /v0/user/{address}/positions` List the open positions held by `address` across Polymarket, Opinion, and Limitless. Each position carries the market, outcome, share count, and average entry price. Mark-to-market fields (`current_price`, `current_value`, `unrealized_pnl`) are reserved for a future release and currently return `null`. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `address` | string | Yes | EVM wallet address. | **Python:** ```python # Hosted reads need: pmxt_api_key + wallet_address (private_key is only required for writes). import pmxt client = pmxt.Polymarket( pmxt_api_key="YOUR_PMXT_API_KEY", wallet_address="0xYourWallet", ) for position in client.fetch_positions(): print(position.market_id, position.shares, position.current_value) ``` **TypeScript:** ```typescript // Hosted reads need: pmxtApiKey + walletAddress (privateKey is only required for writes). import { Polymarket } from "pmxtjs"; const client = new Polymarket({ pmxtApiKey: "YOUR_PMXT_API_KEY", walletAddress: "0xYourWallet", }); for (const position of await client.fetchPositions()) { console.log(position.marketId, position.shares, position.currentValue); } ``` ### Fetch Balance `GET /v0/user/{address}/balances` Check the USDC available for trading. Returns the wallet's balance inside the PMXT escrow on Polygon — *not* the wallet's on-chain USDC balance. Top up by depositing at [pmxt.dev/dashboard/wallet](https://pmxt.dev/dashboard/wallet). | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `address` | string | Yes | EVM wallet address (lowercase or checksum). | **Python:** ```python # Hosted reads need: pmxt_api_key + wallet_address (private_key is only required for writes). import pmxt client = pmxt.Polymarket( pmxt_api_key="YOUR_PMXT_API_KEY", wallet_address="0xYourWallet", ) balances = client.fetch_balance() balance = balances[0] print(balance.available, balance.currency) ``` **TypeScript:** ```typescript // Hosted reads need: pmxtApiKey + walletAddress (privateKey is only required for writes). import { Polymarket } from "pmxtjs"; const client = new Polymarket({ pmxtApiKey: "YOUR_PMXT_API_KEY", walletAddress: "0xYourWallet", }); const balances = await client.fetchBalance(); console.log(balances[0].available, balances[0].currency); ``` ## Cross Exchange ### Fetch matched event clusters `GET /v0/matched-event-clusters` Returns connected clusters of semantically matched events across venues. Use this endpoint to browse or anchor event-level matches while preserving each venue's child markets. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `eventId` | string | No | Anchor the response to a specific event ID. | | `slug` | string | No | Anchor the response to a specific event slug. | | `url` | string | No | Anchor the response to a specific event URL. | | `query` | string | No | Text search across cluster titles. | | `category` | string | No | Filter both sides of matched edges by event category. | | `relations` | string | No | Comma-separated relation filter. Valid values: identity (same resolution), subset (A yes implies B yes), superset (B yes implies A yes), overlap (some shared scenarios), and disjoint (mutually exclusive). Defaults to identity. For subset and superset, direction follows the pairwise edge direction returned in rawMatches when includeRawMatches=true. | | `relation` | string | No | Single relation filter. Alias for relations. | | `minConfidence` | number | No | | | `venues` | string | No | Comma-separated venue allow-list. | | `excludeVenues` | string | No | Comma-separated venue deny-list. | | `minVenues` | integer | No | Minimum number of venues required in a cluster. | | `withOrderbook` | boolean | No | Require at least one live orderbook on each matched edge. | | `updatedSince` | string | No | Only include matches updated after this timestamp. | | `includeRawMatches` | boolean | No | Include the pairwise match edges used to build each cluster. | | `sort` | string | No | | | `limit` | integer | No | | | `offset` | integer | No | | | `edgeLimit` | integer | No | Maximum number of pairwise edges to scan before clustering. | **Python:** ```python import pmxt router = pmxt.Router(pmxt_api_key="YOUR_PMXT_API_KEY") clusters = router.fetch_matched_event_clusters( query="Satoshi", relation="identity", min_venues=2, include_raw_matches=True, limit=5, ) for cluster in clusters: venues = [event.source_exchange for event in cluster.events] print(cluster.canonical_title, venues) ``` **TypeScript:** ```typescript import { Router } from "pmxtjs"; const router = new Router({ pmxtApiKey: "YOUR_PMXT_API_KEY" }); async function main() { const clusters = await router.fetchMatchedEventClusters({ query: "Satoshi", relation: "identity", minVenues: 2, includeRawMatches: true, limit: 5, }); for (const cluster of clusters) { console.log( cluster.canonicalTitle, cluster.events.map((event) => event.sourceExchange), ); } } main(); ``` ### Fetch matched market clusters `GET /v0/matched-market-clusters` Returns connected clusters of semantically matched markets across venues. Use this endpoint to browse or anchor market-level matches without flattening the response into pairwise rows. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `marketId` | string | No | Anchor the response to a specific market ID. | | `slug` | string | No | Anchor the response to a specific market slug. | | `url` | string | No | Anchor the response to a specific market URL. | | `query` | string | No | Text search across cluster titles. | | `category` | string | No | Filter both sides of matched edges by market category. | | `relations` | string | No | Comma-separated relation filter. Valid values: identity (same resolution), subset (A yes implies B yes), superset (B yes implies A yes), overlap (some shared scenarios), and disjoint (mutually exclusive). Defaults to identity. For subset and superset, direction follows the pairwise edge direction returned in rawMatches when includeRawMatches=true. | | `relation` | string | No | Single relation filter. Alias for relations. | | `minConfidence` | number | No | | | `venues` | string | No | Comma-separated venue allow-list. | | `excludeVenues` | string | No | Comma-separated venue deny-list. | | `minVenues` | integer | No | Minimum number of venues required in a cluster. | | `withOrderbook` | boolean | No | Require at least one live orderbook on each matched edge. | | `updatedSince` | string | No | Only include matches updated after this timestamp. | | `includeRawMatches` | boolean | No | Include the pairwise match edges used to build each cluster. | | `sort` | string | No | | | `limit` | integer | No | | | `offset` | integer | No | | | `edgeLimit` | integer | No | Maximum number of pairwise edges to scan before clustering. | **Python:** ```python import pmxt router = pmxt.Router(pmxt_api_key="YOUR_PMXT_API_KEY") clusters = router.fetch_matched_market_clusters( query="Satoshi", relation="identity", min_venues=2, include_raw_matches=True, limit=5, ) for cluster in clusters: venues = [market.source_exchange for market in cluster.markets] print(cluster.canonical_title, venues) ``` **TypeScript:** ```typescript import { Router } from "pmxtjs"; const router = new Router({ pmxtApiKey: "YOUR_PMXT_API_KEY" }); async function main() { const clusters = await router.fetchMatchedMarketClusters({ query: "Satoshi", relation: "identity", minVenues: 2, includeRawMatches: true, limit: 5, }); for (const cluster of clusters) { console.log( cluster.canonicalTitle, cluster.markets.map((market) => market.sourceExchange), ); } } main(); ``` ## Order Book & Trades ### Fetch OHLCV Source: https://pmxt.dev/docs/api-reference/fetch-ohlcv > **Note:** **Hosted and self-hosted data depth** — Hosted `api.pmxt.dev` calls require a PMXT API key. If you omit the key, SDK calls route through your local `pmxt-core` process and use venue-native APIs. `fetchOHLCV` returns normalized `PriceCandle` rows for venues whose implementation supports `fetchOHLCV`; history length, volume, and rate limits remain venue-specific. Check [Feature Support & Compliance](https://github.com/pmxt-dev/pmxt/blob/main/core/COMPLIANCE.md) for the current matrix. [Get your API key](https://pmxt.dev/dashboard). #### Use cases ##### Get hourly candles for a market ```python Python import pmxt # Hosted route: include pmxt_api_key. Omit it for self-hosted/local pmxt-core. api = pmxt.Polymarket(pmxt_api_key="YOUR_PMXT_API_KEY") market = api.fetch_market(slug="will-alexandria-ocasio-cortez-win-the-2028-us-presidential-election") candles = api.fetch_ohlcv(market.yes, resolution="1h", limit=100) for c in candles: print(f"{c.open:.2f} {c.high:.2f} {c.low:.2f} {c.close:.2f} V:{c.volume}") ``` ```javascript JavaScript import { Polymarket } from "pmxtjs"; // Hosted route: include pmxtApiKey. Omit it for self-hosted/local pmxt-core. const api = new Polymarket({ pmxtApiKey: "YOUR_PMXT_API_KEY" }); const market = await api.fetchMarket({ slug: "will-alexandria-ocasio-cortez-win-the-2028-us-presidential-election" }); const candles = await api.fetchOHLCV(market.yes, { resolution: "1h", limit: 100 }); candles.forEach((c) => console.log(`${c.open} ${c.high} ${c.low} ${c.close} V:${c.volume}`)); ``` ```bash curl # First get the outcome ID from a market, then pass it as the outcomeId parameter curl "https://api.pmxt.dev/api/polymarket/fetchOHLCV?outcomeId=YOUR_OUTCOME_ID&resolution=1h&limit=100" \ -H "Authorization: Bearer $PMXT_API_KEY" ``` ### Fetch Order Book Source: https://pmxt.dev/docs/api-reference/fetch-order-book #### Use cases ##### Live order book Fetch the current L2 order book for an outcome from the exchange's live order book endpoint. For Polymarket this is a live CLOB call: pass the outcome token ID directly and omit historical params. > **Warning:** `poly.fetch_order_book(token_id)` without `params.since` or `params.until` is live-only. It does not read PMXT Archive data and may fail for closed, resolved, or otherwise inactive Polymarket markets with an error such as `No orderbook exists`. ```python Python import pmxt poly = pmxt.Polymarket(pmxt_api_key="pmxt_...") book = poly.fetch_order_book("104932610032177696635191871147557737718087870958469629338467406422339967452218") print(f"{len(book.bids)} bids, {len(book.asks)} asks") ``` ```javascript JavaScript import { Polymarket } from "pmxtjs"; const poly = new Polymarket({ pmxtApiKey: "pmxt_..." }); const book = await poly.fetchOrderBook("104932610032177696635191871147557737718087870958469629338467406422339967452218"); console.log(`${book.bids.length} bids, ${book.asks.length} asks`); ``` ```bash curl curl "https://api.pmxt.dev/api/polymarket/fetchOrderBook?outcomeId=104932610032177696635191871147557737718087870958469629338467406422339967452218" \ -H "Authorization: Bearer ***" ``` ##### Historical snapshot Get the order book at a specific point in time by passing `since` as a Unix timestamp in milliseconds. Historical Polymarket queries are served from the PMXT Archive and return the nearest reconstructed snapshot at or before that time. For binary markets, you can pass the Polymarket condition ID as the first argument and choose the side with `params.outcome`. Use `"yes"` or `"no"` instead of copying the long outcome token ID. ```python Python import pmxt poly = pmxt.Polymarket(pmxt_api_key="pmxt_...") # Historical lookup against PMXT Archive, not the live CLOB. book = poly.fetch_order_book( "0xc704f74e2f9dfae70f770cb253ffadde10768eeab41233098bf5ac67995a94b5", params={"since": 1779487200000, "outcome": "yes"}, ) print(f"{book.dt}: {len(book.bids)} bids, {len(book.asks)} asks") print(f" best bid: {max(b.price for b in book.bids):.3f}") print(f" best ask: {min(a.price for a in book.asks):.3f}") ``` ```javascript JavaScript import { Polymarket } from "pmxtjs"; const poly = new Polymarket({ pmxtApiKey: "pmxt_..." }); // Historical lookup against PMXT Archive, not the live CLOB. const book = await poly.fetchOrderBook( "0xc704f74e2f9dfae70f770cb253ffadde10768eeab41233098bf5ac67995a94b5", undefined, { since: 1779487200000, outcome: "yes" } ); console.log(`${book.datetime}: ${book.bids.length} bids, ${book.asks.length} asks`); console.log(` best bid: ${Math.max(...book.bids.map((b) => b.price)).toFixed(3)}`); console.log(` best ask: ${Math.min(...book.asks.map((a) => a.price)).toFixed(3)}`); ``` ```bash curl curl -X POST "https://api.pmxt.dev/api/polymarket/fetchOrderBook" \ -H "Authorization: Bearer ***" \ -H "Content-Type: application/json" \ -d '{"args":["0xc704f74e2f9dfae70f770cb253ffadde10768eeab41233098bf5ac67995a94b5", null, {"since": 1779487200000, "outcome": "yes"}]}' ``` ##### Historical crypto 5m events Polymarket crypto 5-minute slugs such as `btc-updown-5m-1779481500` are event slugs. Use `fetch_event(slug=...)`, then select the nested market you want. Do not use `fetch_market(slug=...)` for these event-level slugs. ```python Python import pmxt poly = pmxt.Polymarket(pmxt_api_key="pmxt_...") event = poly.fetch_event(slug="btc-updown-5m-1779481500") market = event.markets[0] book = poly.fetch_order_book( market.market_id, params={"since": 1779487200000, "outcome": "yes"}, ) print(f"{market.title}: {book.dt}") ``` ```javascript JavaScript import { Polymarket } from "pmxtjs"; const poly = new Polymarket({ pmxtApiKey: "pmxt_..." }); const event = await poly.fetchEvent({ slug: "btc-updown-5m-1779481500" }); const market = event.markets[0]; const book = await poly.fetchOrderBook( market.marketId, undefined, { since: 1779487200000, outcome: "yes" } ); console.log(`${market.title}: ${book.datetime}`); ``` ```bash curl curl -X POST "https://api.pmxt.dev/api/polymarket/fetchEvent" \ -H "Authorization: Bearer ***" \ -H "Content-Type: application/json" \ -d '{"kwargs":{"slug":"btc-updown-5m-1779481500"}}' ``` ##### Historical range (reconstructed L2 books) Pass both `since` and `until` to get an array of fully reconstructed L2 order book snapshots. Each snapshot is a complete book at that moment in time — not deltas. The API returns up to `limit` snapshots from the PMXT Archive. Defaults to 100 snapshots per request, max 1000. For raw bulk downloads, use the Parquet files in the [PMXT Archive](https://archive.pmxt.dev) instead of trying to stream bulk data through the live order book endpoint. ```python Python import pmxt poly = pmxt.Polymarket(pmxt_api_key="pmxt_...") event = poly.fetch_event(slug="btc-updown-5m-1779481500") market = event.markets[0] books = poly.fetch_order_book( market.market_id, params={ "since": 1779480000000, "until": 1779487200000, "outcome": "yes", "limit": 100, } ) print(f"{len(books)} snapshots") for ob in books[:3]: print(f" {ob.dt} bid={ob.bids[0].price} ask={ob.asks[0].price}") ``` ```javascript JavaScript import { Polymarket } from "pmxtjs"; const poly = new Polymarket({ pmxtApiKey: "pmxt_..." }); const event = await poly.fetchEvent({ slug: "btc-updown-5m-1779481500" }); const market = event.markets[0]; const books = await poly.fetchOrderBook( market.marketId, undefined, { since: 1779480000000, until: 1779487200000, outcome: "yes", limit: 100 } ); console.log(`${books.length} snapshots`); books.slice(0, 3).forEach((ob) => console.log(` ${ob.datetime} bid=${ob.bids[0].price} ask=${ob.asks[0].price}`) ); ``` ```bash curl curl -X POST "https://api.pmxt.dev/api/polymarket/fetchOrderBook" \ -H "Authorization: Bearer ***" \ -H "Content-Type: application/json" \ -d '{"args":["0xc704f74e2f9dfae70f770cb253ffadde10768eeab41233098bf5ac67995a94b5", null, {"since": 1779480000000, "until": 1779487200000, "outcome": "yes", "limit": 100}]}' ``` > **Note:** Historical order book data is backed by the [PMXT Archive](https://archive.pmxt.dev) — the same tick-level data available as Parquet files, but queryable directly from the API without downloading or parsing files. Historical `fetchOrderBook` supports Polymarket, Kalshi, Limitless, and Opinion. ### Fetch Order Books `POST /api/{exchange}/fetchOrderBooks` Batch variant of {@link fetchOrderBook}. Fetches order books for multiple outcomes in a single request where the exchange supports it. **Response:** `{ success: true, data: ... }` **Python:** ```python import pmxt # API key optional — enables faster catalog-backed lookups exchange = pmxt.Kalshi( pmxt_api_key="YOUR_PMXT_API_KEY", ) result = exchange.fetch_order_books(["67890"]) ``` **TypeScript:** ```typescript import { Kalshi } from "pmxtjs"; // API key optional — enables faster catalog-backed lookups const exchange = new Kalshi({ pmxtApiKey: "YOUR_PMXT_API_KEY", }); const result = await exchange.fetchOrderBooks(["67890"]); ``` ### Fetch Trades `GET /api/{exchange}/fetchTrades` Fetch raw trade history for a specific outcome. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `outcomeId` | string | Yes | | | `start` | string | No | Start of the time range | | `end` | string | No | End of the time range | | `limit` | number | No | Maximum number of results to return (max {@link MAX_TRADES_LIMIT}) | **Response:** `{ success: true, data: Trade[] }` **Python:** ```python import pmxt # API key optional — enables faster catalog-backed lookups exchange = pmxt.Kalshi( pmxt_api_key="YOUR_PMXT_API_KEY", ) result = exchange.fetch_trades( "67890", start="2026-01-01T00:00:00Z", end="2026-01-31T00:00:00Z", limit=10, ) ``` **TypeScript:** ```typescript import { Kalshi } from "pmxtjs"; // API key optional — enables faster catalog-backed lookups const exchange = new Kalshi({ pmxtApiKey: "YOUR_PMXT_API_KEY", }); const result = await exchange.fetchTrades( "67890", { start: new Date("2026-01-01T00:00:00Z"), end: new Date("2026-01-31T00:00:00Z"), limit: 10, }, ); ``` ### Get Execution Price `POST /api/{exchange}/getExecutionPrice` Calculate the volume-weighted average execution price for a given order size. Returns 0 if the order cannot be fully filled. **Response:** `{ success: true, data: ... }` **Python:** ```python import pmxt # API key optional — enables faster catalog-backed lookups exchange = pmxt.Kalshi( pmxt_api_key="YOUR_PMXT_API_KEY", ) order_book = pmxt.OrderBook( bids=[pmxt.OrderLevel(price=0.52, size=100)], asks=[pmxt.OrderLevel(price=0.54, size=100)], ) result = exchange.get_execution_price(order_book, "buy", 10) ``` **TypeScript:** ```typescript import { Kalshi } from "pmxtjs"; // API key optional — enables faster catalog-backed lookups const exchange = new Kalshi({ pmxtApiKey: "YOUR_PMXT_API_KEY", }); const orderBook = { bids: [{ price: 0.52, size: 100 }], asks: [{ price: 0.54, size: 100 }], timestamp: Date.now(), }; const result = exchange.getExecutionPrice(orderBook, "buy", 10); ``` ### Get Execution Price Detailed `POST /api/{exchange}/getExecutionPriceDetailed` Calculate detailed execution price information including partial fill data. **Response:** `{ success: true, data: ... }` **Python:** ```python import pmxt # API key optional — enables faster catalog-backed lookups exchange = pmxt.Kalshi( pmxt_api_key="YOUR_PMXT_API_KEY", ) order_book = pmxt.OrderBook( bids=[pmxt.OrderLevel(price=0.52, size=100)], asks=[pmxt.OrderLevel(price=0.54, size=100)], ) result = exchange.get_execution_price_detailed(order_book, "buy", 10) ``` **TypeScript:** ```typescript import { Kalshi } from "pmxtjs"; // API key optional — enables faster catalog-backed lookups const exchange = new Kalshi({ pmxtApiKey: "YOUR_PMXT_API_KEY", }); const orderBook = { bids: [{ price: 0.52, size: 100 }], asks: [{ price: 0.54, size: 100 }], timestamp: Date.now(), }; const result = await exchange.getExecutionPriceDetailed(orderBook, "buy", 10); ``` ## Realtime ### WebSocket Overview Source: https://pmxt.dev/docs/api-reference/websocket #### Connection Connect to the WebSocket endpoint with your API key: | Environment | URL | |---|---| | Local server | `ws://localhost:3847/ws` | | Hosted API | `wss://api.pmxt.dev/ws?apiKey=YOUR_KEY` | All messages are JSON text frames. A single connection supports multiple concurrent subscriptions. #### Protocol ##### Subscribe ```json { "id": "req-1", "action": "subscribe", "exchange": "polymarket", "method": "watchOrderBook", "args": ["OUTCOME_ID", 50] } ``` ##### Data event ```json { "event": "data", "id": "req-1", "method": "watchOrderBook", "symbol": "OUTCOME_ID", "source": "polymarket", "data": { "bids": [{ "price": 0.42, "size": 100 }], "asks": [{ "price": 0.58, "size": 200 }], "timestamp": 1778450010713 } } ``` ##### Unsubscribe ```json { "id": "req-1", "action": "unsubscribe", "exchange": "polymarket", "method": "watchOrderBook", "args": ["OUTCOME_ID"] } ``` ##### Error ```json { "event": "error", "id": "req-1", "error": { "message": "..." } } ``` ### watchAllOrderBooks Source: https://pmxt.dev/docs/api-reference/watch-all-order-books > **Note:** **Hosted only** — This method requires a PMXT API key and connects to the hosted WebSocket API at `wss://api.pmxt.dev/ws?apiKey=YOUR_KEY`. Not available on the local server. > **Note:** SDK defaults depend on which client you instantiate. `Router` streams all venues by default. Venue clients such as `Kalshi`, `Polymarket`, `Limitless`, and `Opinion` stream only their own venue by default. Pass an explicit `venues` list to override either default. #### Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `venues` | string[] | No | Optional venue filter. Omit to use the SDK client's default; pass `[]` to force all venues from SDK clients. | Available venues: `polymarket`, `kalshi`, `limitless`, `opinion`. Raw WebSocket subscriptions do not have an SDK client default. For raw WebSocket, connect to the WebSocket URL, send one JSON text frame, then read JSON text frames. Omit `args` to stream all venues, or send `args: [["kalshi"]]` to stream only Kalshi. #### Response Returns a `FirehoseEvent` object for each orderbook update in the selected stream: | Field | Type | Description | |-------|------|-------------| | `source` | string | The venue (e.g. `"polymarket"`, `"limitless"`) | | `symbol` | string | The outcome token ID | | `orderbook` | `OrderBook` | The orderbook snapshot (`bids`, `asks`, `timestamp`) | #### Usage ##### All venues with Router ```python Python import pmxt router = pmxt.Router(pmxt_api_key="YOUR_PMXT_API_KEY") while True: event = router.watch_all_order_books() book = event.orderbook if not book.bids or not book.asks: continue print(f"[{event.source}] {event.symbol[:20]}...") print(f" bid={book.bids[0].price} ask={book.asks[0].price}") ``` ```javascript JavaScript import { Router } from "pmxtjs"; const router = new Router({ pmxtApiKey: "YOUR_PMXT_API_KEY" }); while (true) { const event = await router.watchAllOrderBooks(); const { bids, asks } = event.orderbook; if (!bids.length || !asks.length) continue; console.log(`[${event.source}] ${event.symbol.slice(0, 20)}...`); console.log(` bid=${bids[0].price} ask=${asks[0].price}`); } ``` ##### Raw WebSocket without an SDK Use this when you are not using the Python or TypeScript SDKs, or when you are implementing the stream in Rust, Go, Java, or another WebSocket client. | Environment | URL | |-------------|-----| | Hosted PMXT | `wss://api.pmxt.dev/ws?apiKey=YOUR_PMXT_API_KEY` | This Python example does not use the PMXT SDK; it connects to the same raw WebSocket URL that a Rust or Go client would use. ```bash pip install websockets PMXT_API_KEY="YOUR_PMXT_API_KEY" python raw-watch-all-order-books.py ``` ```python raw-watch-all-order-books.py import asyncio import json import os import websockets PMXT_API_KEY = os.environ["PMXT_API_KEY"] async def main(): url = f"wss://api.pmxt.dev/ws?apiKey={PMXT_API_KEY}" async with websockets.connect(url) as ws: await ws.send(json.dumps({ "id": "all-books", "action": "subscribe", "method": "watchAllOrderBooks", })) async for raw in ws: print(raw) asyncio.run(main()) ``` For any other language, the wire protocol is the same: open the WebSocket URL, send this JSON text frame, then read JSON text frames from the server. ```json { "id": "kalshi-books", "action": "subscribe", "method": "watchAllOrderBooks", "args": [["kalshi"]] } ``` Data frames look like this: ```json { "event": "data", "id": "kalshi-books", "method": "watchAllOrderBooks", "symbol": "OUTCOME_ID", "source": "kalshi", "data": { "bids": [{ "price": 0.42, "size": 100 }], "asks": [{ "price": 0.58, "size": 200 }], "timestamp": 1778450010713 } } ``` ##### One venue with a venue client ```python Python import pmxt kalshi = pmxt.Kalshi(pmxt_api_key="YOUR_PMXT_API_KEY") # Defaults to Kalshi events only while True: event = kalshi.watch_all_order_books() if not event.orderbook.asks: continue print(f"[{event.source}] ask={event.orderbook.asks[0].price}") ``` ```javascript JavaScript import { Kalshi } from "pmxtjs"; const kalshi = new Kalshi({ pmxtApiKey: "YOUR_PMXT_API_KEY" }); // Defaults to Kalshi events only while (true) { const event = await kalshi.watchAllOrderBooks(); if (!event.orderbook.asks.length) continue; console.log(`[${event.source}] ask=${event.orderbook.asks[0].price}`); } ``` ##### Explicit venue filter ```python Python import pmxt router = pmxt.Router(pmxt_api_key="YOUR_PMXT_API_KEY") # Only Polymarket and Limitless events while True: event = router.watch_all_order_books(["polymarket", "limitless"]) if not event.orderbook.bids: continue print(f"[{event.source}] bid={event.orderbook.bids[0].price}") ``` ```javascript JavaScript import { Router } from "pmxtjs"; const router = new Router({ pmxtApiKey: "YOUR_PMXT_API_KEY" }); // Only Polymarket and Limitless events while (true) { const event = await router.watchAllOrderBooks(["polymarket", "limitless"]); if (!event.orderbook.bids.length) continue; console.log(`[${event.source}] bid=${event.orderbook.bids[0].price}`); } ``` ##### Force all venues from a venue client ```python Python import pmxt kalshi = pmxt.Kalshi(pmxt_api_key="YOUR_PMXT_API_KEY") # Empty list overrides the Kalshi default and streams all venues event = kalshi.watch_all_order_books([]) ``` ```javascript JavaScript import { Kalshi } from "pmxtjs"; const kalshi = new Kalshi({ pmxtApiKey: "YOUR_PMXT_API_KEY" }); // Empty array overrides the Kalshi default and streams all venues const event = await kalshi.watchAllOrderBooks([]); ``` #### Use cases ##### Cross-venue monitoring dashboard Build a real-time dashboard showing all orderbook activity: ```python Python import pmxt from collections import defaultdict router = pmxt.Router(pmxt_api_key="YOUR_PMXT_API_KEY") counts = defaultdict(int) while True: event = router.watch_all_order_books() counts[event.source] += 1 total = sum(counts.values()) if total % 100 == 0: print(f"Events: {dict(counts)} (total: {total})") ``` ```javascript JavaScript import { Router } from "pmxtjs"; const router = new Router({ pmxtApiKey: "YOUR_PMXT_API_KEY" }); const counts = {}; while (true) { const event = await router.watchAllOrderBooks(); counts[event.source] = (counts[event.source] || 0) + 1; const total = Object.values(counts).reduce((a, b) => a + b, 0); if (total % 100 === 0) { console.log(`Events:`, counts, `(total: ${total})`); } } ``` ##### Real-time arbitrage scanner Detect cross-venue price discrepancies as they happen: ```python Python import pmxt router = pmxt.Router(pmxt_api_key="YOUR_PMXT_API_KEY") latest = {} # symbol -> {source -> mid_price} while True: event = router.watch_all_order_books() book = event.orderbook if not book.bids or not book.asks: continue mid = (book.bids[0].price + book.asks[0].price) / 2 if event.symbol not in latest: latest[event.symbol] = {} latest[event.symbol][event.source] = mid prices = latest[event.symbol] if len(prices) >= 2: venues = list(prices.keys()) spread = abs(prices[venues[0]] - prices[venues[1]]) if spread > 0.03: print(f"Spread {spread:.1%}: {venues[0]}={prices[venues[0]]:.1%} vs {venues[1]}={prices[venues[1]]:.1%}") ``` ```javascript JavaScript import { Router } from "pmxtjs"; const router = new Router({ pmxtApiKey: "YOUR_PMXT_API_KEY" }); const latest = new Map(); while (true) { const event = await router.watchAllOrderBooks(); const { bids, asks } = event.orderbook; if (!bids.length || !asks.length) continue; const mid = (bids[0].price + asks[0].price) / 2; if (!latest.has(event.symbol)) latest.set(event.symbol, {}); latest.get(event.symbol)[event.source] = mid; const prices = latest.get(event.symbol); const venues = Object.keys(prices); if (venues.length >= 2) { const spread = Math.abs(prices[venues[0]] - prices[venues[1]]); if (spread > 0.03) { console.log(`Spread ${(spread * 100).toFixed(1)}%: ${venues[0]}=${(prices[venues[0]] * 100).toFixed(1)}% vs ${venues[1]}=${(prices[venues[1]] * 100).toFixed(1)}%`); } } } ``` ### watchOrderBook Source: https://pmxt.dev/docs/api-reference/watch-order-book > **Note:** **WebSocket endpoint** — This method uses WebSocket streaming, not HTTP. Connect to `wss://api.pmxt.dev/ws?apiKey=YOUR_KEY` (hosted) or `ws://localhost:3847/ws` (local server). Supported venues: `polymarket`, `kalshi`, `limitless`, `opinion`, `polymarket_us`, `probable`, `baozi`, `myriad`, `gemini-titan`, `rain`, `hunch`. #### Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `outcomeId` | string | Yes | The outcome token ID to watch | | `limit` | number | No | Optional depth limit for the streamed order book | | `params` | object | No | Optional exchange-specific parameters | #### Response Returns an `OrderBook` object each time the orderbook updates: | Field | Type | Description | |-------|------|-------------| | `bids` | `OrderLevel[]` | Bid orders, sorted high to low | | `asks` | `OrderLevel[]` | Ask orders, sorted low to high | | `timestamp` | number | Unix timestamp in milliseconds | Each `OrderLevel` has `price` (0–1 probability) and `size` (number of contracts). #### Usage ```python Python import pmxt exchange = pmxt.Polymarket(pmxt_api_key="YOUR_PMXT_API_KEY") # Stream orderbook updates — each call returns the next snapshot while True: book = exchange.watch_order_book("OUTCOME_ID", limit=10) if not book.bids or not book.asks: continue print(f"Best bid: {book.bids[0].price} ({book.bids[0].size} contracts)") print(f"Best ask: {book.asks[0].price} ({book.asks[0].size} contracts)") print(f"Spread: {book.asks[0].price - book.bids[0].price:.4f}") ``` ```javascript JavaScript import { Polymarket } from "pmxtjs"; const exchange = new Polymarket({ pmxtApiKey: "YOUR_PMXT_API_KEY" }); // Stream orderbook updates — each call returns the next snapshot while (true) { const book = await exchange.watchOrderBook("OUTCOME_ID", 10); if (!book.bids.length || !book.asks.length) continue; console.log(`Best bid: ${book.bids[0].price} (${book.bids[0].size} contracts)`); console.log(`Best ask: ${book.asks[0].price} (${book.asks[0].size} contracts)`); console.log(`Spread: ${(book.asks[0].price - book.bids[0].price).toFixed(4)}`); } ``` ```json WebSocket // → Subscribe { "id": "req-1", "action": "subscribe", "exchange": "polymarket", "method": "watchOrderBook", "args": ["OUTCOME_ID", 10] } // ← Subscription confirmed { "event": "subscribed", "id": "req-1" } // ← Data event (repeats on each orderbook change) { "event": "data", "id": "req-1", "method": "watchOrderBook", "symbol": "OUTCOME_ID", "source": "polymarket", "data": { "bids": [{ "price": 0.42, "size": 100 }, { "price": 0.41, "size": 250 }], "asks": [{ "price": 0.58, "size": 200 }, { "price": 0.59, "size": 300 }], "timestamp": 1778450010713 } } // → Unsubscribe { "id": "req-1", "action": "unsubscribe", "exchange": "polymarket", "method": "watchOrderBook", "args": ["OUTCOME_ID"] } ``` #### Use cases ##### Monitor a specific market Track the "Will Trump win 2028?" market on Polymarket: ```python Python import pmxt poly = pmxt.Polymarket(pmxt_api_key="YOUR_PMXT_API_KEY") # Use the outcome's token_id from fetchMarket while True: book = poly.watch_order_book("48388283710620338...") bid = book.bids[0].price if book.bids else 0 ask = book.asks[0].price if book.asks else 1 print(f"YES price: {(bid + ask) / 2:.1%}") ``` ```javascript JavaScript import { Polymarket } from "pmxtjs"; const poly = new Polymarket({ pmxtApiKey: "YOUR_PMXT_API_KEY" }); while (true) { const book = await poly.watchOrderBook("48388283710620338..."); const bid = book.bids[0]?.price ?? 0; const ask = book.asks[0]?.price ?? 1; console.log(`YES price: ${((bid + ask) / 2 * 100).toFixed(1)}%`); } ``` ##### Cross-venue spread detection Watch the same market on two venues to detect arbitrage: ```python Python import pmxt import asyncio poly = pmxt.Polymarket(pmxt_api_key="YOUR_PMXT_API_KEY") kalshi = pmxt.Kalshi(pmxt_api_key="YOUR_PMXT_API_KEY") # Run both in parallel (simplified) while True: poly_book = poly.watch_order_book("POLY_OUTCOME_ID") kalshi_book = kalshi.watch_order_book("KALSHI_OUTCOME_ID") if not poly_book.bids or not kalshi_book.asks: continue spread = poly_book.bids[0].price - kalshi_book.asks[0].price if spread > 0.02: print(f"Arbitrage opportunity: {spread:.4f}") ``` ```javascript JavaScript import { Polymarket, Kalshi } from "pmxtjs"; const poly = new Polymarket({ pmxtApiKey: "YOUR_PMXT_API_KEY" }); const kalshi = new Kalshi({ pmxtApiKey: "YOUR_PMXT_API_KEY" }); while (true) { const [polyBook, kalshiBook] = await Promise.all([ poly.watchOrderBook("POLY_OUTCOME_ID"), kalshi.watchOrderBook("KALSHI_OUTCOME_ID"), ]); if (!polyBook.bids.length || !kalshiBook.asks.length) continue; const spread = polyBook.bids[0].price - kalshiBook.asks[0].price; if (spread > 0.02) { console.log(`Arbitrage opportunity: ${spread.toFixed(4)}`); } } ``` ### watchOrderBooks Source: https://pmxt.dev/docs/api-reference/watch-order-books > **Note:** **WebSocket endpoint** — This method uses WebSocket streaming, not HTTP. Connect to `wss://api.pmxt.dev/ws?apiKey=YOUR_KEY` (hosted) or `ws://localhost:3847/ws` (local server). Supported venues: `polymarket`, `kalshi`, `limitless`, `opinion`, `polymarket_us`, `probable`, `baozi`, `myriad`, `gemini-titan`, `rain`, `hunch`. #### Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `outcomeIds` | string[] | Yes | Array of outcome token IDs to watch | | `limit` | number | No | Optional depth limit for each streamed order book | | `params` | object | No | Optional exchange-specific parameters | #### Response Returns a `Record` — one `OrderBook` per outcome, keyed by outcome ID. Each update arrives as a separate data event. | Field | Type | Description | |-------|------|-------------| | `bids` | `OrderLevel[]` | Bid orders, sorted high to low | | `asks` | `OrderLevel[]` | Ask orders, sorted low to high | | `timestamp` | number | Unix timestamp in milliseconds | #### Usage ```python Python import pmxt exchange = pmxt.Polymarket(pmxt_api_key="YOUR_PMXT_API_KEY") ids = ["OUTCOME_ID_1", "OUTCOME_ID_2", "OUTCOME_ID_3"] while True: books = exchange.watch_order_books(ids, limit=10) for outcome_id, book in books.items(): bid = book.bids[0].price if book.bids else None ask = book.asks[0].price if book.asks else None print(f"{outcome_id[:20]}... bid={bid} ask={ask}") ``` ```javascript JavaScript import { Polymarket } from "pmxtjs"; const exchange = new Polymarket({ pmxtApiKey: "YOUR_PMXT_API_KEY" }); const ids = ["OUTCOME_ID_1", "OUTCOME_ID_2", "OUTCOME_ID_3"]; while (true) { const books = await exchange.watchOrderBooks(ids, 10); for (const [id, book] of Object.entries(books)) { console.log(`${id.slice(0, 20)}... bid=${book.bids[0]?.price} ask=${book.asks[0]?.price}`); } } ``` ```json WebSocket // → Subscribe to multiple outcomes at once { "id": "req-1", "action": "subscribe", "exchange": "polymarket", "method": "watchOrderBooks", "args": [["OUTCOME_ID_1", "OUTCOME_ID_2", "OUTCOME_ID_3"], 10] } // ← One data event per outcome, per update { "event": "data", "id": "req-1", "method": "watchOrderBooks", "symbol": "OUTCOME_ID_1", "source": "polymarket", "data": { "bids": [{ "price": 0.42, "size": 100 }], "asks": [{ "price": 0.58, "size": 200 }], "timestamp": 1778450010713 } } ``` #### Use cases ##### Watch all outcomes in a market Fetch a market's outcomes, then stream all their orderbooks: ```python Python import pmxt poly = pmxt.Polymarket(pmxt_api_key="YOUR_PMXT_API_KEY") # Get all outcomes for a market market = poly.fetch_market(market_id="MARKET_ID") ids = [o.outcome_id for o in market.outcomes] # Stream all orderbooks while True: books = poly.watch_order_books(ids) for outcome_id, book in books.items(): mid = (book.bids[0].price + book.asks[0].price) / 2 if book.bids and book.asks else 0 print(f"{outcome_id[:20]}... mid={mid:.1%}") ``` ```javascript JavaScript import { Polymarket } from "pmxtjs"; const poly = new Polymarket({ pmxtApiKey: "YOUR_PMXT_API_KEY" }); const market = await poly.fetchMarket({ marketId: "MARKET_ID" }); const ids = market.outcomes.map((o) => o.outcomeId); while (true) { const books = await poly.watchOrderBooks(ids); for (const [id, book] of Object.entries(books)) { const mid = book.bids[0] && book.asks[0] ? (book.bids[0].price + book.asks[0].price) / 2 : 0; console.log(`${id.slice(0, 20)}... mid=${(mid * 100).toFixed(1)}%`); } } ``` ### watchTrades Source: https://pmxt.dev/docs/api-reference/watch-trades > **Note:** **WebSocket endpoint** — This method uses WebSocket streaming, not HTTP. Connect to `wss://api.pmxt.dev/ws?apiKey=YOUR_KEY` (hosted) or `ws://localhost:3847/ws` (local server). Supported venues: `polymarket`, `kalshi`, `limitless`, `opinion`, `polymarket_us`, `myriad`, `gemini-titan`, `rain`, `hunch`. #### Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `outcomeId` | string | Yes | The outcome token ID to watch | #### Response Returns a `Trade[]` array each time new trades occur: | Field | Type | Description | |-------|------|-------------| | `id` | string | Trade ID | | `timestamp` | number | Unix timestamp in milliseconds | | `price` | number | Trade price (0–1 probability) | | `amount` | number | Number of contracts | | `side` | string | `"buy"`, `"sell"`, or `"unknown"` | #### Usage ```python Python import pmxt exchange = pmxt.Polymarket(pmxt_api_key="YOUR_PMXT_API_KEY") while True: trades = exchange.watch_trades("OUTCOME_ID") for trade in trades: print(f"{trade.side.upper()} {trade.amount} contracts @ {trade.price:.1%}") ``` ```javascript JavaScript import { Polymarket } from "pmxtjs"; const exchange = new Polymarket({ pmxtApiKey: "YOUR_PMXT_API_KEY" }); while (true) { const trades = await exchange.watchTrades("OUTCOME_ID"); for (const trade of trades) { console.log(`${trade.side.toUpperCase()} ${trade.amount} contracts @ ${(trade.price * 100).toFixed(1)}%`); } } ``` ```json WebSocket // → Subscribe { "id": "req-1", "action": "subscribe", "exchange": "polymarket", "method": "watchTrades", "args": ["OUTCOME_ID"] } // ← Data event { "event": "data", "id": "req-1", "method": "watchTrades", "symbol": "OUTCOME_ID", "source": "polymarket", "data": [ { "id": "trade-abc123", "timestamp": 1778450010713, "price": 0.42, "amount": 100, "side": "buy" } ] } ``` #### Use cases ##### Trade volume tracker Monitor real-time trading volume on a market: ```python Python import pmxt exchange = pmxt.Polymarket(pmxt_api_key="YOUR_PMXT_API_KEY") total_volume = 0 while True: trades = exchange.watch_trades("OUTCOME_ID") for trade in trades: total_volume += trade.amount print(f"Trade: {trade.side} {trade.amount} @ {trade.price:.1%} | Total volume: {total_volume}") ``` ```javascript JavaScript import { Polymarket } from "pmxtjs"; const exchange = new Polymarket({ pmxtApiKey: "YOUR_PMXT_API_KEY" }); let totalVolume = 0; while (true) { const trades = await exchange.watchTrades("OUTCOME_ID"); for (const trade of trades) { totalVolume += trade.amount; console.log(`Trade: ${trade.side} ${trade.amount} @ ${(trade.price * 100).toFixed(1)}% | Total: ${totalVolume}`); } } ``` ## Data Feeds ### List Available Feeds `GET /api/feeds` Returns the list of available data feed providers. **Response:** `{ success: true, data: string[] }` **Python:** ```python import requests resp = requests.get( "https://api.pmxt.dev/api/feeds", headers={"Authorization": "Bearer YOUR_PMXT_API_KEY"}, ) print(resp.json()["data"]) ``` **TypeScript:** ```typescript const resp = await fetch("https://api.pmxt.dev/api/feeds", { headers: { Authorization: "Bearer YOUR_PMXT_API_KEY" }, }); const { data } = await resp.json(); console.log(data); ``` ### Load Feed Markets `GET /api/feeds/{feed}/loadMarkets` Returns all trading pairs supported by this feed. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `feed` | string | Yes | The data feed provider. | **Response:** `{ success: true, data: ... }` **Python:** ```python from pmxt.feed_client import FeedClient feed = FeedClient("chainlink", pmxt_api_key="YOUR_PMXT_API_KEY") markets = feed.load_markets() for symbol, market in markets.items(): print(symbol, market.base, market.quote) ``` **TypeScript:** ```typescript import { FeedClient } from "pmxtjs"; const feed = new FeedClient("chainlink", { pmxtApiKey: "YOUR_PMXT_API_KEY" }); const markets = await feed.loadMarkets(); for (const [symbol, market] of Object.entries(markets)) { console.log(symbol, market.base, market.quote); } ``` ### Fetch Ticker `GET /api/feeds/{feed}/fetchTicker` Returns the latest ticker for a single symbol. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `feed` | string | Yes | The data feed provider. | | `symbol` | string | Yes | Trading pair (e.g. BTC/USD, BTC/USDT) | **Response:** `{ success: true, data: ... }` **Python:** ```python from pmxt.feed_client import FeedClient # Chainlink oracle price feed = FeedClient("chainlink", pmxt_api_key="YOUR_PMXT_API_KEY") ticker = feed.fetch_ticker("BTC/USD") print(f"BTC/USD: ${ticker.last}") # Binance spot price feed = FeedClient("binance", pmxt_api_key="YOUR_PMXT_API_KEY") ticker = feed.fetch_ticker("BTC/USDT") print(f"BTC/USDT: ${ticker.last}") ``` **TypeScript:** ```typescript import { FeedClient } from "pmxtjs"; // Chainlink oracle price const chainlink = new FeedClient("chainlink", { pmxtApiKey: "YOUR_PMXT_API_KEY" }); const ticker = await chainlink.fetchTicker("BTC/USD"); console.log(`BTC/USD: $${ticker.last}`); // Binance spot price const binance = new FeedClient("binance", { pmxtApiKey: "YOUR_PMXT_API_KEY" }); const btc = await binance.fetchTicker("BTC/USDT"); console.log(`BTC/USDT: $${btc.last}`); ``` ### Fetch Tickers `GET /api/feeds/{feed}/fetchTickers` Returns the latest tickers for all symbols, or a filtered subset. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `feed` | string | Yes | The data feed provider. | | `symbols` | string | No | Comma-separated symbols to filter (optional) | **Response:** `{ success: true, data: ... }` **Python:** ```python from pmxt.feed_client import FeedClient feed = FeedClient("chainlink", pmxt_api_key="YOUR_PMXT_API_KEY") tickers = feed.fetch_tickers() for symbol, ticker in tickers.items(): print(f"{symbol}: ${ticker.last}") ``` **TypeScript:** ```typescript import { FeedClient } from "pmxtjs"; const feed = new FeedClient("chainlink", { pmxtApiKey: "YOUR_PMXT_API_KEY" }); const tickers = await feed.fetchTickers(); for (const [symbol, ticker] of Object.entries(tickers)) { console.log(`${symbol}: $${ticker.last}`); } ``` ### Fetch OHLCV `GET /api/feeds/{feed}/fetchOHLCV` Returns OHLCV candle data for a symbol. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `feed` | string | Yes | The data feed provider. | | `symbol` | string | Yes | Trading pair | | `timeframe` | string | No | Candle interval (e.g. 1m, 5m, 1h, 1d) | | `since` | integer | No | Start timestamp in ms | | `limit` | integer | No | Max candles to return | **Response:** `{ success: true, data: array[] }` **Python:** ```python from pmxt.feed_client import FeedClient feed = FeedClient("binance", pmxt_api_key="YOUR_PMXT_API_KEY") candles = feed.fetch_ohlcv("BTC/USDT", timeframe="1m", since=1700000000000, limit=3) for timestamp, open_, high, low, close, volume in candles: print(timestamp, close) ``` **TypeScript:** ```typescript import { FeedClient } from "pmxtjs"; const feed = new FeedClient("binance", { pmxtApiKey: "YOUR_PMXT_API_KEY" }); const candles = await feed.fetchOHLCV("BTC/USDT", "1m", 1700000000000, 3); for (const [timestamp, _open, _high, _low, close, _volume] of candles) { console.log(timestamp, close); } ``` ### Fetch Order Book `GET /api/feeds/{feed}/fetchOrderBook` Returns the current order book for a symbol (where supported). | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `feed` | string | Yes | The data feed provider. | | `symbol` | string | Yes | Trading pair | | `limit` | integer | No | Depth limit | **Response:** `{ success: true, data: ... }` **Python:** ```python import requests resp = requests.get( "https://api.pmxt.dev/api/feeds/binance/fetchOrderBook", params={"symbol": "BTC/USDT", "limit": 10}, headers={"Authorization": "Bearer YOUR_PMXT_API_KEY"}, ) book = resp.json()["data"] print(book["bids"][:3]) ``` **TypeScript:** ```typescript const params = new URLSearchParams({ symbol: "BTC/USDT", limit: "10" }); const resp = await fetch(`https://api.pmxt.dev/api/feeds/binance/fetchOrderBook?${params}`, { headers: { Authorization: "Bearer YOUR_PMXT_API_KEY" }, }); const { data: book } = await resp.json(); console.log(book.bids.slice(0, 3)); ``` ### Watch Ticker (WebSocket) `GET /api/feeds/{feed}/watchTicker` Stream live ticker updates for a symbol via WebSocket. **Transport:** WebSocket | Environment | URL | |---|---| | Hosted API | `wss://api.pmxt.dev/ws?apiKey=YOUR_KEY` | **Subscribe:** ```json { "id": "btc-stream", "action": "subscribe", "method": "watchTicker", "args": ["BTC/USDT"], "feed": "binance" } ``` **Server response:** ```json { "event": "data", "id": "btc-stream", "method": "watchTicker", "symbol": "BTC/USDT", "source": "binance", "data": { "symbol": "BTC/USDT", "last": 76949.16, "timestamp": 1716148800000, ... } } ``` **Unsubscribe:** ```json { "id": "btc-stream", "action": "unsubscribe" } ``` Currently supports Binance feeds only. Tickers stream as they arrive from the Binance trade relay. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `feed` | string | Yes | The data feed provider. | | `symbol` | string | Yes | Trading pair to stream (e.g. BTC/USDT) | **Response:** `{ success: true, data: ... }` **Python:** ```python import asyncio import json import websockets async def main(): url = "wss://api.pmxt.dev/ws?apiKey=YOUR_PMXT_API_KEY" async with websockets.connect(url) as ws: await ws.send(json.dumps({ "id": "btc-stream", "action": "subscribe", "method": "watchTicker", "args": ["BTC/USDT"], "feed": "binance", })) async for raw in ws: msg = json.loads(raw) if msg.get("event") == "data": print(f'{msg["data"]["symbol"]}: ${msg["data"]["last"]}') asyncio.run(main()) ``` **TypeScript:** ```typescript const ws = new WebSocket("wss://api.pmxt.dev/ws?apiKey=YOUR_PMXT_API_KEY"); ws.onopen = () => { ws.send(JSON.stringify({ id: "btc-stream", action: "subscribe", method: "watchTicker", args: ["BTC/USDT"], feed: "binance", })); }; ws.onmessage = (e) => { const msg = JSON.parse(e.data); if (msg.event === "data") { console.log(`${msg.data.symbol}: $${msg.data.last}`); } }; ``` ### Fetch Oracle Round `GET /api/feeds/{feed}/fetchOracleRound` Returns the latest Chainlink oracle round for a price feed. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `feed` | string | Yes | The data feed provider. | | `feed` | string | Yes | Price feed pair (e.g. BTC/USD) | **Response:** `{ success: true, data: ... }` **Python:** ```python from pmxt.feed_client import FeedClient feed = FeedClient("chainlink", pmxt_api_key="YOUR_PMXT_API_KEY") round = feed.fetch_oracle_round("BTC/USD") print(f"Round {round.round_id}: ${round.answer} (decimals: {round.decimals})") ``` **TypeScript:** ```typescript import { FeedClient } from "pmxtjs"; const feed = new FeedClient("chainlink", { pmxtApiKey: "YOUR_PMXT_API_KEY" }); const round = await feed.fetchOracleRound("BTC/USD"); console.log(`Round ${round.roundId}: $${round.answer}`); ``` ### Fetch Oracle History `GET /api/feeds/{feed}/fetchOracleHistory` Returns historical Chainlink oracle rounds for a price feed. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `feed` | string | Yes | The data feed provider. | | `feed` | string | Yes | Price feed pair (e.g. BTC/USD) | | `limit` | integer | No | Max rounds to return (default 500) | **Response:** `{ success: true, data: FeedOracleRound[] }` **Python:** ```python from pmxt.feed_client import FeedClient feed = FeedClient("chainlink", pmxt_api_key="YOUR_PMXT_API_KEY") rounds = feed.fetch_oracle_history("BTC/USD", limit=10) for r in rounds: print(f"{r.round_id}: ${r.answer}") ``` **TypeScript:** ```typescript import { FeedClient } from "pmxtjs"; const feed = new FeedClient("chainlink", { pmxtApiKey: "YOUR_PMXT_API_KEY" }); const rounds = await feed.fetchOracleHistory("BTC/USD", 10); for (const r of rounds) { console.log(`${r.roundId}: $${r.answer}`); } ``` ### Fetch Historical Prices `GET /api/feeds/{feed}/fetchHistoricalPrices` Returns historical price data as tickers within a time range. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `feed` | string | Yes | The data feed provider. | | `symbol` | string | Yes | Trading pair (e.g. BTC/USD) | | `fromTimestamp` | integer | No | Start unix timestamp (seconds) | | `untilTimestamp` | integer | No | End unix timestamp (seconds) | | `maxSize` | integer | No | Max records to return | | `order` | string | No | Sort order | **Response:** `{ success: true, data: FeedTicker[] }` **Python:** ```python from pmxt.feed_client import FeedClient feed = FeedClient("chainlink", pmxt_api_key="YOUR_PMXT_API_KEY") prices = feed.fetch_historical_prices("BTC/USD", max_size=20, order="desc") for p in prices: print(f"{p.datetime}: ${p.last}") ``` **TypeScript:** ```typescript import { FeedClient } from "pmxtjs"; const feed = new FeedClient("chainlink", { pmxtApiKey: "YOUR_PMXT_API_KEY" }); const prices = await feed.fetchHistoricalPrices("BTC/USD", { maxSize: 20, order: "desc", }); for (const p of prices) { console.log(`${p.datetime}: $${p.last}`); } ``` ## Other ### Fetch Series `GET /api/{exchange}/fetchSeries` Fetch the recurring series (fourth tier above Event -> Market -> Outcome) that this venue exposes. Returns an empty array on venues without a series concept (Limitless, Smarkets, Probable, Metaculus, Baozi, Hyperliquid, SuiBets, Polymarket US). - `params.id` -> a single matching series with its events populated where supported. - no params -> the full list, typically without nested events for payload size. **Response:** `{ success: true, data: UnifiedSeries[] }` **Python:** ```python import pmxt # API key optional — enables faster catalog-backed lookups exchange = pmxt.Router( pmxt_api_key="YOUR_PMXT_API_KEY", ) result = exchange.fetch_series() ``` **TypeScript:** ```typescript import { Router } from "pmxtjs"; // API key optional — enables faster catalog-backed lookups const exchange = new Router({ pmxtApiKey: "YOUR_PMXT_API_KEY", }); const result = await exchange.fetchSeries(); ``` ### Fetch Event Metadata `GET /api/{exchange}/fetchEventMetadata` Fetch venue-native metadata for a specific event when the exchange exposes a dedicated metadata endpoint. Kalshi implements this for `GET /events/{event_ticker}/metadata`. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `eventTicker` | string | Yes | | **Response:** `{ success: true, data: ... }` **Python:** ```python import pmxt # API key optional — enables faster catalog-backed lookups exchange = pmxt.Kalshi( pmxt_api_key="YOUR_PMXT_API_KEY", ) result = exchange.fetch_event_metadata(event_ticker="value") ``` **TypeScript:** ```typescript import { Kalshi } from "pmxtjs"; // API key optional — enables faster catalog-backed lookups const exchange = new Kalshi({ pmxtApiKey: "YOUR_PMXT_API_KEY", }); const result = await exchange.fetchEventMetadata({ eventTicker: "value" }); ``` ## Enterprise ### Execute a read-only SQL query against ClickHouse `POST /v0/sql` **Response:** `{ success: true, data: object[] }` --- # Reference ## Error Codes Every error response follows the same envelope: ```json { "success": false, "error": { "message": "Human-readable description", "code": "ERROR_CODE", "retryable": false, "exchange": "polymarket" } } ``` | Code | HTTP Status | Retryable | Description | | --- | --- | --- | --- | | `AUTHENTICATION_ERROR` | 401 | No | Missing or invalid API key. | | `INVALID_API_KEY` | 401 | No | Key unknown, revoked, or expired. | | `RATE_LIMIT_EXCEEDED` | 429 | Yes | Per-minute rate limit hit. Retry after the window resets. | | `MONTHLY_QUOTA_EXCEEDED` | 429 | No | Monthly request quota exhausted. | | `EXCHANGE_ERROR` | 502 | Yes | Upstream venue returned an error. Check `error.message` for details. | | `EXCHANGE_TIMEOUT` | 504 | Yes | Upstream venue did not respond in time. | | `MARKET_NOT_FOUND` | 404 | No | The requested marketId/slug does not exist on this venue. | | `EVENT_NOT_FOUND` | 404 | No | The requested eventId/slug does not exist on this venue. | | `ORDER_NOT_FOUND` | 404 | No | The requested orderId does not exist. | | `INSUFFICIENT_BALANCE` | 400 | No | Not enough funds to place the order. | | `INVALID_ORDER` | 400 | No | Order parameters are invalid (bad price, amount, etc.). | | `VALIDATION_ERROR` | 400 | No | Request body failed schema validation. | ## Rate Limits | Endpoint | Per-minute | Per-month | | --- | --- | --- | | `/v0/*` (Router) | 60 | 25,000 | | `/api/*` (Venue pass-through) | 60 | 25,000 | ## End-to-End Recipe: Place a Limit Order and Poll Until Filled ```python import pmxt, time poly = pmxt.Polymarket( pmxt_api_key="pmxt_live_...", private_key="0x...", proxy_address="0x...", signature_type="gnosis-safe", ) # 1. Find the market markets = poly.fetch_markets(query="bitcoin 100k", limit=1) market = markets[0] # 2. Check execution price before placing quote = poly.get_execution_price( market_id=market.market_id, outcome_id=market.yes.outcome_id, side="buy", amount=50, ) print(f"Expected fill: {quote.price:.4f}, fully filled: {quote.fully_filled}") # 3. Place a limit order order = poly.create_order( market_id=market.market_id, outcome_id=market.yes.outcome_id, side="buy", type="limit", price=0.55, amount=50, ) print(f"Order placed: {order.id}, status: {order.status}") # 4. Poll until filled or cancelled while order.status in ("pending", "open"): time.sleep(5) order = poly.fetch_order(order_id=order.id) print(f" status: {order.status}, filled: {order.filled}/{order.amount}") print(f"Final: {order.status}") ```