# 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}")
```