# Backtest Copy PnL — Batch
Source: https://docs.polynode.dev/api-reference/backtesting/batch
POST /v2/copy-pnl/batch
Score multiple wallets in one call. Same math as the single-wallet endpoint.
```http theme={null}
POST https://api.polynode.dev/v2/copy-pnl/batch
```
Use this when you need to score many wallets at once — for example, ranking a candidate-leader list. The endpoint returns one result per wallet, processed in parallel so 50 wallets don't take 50× single-wallet time.
## Request body
| Field | Type | Required | Description |
| ---------------- | ------------------ | -------- | ----------------------------------------------------------------------------------------------------------------- |
| `addresses` | `string[]` | yes | Array of wallet addresses (Safe proxy or EOA). 1 to 100 entries per call. |
| `from` | `string \| number` | no | Window start. `YYYY-MM-DD` (UTC midnight) or unix seconds. Defaults to last 30 days when omitted. |
| `to` | `string \| number` | no | Window end. Same format as `from`. Defaults to current time. |
| `include_trades` | `boolean` | no | When `true`, every result includes the per-fill `trades` array. Off by default — payloads can grow large quickly. |
The `from`/`to` window applies to **every** wallet in the batch — pass per-wallet windows by making separate calls.
`?period=` preset is **not supported** in the batch body. Use explicit `from`/`to`.
## Response
```
{
count: int,
elapsed_ms: int,
results: [ /* one entry per address — same shape as the single-wallet endpoint */ ]
}
```
Per-result fields are identical to the [single-wallet response](/api-reference/backtesting/copy-pnl#response-fields).
## Example: 3 wallets over a 10-day window
Request:
```bash theme={null}
curl -H "x-api-key: $YOUR_KEY" \
-H "Content-Type: application/json" \
-X POST \
-d '{
"addresses": [
"0x0c73d8abc3cd918bc62d0204e33ee05c3f5a68e7",
"0xba4ac793e68eacb93b41566137f25757656a9fa6",
"0x4b2995b6c8cc2cf56899ad62ee87c3f70c97716f"
],
"from": "2026-04-15",
"to": "2026-04-25"
}' \
"https://api.polynode.dev/v2/copy-pnl/batch"
```
Response (`200 OK`, abridged):
```json theme={null}
{
"count": 3,
"elapsed_ms": 1948,
"results": [
{
"wallet": "0x0c73d8abc3cd918bc62d0204e33ee05c3f5a68e7",
"actual_pnl_usdc": -7323.06,
"backtest_copy_pnl_usdc": -9664.1,
"slippage_amount_usdc": 2341.04,
"slippage_cost_rate_pct": 31.97,
"toxic_for_copying": true,
"trade_count": 1637,
"pnl_definition": "cashflow",
"applied_filters": {
"from": 1776211200,
"to": 1777075200,
"window_days": null
},
"partial": false,
"sources": { "...": "..." }
},
{
"wallet": "0xba4ac793e68eacb93b41566137f25757656a9fa6",
"actual_pnl_usdc": 0,
"backtest_copy_pnl_usdc": 0,
"slippage_amount_usdc": 0,
"slippage_cost_rate_pct": null,
"toxic_for_copying": false,
"trade_count": 0,
"pnl_definition": "cashflow",
"applied_filters": { "from": 1776211200, "to": 1777075200, "window_days": null },
"partial": false,
"sources": { "...": "..." }
},
{
"wallet": "0x4b2995b6c8cc2cf56899ad62ee87c3f70c97716f",
"actual_pnl_usdc": -2637.15,
"backtest_copy_pnl_usdc": -2690.17,
"slippage_amount_usdc": 53.02,
"slippage_cost_rate_pct": 2.01,
"toxic_for_copying": false,
"trade_count": 12,
"pnl_definition": "cashflow",
"applied_filters": { "from": 1776211200, "to": 1777075200, "window_days": null },
"partial": false,
"sources": { "...": "..." }
}
]
}
```
Note the second wallet — no on-chain trading activity in the window — returns the standard zero-shape with `slippage_cost_rate_pct: null` (denominator unstable per the spec edge case). Wallets that error individually return `{"wallet": "...", "error": "..."}` instead of the full shape, so a single bad wallet won't fail the whole batch.
## Errors
`400 Body must include "addresses": [...]`:
```json theme={null}
{ "error": "Body must include \"addresses\": [...]." }
```
`400 Invalid wallet`:
```json theme={null}
{ "error": "Invalid wallet: not-a-wallet" }
```
`400 Too many addresses`:
```json theme={null}
{ "error": "Max 100 addresses per batch." }
```
Auth errors (`401`, `403`) and rate-limit errors (`429`) are identical to the single-wallet endpoint — see [Backtest Copy PnL](/api-reference/backtesting/copy-pnl#errors).
## Limits and behavior
* **Max 100 addresses per call**.
* **Typical batch latency** — a 100-wallet batch with average \~1.5s per wallet completes in roughly 20s.
* **45-second total timeout**. Heavy batches (many high-volume wallets) may exceed — split the address list across multiple calls if so.
* **Same rate limit** as the single endpoint: 1 request per 5 seconds per API key. The batch counts as one request regardless of how many wallets are inside.
* **Per-wallet errors don't fail the batch.** A wallet that errors during processing returns `{"wallet": "...", "error": "..."}` in its slot. Other wallets still return their full result.
* **Same `partial: true` behavior per wallet** as the single endpoint — extreme high-frequency wallets may flag partial inside their result.
# BYOB — Add Wallets
Source: https://docs.polynode.dev/api-reference/backtesting/byob-add-wallets
POST /v2/copy-pnl/wallets
Add wallets to your BYOB tracked-pool. Newly-added wallets get scored within seconds (on-add freshening).
```http theme={null}
POST https://api.polynode.dev/v2/copy-pnl/wallets
```
Add one or more wallets to your private BYOB tracked-pool. Wallets are immediately added to the global refresh queue AND scored right away by the on-add freshening pass — usually within \~30 seconds for normal wallets, longer for whales.
## Request body
| Field | Type | Required | Description |
| ----------- | ---------- | -------- | ----------------------------------------------------------------------------------------------------------------------- |
| `addresses` | `string[]` | yes | Array of valid `0x…` wallet addresses (Safe proxy or EOA). Addresses are normalized to lowercase. Max 1000 per request. |
## Response fields
| Field | Type | Description |
| ----------------- | ------ | -------------------------------------------------------------------------------------------- |
| `added` | int | Number of NEW wallets added to your pool (excludes those already tracked) |
| `already_tracked` | int | Number of wallets in the request body that were already in your pool |
| `total` | int | Current size of your pool after this request |
| `max` | int | Hard cap on pool size per API key (currently 1000) |
| `capped` | bool | `true` when your request would have exceeded `max` and we accepted only the first N that fit |
| `refresh_kicked` | int | Number of newly-added wallets queued for immediate background scoring (matches `added`) |
| `hint` | string | Only present when `capped: true` — explains how many of your wallets were dropped |
## Example: fresh add
Request:
```bash theme={null}
curl -H "x-api-key: $YOUR_KEY" \
-H "Content-Type: application/json" \
-X POST \
-d '{
"addresses": [
"0xdead0000000000000000000000000000000000aa",
"0xdead0000000000000000000000000000000000bb"
]
}' \
"https://api.polynode.dev/v2/copy-pnl/wallets"
```
Response (`200 OK`):
```json theme={null}
{
"added": 2,
"already_tracked": 0,
"total": 110,
"max": 1000,
"capped": false,
"refresh_kicked": 2
}
```
The two new wallets are now in your pool AND queued for immediate scoring. Within \~30s (faster for small wallets, longer for whales) you'll see them populated in `/v2/copy-pnl/leaderboard`.
## Example: re-add (dedupe)
Wallets already in your pool are silently skipped — sending the same address twice doesn't count toward your cap.
Request (one wallet already tracked, one new):
```bash theme={null}
curl -H "x-api-key: $YOUR_KEY" \
-H "Content-Type: application/json" \
-X POST \
-d '{
"addresses": [
"0xdead0000000000000000000000000000000000aa",
"0xdead0000000000000000000000000000000000cc"
]
}' \
"https://api.polynode.dev/v2/copy-pnl/wallets"
```
Response (`200 OK`):
```json theme={null}
{
"added": 1,
"already_tracked": 1,
"total": 111,
"max": 1000,
"capped": false,
"refresh_kicked": 1
}
```
Only the genuinely new wallet (`0xdead…cc`) was added and refresh-kicked. The duplicate was a no-op.
## Errors
`400 Missing or invalid addresses` (covers: missing `addresses` field, non-string in array, malformed hex):
```json theme={null}
{ "error": "Body must include \"addresses\" — array of valid 0x… wallet addresses." }
```
`400 Too many addresses in one request`:
```json theme={null}
{ "error": "At most 1000 wallets per request (and per tenant)." }
```
`401 Missing API key`, `403 Free tier`, `429 Rate limited` — same as the on-demand `/v2/copy-pnl/{wallet}` endpoint. See [Backtest Copy PnL → Errors](/api-reference/backtesting/copy-pnl#errors).
## Notes
* **Wallets are normalized to lowercase.** Mixing case across requests is fine; the same wallet in different case won't double-track.
* **Within a single request, duplicates are de-duped** before the dedup-against-pool check. So passing the same address twice in one body counts as one.
* **Pool isolation:** each API key has its own private pool keyed on the SHA256 of your key. Adding wallets to one pool doesn't affect any other customer.
* **On-add freshening** runs in the background — the HTTP response returns within milliseconds even though scoring takes 30s+ to complete. Poll `/v2/copy-pnl/leaderboard` to see scores land.
* **Capacity behavior**: if your request would have pushed your pool past `max`, we accept the first N that fit and set `capped: true` plus a `hint` explaining what was dropped. We do NOT reject the entire request.
# BYOB — Leaderboard
Source: https://docs.polynode.dev/api-reference/backtesting/byob-leaderboard
GET /v2/copy-pnl/leaderboard
Sub-second sorted, filtered, paginated leaderboard over your tracked-wallet pool. Reads precomputed scores from cache.
```http theme={null}
GET https://api.polynode.dev/v2/copy-pnl/leaderboard
```
Query the precomputed leaderboard over your private wallet pool. All reads come from cache — sub-second latency regardless of pool size or window. Sort by any output field, filter out toxic wallets or low-volume traders, paginate.
## Query parameters
Time window the scores were computed against. One of: `7d`, `14d`, `30d`, `60d`, `90d`, `180d`. Scores are precomputed for **all six periods** so any choice is sub-second.
Field to sort by. One of: `backtest_copy_pnl_usdc`, `actual_pnl_usdc`, `slippage_amount_usdc`, `slippage_cost_rate_pct`, `trade_count`.
`asc` or `desc`. Wallets with `null` `slippage_cost_rate_pct` (when `abs(actual_pnl) < 1`) always sort last regardless of direction.
Max results to return. 1 to 1000.
Skip the first N results. Useful for paginating beyond `limit`.
Filter out wallets with fewer than N fills in the window. Useful to drop sample-too-small noise.
When `true`, drops wallets where `slippage_cost_rate_pct > 15`. The leader-selection power filter.
## Response shape
| Field | Type | Description |
| ---------------- | ------------------ | -------------------------------------------------------------------------------------------------- |
| `period` | string | Echo of the requested period |
| `sort_by` | string | Echo of the sort field |
| `order` | string | Echo of the sort direction |
| `filters` | object | `{min_trade_count, exclude_toxic}` echo |
| `total_in_pool` | int | Total wallets in your pool |
| `scored` | int | Wallets with a score for this period |
| `filtered_in` | int | Wallets that passed the `min_trade_count` + `exclude_toxic` filters |
| `pending` | int | Wallets in your pool with no score yet (newly added or queued for next refresh) |
| `errored` | int | Wallets whose last refresh attempt failed (typically heavy whales hitting timeout on long windows) |
| `offset` | int | Echo |
| `limit` | int | Echo |
| `last_refresh` | int (unix) \| null | When the last full refresh cycle completed |
| `results` | array | Top N matching rows after filters and sort |
| `errored_sample` | array | Up to 10 errored wallets with their last-error message — only present when `errored > 0` |
| `pending_sample` | array | Up to 10 pending wallet addresses — only present when `pending > 0` |
### Per-result row
| Field | Type | Description |
| ------------------------ | -------------- | ----------------------------------------------------------------------------------------- |
| `local_rank` | int | 1-indexed rank within your pool (after offset) |
| `wallet` | string | Lowercased address |
| `actual_pnl_usdc` | number | Cashflow PnL over the period |
| `backtest_copy_pnl_usdc` | number | Same walk with 2% slippage |
| `slippage_amount_usdc` | number | Dollar friction the copier eats |
| `slippage_cost_rate_pct` | number \| null | Friction as % of actual PnL (null when \|actual\| \< \$1) |
| `toxic_for_copying` | bool | `true` when rate > 15% |
| `trade_count` | int | Number of fills in the window |
| `partial` | bool | `true` if the underlying walk hit the per-wallet 180s budget |
| `computed_at` | int (unix) | When this specific score was last refreshed — surface this in your UI as freshness signal |
## Example: top 3 by actual\_pnl\_usdc desc
```bash theme={null}
curl -H "x-api-key: $YOUR_KEY" \
"https://api.polynode.dev/v2/copy-pnl/leaderboard?period=30d&sort_by=actual_pnl_usdc&order=desc&limit=3"
```
Response (`200 OK`):
```json theme={null}
{
"period": "30d",
"sort_by": "actual_pnl_usdc",
"order": "desc",
"filters": { "min_trade_count": 0, "exclude_toxic": false },
"total_in_pool": 108,
"scored": 104,
"filtered_in": 104,
"pending": 1,
"errored": 3,
"offset": 0,
"limit": 3,
"last_refresh": 1777521943,
"results": [
{
"local_rank": 1,
"wallet": "0x492442eab586f242b53bda933fd5de859c8a3782",
"actual_pnl_usdc": 22915787.92,
"backtest_copy_pnl_usdc": 22129678.06,
"slippage_amount_usdc": 786109.86,
"slippage_cost_rate_pct": 3.43,
"toxic_for_copying": false,
"trade_count": 8045,
"partial": false,
"computed_at": 1777522146
}
]
}
```
## Example: leader screening — best non-toxic, min volume
```bash theme={null}
curl -H "x-api-key: $YOUR_KEY" \
"https://api.polynode.dev/v2/copy-pnl/leaderboard?period=30d&sort_by=slippage_cost_rate_pct&order=asc&exclude_toxic=true&min_trade_count=1000&limit=3"
```
Response (`200 OK`):
```json theme={null}
{
"period": "30d",
"sort_by": "slippage_cost_rate_pct",
"order": "asc",
"filters": { "min_trade_count": 1000, "exclude_toxic": true },
"total_in_pool": 108,
"scored": 104,
"filtered_in": 50,
"pending": 2,
"errored": 2,
"offset": 0,
"limit": 3,
"results": [
{
"local_rank": 1,
"wallet": "0x9c16127eccf031df45461ef1e04b52ea286a09cb",
"actual_pnl_usdc": 102171.68,
"backtest_copy_pnl_usdc": 101643.41,
"slippage_amount_usdc": 528.27,
"slippage_cost_rate_pct": 0.52,
"toxic_for_copying": false,
"trade_count": 9472,
"partial": false,
"computed_at": 1777521515
}
]
}
```
This is the canonical "find good leaders to copy" query: lowest slippage rate, but only among wallets that have actually traded enough (`min_trade_count: 1000`) and aren't already flagged toxic.
## Example: pagination (offset)
```bash theme={null}
curl -H "x-api-key: $YOUR_KEY" \
"https://api.polynode.dev/v2/copy-pnl/leaderboard?period=30d&offset=10&limit=3"
```
Response includes results with `local_rank: 11, 12, 13` — i.e. ranks within your pool start at `offset + 1`. The default sort is `backtest_copy_pnl_usdc desc`.
## Errors
`400 Invalid period`:
```json theme={null}
{ "error": "Invalid period. Allowed: 7d, 14d, 30d, 60d, 90d, 180d" }
```
`400 Invalid sort_by`:
```json theme={null}
{ "error": "Invalid sort_by. Allowed: backtest_copy_pnl_usdc, actual_pnl_usdc, slippage_amount_usdc, slippage_cost_rate_pct, trade_count" }
```
`400 Invalid order`:
```json theme={null}
{ "error": "Invalid order. Allowed: asc | desc" }
```
Auth/rate-limit errors mirror the rest of the `/v2/copy-pnl/*` family.
## Notes
* **Sub-second.** All scores are precomputed and served from cache. Even a 1000-wallet pool with all six periods scored returns in under 100ms.
* **`pending` and `errored` aren't included in `results`.** Use `pending_sample` and `errored_sample` to identify which specific wallets need attention. The `errored_sample` includes the upstream error message — typically `timeout_180s` for the heaviest whales on long windows.
* **`computed_at` per row.** Use this to render "as of X minutes ago" in your UI. Newly-added wallets (via on-add freshening) typically have a `computed_at` within \~30s of the add. Periodic refresh updates it once per chunk slot.
* **`last_refresh` at top level.** When the most recent full refresh cycle completed. Individual wallets may have been refreshed earlier or later within the cycle — use the per-row `computed_at` field for exact freshness.
* **Sort ties + nulls.** When two wallets share the exact sort\_by value, secondary order is undefined. Wallets with `null` `slippage_cost_rate_pct` always sort last when sorting on that field.
# BYOB — List Wallets
Source: https://docs.polynode.dev/api-reference/backtesting/byob-list-wallets
GET /v2/copy-pnl/wallets
Return every wallet in your BYOB tracked-pool, sorted lexicographically.
```http theme={null}
GET https://api.polynode.dev/v2/copy-pnl/wallets
```
Returns every wallet currently in your private BYOB pool. Useful for showing your tracked-set in a UI, exporting, or auditing what's queued for the leaderboard.
## Response fields
| Field | Type | Description |
| --------- | ---------- | ------------------------------------------------------------------------------ |
| `wallets` | `string[]` | Every wallet in your pool, lowercased, sorted lexicographically (stable order) |
| `count` | int | Length of the `wallets` array |
| `max` | int | Hard cap on pool size per API key (1000) |
## Example
Request:
```bash theme={null}
curl -H "x-api-key: $YOUR_KEY" \
"https://api.polynode.dev/v2/copy-pnl/wallets"
```
Response (`200 OK`):
```json theme={null}
{
"wallets": [
"0x000d257d2dc7616feaef4ae0f14600fdf50a758e",
"0x01c78f8873c0c86d6b6b92ff627e3802237ee995",
"0x034475def048324ad32d87c5fd90e99f0f7b2538",
"0x090a9efe46c0e42e4fd598cd81be011ad72f27e7",
"0x0979bad57d7a1403db89cbcd9c52bf43f2138d9b"
],
"count": 108,
"max": 1000
}
```
## Notes
* **Sorted output.** Wallets are returned in lexicographic order, not in the order you added them. The order is stable — useful for paginating through your own pool client-side without surprises.
* **No pagination on this endpoint.** The full list returns in one response. With `max: 1000` and \~42 bytes per wallet address, the largest possible payload is well under 50 KB.
* **Pool isolation.** Only wallets added by THIS API key are returned. No way to see other customers' pools.
* **Includes wallets that haven't been scored yet.** A wallet you just added via `POST /v2/copy-pnl/wallets` shows here immediately, even if its `computed_at` won't appear in the leaderboard for \~30 seconds while on-add freshening runs.
# BYOB — Remove Wallets
Source: https://docs.polynode.dev/api-reference/backtesting/byob-remove-wallets
DELETE /v2/copy-pnl/wallets
Remove wallets from your BYOB tracked-pool. Removal is idempotent — re-removing a wallet that's not in the pool returns 0 with no error.
```http theme={null}
DELETE https://api.polynode.dev/v2/copy-pnl/wallets
```
Remove one or more wallets from your private BYOB pool. Wallets not in the pool (or already removed) are silently skipped — `removed` reflects only how many wallets were actually present.
## Request body
| Field | Type | Required | Description |
| ----------- | ---------- | -------- | ------------------------------------------------------------------------------- |
| `addresses` | `string[]` | yes | Array of `0x…` wallet addresses to remove. Wallets not in your pool are no-ops. |
## Response fields
| Field | Type | Description |
| --------- | ---- | ------------------------------------------------- |
| `removed` | int | Number of wallets actually removed from your pool |
| `total` | int | Current size of your pool after removal |
| `max` | int | Hard cap on pool size per API key (1000) |
## Example: remove an existing wallet
Request:
```bash theme={null}
curl -H "x-api-key: $YOUR_KEY" \
-H "Content-Type: application/json" \
-X DELETE \
-d '{
"addresses": [
"0xfeed0000000000000000000000000000000000aa",
"0xfeed0000000000000000000000000000000000ff"
]
}' \
"https://api.polynode.dev/v2/copy-pnl/wallets"
```
Response (`200 OK`):
```json theme={null}
{
"removed": 1,
"total": 109,
"max": 1000
}
```
Only the first wallet was actually in the pool — the second was never tracked, so it's a silent no-op. The response tells you exactly how many were touched.
## Example: idempotent re-remove
Calling delete on a wallet already removed is safe — returns `removed: 0` with no error.
Request:
```bash theme={null}
curl -H "x-api-key: $YOUR_KEY" \
-H "Content-Type: application/json" \
-X DELETE \
-d '{"addresses": ["0xfeed0000000000000000000000000000000000aa"]}' \
"https://api.polynode.dev/v2/copy-pnl/wallets"
```
Response (`200 OK`):
```json theme={null}
{
"removed": 0,
"total": 109,
"max": 1000
}
```
This makes "set my pool to exactly this list" workflows simple — DELETE old, POST new, both safe to re-run.
## Errors
`400 Missing or invalid addresses`:
```json theme={null}
{ "error": "Body must include \"addresses\" — array of valid 0x… wallet addresses." }
```
Same auth/rate-limit errors as the rest of the `/v2/copy-pnl/*` family.
## Notes
* **Removal is per-tenant.** Removing a wallet from your pool does NOT remove it from the global refresh queue if other tenants still track it — the wallet's score keeps refreshing for them. Your private query just stops returning that wallet.
* **Cached scores survive briefly.** The wallet's last-computed score stays in our cache (24h TTL). If you re-add the same wallet within 24h, the leaderboard will show its existing score immediately while the on-add freshening kicks off a fresh recompute.
* **No bulk-clear endpoint.** To wipe your entire pool, GET your wallet list first, then DELETE all of them in chunks of 1000.
# BYOB — Snapshot
Source: https://docs.polynode.dev/api-reference/backtesting/byob-snapshot
GET /v2/copy-pnl/snapshot
Every wallet in your tracked pool with backtest scores across every period in a single response. The headline read for stats-card UIs.
```http theme={null}
GET https://api.polynode.dev/v2/copy-pnl/snapshot
```
Returns every wallet currently in your BYOB pool together with their backtest copy-PnL scores across all six time windows (or a chosen subset). One request — no per-period round trips, no client-side stitching.
This is the canonical read for any UI that renders stats cards, dashboards, or grids over the entire tracked-wallet set. Pair it with `GET /wallets` if you need to render the empty-state pool too.
## Query parameters
Comma-separated subset of periods to include. Defaults to all six. Useful when you only render a single time window — e.g. `?periods=30d` returns \~1/6 the payload.
## Response shape
| Field | Type | Description |
| --------------- | ------------------ | ---------------------------------------------------------------------------------------- |
| `total_in_pool` | int | Wallets currently in your pool |
| `scored_any` | int | Wallets with a score for at least one of the requested periods |
| `errored_any` | int | Wallets whose last refresh attempt failed (heavy whales hitting timeout on long windows) |
| `pending_any` | int | Wallets with no score yet (newly added or queued for the next refresh) |
| `periods` | string\[] | Echo of the periods returned (matches input or default order) |
| `last_refresh` | int (unix) \| null | When the last full refresh cycle completed |
| `wallets` | array | One entry per pooled wallet (see below) |
### Per-wallet entry
| Field | Type | Description |
| --------- | ------ | ---------------------------------------------------------------------------------------------------------------- |
| `wallet` | string | Lowercased address |
| `periods` | object | Map keyed by period (`7d`, `14d`, …). Each value is the score blob, or `null` if that specific period is pending |
| `error` | object | Only present if this wallet's last refresh errored. `{ error: string, ts: int }` |
### Per-period score blob
| Field | Type | Description |
| ------------------------ | -------------- | --------------------------------------------------------------- |
| `actual_pnl_usdc` | number | Cashflow PnL the wallet realized over the period |
| `backtest_copy_pnl_usdc` | number | Same trade walk with 2 % slippage on every entry/exit |
| `slippage_amount_usdc` | number | Dollar friction the copier eats |
| `slippage_cost_rate_pct` | number \| null | Friction as % of `\|actual_pnl\|` (null when `\|actual\| < $1`) |
| `toxic_for_copying` | bool | `true` when rate > 15 % |
| `trade_count` | int | Number of fills in the window |
| `partial` | bool | `true` if the underlying walk hit the per-wallet 180 s budget |
| `computed_at` | int (unix) | When this specific score was last refreshed |
## Examples
### Full snapshot (default)
```bash theme={null}
curl -H "x-api-key: $YOUR_KEY" \
"https://api.polynode.dev/v2/copy-pnl/snapshot"
```
Response (`200 OK`, abridged):
```json theme={null}
{
"total_in_pool": 108,
"scored_any": 107,
"errored_any": 1,
"pending_any": 0,
"periods": ["7d", "14d", "30d", "60d", "90d", "180d"],
"last_refresh": 1777545636,
"wallets": [
{
"wallet": "0x000d257d2dc7616feaef4ae0f14600fdf50a758e",
"periods": {
"7d": { "actual_pnl_usdc": 104635.85, "backtest_copy_pnl_usdc": 101463.76, "slippage_amount_usdc": 3172.09, "slippage_cost_rate_pct": 3.03, "toxic_for_copying": false, "trade_count": 90, "partial": false, "computed_at": 1777544903 },
"14d": { "actual_pnl_usdc": -123675.90, "backtest_copy_pnl_usdc": -134838.05, "slippage_amount_usdc": 11162.15, "slippage_cost_rate_pct": 9.03, "toxic_for_copying": false, "trade_count": 458, "partial": false, "computed_at": 1777544903 },
"30d": { "actual_pnl_usdc": 317864.92, "backtest_copy_pnl_usdc": 266790.44, "slippage_amount_usdc": 51074.48, "slippage_cost_rate_pct": 16.07, "toxic_for_copying": true, "trade_count": 2253, "partial": false, "computed_at": 1777544903 },
"60d": { "actual_pnl_usdc": -66101.80, "backtest_copy_pnl_usdc": -181792.53, "slippage_amount_usdc": 115690.73, "slippage_cost_rate_pct": 175.02, "toxic_for_copying": true, "trade_count": 5916, "partial": false, "computed_at": 1777544903 },
"90d": { "actual_pnl_usdc": 289034.81, "backtest_copy_pnl_usdc": 122089.67, "slippage_amount_usdc": 166945.14, "slippage_cost_rate_pct": 57.76, "toxic_for_copying": true, "trade_count": 8761, "partial": false, "computed_at": 1777544903 },
"180d": { "actual_pnl_usdc": 665955.87, "backtest_copy_pnl_usdc": 311352.42, "slippage_amount_usdc": 354603.45, "slippage_cost_rate_pct": 53.25, "toxic_for_copying": true, "trade_count": 18545, "partial": false, "computed_at": 1777544903 }
}
}
]
}
```
### Subset of periods
```bash theme={null}
curl -H "x-api-key: $YOUR_KEY" \
"https://api.polynode.dev/v2/copy-pnl/snapshot?periods=7d,30d"
```
Returns the same shape but with only the requested periods inside each `wallets[].periods` object. Use this when your UI only renders a single window — payload drops proportionally.
## Performance
| Pool size | Periods | Payload | Latency (warm) |
| ------------ | ------- | -------- | -------------- |
| 100 wallets | all 6 | \~150 KB | \~70 ms |
| 100 wallets | 1 | \~30 KB | under 30 ms |
| 1000 wallets | all 6 | \~1.5 MB | \~500 ms |
| 1000 wallets | 1 | \~250 KB | \~100 ms |
All reads come from the precomputed score cache.
## Notes
* **`scored_any` vs `scored` on the leaderboard.** A wallet counts as `scored_any` here if it has a score for *at least one* of the requested periods. The leaderboard's `scored` is per-requested-period.
* **Per-period `null`.** A wallet may be scored for `30d` but `null` for `180d` (heavy whales often time out on long windows). Read each period's value independently.
* **`error` is wallet-global.** A single `error` object per wallet covers the most recent refresh failure across any period. Use it to surface "X wallets retrying" in your UI.
* **No pagination.** This endpoint always returns the full pool. Use the leaderboard endpoint with `limit`/`offset` if you need server-side paging — that's the trade-off vs the one-shot snapshot.
* **Race-safe with adds.** Wallets you add via `POST /v2/copy-pnl/wallets` show up here immediately, with `null` periods until the on-add freshening finishes (\~30 s).
# Backtest Copy PnL
Source: https://docs.polynode.dev/api-reference/backtesting/copy-pnl
GET /v2/copy-pnl/{wallet}
Score a wallet for copy-trading quality. Returns realized PnL, cashflow PnL, simulated copier PnL with 2% slippage friction, and a toxic-for-copying flag.
```http theme={null}
GET https://api.polynode.dev/v2/copy-pnl/{wallet}
```
## TL;DR — which PnL field do you actually want?
This endpoint returns **two different PnL numbers** that can look very different for the same wallet. Pick the right one for your use case:
| Field | What it answers | When to use |
| ------------------------- | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `total_realized_pnl_usdc` | "Did this wallet **make money** trading?" | **Default for customer-facing UI.** Closest to what other trading platforms call "PnL". Equivalent to summing per-position WAVG-realized PnL across every position the wallet touched. |
| `actual_pnl_usdc` | "How much **net cash** moved through this wallet's USDC balance from trading?" | Diagnostic / slippage-analysis. Used for `slippage_*` fields. Inflated for active wallets that hold inventory. |
**Both numbers are correct** — they measure different things. See [the worked example below](#understanding-cashflow-pnl-vs-realized-pnl) for the math.
## Path parameters
The Polymarket wallet address (Safe proxy or EOA, normalized to lowercase).
## Query parameters
Convenience window preset. One of: `7d`, `14d`, `30d`, `60d`, `90d`, `180d`. Anchored to "now". If you pass `period`, you don't need `from`.
Start of the window. Accepts `YYYY-MM-DD` (UTC midnight) or unix seconds. Overrides `period` if both are passed.
End of the window. Same format as `from`. Defaults to current time when omitted.
When `1` or `true`, response includes a `trades` array with one entry per fill (`ts`, `side`, `price`, `shares`, `actual_usd`, `backtest_usd`). Useful for transparency and debugging.
## Response fields
| Field | Type | Description | | | | | | |
| ----------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ----- | -------- | --------------- | -------- | --------- |
| `wallet` | string | Echo of the requested wallet (lowercased) | | | | | | |
| **`total_realized_pnl_usdc`** | **number** | **Realized PnL: signed sum of per-position WAVG-realized P/L across every position the matcher touched in the window. Counts only profit/loss on positions the wallet actually closed (or partially closed). Recommended primary "PnL" for customer UI.** | | | | | | |
| `actual_pnl_usdc` | number | **Cashflow PnL** over the window: `-buys + sells + settlements`. Counts every USDC inflow/outflow from trading. Includes inventory bought but not yet sold (so often very negative for active buyers). Used as input to slippage formula. | | | | | | |
| `backtest_copy_pnl_usdc` | number | Same cashflow walk with 2% slippage applied to buys (capped at \$1) and sells. Settlements unchanged. | | | | | | |
| `slippage_amount_usdc` | number | `actual_pnl_usdc − backtest_copy_pnl_usdc`. The dollar friction the copier eats. | | | | | | |
| `slippage_cost_rate_pct` | number \| null | `slippage / abs(actual_pnl) × 100`. `null` when `abs(actual_pnl) < 1` (denominator unstable). | | | | | | |
| `toxic_for_copying` | boolean | `true` when `slippage_cost_rate_pct > 15`. | | | | | | |
| `trade_count` | int | Number of fills walked in the window. | | | | | | |
| `positions_closed` | int | Count of positions that contributed non-zero realized PnL in the window. | | | | | | |
| `avg_entry_prob_weighted` | number \| null | PnL-weighted average entry probability across closed positions. \`Σ(entry\_price × | realized | ) / Σ | realized | `. Null when `Σ | realized | \< \$1\`. |
| `avg_hold_seconds_weighted` | number \| null | PnL-weighted average hold duration. Currently always `null` (WAVG matcher doesn't track per-segment timestamps; planned for future). | | | | | | |
| `pnl_definition` | string | Always `"cashflow"` for `actual_pnl_usdc`. Reminds callers `actual_pnl_usdc` differs from PM website PnL (which marks open positions). | | | | | | |
| `applied_filters` | object | Resolved `from` (unix), `to` (unix), and `window_days` (only set when default 30d window was used). | | | | | | |
| `partial` | boolean | `true` if the underlying walk hit the 30s budget and returned incomplete data. | | | | | | |
| `sources` | object | Breakdown: `window_trades`, `window_activity`, per-event-type counts, `cashflow_breakdown` (buys/sells/settlements), `fifo_breakdown` (matcher diagnostics including `total_realized_pnl_usdc`). For transparency. | | | | | | |
| `trades` | array | Only present when `include_trades=1`. One entry per fill. | | | | | | |
## Understanding cashflow PnL vs realized PnL
The most common source of confusion with this endpoint is "why don't `actual_pnl_usdc` and `total_realized_pnl_usdc` agree?" — they're answering different questions.
**Worked example.** A wallet's first 30 days:
```
Day | Action | actual_pnl_usdc | total_realized_pnl_usdc
-----+-----------------------+-----------------+-------------------------
1 | BUY 100 YES @ $0.40 | -$40.00 | $0.00
5 | SELL 50 YES @ $0.60 | -$10.00 | +$10.00
10 | BUY 100 YES @ $0.50 | -$60.00 | +$10.00
15 | SELL 80 YES @ $0.55 | -$16.00 | +$17.20
30 | (still hold 70) | -$16.00 | +$17.20
```
**Final state explained:**
* `actual_pnl_usdc = -$16` — the wallet paid out \$16 more USDC than it received over the 30 days.
* `total_realized_pnl_usdc = +$17.20` — actual trading profit on the 130 shares the wallet closed.
* The wallet still HOLDS 70 shares (cost basis \$32.20). Cashflow counts that as money "spent". Realized ignores it.
**Reconciliation:** `cashflow_pnl + cost_basis_of_open_positions ≈ realized_pnl`. In this example: `-$16 + $32.20 = $16.20 ≈ $17.20` (small rounding from WAVG cost basis updates).
**Which to use:**
* For "did the wallet make money trading?" → **`total_realized_pnl_usdc`**. This is what trading platforms typically call "PnL" and what users intuitively expect.
* For "how much net cash flowed through this wallet?" → `actual_pnl_usdc`. Useful for liquidity analysis and the slippage formula, but **not** what most people mean by "PnL".
* For copy-trading slippage analysis → use `slippage_*` fields (which are derived from cashflow). The slippage formula needs apples-to-apples cashflow comparison; this is why `actual_pnl_usdc` is defined as cashflow.
**One-line summary:** realized = closed-trade profit; cashflow = USDC delta in/out of the wallet. They reconcile when the wallet holds zero open inventory.
***
## Example: default 30-day window
Request:
```bash theme={null}
curl -H "x-api-key: $YOUR_KEY" \
"https://api.polynode.dev/v2/copy-pnl/0x0c73d8abc3cd918bc62d0204e33ee05c3f5a68e7"
```
Response (`200 OK`):
```json theme={null}
{
"wallet": "0x0c73d8abc3cd918bc62d0204e33ee05c3f5a68e7",
"actual_pnl_usdc": -5697.9,
"backtest_copy_pnl_usdc": -8932.81,
"slippage_amount_usdc": 3234.91,
"slippage_cost_rate_pct": 56.77,
"toxic_for_copying": true,
"trade_count": 1726,
"avg_entry_prob_weighted": 0.4892,
"avg_hold_seconds_weighted": null,
"positions_closed": 142,
"total_realized_pnl_usdc": -2412.06,
"pnl_definition": "cashflow",
"applied_filters": {
"from": 1774916101,
"to": 1777508101,
"window_days": 30
},
"partial": false,
"sources": {
"window_trades": 1726,
"window_activity": 51,
"activity_breakdown": { "redemption": 51 },
"cashflow_breakdown": {
"actual_buy_cost": 85766.5,
"actual_sell_rev": 77266.87,
"settlement_in": 2801.73,
"settlement_out": 0
},
"fifo_breakdown": {
"over_sells": 3,
"unresolved_activity": 0,
"total_abs_pnl_usdc": 4823.18,
"total_realized_pnl_usdc": -2412.06
},
"fetch_ms": 1361
}
}
```
Reading this response: the wallet's **realized** trading P/L is \*\*-$2,412** (lost ~$2.4K on 142 positions that closed in the window). The **cashflow** says \*\*-$5,697** — the extra $3,285 is the cost basis of positions still open at window-end (inventory the wallet bought but hasn't sold yet). For "did this wallet make money?" the answer is `total_realized_pnl_usdc` — and the answer is no.
## Example: 7-day window via preset
Request:
```bash theme={null}
curl -H "x-api-key: $YOUR_KEY" \
"https://api.polynode.dev/v2/copy-pnl/0x9977760c6bd6f824cac834d1a36ee99478d63020?period=7d"
```
Response (`200 OK`):
```json theme={null}
{
"wallet": "0x9977760c6bd6f824cac834d1a36ee99478d63020",
"actual_pnl_usdc": 8082.45,
"backtest_copy_pnl_usdc": 7077.07,
"slippage_amount_usdc": 1005.38,
"slippage_cost_rate_pct": 12.44,
"toxic_for_copying": false,
"trade_count": 1753,
"avg_entry_prob_weighted": 0.5210,
"avg_hold_seconds_weighted": null,
"positions_closed": 87,
"total_realized_pnl_usdc": 6428.91,
"pnl_definition": "cashflow",
"applied_filters": {
"from": 1776903319,
"to": 1777508119,
"window_days": null
},
"partial": false,
"sources": {
"window_trades": 1753,
"window_activity": 144,
"activity_breakdown": { "redemption": 144 },
"cashflow_breakdown": {
"actual_buy_cost": 339438.1,
"actual_sell_rev": 596.28,
"settlement_in": 346924.27,
"settlement_out": 0
},
"fifo_breakdown": {
"over_sells": 0,
"unresolved_activity": 0,
"total_abs_pnl_usdc": 12857.82,
"total_realized_pnl_usdc": 6428.91
},
"fetch_ms": 632
}
}
```
## Example: explicit date range
Request — score the window from April 15 to April 25 inclusive:
```bash theme={null}
curl -H "x-api-key: $YOUR_KEY" \
"https://api.polynode.dev/v2/copy-pnl/0x9977760c6bd6f824cac834d1a36ee99478d63020?from=2026-04-15&to=2026-04-25"
```
Response (`200 OK`):
```json theme={null}
{
"wallet": "0x9977760c6bd6f824cac834d1a36ee99478d63020",
"actual_pnl_usdc": -70450.73,
"backtest_copy_pnl_usdc": -74413.34,
"slippage_amount_usdc": 3962.61,
"slippage_cost_rate_pct": 5.62,
"toxic_for_copying": false,
"trade_count": 3787,
"pnl_definition": "cashflow",
"applied_filters": {
"from": 1776211200,
"to": 1777075200,
"window_days": null
},
"partial": false,
"sources": { "...": "..." }
}
```
## Example: per-fill drill-down
Request:
```bash theme={null}
curl -H "x-api-key: $YOUR_KEY" \
"https://api.polynode.dev/v2/copy-pnl/0x0c73d8abc3cd918bc62d0204e33ee05c3f5a68e7?period=7d&include_trades=1"
```
Response (`200 OK`, truncated):
```json theme={null}
{
"wallet": "0x0c73d8abc3cd918bc62d0204e33ee05c3f5a68e7",
"actual_pnl_usdc": -5027.89,
"backtest_copy_pnl_usdc": -6783.42,
"slippage_amount_usdc": 1755.53,
"slippage_cost_rate_pct": 34.92,
"toxic_for_copying": true,
"trade_count": 838,
"pnl_definition": "cashflow",
"trades": [
{
"ts": 1777232322,
"side": "BUY",
"price": 0.7916,
"shares": 252.66,
"actual_usd": 200,
"backtest_usd": 204
},
{
"ts": 1777232322,
"side": "SELL",
"price": 0.21,
"shares": 48,
"actual_usd": 10.08,
"backtest_usd": 9.88
}
]
}
```
## Errors
`400 Invalid period`:
```json theme={null}
{ "error": "Invalid period. Allowed: 7d, 14d, 30d, 60d, 90d, 180d" }
```
`401 No API key`:
```json theme={null}
{ "error": "API key required. Pass via ?key= or x-api-key header." }
```
`403 Free tier`:
```json theme={null}
{ "error": "V2 endpoints require a paid plan. See polynode.dev/pricing for details." }
```
`429 Rate limited`:
```json theme={null}
{
"error": "copy-pnl rate limited (1 req per 5s per key).",
"retryAfterMs": 4231
}
```
The `Retry-After` header (in seconds, rounded up) is also set on 429 responses. The rate limit is shared across all `/v2/copy-pnl/*` calls per API key — calling the endpoint with different wallets or different params still counts toward the same 1 req per 5 seconds budget.
## Notes
* **Wallet address is case-insensitive**. Internally lowercased.
* **Time precision**: when `from` is `YYYY-MM-DD`, it resolves to that date at 00:00 UTC. Pass unix seconds for sub-day precision.
* **Settlement events** (redemption, merge, split, neg\_risk\_conversion) are processed at face value on both `actual_pnl` and `backtest_copy_pnl`, so they cancel out in `slippage_amount` but remain in the absolute PnL numbers.
* **High-volume wallets**: the underlying walk is paginated by time bucket and runs in parallel. Wallets with up to \~1.6M fills in the window have been validated. Beyond that, pass a tighter window or expect `partial: true`.
# Backtesting Overview
Source: https://docs.polynode.dev/api-reference/backtesting/overview
Score any wallet for copy-trading quality. Walks every fill in a window, applies realistic slippage, returns a single rate that flags toxic-to-copy wallets.
The backtesting endpoint answers one question for any Polymarket wallet:
> "If I had copied every trade this wallet made, how much would slippage friction have eaten?"
You pass a wallet and a time window. We walk every fill in that window, apply a realistic 2% slippage on each buy and sell (capped at \$1.00 per share for buys), settle redemptions / merges / splits at face value, and return both the wallet's actual cashflow PnL and the simulated copier's PnL. The dollar gap between them — divided by the trader's actual PnL — is the **slippage cost rate**.
A wallet with a high slippage rate (>15%) profits primarily from execution speed and tight spreads. A copier following them with normal latency and slippage won't replicate the returns. We flag those as `toxic_for_copying`.
## How the math works
For each fill in the window:
* **BUY**: copier pays `price × 1.02 × shares`, capped at `1.00 × shares`
* **SELL**: copier receives `price × 0.98 × shares`
For each settlement event (redemption, merge, split, neg\_risk\_conversion):
* Processed at face value, no slippage applied to either side
```
actual_pnl = -sum(buy_usd) + sum(sell_usd) + settlements_in - settlements_out
backtest_pnl = -sum(buy_usd × slippage_mult) + sum(sell_usd × 0.98) + settlements_in - settlements_out
slippage_amount = actual_pnl - backtest_pnl
slippage_cost_rate_pct = slippage_amount / abs(actual_pnl) × 100
toxic_for_copying = slippage_cost_rate_pct > 15
```
When `abs(actual_pnl) < $1`, the rate denominator is unstable and we return `null` for the rate.
## What "PnL" means here
`actual_pnl_usdc` is **cashflow PnL** over the requested window: the real dollars that moved through this wallet's Safe. It does **not** mark open positions to current price the way Polymarket's website does. The response includes a `pnl_definition: "cashflow"` field to make this explicit.
This is the right number for copy-trading quality scoring because a copier eventually realizes their open positions too — what matters is the friction-adjusted dollars actually banked, not paper marks that can evaporate.
If you want PM-website style realized + unrealized PnL, use the [Trader PnL Series](/api-reference/enriched/trader-pnl) endpoint.
### Per-position metrics: parity with Polymarket
The `avg_entry_prob_weighted` and `positions_closed` fields are computed via per-position weighted-average cost basis matching across every fill, split, merge, redemption, and neg-risk conversion in the requested window — the same method used to match Polymarket's own data-api.
Validation against `data-api.polymarket.com/positions` `realizedPnl` across 30 positions on diverse wallets (standard CTF + neg-risk markets):
| Threshold | Match rate |
| --------- | ---------- |
| sub-penny | 97 % |
| sub-\$1 | **100 %** |
| sub-\$10 | **100 %** |
If your wallet has positions on Polymarket's positions page, the per-position basis our matcher tracks is the same one Polymarket displays. Any drift larger than \$1 indicates a data-fetching issue (e.g. very old activity outside our event-fetch window), not an algorithm difference.
## Endpoints
**Score on demand:**
| Method | Endpoint | Description |
| ------ | ----------------------- | ----------------------------------- |
| `GET` | `/v2/copy-pnl/{wallet}` | Score one wallet over a time window |
| `POST` | `/v2/copy-pnl/batch` | Score up to 100 wallets in one call |
**BYOB — Bring Your Own Backtest** (precomputed leaderboard):
| Method | Endpoint | Description |
| -------- | -------------------------- | ------------------------------------------------------ |
| `POST` | `/v2/copy-pnl/wallets` | Add wallets to your private tracked-pool |
| `DELETE` | `/v2/copy-pnl/wallets` | Remove wallets from your pool |
| `GET` | `/v2/copy-pnl/wallets` | List wallets in your pool |
| `GET` | `/v2/copy-pnl/leaderboard` | Sorted, filtered, paginated leaderboard over your pool |
## BYOB — when to use it
The on-demand endpoints (`GET /v2/copy-pnl/{wallet}` and `POST /v2/copy-pnl/batch`) are **synchronous** — you wait while we walk the wallet's history, which can take up to 30 seconds per wallet. Great for one-off lookups.
**BYOB inverts that.** You hand us a list of wallets you care about; we precompute their copy-pnl scores in the background across multiple time windows; you query the resulting leaderboard with sub-second latency. Best for:
* Picking leader wallets to surface in your UI from a candidate pool
* Daily-refreshed dashboards
* Anywhere you need "top N by backtest\_copy\_pnl\_usdc, excluding toxic, with min trade count" answered fast
**How it works:**
1. POST your wallet list to `/v2/copy-pnl/wallets` (max 1000 wallets per API key)
2. Newly-added wallets get scored immediately (on-add freshening — usually within \~30s of the add)
3. The full pool also refreshes daily in the background
4. Query `/v2/copy-pnl/leaderboard` to read sorted/filtered results — all from cache, sub-second
5. Each result row includes `computed_at` so you can show freshness in your UI
**Tenant isolation:** each API key has its own private pool. Pools are keyed on the SHA256 of your API key — your wallets are never visible to other customers.
## Time-window options
* **Default**: last 30 days (when no `from`, `to`, or `period` is provided)
* **Preset**: `?period=7d|14d|30d|60d|90d|180d`
* **Explicit**: `?from=YYYY-MM-DD` and/or `?to=YYYY-MM-DD` (also accepts unix seconds)
* **Precedence**: explicit `from`/`to` beats `period` beats default
For BYOB, scores are precomputed for **all six period presets** (7/14/30/60/90/180 days) so the leaderboard query just picks one.
## Time-window options
* **Default**: last 30 days (when no `from`, `to`, or `period` is provided)
* **Preset**: `?period=7d|14d|30d|60d|90d|180d`
* **Explicit**: `?from=YYYY-MM-DD` and/or `?to=YYYY-MM-DD` (also accepts unix seconds)
* **Precedence**: explicit `from`/`to` beats `period` beats default
## Limits and behavior
**On-demand endpoints (`{wallet}` + `batch`):**
* **Paid tier required** — free tier returns 403
* **Rate limit**: 1 request per 5 seconds per API key (backtest is compute-heavy)
* **Server budget**: 30 second hard cap on the underlying walk. Extreme high-frequency wallets (1M+ fills in the window) may return `partial: true` — pass a tighter window to fit in budget
* **Maximum window**: `180d` (6 months). Past that, even normal wallets exceed budget
* **Batch**: max 100 wallets per call
**BYOB:**
* **Pool size**: 1000 wallets per API key max
* **Refresh cadence**: 24 hour periodic cycle, processed in chunks of 50 wallets spread across the day. Plus on-add freshening (new wallets scored immediately, usually within seconds).
* **Per-wallet budget on background refresh**: 180 seconds (3 minutes) — longer than the on-demand 30s cap because the customer isn't waiting on the response
* **Leaderboard query latency**: sub-second (all reads from cache)
* **Freshness signal**: every result row includes `computed_at` (unix ts of when that score was last computed); top-level response includes `last_refresh` (last full-cycle complete)
## FAQ: Why do two traders on the same market get different rates?
The math (2% friction per fill) is identical for every wallet. What changes between traders is **how many fills they need** to extract their PnL, and how big that PnL is. The rate is `slippage / |actual_pnl|` — same numerator math, different denominators and trade counts.
The cleanest rule of thumb: **the more trades it takes to extract a given dollar of PnL, the less of that PnL survives reproduction.**
| Wallet | Strategy | Actual PnL | Trades | Slip \$ | Rate | Copyable? |
| ------------- | ---------------------------------------------------------- | ---------- | -----: | ------: | ----------------: | ---------------------------------- |
| `0x4924…3782` | Concentrated whale: few large positions held to resolution | \$20.0M | 8K | \$786K | **3.9 %** | Yes — copier captures \~\$19.2M |
| `0x37c1…74a6` | Profitable scalper: tight spreads, fast execution | +\$755K | 93K | \$925K | **122 %** (toxic) | No — copier ends up at -\$170K |
| `0xee61…fc18` | High-frequency loser: many fills for tiny per-trade margin | -\$132K | 303K | \$929K | **705 %** (toxic) | No — copier loses 7x what they did |
All three traded across the same broad set of Polymarket markets in the same 30-day window. The rate does not measure "is this trader good?" — it measures **"how much of their edge survives reproduction by a copier with realistic friction?"**
* **Strategic edge** (market reads, conviction, timing of large positions) → low rate, reproducible
* **Execution edge** (HFT, queue priority, spread capture) → high rate, not reproducible
A wallet can be massively profitable AND uncopyable. The rate tells you which kind they are before you wire money to copy them.
# Add Wallets
Source: https://docs.polynode.dev/api-reference/byol/add-wallets
POST /v2/leaderboard/wallets
Add wallets to your tracked leaderboard set.
Add one or more Polymarket proxy wallet addresses to your BYOL set. Newly added wallets are tracked immediately in the background and will appear in your leaderboard queries within seconds.
```
POST /v2/leaderboard/wallets
```
## Authentication
Pass your API key via query parameter `?key=`, header `x-api-key`, or `Authorization: Bearer`.
## Request body
```json theme={null}
{
"wallets": [
"0x94f199fb7789f1aef7fff6b758d6b375100f4c7a",
"0xe9ad918c7678cd38b12603a762e638a5d1ee7091"
]
}
```
Array of Polymarket proxy wallet addresses. Each must be a valid Ethereum address (`0x` + 40 hex characters). Maximum 500 wallets per request.
## Response
```json theme={null}
{
"added": 2,
"already_tracked": 0,
"total": 106,
"limit": 5000
}
```
| Field | Type | Description |
| ----------------- | ------ | -------------------------------------------------------------------- |
| `added` | number | Number of new wallets added |
| `already_tracked` | number | Wallets that were already in your set (not counted toward the limit) |
| `total` | number | Total wallets now in your set |
| `limit` | number | Maximum wallets allowed (5,000) |
## Deduplication
If you add wallets that are already in your set, they are silently ignored. This means you can safely re-send your full wallet list without worrying about duplicates.
```json theme={null}
// Request: adding 2 wallets that are already tracked
{
"wallets": [
"0x94f199fb7789f1aef7fff6b758d6b375100f4c7a",
"0xe9ad918c7678cd38b12603a762e638a5d1ee7091"
]
}
// Response: nothing was added, total unchanged
{
"added": 0,
"already_tracked": 2,
"total": 106,
"limit": 5000
}
```
## Examples
```bash cURL theme={null}
curl -X POST "https://api.polynode.dev/v2/leaderboard/wallets?key=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"wallets": [
"0x94f199fb7789f1aef7fff6b758d6b375100f4c7a",
"0xe9ad918c7678cd38b12603a762e638a5d1ee7091"
]
}'
```
```javascript JavaScript theme={null}
const response = await fetch(
"https://api.polynode.dev/v2/leaderboard/wallets?key=YOUR_API_KEY",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
wallets: [
"0x94f199fb7789f1aef7fff6b758d6b375100f4c7a",
"0xe9ad918c7678cd38b12603a762e638a5d1ee7091"
]
})
}
);
const data = await response.json();
console.log(`Added: ${data.added}, Total: ${data.total}`);
```
```python Python theme={null}
import requests
response = requests.post(
"https://api.polynode.dev/v2/leaderboard/wallets",
params={"key": "YOUR_API_KEY"},
json={
"wallets": [
"0x94f199fb7789f1aef7fff6b758d6b375100f4c7a",
"0xe9ad918c7678cd38b12603a762e638a5d1ee7091"
]
}
)
data = response.json()
print(f"Added: {data['added']}, Total: {data['total']}")
```
Newly added wallets are fetched immediately in the background. They typically appear in leaderboard queries within a few seconds.
# Get Leaderboard
Source: https://docs.polynode.dev/api-reference/byol/get-leaderboard
GET /v2/leaderboard/custom
Query your custom leaderboard ranked by P&L or volume, filtered by time period and category.
Returns your custom leaderboard built from the wallets in your BYOL set. By default, returns all categories in a single response. You can also filter to a specific category.
```
GET /v2/leaderboard/custom
```
## Authentication
Pass your API key via query parameter `?key=`, header `x-api-key`, or `Authorization: Bearer`.
## Parameters
Time period for P\&L and volume. Matches Polymarket's leaderboard tabs.
* `day` — Today
* `week` — Last 7 days
* `month` — Last 30 days
* `all` — All time
Market category filter. Use `all` to get every category in one response, or specify one:
* `all` — Returns all categories grouped in a `categories` object (default)
* `overall` — All markets combined
* `politics` `sports` `crypto` `finance` `tech` `weather` `culture` `mentions`
Sort metric. `pnl` (Profit/Loss) or `vol` (Volume).
Maximum entries per category. Max 500.
Pagination offset.
## Response: all categories (default)
When `category=all` or omitted, the response groups results by category. Each category is sorted and ranked independently.
```bash theme={null}
curl "https://api.polynode.dev/v2/leaderboard/custom?timePeriod=all&key=YOUR_API_KEY"
```
```json theme={null}
{
"timePeriod": "all",
"category": "all",
"orderBy": "pnl",
"offset": 0,
"limit": 50,
"lastRefreshed": "2026-04-01T16:04:19.000Z",
"categories": {
"overall": [
{
"localRank": 1,
"rank": "1",
"proxyWallet": "0x56687bf447db6ffa42ffe2204a05edaa20f55839",
"userName": "Theo4",
"xUsername": "",
"verifiedBadge": false,
"vol": 43013258.52,
"pnl": 22053933.75,
"profileImage": ""
},
{
"localRank": 2,
"rank": "8",
"proxyWallet": "0x8119010a6e589062aa03583bb3f39ca632d9f887",
"userName": "PrincessCaro",
"xUsername": "",
"verifiedBadge": false,
"vol": 23520809.95,
"pnl": 6083643.10,
"profileImage": ""
},
{
"localRank": 3,
"rank": "5004",
"proxyWallet": "0xb595d09ce5bbc4d39e3b3d04e80c402d2c8d5922",
"userName": "...",
"xUsername": "",
"verifiedBadge": false,
"vol": 2467445.01,
"pnl": 22617.64,
"profileImage": ""
}
],
"politics": [
{
"localRank": 1,
"rank": "1",
"proxyWallet": "0x56687bf447db6ffa42ffe2204a05edaa20f55839",
"userName": "Theo4",
"xUsername": "",
"verifiedBadge": false,
"vol": 43012710.75,
"pnl": 22053952.53,
"profileImage": ""
}
],
"sports": [
{
"localRank": 1,
"rank": "1696",
"proxyWallet": "0xb595d09ce5bbc4d39e3b3d04e80c402d2c8d5922",
"userName": "...",
"xUsername": "",
"verifiedBadge": false,
"vol": 2466252.61,
"pnl": 22677.91,
"profileImage": ""
}
],
"crypto": [...],
"finance": [...],
"tech": [],
"weather": [],
"culture": [...],
"mentions": [...]
}
}
```
## Response: single category
When you specify a category like `category=crypto`, the response is a flat leaderboard with a `total` count.
```bash theme={null}
curl "https://api.polynode.dev/v2/leaderboard/custom?timePeriod=week&category=crypto&key=YOUR_API_KEY"
```
```json theme={null}
{
"timePeriod": "week",
"category": "crypto",
"orderBy": "pnl",
"total": 6,
"offset": 0,
"limit": 50,
"lastRefreshed": "2026-04-01T16:04:19.000Z",
"leaderboard": [
{
"localRank": 1,
"rank": "551",
"proxyWallet": "0x53decedc72531ef57b2d54b5542c509e233c822f",
"userName": "jack118",
"xUsername": "",
"verifiedBadge": false,
"vol": 0,
"pnl": 11077.58,
"profileImage": ""
},
{
"localRank": 2,
"rank": "34808",
"proxyWallet": "0x05d5e0403427a6223f5673b3234ac9405c19db37",
"userName": "0x05d5e0403427a6223f5673b3234ac9405c19db37",
"xUsername": "",
"verifiedBadge": false,
"vol": 0,
"pnl": 23.24,
"profileImage": ""
}
]
}
```
## Response: sorted by volume
```bash theme={null}
curl "https://api.polynode.dev/v2/leaderboard/custom?timePeriod=month&category=overall&orderBy=vol&limit=5&key=YOUR_API_KEY"
```
```json theme={null}
{
"timePeriod": "month",
"category": "overall",
"orderBy": "vol",
"total": 18,
"offset": 0,
"limit": 5,
"lastRefreshed": "2026-04-01T16:04:19.000Z",
"leaderboard": [
{
"localRank": 1,
"rank": "1086",
"proxyWallet": "0xb595d09ce5bbc4d39e3b3d04e80c402d2c8d5922",
"userName": "...",
"xUsername": "",
"verifiedBadge": false,
"vol": 43406.94,
"pnl": 22107.49,
"profileImage": ""
},
{
"localRank": 2,
"rank": "129764",
"proxyWallet": "0xc0220b02ad4cf50f8d612dbb3aa7783973266494",
"userName": "...",
"xUsername": "",
"verifiedBadge": false,
"vol": 25.75,
"pnl": 11.87,
"profileImage": ""
}
]
}
```
## Response: today's activity
```bash theme={null}
curl "https://api.polynode.dev/v2/leaderboard/custom?timePeriod=day&category=sports&key=YOUR_API_KEY"
```
```json theme={null}
{
"timePeriod": "day",
"category": "sports",
"orderBy": "pnl",
"total": 9,
"offset": 0,
"limit": 50,
"lastRefreshed": "2026-04-01T16:04:19.000Z",
"leaderboard": [
{
"localRank": 1,
"rank": "768",
"proxyWallet": "0xb595d09ce5bbc4d39e3b3d04e80c402d2c8d5922",
"userName": "...",
"xUsername": "",
"verifiedBadge": false,
"vol": 43406.94,
"pnl": 516.36,
"profileImage": ""
},
{
"localRank": 2,
"rank": "96658",
"proxyWallet": "0x1116147499ba1da24081a5f09077cdbc4586997c",
"userName": "...",
"xUsername": "",
"verifiedBadge": false,
"vol": 0,
"pnl": 0.13,
"profileImage": ""
}
]
}
```
## Top-level response fields
| Field | Type | Description |
| --------------- | ------ | ------------------------------------------------------------------ |
| `timePeriod` | string | The time period used for this query |
| `category` | string | `"all"` or the specific category queried |
| `orderBy` | string | Sort metric used (`pnl` or `vol`) |
| `total` | number | Total wallets with data for this query (single-category mode only) |
| `offset` | number | Pagination offset |
| `limit` | number | Maximum entries returned |
| `lastRefreshed` | string | ISO timestamp of the last background data refresh |
| `categories` | object | Leaderboard entries grouped by category (when `category=all`) |
| `leaderboard` | array | Flat leaderboard entries (when a specific category is requested) |
## Leaderboard entry fields
| Field | Type | Description |
| --------------- | ------- | ------------------------------------------------------------------------------------------- |
| `localRank` | number | Rank within your custom leaderboard set |
| `rank` | string | Global rank on Polymarket's leaderboard. This is a string matching Polymarket's API format. |
| `proxyWallet` | string | Polymarket proxy wallet address |
| `userName` | string | Polymarket display name |
| `xUsername` | string | Twitter/X username (if set) |
| `verifiedBadge` | boolean | Polymarket verified badge |
| `vol` | number | Trading volume (USD) for this time period |
| `pnl` | number | Profit/Loss (USD) for this time period |
| `profileImage` | string | Profile image URL |
## Code examples
### Get everything for the current week
```bash cURL theme={null}
curl "https://api.polynode.dev/v2/leaderboard/custom?timePeriod=week&key=YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const response = await fetch(
"https://api.polynode.dev/v2/leaderboard/custom?timePeriod=week&key=YOUR_API_KEY"
);
const data = await response.json();
// All 9 categories in one response
for (const [category, traders] of Object.entries(data.categories)) {
if (traders.length > 0) {
console.log(`${category}: ${traders.length} traders`);
console.log(` Top trader: ${traders[0].userName} ($${traders[0].pnl.toFixed(2)})`);
}
}
```
```python Python theme={null}
import requests
response = requests.get(
"https://api.polynode.dev/v2/leaderboard/custom",
params={"timePeriod": "week", "key": "YOUR_API_KEY"}
)
data = response.json()
for category, traders in data["categories"].items():
if traders:
top = traders[0]
print(f"{category}: {len(traders)} traders, top: {top['userName']} (${top['pnl']:,.2f})")
```
### Get a single category
```bash cURL theme={null}
curl "https://api.polynode.dev/v2/leaderboard/custom?timePeriod=month&category=crypto&orderBy=pnl&key=YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const response = await fetch(
"https://api.polynode.dev/v2/leaderboard/custom?timePeriod=month&category=crypto&orderBy=pnl&key=YOUR_API_KEY"
);
const data = await response.json();
// Flat leaderboard array
console.log(`${data.total} traders in crypto this month`);
for (const trader of data.leaderboard || []) {
console.log(`#${trader.localRank} ${trader.userName} — $${trader.pnl.toFixed(2)} P&L (global rank: ${trader.rank})`);
}
```
```python Python theme={null}
import requests
response = requests.get(
"https://api.polynode.dev/v2/leaderboard/custom",
params={
"timePeriod": "month",
"category": "crypto",
"orderBy": "pnl",
"key": "YOUR_API_KEY"
}
)
data = response.json()
print(f"{data['total']} traders in crypto this month")
for trader in data["leaderboard"]:
print(f"#{trader['localRank']} {trader['userName']} — ${trader['pnl']:,.2f} (global #{trader['rank']})")
```
### Sort by volume, paginated
```bash cURL theme={null}
# Top 10 by volume, all time
curl "https://api.polynode.dev/v2/leaderboard/custom?timePeriod=all&category=overall&orderBy=vol&limit=10&key=YOUR_API_KEY"
# Next 10
curl "https://api.polynode.dev/v2/leaderboard/custom?timePeriod=all&category=overall&orderBy=vol&limit=10&offset=10&key=YOUR_API_KEY"
```
```javascript JavaScript theme={null}
// Paginate through all results
let offset = 0;
const limit = 50;
while (true) {
const response = await fetch(
`https://api.polynode.dev/v2/leaderboard/custom?timePeriod=all&category=overall&orderBy=vol&limit=${limit}&offset=${offset}&key=YOUR_API_KEY`
);
const data = await response.json();
for (const trader of data.leaderboard || []) {
console.log(`#${trader.localRank} ${trader.userName} — $${trader.vol.toFixed(2)} volume`);
}
if (!data.leaderboard || data.leaderboard.length < limit) break;
offset += limit;
// Respect rate limit: 1 request per 5 seconds
await new Promise(resolve => setTimeout(resolve, 5000));
}
```
## Rate limiting
This endpoint is rate limited to **1 request per 5 seconds** per API key. If you exceed this, you'll get a 429 response:
```json theme={null}
{
"error": "Leaderboard is rate limited to 1 request per 5 seconds.",
"retryAfterMs": 3784
}
```
| Field | Type | Description |
| -------------- | ------ | ------------------------------------ |
| `error` | string | Error message |
| `retryAfterMs` | number | Milliseconds to wait before retrying |
The response also includes a `Retry-After` header (in seconds).
Data refreshes every \~30 minutes in the background. Repeat requests within 60 seconds are served from an in-memory cache for maximum speed.
# List Wallets
Source: https://docs.polynode.dev/api-reference/byol/list-wallets
GET /v2/leaderboard/wallets
List all wallets currently in your tracked leaderboard set.
Returns every wallet address in your BYOL set along with your wallet limit.
```
GET /v2/leaderboard/wallets
```
## Authentication
Pass your API key via query parameter `?key=`, header `x-api-key`, or `Authorization: Bearer`.
## Response
```json theme={null}
{
"wallets": [
"0x56687bf447db6ffa42ffe2204a05edaa20f55839",
"0xb595d09ce5bbc4d39e3b3d04e80c402d2c8d5922",
"0x53decedc72531ef57b2d54b5542c509e233c822f",
"0xd252bce657d99d4e943708a8d7a2b3222da2775b",
"0x05d5e0403427a6223f5673b3234ac9405c19db37"
],
"count": 104,
"limit": 5000
}
```
| Field | Type | Description |
| --------- | --------- | -------------------------------- |
| `wallets` | string\[] | All wallet addresses in your set |
| `count` | number | Total number of wallets |
| `limit` | number | Maximum wallets allowed (5,000) |
## Examples
```bash cURL theme={null}
curl "https://api.polynode.dev/v2/leaderboard/wallets?key=YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const response = await fetch(
"https://api.polynode.dev/v2/leaderboard/wallets?key=YOUR_API_KEY"
);
const data = await response.json();
console.log(`Tracking ${data.count} wallets (limit: ${data.limit})`);
```
```python Python theme={null}
import requests
response = requests.get(
"https://api.polynode.dev/v2/leaderboard/wallets",
params={"key": "YOUR_API_KEY"}
)
data = response.json()
print(f"Tracking {data['count']} wallets (limit: {data['limit']})")
```
# BYOL Overview
Source: https://docs.polynode.dev/api-reference/byol/overview
Bring Your Own Leaderboard. Track any set of Polymarket wallets and get a private, ranked leaderboard with P&L across all time periods and categories.
BYOL (Bring Your Own Leaderboard) lets you build a custom Polymarket leaderboard from any set of wallets you choose. Add the wallets you care about, and polynode continuously tracks their P\&L, volume, and global rank across every time period and category that Polymarket tracks.
## How it works
1. **Add wallets** you want to track via the API
2. polynode tracks every wallet across all time periods and categories in the background (\~30 min refresh cycle)
3. **Query your leaderboard** by time period, category, or get everything at once
4. Each API key has its own private wallet set. Your wallets and leaderboard are completely isolated from other users.
## What you get back
Every wallet in your set is tracked across:
**4 time periods** matching Polymarket's leaderboard:
* `day` (Today)
* `week` (Weekly)
* `month` (Monthly)
* `all` (All time)
**9 categories:**
* `overall` `politics` `sports` `crypto` `finance` `tech` `weather` `culture` `mentions`
**2 sort options:**
* `pnl` (Profit/Loss)
* `vol` (Volume)
The response fields match Polymarket's leaderboard format exactly: `rank`, `proxyWallet`, `userName`, `xUsername`, `verifiedBadge`, `vol`, `pnl`, `profileImage`. We add `localRank` to show where each wallet ranks within your custom set.
## Limits
* **5,000 wallets max** per API key
* Free tier keys cannot access BYOL. Any paid tier has full access.
* Adding a wallet that's already in your set doesn't count toward the limit
## Endpoints
| Method | Endpoint | Description |
| -------- | ------------------------- | ------------------------------- |
| `POST` | `/v2/leaderboard/wallets` | Add wallets to your tracked set |
| `DELETE` | `/v2/leaderboard/wallets` | Remove wallets from your set |
| `GET` | `/v2/leaderboard/wallets` | List all wallets in your set |
| `GET` | `/v2/leaderboard/custom` | Query your leaderboard |
# Remove Wallets
Source: https://docs.polynode.dev/api-reference/byol/remove-wallets
DELETE /v2/leaderboard/wallets
Remove wallets from your tracked leaderboard set.
Remove one or more wallets from your BYOL set. They will no longer appear in your leaderboard queries.
```
DELETE /v2/leaderboard/wallets
```
## Authentication
Pass your API key via query parameter `?key=`, header `x-api-key`, or `Authorization: Bearer`.
## Request body
```json theme={null}
{
"wallets": [
"0x94f199fb7789f1aef7fff6b758d6b375100f4c7a",
"0xe9ad918c7678cd38b12603a762e638a5d1ee7091"
]
}
```
Array of wallet addresses to remove from your set.
## Response
```json theme={null}
{
"removed": 2,
"total": 104
}
```
| Field | Type | Description |
| --------- | ------ | ----------------------------- |
| `removed` | number | Number of wallets removed |
| `total` | number | Remaining wallets in your set |
## Examples
```bash cURL theme={null}
curl -X DELETE "https://api.polynode.dev/v2/leaderboard/wallets?key=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"wallets": [
"0x94f199fb7789f1aef7fff6b758d6b375100f4c7a",
"0xe9ad918c7678cd38b12603a762e638a5d1ee7091"
]
}'
```
```javascript JavaScript theme={null}
const response = await fetch(
"https://api.polynode.dev/v2/leaderboard/wallets?key=YOUR_API_KEY",
{
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
wallets: [
"0x94f199fb7789f1aef7fff6b758d6b375100f4c7a",
"0xe9ad918c7678cd38b12603a762e638a5d1ee7091"
]
})
}
);
const data = await response.json();
console.log(`Removed: ${data.removed}, Remaining: ${data.total}`);
```
```python Python theme={null}
import requests
response = requests.delete(
"https://api.polynode.dev/v2/leaderboard/wallets",
params={"key": "YOUR_API_KEY"},
json={
"wallets": [
"0x94f199fb7789f1aef7fff6b758d6b375100f4c7a",
"0xe9ad918c7678cd38b12603a762e638a5d1ee7091"
]
}
)
data = response.json()
print(f"Removed: {data['removed']}, Remaining: {data['total']}")
```
Removing a wallet from your set does not affect other API keys that may be tracking the same wallet.
# Active 5-Minute Markets
Source: https://docs.polynode.dev/api-reference/crypto/active
GET /v1/crypto/active
Currently active 5-minute up-or-down markets for all 7 coins.
Returns the currently active 5-minute up-or-down markets for all 7 supported coins. These markets rotate every 5 minutes. Each response includes token IDs, outcomes, and current odds for immediate trading.
```json theme={null}
{
"markets": [
{
"id": "312746",
"slug": "btc-updown-5m-1774674300",
"title": "Bitcoin Up or Down - March 28, 1:05AM-1:10AM ET",
"active": true,
"closed": false,
"startDate": "2026-03-27T05:16:04.211285Z",
"endDate": "2026-03-28T05:10:00Z",
"markets": [
{
"question": "Bitcoin Up or Down - March 28, 1:05AM-1:10AM ET",
"conditionId": "0x...",
"outcomes": ["Up", "Down"],
"outcomePrices": [0.465, 0.535],
"tokenId": "12345...",
"active": true,
"closed": false
}
]
}
],
"count": 7,
"windowStart": 1774674300
}
```
Cached for 30 seconds. The `windowStart` field is the epoch timestamp of the current 5-minute window. Use this as the `window` parameter for the [Price to Beat](/api-reference/crypto/price) endpoint.
# Oracle Candles
Source: https://docs.polynode.dev/api-reference/crypto/candles
GET /v1/crypto/candles
5-minute OHLC candles from PolyNode's live Chainlink tick archive.
Returns 5-minute OHLC candles from PolyNode's live Chainlink tick archive for a given crypto asset. Approximately 30 candles (\~2.5 hours of history) are returned.
These are the same oracle prices used to resolve Polymarket's short-form crypto markets.
```json theme={null}
{
"candles": [
{
"time": 1774665300,
"open": 65954.03,
"high": 65992.01,
"low": 65947.14,
"close": 65949.69
},
{
"time": 1774665600,
"open": 65949.69,
"high": 65987.20,
"low": 65949.69,
"close": 65987.20
}
]
}
```
Cached for about 5 seconds. The `symbol` parameter accepts bare asset symbols like `BTC` and feed names like `BTC/USD`. Each candle's `time` field is a Unix epoch timestamp marking the start of that 5-minute window.
# Crypto Markets
Source: https://docs.polynode.dev/api-reference/crypto/markets
GET /v1/crypto/markets
All crypto prediction markets with liquidity, volume, and open interest.
Returns all currently active crypto prediction markets on Polymarket, including monthly, weekly, and daily markets across 7 supported assets (BTC, ETH, SOL, BNB, XRP, DOGE, HYPE).
```json theme={null}
{
"events": [
{
"id": "238474",
"slug": "what-price-will-bitcoin-hit-in-march-2026",
"title": "What price will Bitcoin hit in March?",
"startDate": "2026-03-01T05:20:27.791222Z",
"endDate": "2026-04-01T04:00:00Z",
"active": true,
"volume": 88886711.74,
"volume24hr": 5733750.73,
"liquidity": 6074250.39,
"openInterest": 12827944.10,
"seriesSlug": "bitcoin-hit-price-monthly",
"markets": [
{
"id": "1629442",
"question": "$100,000?",
"conditionId": "0x...",
"outcomes": ["Yes", "No"],
"outcomePrices": [0.045, 0.955],
"volume": 24084104.74,
"liquidity": 2028310.67,
"active": true,
"closed": false,
"groupItemTitle": "↑ 150,000"
}
]
}
]
}
```
Cached for 3 minutes.
# Price to Beat
Source: https://docs.polynode.dev/api-reference/crypto/price
GET /v1/crypto/price
Open and close price for a specific crypto market window.
Returns the opening and current closing price for a crypto market window. This is the "price to beat" for short-form markets.
The `openPrice` is the Chainlink oracle price at market open. The `closePrice` updates in real time until the window completes (`completed: true`).
```json theme={null}
{
"openPrice": 66285.01,
"closePrice": 66238.61,
"timestamp": 1774674466843,
"completed": false,
"incomplete": true,
"cached": false
}
```
Real-time. No cache. `closePrice` is locked in the instant a window ends — no waiting. The `window` parameter is a Unix epoch timestamp. Get window timestamps from the `/v1/crypto/active` endpoint.
# Crypto Series
Source: https://docs.polynode.dev/api-reference/crypto/series
GET /v1/crypto/series
Recurring crypto market series.
Returns all recurring crypto market series across different intervals (5m, 15m, 1h, 4h, daily, weekly, monthly). Each series generates new markets on its recurrence schedule.
```json theme={null}
{
"series": [
{
"id": "10422",
"slug": "xrp-up-or-down-15m",
"title": "XRP Up or Down 15m",
"recurrence": "15m",
"active": true,
"eventCount": null
},
{
"id": "10065",
"slug": "ethereum-neg-risk-weekly",
"title": "Ethereum Neg Risk Weekly",
"recurrence": "weekly",
"active": true,
"eventCount": null
}
],
"count": 7
}
```
Cached for 5 minutes.
# Historical Price Ticks
Source: https://docs.polynode.dev/api-reference/crypto/ticks
GET /v1/crypto/ticks
Historical 1-second crypto price ticks for short-form chart backfill.
Returns historical 1-second price ticks for the same crypto feeds available on the `price_feed` WebSocket stream. Use this endpoint to backfill a 5-minute, 15-minute, or 1-hour chart when a user opens it part way through the market window, then keep the chart current with live `price_feed` events.
Asset symbol. One of `BTC`, `ETH`, `SOL`, `BNB`, `XRP`, `DOGE`, `HYPE`.
Start of the range as a Unix timestamp in milliseconds.
End of the range as a Unix timestamp in milliseconds.
Maximum ticks to return. Max 100000.
Optional feed source filter. Valid values are `chainlink_data_streams` and `polymarket_chainlink`.
```bash theme={null}
curl -H "x-api-key: YOUR_API_KEY" \
"https://api.polynode.dev/v1/crypto/ticks?symbol=BTC&from=1780458961000&to=1780459082000&limit=5"
```
```json theme={null}
{
"symbol": "BTC",
"feed": "BTC/USD",
"from": 1780458961000,
"to": 1780459082000,
"limit": 5,
"count": 5,
"truncated": true,
"source": null,
"ticks": [
{
"id": "1780458962000-0",
"type": "price_feed",
"feed": "BTC/USD",
"timestamp": 1780458962,
"timestamp_ms": 1780458962000,
"source": "chainlink_data_streams",
"data": {
"feed": "BTC/USD",
"price": 65869.02124677686,
"bid": 65865.65936734258,
"ask": 65870.4120597498,
"timestamp": 1780458962
}
}
]
}
```
## Range limits
Each request can cover up to 24 hours. If `truncated` is `true`, the response hit the requested `limit`; request a narrower time range or increase `limit` to retrieve more ticks.
Historical availability starts when tick collection was enabled. Ranges before available history return an empty `ticks` array.
## Chart backfill
For a 15-minute market, request ticks from the market window start to now, render those points, then append live WebSocket `price_feed` events as they arrive.
```javascript theme={null}
const apiKey = "YOUR_API_KEY";
const symbol = "BTC";
const now = Date.now();
const windowStart = Math.floor(now / 1000 / 900) * 900 * 1000;
const url = new URL("https://api.polynode.dev/v1/crypto/ticks");
url.searchParams.set("symbol", symbol);
url.searchParams.set("from", String(windowStart));
url.searchParams.set("to", String(now));
url.searchParams.set("limit", "2000");
const response = await fetch(url, {
headers: { "x-api-key": apiKey }
});
const history = await response.json();
const points = history.ticks.map((tick) => ({
t: tick.timestamp_ms,
price: tick.data.price
}));
console.log(points.at(0), points.at(-1));
```
## Pair with the live stream
The response uses the same event shape as the WebSocket `price_feed` stream. The main difference is that historical responses include an `id`, `timestamp_ms`, and optional `source` label for replay and deduplication.
```javascript theme={null}
const ws = new WebSocket("wss://ws.polynode.dev/ws?key=YOUR_API_KEY");
ws.addEventListener("open", () => {
ws.send(JSON.stringify({
action: "subscribe",
type: "chainlink",
filters: { feeds: ["BTC/USD"] }
}));
});
ws.addEventListener("message", (event) => {
const msg = JSON.parse(event.data);
if (msg.type !== "price_feed") return;
const point = {
t: msg.data.timestamp * 1000,
price: msg.data.price
};
console.log(point);
});
```
BTC/USD can include more than one source for the same second. Deduplicate by `timestamp_ms` or choose a single `source` when your chart needs exactly one point per second.
# Activity Feed
Source: https://docs.polynode.dev/api-reference/enriched/activity
GET /v1/activity
Live platform-wide trade feed.
Returns the 50 most recent trades across all Polymarket markets. Includes full trade metadata, wallet addresses, and transaction hashes.
```json theme={null}
{
"trades": [
{
"wallet": "0x312ac123ff9551126f79e9ec53dab4df87637f59",
"side": "BUY",
"tokenId": "34377190941598917...",
"conditionId": "0x7f76a575cfb2d0b8...",
"size": 78.43,
"price": 0.607,
"timestamp": 1774115115,
"title": "Bitcoin Up or Down - March 21, 1:45PM-1:50PM ET",
"slug": "btc-updown-5m-1774115100",
"eventSlug": "btc-updown-5m-1774115100",
"outcome": "Up",
"name": "Elastic-Alpenhorn",
"txHash": "0xa0fe7f342aef603d..."
}
],
"count": 50
}
```
Activity data refreshes every 60 seconds.
# Equity Curve
Source: https://docs.polynode.dev/api-reference/enriched/equity-curve
GET /v2/trader/{wallet}/equity
Time-ordered equity curve for any Polymarket wallet, with optional dollar-normalized copy-trade analysis.
Returns a time-ordered equity curve showing cumulative profit and loss across every position a wallet has ever taken on Polymarket.
By default, the curve is built from **realized** P\&L only — the locked-in result from every closed position, every partial sell, every market resolution, every redemption. This makes the curve fully deterministic: the same query returns the same numbers regardless of when you call it. It's also fast, typically returning in under a second.
When `normalize=1` is set, the response also includes a **\$1-normalized** equity curve. Each position is scaled so its entry cost equals exactly \$1, then summed. This shows what your returns would look like if you copied every trade with \$1 of risk per position — the standard format for evaluating a wallet's edge across thousands of trades.
## Simple example
```bash theme={null}
curl "https://api.polynode.dev/v2/trader/0xb4ab48b451101a779bf8c644318bb17fe652571d/equity?period=all&normalize=1" \
-H "Authorization: Bearer YOUR_API_KEY"
```
Response (truncated for clarity — `curve` arrays contain up to 500 points each):
```json theme={null}
{
"wallet": "0xb4ab48b451101a779bf8c644318bb17fe652571d",
"period": "all",
"source": "onchain",
"positions_count": 564,
"markets_count": 296,
"open_count": 30,
"applied_filters": {
"fromTs": null,
"toTs": null,
"maxMarkets": null,
"includeUnrealized": false
},
"partial": false,
"raw": {
"points": 500,
"final_pnl": -857.48,
"curve": [
{ "t": 1774555427, "pnl": 10.56 },
{ "t": 1774555427, "pnl": -14.44 },
{ "t": 1774555441, "pnl": -6.62 }
]
},
"normalized": {
"bet_size": 1,
"positions": 564,
"points": 500,
"final_pnl": -18.4366,
"curve": [
{ "t": 1774555427, "pnl": 0.1234 },
{ "t": 1774555427, "pnl": -0.0466 },
{ "t": 1774555441, "pnl": 0.5934 }
]
}
}
```
## Query parameters
Polymarket wallet address.
Time window. Positions whose first activity falls before the start of the window are excluded. One of: `7d`, `30d`, `90d`, `1y`, `all`.
Set to `1` to include the \$1-normalized equity curve alongside the raw curve.
Start date. Accepts `YYYY-MM-DD` (treated as UTC midnight) or a unix timestamp in seconds. Drops positions whose first activity is before this date.
End date. Same format as `from`. Drops positions whose first activity is after this date.
Keep only the most recent N markets the wallet has touched. A "market" is a unique outcome group; binary markets count as one. Useful for quickly evaluating a wallet's recent performance without scanning their full history.
Set to `1` to mark currently-open positions to current market price and include the unrealized P\&L in the curve. **Off by default.** See [Realized vs unrealized](#realized-vs-unrealized) below before turning this on.
## Realized vs unrealized
The default response is **realized-only**. Every closed position contributes its locked-in P\&L. Open positions contribute whatever they've already realized through partial sells (often zero if the wallet hasn't sold any shares of that position yet).
This is the right default for backtesting and for evaluating a wallet's edge over time. It's deterministic — the same query returns the same numbers regardless of when you call it — and it returns in under a second even for whales.
If you want a **current-portfolio snapshot** that marks open positions to the latest market price, pass `include_unrealized=1`. The curve's final point will reflect what the wallet would have if it closed all open positions right now. Tradeoffs:
* **Slower.** Open positions need a current price each, fetched live.
* **Non-deterministic.** Two queries five minutes apart return slightly different numbers as prices move.
* **Worth it** when you specifically want a "what's my position worth right now" view rather than a backtest signal.
```bash theme={null}
# Realized-only (default, fast, deterministic)
curl ".../equity?period=all&normalize=1"
# Mark-to-market (slower, current-snapshot)
curl ".../equity?period=all&normalize=1&include_unrealized=1"
```
## Filtering examples
Last 30 days:
```bash theme={null}
curl ".../equity?period=30d&normalize=1"
```
Specific date range — backtest the wallet's behavior in early April 2026:
```bash theme={null}
curl ".../equity?from=2026-04-01&to=2026-04-15&normalize=1"
```
Recent 50 markets only — fast scan of the wallet's most recent activity:
```bash theme={null}
curl ".../equity?period=all&max_markets=50&normalize=1"
```
Composed filters — fast realized-only curve over the last 100 markets in a specific window:
```bash theme={null}
curl ".../equity?from=2026-04-01&max_markets=100"
```
## Response fields
| Field | Type | Description |
| ------------------------ | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `wallet` | string | The lowercased wallet address. |
| `period` | string | The period filter that was applied (`7d`, `30d`, `90d`, `1y`, or `all`). |
| `source` | string | Always `"onchain"`. |
| `positions_count` | integer | Total positions included in the curve after all filters. |
| `markets_count` | integer | Distinct markets (unique outcome groups). For binary markets, two positions (Yes + No) on the same market count as one market. |
| `open_count` | integer | Positions that still hold a non-zero balance. |
| `applied_filters` | object | Echoes back exactly which filters were applied — useful for debugging. |
| `partial` | boolean | `true` if the request hit the 10-second server-side timeout. The response will still contain whatever was computed; the `error` field will be set. |
| `raw.points` | integer | Number of points in the raw curve (downsampled to a max of 500). |
| `raw.final_pnl` | number | Final cumulative P\&L across the included positions, in USD. |
| `raw.curve[].t` | integer | Unix timestamp (seconds) when the position was first opened. |
| `raw.curve[].pnl` | number | Cumulative P\&L at that point, in USD. |
| `normalized.bet_size` | number | Always `1`. Each position is scaled to \$1 of risk. |
| `normalized.positions` | integer | Total positions in the normalized curve. |
| `normalized.points` | integer | Number of points (downsampled to a max of 500). |
| `normalized.final_pnl` | number | Final cumulative \$1-normalized P\&L. |
| `normalized.curve[].pnl` | number | Cumulative \$1-normalized P\&L at that point. |
## Performance
| Query shape | Typical first-hit latency | Cached |
| ------------------------------------------------- | ------------------------- | ------ |
| Default (any wallet) | 50ms – 2s | \<50ms |
| `include_unrealized=1`, large open-position count | 1s – 8s | \<50ms |
| Composed filters (`from` + `max_markets`) | \<1s | \<50ms |
Responses are cached for 5 minutes per unique parameter combination. The cache key separates every filter, so two callers using different filters won't collide.
## Rate limit
This endpoint is rate-limited at **1 request per 10 seconds per API key**. It's a heavy endpoint — caching results client-side is recommended.
## \$1 normalization math
For every position in the included set:
1. Take the position's P\&L (realized only by default; realized + unrealized when opted in).
2. Divide by `total_bought` for that position. `total_bought` is the total number of shares ever acquired across every acquisition method.
3. Add the result to the running normalized cumulative.
If a wallet put \$500 into a market and made \$50 realized, the normalized contribution is `$50 / $500 = $0.10`. For every dollar risked on that market, they made 10 cents.
Sum across all positions and you get the normalized final P\&L. A wallet with 2,000 positions and a normalized final P\&L of `+12.0` means each \$1 risked returned about \$0.006 on average. Across thousands of positions, that indicates a real and consistent edge.
## Notes on coverage
* **NegRisk-split positions.** When a wallet acquires tokens via a USDC split into a Yes/No pair (rather than a CLOB trade), there is no on-chain trade event for that token. We use sibling-token timestamps and redemption timestamps as fallbacks where available, but in rare cases a split-derived position with no sibling activity and no redemption record can fall back to "now" on the curve. This affects shape, not totals — `final_pnl` remains correct.
* **Curve downsampling.** Both the raw and normalized curves are downsampled to a maximum of 500 points. The first and last points are always preserved.
# Event Detail
Source: https://docs.polynode.dev/api-reference/enriched/event
GET /v1/event/{slug}
Full event data with all markets, prices, token IDs, and metadata.
Returns comprehensive data for a single Polymarket event, including all sub-markets with current outcome prices, volumes, liquidity, and token IDs for price history lookups.
Event slug (e.g., `how-many-fed-rate-cuts-in-2026`). Found in event URLs on Polymarket.
```json theme={null}
{
"id": "51456",
"slug": "how-many-fed-rate-cuts-in-2026",
"title": "How many Fed rate cuts in 2026?",
"description": "This market will resolve according to the exact amount of cuts...",
"image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/how-many-fed-rate-cuts-in-2025-9qstZkSL1dn0.jpg",
"icon": "https://polymarket-upload.s3.us-east-2.amazonaws.com/how-many-fed-rate-cuts-in-2025-9qstZkSL1dn0.jpg",
"active": true,
"closed": false,
"volume": 12679447.65,
"volume24hr": 531530.07,
"liquidity": 1120355.33,
"openInterest": 711723.41,
"startDate": "2025-09-29T22:29:04.30848Z",
"endDate": "2026-12-31T00:00:00Z",
"markets": [
{
"question": "Will no Fed rate cuts happen in 2026?",
"conditionId": "0xd4e77ba6f29fc093509d24f508631abd445ecf506bbdc9c4c80e60256a318527",
"outcomes": ["Yes", "No"],
"outcomePrices": ["0.3455", "0.6545"],
"volume": 2368774.98,
"liquidity": 54958.02,
"active": true,
"closed": false,
"groupItemTitle": "0 (0 bps)",
"tokenId": "12403602920039269077597917340921667997547115084613238528792639013246536343316"
},
{
"question": "Will 1 Fed rate cut happen in 2026?",
"conditionId": "0x5e082f0b57f47a29044aa35b4c5658393122e659d5feae521c06b57cdd7f905c",
"outcomes": ["Yes", "No"],
"outcomePrices": ["0.195", "0.805"],
"volume": 780389.52,
"liquidity": 65401.81,
"active": true,
"closed": false,
"groupItemTitle": "1 (25 bps)",
"tokenId": "113379839734351069617987084078322474966003108854908079701423911002443710490196"
},
{
"question": "Will 2 Fed rate cuts happen in 2026?",
"conditionId": "0xe0d9f508a249e0070db06eb7d1e1fb17eb23c963f6fb722c4c3f81e23240c1cd",
"outcomes": ["Yes", "No"],
"outcomePrices": ["0.165", "0.835"],
"volume": 754043.87,
"liquidity": 75307.43,
"active": true,
"closed": false,
"groupItemTitle": "2 (50 bps)",
"tokenId": "72535544017897924722695722172278828562733090748474862987195303914909938482758"
}
],
"series": null,
"similarMarkets": 0,
"annotations": 0
}
```
# Event Search
Source: https://docs.polynode.dev/api-reference/enriched/event-search
GET /v1/events/search
Search Polymarket events by text query. Returns events with all sub-markets, token IDs, and current prices.
Search for events by keyword. Each result includes the full list of sub-markets with `tokenId` (YES token for CLOB price history) and current `price`.
This is an event-level search — results are grouped by event, not individual markets. A multi-outcome event like "How many Fed rate cuts in 2026?" returns as one result with all 13 outcomes.
Search query (e.g., `recession`, `Fed rate`, `Trump tariff`).
Maximum number of events to return. Max 20.
```json theme={null}
{
"query": "Fed rate",
"events": [
{
"id": "51456",
"slug": "how-many-fed-rate-cuts-in-2026",
"title": "How many Fed rate cuts in 2026?",
"image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/how-many-fed-rate-cuts-in-2025-9qstZkSL1dn0.jpg",
"active": true,
"markets": [
{
"question": "Will no Fed rate cuts happen in 2026?",
"groupItemTitle": "0 (0 bps)",
"conditionId": "0xd4e77ba6f29fc093509d24f508631abd445ecf506bbdc9c4c80e60256a318527",
"tokenId": "12403602920039269077597917340921667997547115084613238528792639013246536343316",
"price": 0.348,
"active": true
},
{
"question": "Will 1 Fed rate cut happen in 2026?",
"groupItemTitle": "1 (25 bps)",
"conditionId": "0x5e082f0b57f47a29044aa35b4c5658393122e659d5feae521c06b57cdd7f905c",
"tokenId": "113379839734351069617987084078322474966003108854908079701423911002443710490196",
"price": 0.19,
"active": true
},
{
"question": "Will 2 Fed rate cuts happen in 2026?",
"groupItemTitle": "2 (50 bps)",
"conditionId": "0xe0d9f508a249e0070db06eb7d1e1fb17eb23c963f6fb722c4c3f81e23240c1cd",
"tokenId": "72535544017897924722695722172278828562733090748474862987195303914909938482758",
"price": 0.165,
"active": true
}
]
},
{
"id": "101936",
"slug": "fed-rate-hike-in-2026",
"title": "Fed rate hike in 2026?",
"image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/will-the-fed-raise-interest-rates-in-2025-PQTEYZMvmAGr.jpg",
"active": true,
"markets": [
{
"question": "Fed rate hike in 2026?",
"groupItemTitle": "",
"conditionId": "0x80b3af88cb991980e8da1ce86b9794a0957f96ec98c29319dd7ba65e9744d82b",
"tokenId": "75028752776148090296091099469912621384650554615761384992997579209329182670110",
"price": 0.33,
"active": true
}
]
}
],
"count": 2
}
```
### Response Fields
| Field | Type | Description |
| ----------------------------------- | ------------ | ---------------------------------------------------------- |
| `query` | string | The search query |
| `events` | array | Matching events |
| `events[].id` | string | Event ID |
| `events[].slug` | string | URL slug |
| `events[].title` | string | Event title |
| `events[].image` | string\|null | Event image URL |
| `events[].active` | boolean | Whether event is active |
| `events[].markets` | array | All sub-markets in this event |
| `events[].markets[].question` | string | Full market question |
| `events[].markets[].groupItemTitle` | string | Short outcome label (e.g., "0 (0 bps)") |
| `events[].markets[].conditionId` | string | Condition ID for CLOB queries |
| `events[].markets[].tokenId` | string\|null | YES token ID for price history via `/v1/candles/{tokenId}` |
| `events[].markets[].price` | number\|null | Current YES price (0-1) |
| `events[].markets[].active` | boolean | Whether market is active |
| `count` | number | Number of events returned |
### SDK Usage
```typescript theme={null}
import { PolyNode } from 'polynode-sdk';
const pn = new PolyNode({ apiKey: 'pn_live_...' });
const results = await pn.searchEvents('Fed rate', { limit: 5 });
for (const event of results.events) {
console.log(event.title, `(${event.markets.length} outcomes)`);
for (const m of event.markets) {
console.log(` ${m.groupItemTitle || m.question}: ${m.price} — tokenId: ${m.tokenId}`);
}
}
```
### Notes
* The `tokenId` is the YES token for each market. Use it with `/v1/candles/{tokenId}` to fetch OHLCV price history.
* `groupItemTitle` is the short outcome label (e.g., "2 (50 bps)", "June Meeting"). Empty string for single-outcome events.
* Rate limited to 1 request per second per API key (shared with other enriched data endpoints).
* Results are cached for 3 minutes.
# Leaderboard
Source: https://docs.polynode.dev/api-reference/enriched/leaderboard
GET /v1/leaderboard
Top traders ranked by profit or volume.
Returns the top 20 Polymarket traders for the specified time period and ranking metric.
Time period. One of: `daily`, `weekly`, `monthly`, `all`.
Ranking metric. One of: `profit`, `volume`.
```json theme={null}
{
"traders": [
{
"rank": 1,
"wallet": "0xc2e7800b5af46e6093872b177b7a5e7f0563be51",
"name": "beachboy4",
"pnl": 4320443.26,
"volume": 12846475.79,
"profileImage": "https://..."
}
],
"period": "monthly",
"sort": "profit",
"count": 20
}
```
Data refreshes every 2-3 minutes. Rate limit: 1 request per second.
# Markets by Category
Source: https://docs.polynode.dev/api-reference/enriched/markets
GET /v2/markets/{category}
Browse markets by category with pagination and event counts.
Returns markets for a specific category, sorted by 24h volume. Supports pagination with `limit` and `offset`.
Category slug. One of:
| Category | Slug |
| ----------- | ------------- |
| Crypto | `crypto` |
| Politics | `politics` |
| Sports | `sports` |
| Finance | `finance` |
| Economy | `economy` |
| Tech | `tech` |
| Pop Culture | `pop-culture` |
| Geopolitics | `geopolitics` |
| Weather | `weather` |
| Iran | `iran` |
| Elections | `elections` |
| Mentions | `mentions` |
| Esports | `esports` |
Number of events to return. Min 1, max 100.
Pagination offset. Use with `limit` to page through results.
### Example
```bash theme={null}
curl "https://api.polynode.dev/v2/markets/crypto?limit=50&offset=0&key=YOUR_API_KEY"
```
```json theme={null}
{
"category": "crypto",
"counts": {
"all": 260,
"fiveM": 7,
"fifteenM": 7,
"pre-market": 112,
"etf": 2,
"hourly": 9,
"fourhour": 7,
"daily": 11,
"weekly": 64,
"monthly": 23,
"yearly": 22,
"bitcoin": 35,
"ethereum": 21,
"solana": 12,
"xrp": 11,
"dogecoin": 6,
"bnb": 6,
"microstrategy": 8
},
"events": [
{
"id": "273112",
"slug": "bitcoin-above-on-april-23",
"title": "Bitcoin above ___ on April 23?",
"image": "https://...",
"volume": 12345678,
"volume24hr": 5678901,
"liquidity": 1234567,
"openInterest": 890123,
"startDate": "2025-04-22T00:00:00Z",
"endDate": "2025-04-23T23:59:59Z",
"active": true,
"closed": false,
"new": false,
"featured": true,
"competitive": true,
"commentCount": 42,
"tags": ["Crypto", "Bitcoin"]
}
],
"limit": 50,
"offset": 0
}
```
### Response Fields
Each event object includes:
| Field | Type | Description |
| -------------- | --------- | -------------------------------- |
| `id` | string | Event ID |
| `slug` | string | URL slug |
| `title` | string | Event title |
| `image` | string | Event image URL |
| `volume` | number | Total lifetime volume |
| `volume24hr` | number | 24-hour trading volume |
| `liquidity` | number | Current liquidity |
| `openInterest` | number | Open interest |
| `startDate` | string | Event start date (ISO 8601) |
| `endDate` | string | Event end date (ISO 8601) |
| `active` | boolean | Whether the event is active |
| `closed` | boolean | Whether the event is closed |
| `new` | boolean | Whether the event is new |
| `featured` | boolean | Whether the event is featured |
| `competitive` | boolean | Whether the event is competitive |
| `commentCount` | number | Number of comments |
| `tags` | string\[] | Category tags |
### Pagination
Page through all markets in a category:
```python theme={null}
import requests
API_KEY = "pn_live_..."
category = "politics"
all_events = []
offset = 0
while True:
resp = requests.get(
f"https://api.polynode.dev/v2/markets/{category}",
params={"key": API_KEY, "limit": 100, "offset": offset},
).json()
events = resp.get("events", [])
if not events:
break
all_events.extend(events)
offset += len(events)
print(f"Total {category} events: {len(all_events)}")
```
The `counts` object varies by category. Crypto includes subcounts by asset and timeframe. Other categories return a simple `{"total": N}` count. Counts are cached and may not exactly match the paginated event total.
# Biggest Movers
Source: https://docs.polynode.dev/api-reference/enriched/movers
GET /v1/movers
Markets with the largest 24-hour price changes.
Returns markets that have experienced the biggest price swings in the last 24 hours. Useful for volatility alerts and "markets that moved" dashboards.
```json theme={null}
{
"markets": [
{
"id": "1437746",
"slug": "will-elon-musks-net-worth-be-between-670b-and-680b-on-march-31",
"question": "Will Elon Musk's net worth be between $670b and $680b on March 31?",
"image": "https://...",
"outcomePrices": ["0.026", "0.974"],
"oneDayPriceChange": -0.945
}
],
"count": 20
}
```
`oneDayPriceChange` is the absolute change in the Yes price over 24 hours. Negative means the Yes price dropped.
# Trader PnL Series
Source: https://docs.polynode.dev/api-reference/enriched/trader-pnl
GET /v1/trader/{wallet}/pnl
PnL time series for a trader at multiple resolutions.
Returns a time series of cumulative PnL values for a trader.
Ethereum wallet address.
Resolution. One of: `1D` (hourly points), `1W` (\~57 points), `1M` (\~41 points), `ALL` (\~223 points).
```json theme={null}
{
"wallet": "0xc2e7800b5af46e6093872b177b7a5e7f0563be51",
"period": "1W",
"series": [
{ "timestamp": 1773511200, "pnl": 1391835.20 },
{ "timestamp": 1773522000, "pnl": 3438215.00 },
{ "timestamp": 1773532800, "pnl": 3441273.50 }
],
"count": 57
}
```
# Trader Profile
Source: https://docs.polynode.dev/api-reference/enriched/trader-profile
GET /v1/trader/{wallet}
Full profile for any Polymarket trader.
Returns comprehensive stats for a trader: PnL, volume, trade count, largest win, current portfolio value, and account metadata. Data is sourced from Polymarket's own profile page.
For V3 wallet-level accounting, including exact all-time trader-paid fees, use [`GET /v3/wallets/{address}?include_accounting_summary=true`](/data/wallets/summary). The V3 accounting summary is sourced from PolyNode's indexed onchain fill data and does not require paging raw fee rows.
`totalPnl` is the net portfolio PnL that matches the number shown on a Polymarket profile page. This factors in realized gains, cost basis still deployed in open positions, and unrealized gains/losses. If you want total realized gains from closed positions instead, use the [onchain positions](/api-reference/wallets/onchain-positions) endpoint's `total_realized_pnl` field.
The response includes an `eoaWallet` field that resolves the underlying EOA (externally owned account) for Polymarket Safe proxy wallets. This is derived onchain via the Gnosis Safe `getOwners()` call. For older lightweight proxy wallets that don't support this method, `eoaWallet` returns `null`.
Polymarket wallet address (e.g., `0xc2e7800b5af46e6093872b177b7a5e7f0563be51`).
```json theme={null}
{
"wallet": "0xc2e7800b5af46e6093872b177b7a5e7f0563be51",
"eoaWallet": "0xb49e5499562a4bc3345c1a1f2db13a5360dfddac",
"name": "beachboy4",
"pseudonym": "Threadbare-Skunk",
"profileSlug": "@beachboy4",
"joinDate": "2025-11-30T17:26:07.997000Z",
"trades": 149,
"marketsTraded": 149,
"largestWin": 3487512.609533,
"views": 111735,
"totalVolume": 199137316.6,
"totalPnl": 4142102.07,
"realizedPnl": 0,
"unrealizedPnl": 0,
"positionValue": 4418.03,
"profileImage": null
}
```
# Trending
Source: https://docs.polynode.dev/api-reference/enriched/trending
GET /v1/trending
Trending markets, hot topics, featured events, and biggest movers.
Returns a curated snapshot of what's trending on Polymarket right now. Combines carousel highlights, breaking news markets, hot search topics, featured events, and biggest 24h price movers.
```json theme={null}
{
"carousel": [
{
"id": "69987",
"slug": "2026-ncaa-tournament-winner",
"title": "2026 NCAA Tournament Winner",
"image": "https://...",
"volume24hr": 1234567
}
],
"breaking": [
{
"id": "1193228",
"slug": "will-robert-golob-be-the-next-prime-minister-of-slovenia",
"question": "Will Robert Golob be the next Prime Minister of Slovenia?",
"outcomePrices": ["0.595", "0.405"],
"oneDayPriceChange": -0.015
}
],
"hotTopics": [
{ "title": "Gen", "volume": 24471729.10, "url": "/search?_q=gen" },
{ "title": "Bayern", "volume": 4733154.92, "url": "/predictions/bayern" }
],
"featured": [],
"movers": []
}
```
# Candles (Trade-Indexed OHLCV)
Source: https://docs.polynode.dev/api-reference/onchain/candles
GET /v2/onchain/candles/{token_id}
Server-built OHLCV candles from real onchain trades. Anchor-based pagination with buy/sell volume split, VWAP, and trade counts. Resolutions from 1m to 1d.
Build OHLCV candles directly from settled CLOB fills. Each candle includes open, high, low, close, total volume in USD and shares, buy and sell volume split, trade count, and VWAP. Backed by the same trade source as `/v2/onchain/trades`, so candles and trades stay in lockstep.
Pagination is **anchor-based** rather than range-based. Each request returns up to 1000 trades worth of candles, anchored at a timestamp, block, or transaction hash. Walk older history by passing the cursor from the previous response. This is the same model used by major exchange APIs (Binance, Kraken, Coinbase) and avoids open-ended range queries timing out on hot markets.
The response includes a `window.duration_seconds` field so callers immediately see how dense the market is — a hot market may pack 500 trades into 10 seconds, while a sleepy market may span weeks.
## Which candles endpoint should I use?
PolyNode exposes three candle-shaped endpoints. They cover different data sources and are not interchangeable.
| Endpoint | Source | Best for |
| ------------------------------------ | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `GET /v2/onchain/candles/{token_id}` | Real settled CLOB fills on Polygon | Charting any Polymarket outcome token with full history, VWAP, and buy/sell split. **This is the endpoint you want for a price chart.** |
| `GET /v1/candles/{token_id}` | Live rolling in-memory buffer | Short, live tail of recent activity. No historical depth, no pagination. Useful as a lightweight live poll but not for charts. |
| `GET /v1/crypto/candles` | Chainlink oracle feed | 5-minute OHLC for crypto assets (BTC, ETH, SOL…). These are the same prices that resolve Polymarket's short-form crypto markets — not CLOB trade data. |
If you're building a chart for a Polymarket market, use `/v2/onchain/candles`.
## Build a chart in 2 calls
The fastest way to go from "I have an API key" to "I have candles on screen." Every response below is a real capture — you can paste the curls and get back the same shape.
### 1. Find a market and grab the token you want to chart
`/v2/movers` returns the biggest daily movers with the full outcomes array already enriched — one REST call gives you the `token_id` for both the Yes and No outcome without any extra lookups.
```bash theme={null}
curl "https://api.polynode.dev/v2/movers?limit=3" \
-H "x-api-key: YOUR_KEY"
```
```json theme={null}
{
"markets": [
{
"id": "1707841",
"slug": "israel-x-hezbollah-ceasefire-by-april-30-2026-989-656",
"question": "Israel x Hezbollah ceasefire by April 30, 2026?",
"condition_id": "0xc7140ddb5ae5dc94d4553fb05d4600816f33ff024844cebe8326f4c41c4a1a47",
"outcomes": [
{ "name": "Yes", "token_id": "71076253073516159380702286801576688253388973161726933428722204989810362065275", "price": 0.674 },
{ "name": "No", "token_id": "38902668316823899581329108924389881286009857048696806385615295625967267371713", "price": 0.326 }
],
"one_day_price_change": 0.3815
}
]
}
```
`outcomes[0].token_id` is the Yes side. That's the identifier you pass to the candles endpoint.
`/v2/trending` has the exact same shape if you'd rather chart what's popular than what's moving.
### 2. Pull candles for that token
```bash theme={null}
TOKEN=71076253073516159380702286801576688253388973161726933428722204989810362065275
curl "https://api.polynode.dev/v2/onchain/candles/$TOKEN?resolution=5m&limit=500" \
-H "x-api-key: YOUR_KEY"
```
```json theme={null}
{
"token_id": "71076253073516159380702286801576688253388973161726933428722204989810362065275",
"resolution": "5m",
"window": {
"start_ts": 1776274178,
"end_ts": 1776278436,
"trade_count": 500,
"duration_seconds": 4258
},
"count": 16,
"candles": [
{ "time": 1776273900000, "open": 0.82, "high": 0.8221, "low": 0.82, "close": 0.82, "volume": 1842.3, "trades": 16, "vwap": 0.8205 },
{ "time": 1776274200000, "open": 0.831, "high": 0.8349, "low": 0.765, "close": 0.781, "volume": 6194.1, "trades": 49, "vwap": 0.7912 },
{ "time": 1776276600000, "open": 0.741, "high": 0.766, "low": 0.62, "close": 0.62, "volume": 9338.7, "trades": 68, "vwap": 0.6823 }
],
"pagination": { "older_end_ts": 1776274177, "newer_start_ts": 1776278437 },
"question": "Israel x Hezbollah ceasefire by April 30, 2026?",
"outcome": "Yes",
"condition_id": "0xc7140ddb5ae5dc94d4553fb05d4600816f33ff024844cebe8326f4c41c4a1a47",
"image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/israel+lebanon+dove+flags.png"
}
```
The `window.duration_seconds` tells you the 500 trades covered \~71 minutes of wall time — a moderately active market. The header fields (`question`, `outcome`, `image`, `condition_id`) are included so a chart header can render in the same round trip.
That's the full loop: discover → chart, two REST calls, everything you need.
### 3. Walk older history
Each response returns `pagination.older_end_ts`. Pass it back as `anchor_ts` on the next call to fetch the window immediately before it.
```bash theme={null}
# Older page, using the anchor from the previous response
curl "https://api.polynode.dev/v2/onchain/candles/$TOKEN?resolution=5m&limit=500&anchor_ts=1776274177" \
-H "x-api-key: YOUR_KEY"
```
Real response header from this exact call:
```json theme={null}
{
"window": { "start_ts": 1776267442, "end_ts": 1776274174, "trade_count": 500, "duration_seconds": 6732 },
"count": 23,
"pagination": { "older_end_ts": 1776267441, "newer_start_ts": 1776274175 }
}
```
Same token, 500 older trades, 23 candles over \~112 minutes. Keep walking by chaining `older_end_ts → anchor_ts` until you have the depth you need.
## Request
```
GET /v2/onchain/candles/{token_id}
```
You can also pass the market identifier as a query parameter instead of a path parameter:
```
GET /v2/onchain/candles?token_id=...
```
### Path parameters
| Parameter | Type | Required | Description |
| ---------- | ------ | ------------ | ----------------------------------------------------------------- |
| `token_id` | string | One of these | Outcome token ID. Can also be passed as `?token_id=` query param. |
### Query parameters
| Parameter | Type | Default | Description |
| -------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `token_id` | string | — | Outcome token ID (alternative to path param) |
| `condition_id` | string | — | Condition ID. Errors if the condition has multiple outcomes — pass `token_id` to disambiguate. |
| `market_slug` | string | — | Market slug. Errors if the slug has multiple outcomes — pass `token_id` to disambiguate. |
| `resolution` | string | `1h` | Bucket size: `1m`, `5m`, `15m`, `1h`, `4h`, `1d` |
| `limit` | integer | `500` | Trades per page. Clamped to `[100, 1000]`. The candles are built from this trade window. |
| `direction` | string | `before` | `before` returns the `limit` trades ending at the anchor (newest-first walk). `after` returns the `limit` trades starting at the anchor (oldest-first walk). |
| `anchor_ts` | integer | now | Unix timestamp anchor |
| `anchor_block` | integer | — | Polygon block number anchor. Resolved to its block timestamp. |
| `anchor_tx` | string | — | Polygon transaction hash anchor. Resolved to its block timestamp. |
| `gap_fill` | boolean | `false` | When `true`, empty buckets between active candles are filled with flat carry-forward candles (`O=H=L=C=prev_close`, `volume=0`). |
Exactly one of `token_id`, `condition_id`, or `market_slug` is required. If multiple anchor params are passed, precedence is `anchor_tx > anchor_block > anchor_ts`.
## Response
```json theme={null}
{
"token_id": "85713379202339219190689591569895900631137520291992037720582155738835687752247",
"resolution": "1m",
"window": {
"start_ts": 1776276218,
"end_ts": 1776276228,
"trade_count": 500,
"duration_seconds": 10
},
"count": 1,
"candles": [
{
"time": 1776276180000,
"open": 0.78,
"high": 0.93,
"low": 0.77,
"close": 0.9,
"volume": 7941.87,
"volume_shares": 9094.21,
"volume_buy": 1569.20,
"volume_sell": 6372.67,
"trades": 500,
"vwap": 0.8733
}
],
"pagination": {
"older_end_ts": 1776276217,
"newer_start_ts": 1776276229
},
"question": "Bitcoin Up or Down - April 15, 2:00PM-2:05PM ET",
"slug": "btc-updown-5m-1776276000",
"outcome": "Up",
"condition_id": "0x...",
"image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/BTC+fullsize.png"
}
```
### Candle fields
| Field | Type | Description |
| --------------- | ------ | -------------------------------------------------------------------------- |
| `time` | number | Bucket start time in milliseconds since epoch (TradingView convention) |
| `open` | number | First trade price in the bucket |
| `high` | number | Highest trade price in the bucket |
| `low` | number | Lowest trade price in the bucket |
| `close` | number | Last trade price in the bucket |
| `volume` | number | Total volume in USD |
| `volume_shares` | number | Total volume in outcome shares |
| `volume_buy` | number | USD volume where the taker was buying the outcome token (aggressor buys) |
| `volume_sell` | number | USD volume where the taker was selling the outcome token (aggressor sells) |
| `trades` | number | Count of fills in the bucket |
| `vwap` | number | Volume-weighted average price for the bucket |
### Window fields
| Field | Type | Description |
| ------------------------- | ------ | ---------------------------------------------------------------- |
| `window.start_ts` | number | Unix timestamp of the oldest trade in the window |
| `window.end_ts` | number | Unix timestamp of the newest trade in the window |
| `window.trade_count` | number | Number of trades that built these candles |
| `window.duration_seconds` | number | Wall-clock span of the window. Use this to gauge market density. |
### Pagination fields
| Field | Type | Description |
| --------------------------- | ------ | -------------------------------------------------------------------------- |
| `pagination.older_end_ts` | number | Pass as `anchor_ts` with `direction=before` to fetch the next older window |
| `pagination.newer_start_ts` | number | Pass as `anchor_ts` with `direction=after` to fetch the next newer window |
### Market enrichment fields
`question`, `slug`, `outcome`, `condition_id`, and `image` are pulled from our market index. Returned alongside the candles so a chart can render header metadata in one round trip.
## Examples
### Default — last 500 trades, 1h buckets
```bash cURL theme={null}
curl "https://api.polynode.dev/v2/onchain/candles/85713379202339219190689591569895900631137520291992037720582155738835687752247?resolution=1h" \
-H "x-api-key: YOUR_KEY"
```
```javascript Node.js theme={null}
const tokenId = "85713379202339219190689591569895900631137520291992037720582155738835687752247";
const resp = await fetch(
`https://api.polynode.dev/v2/onchain/candles/${tokenId}?resolution=1h`,
{ headers: { "x-api-key": "YOUR_KEY" } }
);
const data = await resp.json();
console.log(`${data.window.trade_count} trades over ${data.window.duration_seconds}s`);
data.candles.forEach(c => console.log(c.time, c.open, c.high, c.low, c.close, c.volume));
```
```python Python theme={null}
import requests
token_id = "85713379202339219190689591569895900631137520291992037720582155738835687752247"
resp = requests.get(
f"https://api.polynode.dev/v2/onchain/candles/{token_id}",
params={"resolution": "1h"},
headers={"x-api-key": "YOUR_KEY"},
)
data = resp.json()
print(f"{data['window']['trade_count']} trades over {data['window']['duration_seconds']}s")
for c in data["candles"]:
print(c["time"], c["open"], c["high"], c["low"], c["close"], c["volume"])
```
### Walking backwards through history
Each response includes `pagination.older_end_ts`. Pass that as `anchor_ts` to fetch the previous window.
```bash theme={null}
# First page — most recent 500 trades
curl "https://api.polynode.dev/v2/onchain/candles/$TOKEN_ID?resolution=15m&limit=500" \
-H "x-api-key: YOUR_KEY"
# Next page — 500 trades older than the first
curl "https://api.polynode.dev/v2/onchain/candles/$TOKEN_ID?resolution=15m&limit=500&anchor_ts=OLDER_END_TS_FROM_PREVIOUS" \
-H "x-api-key: YOUR_KEY"
```
### Forward walk from a starting point
Use `direction=after` to walk forward from a specific timestamp.
```bash theme={null}
# Earliest 500 trades on this market
curl "https://api.polynode.dev/v2/onchain/candles/$TOKEN_ID?resolution=1h&direction=after&anchor_ts=0" \
-H "x-api-key: YOUR_KEY"
```
### Anchor by block number
Useful when you want to align candles to a specific Polygon block — for example to compare against another onchain event.
```bash theme={null}
curl "https://api.polynode.dev/v2/onchain/candles/$TOKEN_ID?resolution=5m&anchor_block=70000000" \
-H "x-api-key: YOUR_KEY"
```
### Anchor by transaction hash
Same idea, but resolved from a transaction hash. Useful for "show me what the chart looked like around this trade."
```bash theme={null}
curl "https://api.polynode.dev/v2/onchain/candles/$TOKEN_ID?resolution=1m&anchor_tx=0xfffff7ed9080f46d7975f905e7f9358c436a1b177c6e19e54510c3ba6dcfddc4" \
-H "x-api-key: YOUR_KEY"
```
### Gap-filled candles
By default, buckets with zero trades are simply absent from the response. Set `gap_fill=true` to fill them with flat carry-forward candles for charting libraries that expect a continuous time axis.
```bash theme={null}
curl "https://api.polynode.dev/v2/onchain/candles/$TOKEN_ID?resolution=1m&gap_fill=true" \
-H "x-api-key: YOUR_KEY"
```
## Error responses
| Status | Response | Condition |
| ------ | --------------------------------------------------------------------------------------- | ---------------------------- |
| 400 | `{"error": "token_id, condition_id, or market_slug required"}` | No market identifier passed |
| 400 | `{"error": "Invalid resolution. Use one of: 1m, 5m, 15m, 1h, 4h, 1d"}` | Bad `resolution` value |
| 400 | `{"error": "condition_id resolves to multiple outcomes — pass token_id to select one"}` | Multi-outcome `condition_id` |
| 400 | `{"error": "market_slug resolves to multiple outcomes — pass token_id to select one"}` | Multi-outcome `market_slug` |
| 401 | `{"error": "API key required. Pass via ?key= or x-api-key header."}` | Missing API key |
| 403 | `{"error": "V2 endpoints require a paid plan. See polynode.dev/pricing for details."}` | Free tier key |
| 429 | `{"error": "Rate limited. N req/s for your tier.", "retryAfterMs": ...}` | Rate limited |
## Notes
* Candles are built from real onchain fills, not midpoint snapshots. Sparse markets will show sparse candles.
* `volume_buy` and `volume_sell` are taker-side attributions — i.e. the side that lifted/hit the book. Same convention as every major exchange API.
* The trade window is fixed-page, not range-bound. To cover a long history, walk pages via `pagination.older_end_ts`. Use `window.duration_seconds` in each response to gauge market density up front.
* Block and transaction anchors are resolved via Polygon RPC and cached for 24 hours — repeated queries against the same anchor are free after the first hit.
* Responses are cached server-side for 5 minutes per unique anchor, direction, and limit.
# Global Open Interest
Source: https://docs.polynode.dev/api-reference/onchain/global-oi
GET /v2/onchain/oi
Total platform-wide open interest from onchain data.
Returns the total open interest across the entire Polymarket platform, sourced from onchain data.
## Request
```
GET /v2/onchain/oi
```
No parameters required.
## Response
```json theme={null}
{
"source": "onchain",
"open_interest_usdc": 488484168.103811
}
```
| Field | Type | Description |
| -------------------- | ------ | ------------------------------------ |
| `open_interest_usdc` | number | Total platform open interest in USDC |
## Example
```bash theme={null}
curl "https://api.polynode.dev/v2/onchain/oi" \
-H "x-api-key: YOUR_KEY"
```
# Market Open Interest
Source: https://docs.polynode.dev/api-reference/onchain/market-oi
GET /v2/onchain/markets/{condition_id}/oi
Onchain open interest for a specific market.
Returns the current open interest for a specific market, sourced from onchain data. Enriched with market metadata.
## Request
```
GET /v2/onchain/markets/{condition_id}/oi
```
| Parameter | Type | Location | Description |
| -------------- | ------ | -------- | ----------------------------------------------- |
| `condition_id` | string | path | Market condition ID (0x-prefixed, 64 hex chars) |
## Response
```json theme={null}
{
"condition_id": "0x8180fee481707671aa45de84a9c3740ab607f3f2643b456e0380afb02d12cf7d",
"source": "onchain",
"found": true,
"market": "Bitcoin Up or Down - March 28, 10:55AM-11:00AM ET",
"slug": "btc-updown-5m-1774709700",
"image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/BTC+fullsize.png",
"open_interest_usdc": 393.153937
}
```
| Field | Type | Description |
| -------------------- | ------- | ------------------------------------ |
| `found` | boolean | Whether the market was found onchain |
| `market` | string | Market question |
| `slug` | string | Market slug |
| `image` | string | Market image URL |
| `open_interest_usdc` | number | Current open interest in USDC |
## Example
```bash theme={null}
curl "https://api.polynode.dev/v2/onchain/markets/0x8180fee481707671aa45de84a9c3740ab607f3f2643b456e0380afb02d12cf7d/oi" \
-H "x-api-key: YOUR_KEY"
```
# Trade History (Market)
Source: https://docs.polynode.dev/api-reference/onchain/market-trades
GET /v2/onchain/markets/{token_id}/trades
Complete onchain trade history for any market token.
Returns every onchain trade fill for a specific market token, enriched with market metadata.
**Want the entire market in one call?** Use [Trade History (Market) — Bulk](/api-reference/onchain/market-trades-all) (Growth plan and above). Returns up to 250K trades in a single response, no pagination.
## Quick start — get the most recent 100 trades
```bash theme={null}
curl "https://api.polynode.dev/v2/onchain/markets/{token_id}/trades?limit=100" \
-H "x-api-key: YOUR_KEY"
```
Default `limit=100`, max `limit=1000`. That's all most apps need.
## Get every trade in the market (full history)
To walk the entire history, add `?cursor=` (empty value) to the first request, then **for each next page, copy `pagination.cursor` from the response back into the URL**. Stop when `pagination.has_more` is `false`.
Three complete copy-paste scripts that walk an entire market:
```python Python theme={null}
import requests
API_KEY = "YOUR_KEY"
TOKEN_ID = "21743669032210695168079601505378236205866986767926346409604806906483294819314"
def get_all_trades(token_id):
cursor = "" # empty string = first page
all_trades = []
while True:
r = requests.get(
f"https://api.polynode.dev/v2/onchain/markets/{token_id}/trades",
params={"limit": 1000, "cursor": cursor},
headers={"x-api-key": API_KEY},
).json()
all_trades.extend(r["trades"])
if not r["pagination"]["has_more"]:
break
cursor = r["pagination"]["cursor"] # feed this into the next request
return all_trades
trades = get_all_trades(TOKEN_ID)
print(f"got {len(trades)} trades")
```
```javascript JavaScript theme={null}
const API_KEY = "YOUR_KEY";
const TOKEN_ID = "21743669032210695168079601505378236205866986767926346409604806906483294819314";
async function getAllTrades(tokenId) {
let cursor = ""; // empty string = first page
const allTrades = [];
while (true) {
const url = `https://api.polynode.dev/v2/onchain/markets/${tokenId}/trades`
+ `?limit=1000&cursor=${encodeURIComponent(cursor)}`;
const r = await fetch(url, { headers: { "x-api-key": API_KEY } }).then(r => r.json());
allTrades.push(...r.trades);
if (!r.pagination.has_more) break;
cursor = r.pagination.cursor; // feed this into the next request
}
return allTrades;
}
const trades = await getAllTrades(TOKEN_ID);
console.log(`got ${trades.length} trades`);
```
```bash bash theme={null}
KEY="YOUR_KEY"
TOKEN_ID="21743669032210695168079601505378236205866986767926346409604806906483294819314"
CURSOR=""
TOTAL=0
while true; do
RESP=$(curl -s "https://api.polynode.dev/v2/onchain/markets/$TOKEN_ID/trades?limit=1000&cursor=$CURSOR" \
-H "x-api-key: $KEY")
COUNT=$(echo "$RESP" | python3 -c "import json,sys;print(json.load(sys.stdin)['count'])")
HAS_MORE=$(echo "$RESP" | python3 -c "import json,sys;print(json.load(sys.stdin)['pagination']['has_more'])")
TOTAL=$((TOTAL + COUNT))
echo "got $COUNT trades, total: $TOTAL"
[ "$HAS_MORE" = "False" ] && break
CURSOR=$(echo "$RESP" | python3 -c "import json,sys;print(json.load(sys.stdin)['pagination']['cursor'])")
done
```
**Real run** (heavy market, walked live with the Python script above):
```
got 1000 trades (total: 1000)
got 1000 trades (total: 2000)
got 1000 trades (total: 3000)
...
got 1000 trades (total: 20000)
DONE. 20,000 trades in 24 seconds.
```
Each page takes \~1 second regardless of how deep you go — the loop just keeps running until `has_more` is `false`.
## Request
```
GET /v2/onchain/markets/{token_id}/trades?limit=100
GET /v2/onchain/markets/{token_id}/trades?limit=1000&cursor=
```
| Parameter | Type | Location | Description |
| ---------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `token_id` | string | path | CTF token ID (numeric string) |
| `limit` | integer | query | Max results per page (default 100, max 1000) |
| `cursor` | string | query | Pass empty string `?cursor=` for first page, then echo back `pagination.cursor` from each response. Required to walk past the first page reliably on large markets. |
| `offset` | integer | query | Alternative to cursor: skip first N. Fast for the first \~25K results then degrades. Use `cursor` for anything bigger. |
## Response
```json theme={null}
{
"token_id": "110959653450933276250915064669875552310439627880508793089816880777942697720191",
"source": "onchain",
"count": 3,
"market": "US x Iran ceasefire extended by April 22, 2026?",
"slug": "us-x-iran-ceasefire-extended-by-april-22-2026",
"outcome": "No",
"image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/us-x-iran-ceasefire-by-Cgmx3GCuOwjs.jpg",
"condition_id": "0x1d2787cb8aed975d092b2799ed6f4083e9445f7420cdc09e9d47e7d54356c6cd",
"pagination": {
"limit": 3,
"has_more": true,
"cursor": "1777595648:0xeae6ca0aeb75fc866ddb29d3a4b2cd15382895b923fe05b9315445d6346d1c47_0x49ddd1e169186d350488e3d62f58452fe76862c5f81f33987e659759e2c66100"
},
"trades": [
{
"tx_hash": "0x044dc412fa97d972fae1e77f8a30e769b7bea339cc3a27b5045e5668b513a520",
"order_hash": "0x6b3d58dcf19f26a0b2f325bc43dc793765d1abeedfc449a3c5be38b6523613c9",
"timestamp": 1777595650,
"maker": "0x84571f1bf97a5c710cbe51daff2dd4556cc887fd",
"taker": "0xe111180000d2663c0091e4f400237545b87b996b",
"maker_asset_id": "0",
"taker_asset_id": "110959653450933276250915064669875552310439627880508793089816880777942697720191",
"maker_amount": 617.382,
"taker_amount": 618,
"fee": 0,
"direction": "BUY",
"side": "maker"
}
]
}
```
| Field | Type | Description |
| ------------------------- | ------- | ------------------------------------------------------------- |
| `count` | integer | Number of trades in this response |
| `pagination.has_more` | boolean | `true` if more pages available — keep paginating |
| `pagination.cursor` | string | Pass this back as `?cursor=` for the next page |
| `market` | string | Market question |
| `slug` | string | Market slug |
| `outcome` | string | Outcome label for this token |
| `image` | string | Market image URL |
| `condition_id` | string | Market condition ID |
| `trades[].tx_hash` | string | Transaction hash |
| `trades[].order_hash` | string | Order hash that was filled |
| `trades[].timestamp` | number | Unix timestamp |
| `trades[].maker` | string | Maker wallet |
| `trades[].taker` | string | Taker wallet |
| `trades[].maker_asset_id` | string | Asset the maker provided (`"0"` = USDC) |
| `trades[].taker_asset_id` | string | Asset the taker provided |
| `trades[].maker_amount` | number | Amount maker provided |
| `trades[].taker_amount` | number | Amount taker provided |
| `trades[].fee` | number | Fee (USDC) |
| `trades[].direction` | string | `"BUY"` or `"SELL"` from the buyer's perspective on this fill |
| `trades[].side` | string | Always `"maker"` for this endpoint |
# Trade History (Market) — Bulk
Source: https://docs.polynode.dev/api-reference/onchain/market-trades-all
GET /v2/onchain/markets/{token_id}/trades/all
Return every trade for a market token in a single response. Growth plan or above.
**Growth plan or above only.** This endpoint returns the **entire** trade history for a market token in **one response** — no client-side pagination, no offset bookkeeping. Because a single call can return tens of thousands of fills (and tens of MB of JSON), it's gated to the **Growth plan and above**. Starter and free tier requests receive `403`.
* Hard cap: **250,000 trades per call** — beyond that, response includes `"partial": true` with `"partial_reason": "hard_cap_250000"`
* Wall-clock budget: **180 seconds** — first cold-cache call on a busy market may take 1-3 minutes
* Cache: full results cached **5 minutes**. Repeat calls return in under 1 second
* Typical payload: 1-50 MB. Worst case (capped): 100-200 MB JSON
Returns every onchain trade fill for one market token in a single response. The server walks the full trade history with parallel time-bucketed queries server-side — you don't need to paginate. Each trade is enriched with market metadata.
This is the right endpoint when you want **bulk-export** a market for analysis, indexing, or backtesting. For interactive UIs that just need the most recent N fills, use the standard [Market Trade History](/api-reference/onchain/market-trades) instead.
## Request
```
GET /v2/onchain/markets/{token_id}/trades/all?from=&to=
```
| Parameter | Type | Location | Description |
| ---------- | ------- | -------- | --------------------------------------------------------------------------------------------- |
| `token_id` | string | path | CTF token ID (numeric string) |
| `from` | integer | query | Optional start of window in unix seconds. Default: 2024-01-01 (covers all Polymarket history) |
| `to` | integer | query | Optional end of window in unix seconds. Default: now |
## Response
```json theme={null}
{
"token_id": "21743669032210695168079601505378236205866986767926346409604806906483294819314",
"from_ts": 1774588800,
"to_ts": 1775817600,
"count": 27601,
"partial": false,
"partial_reason": null,
"fetched_in_ms": 5437,
"cache": "miss",
"trades": [
{
"tx_hash": "0xa73ccafda4acf5473d65ce9b6ed9f9e41861ea2ae0b5c0a8bb5cfd3869583fdf",
"order_hash": "0x3e5baca0ae04010bfe01ec0a0a3fe8b6e291ca8373bb4803f31620754f83a17d",
"timestamp": 1775817557,
"maker": "0x4ef7e2e97735a144c35c04bf22bcac184d3221be",
"taker": "0x4bfb41d5b3570defd03c39a9a4d8de6bd8b8982e",
"token_id": "21743669032210695168079601505378236205866986767926346409604806906483294819314",
"direction": "SELL",
"price": 0.545,
"shares": 1827.47,
"usd": 995.99,
"fee": 0
}
]
}
```
## Response fields
| Field | Type | Description |
| ------------------- | -------------- | ---------------------------------------------------------------------------------------------------------- |
| `token_id` | string | The CTF token ID requested |
| `from_ts` / `to_ts` | integer | Resolved time window in unix seconds |
| `count` | integer | Total trades returned (≤ 250,000) |
| `partial` | boolean | `true` if either the 250K hard cap or the 180s wall-clock budget was hit before the full window was walked |
| `partial_reason` | string \| null | `"hard_cap_250000"` or `"wall_clock_180s"` when partial; otherwise `null` |
| `fetched_in_ms` | integer | Server-side fetch time. Cached calls show the original miss time |
| `cache` | string | `"hit"` if served from Redis cache, `"miss"` if freshly walked |
| `trades` | array | All trades, sorted by `timestamp` descending (newest first) |
## Trade fields
| Field | Type | Description |
| ------------ | ------- | ------------------------------------------------ |
| `tx_hash` | string | Polygon transaction hash |
| `order_hash` | string | Order hash from the exchange |
| `timestamp` | integer | Unix seconds when the fill settled |
| `maker` | string | Maker address |
| `taker` | string | Taker address |
| `token_id` | string | The outcome token id (echoes the request param) |
| `direction` | string | `"BUY"` or `"SELL"` from the maker's perspective |
| `price` | number | USDC per share, 4-decimal precision |
| `shares` | number | Outcome tokens traded |
| `usd` | number | USDC notional |
| `fee` | number | Maker fee paid (USDC) |
## Example: full lifetime of a market
```bash theme={null}
curl "https://api.polynode.dev/v2/onchain/markets/21743669032210695168079601505378236205866986767926346409604806906483294819314/trades/all?key=$YOUR_KEY"
```
## Example: window inside a longer-running market
```bash theme={null}
curl "https://api.polynode.dev/v2/onchain/markets/$TOKEN/trades/all?from=1774588800&to=1775817600&key=$YOUR_KEY"
```
## Errors
`403 Tier required`:
```json theme={null}
{ "error": "/trades/all requires the Growth plan or above. Your tier: starter. See polynode.dev/pricing." }
```
`401 No API key`:
```json theme={null}
{ "error": "API key required. Pass via ?key= or x-api-key header." }
```
## Notes
* For a multi-token market (binary YES/NO), call this endpoint **twice** — once per `token_id`. A `condition_id` variant that returns both tokens in one call is on the roadmap.
* `partial: true` results are **not cached**. Subsequent calls re-attempt the full walk. Tighten the time window with `from`/`to` if you keep hitting the cap.
* Cache key is `(token_id, from_ts, to_ts)`. Different windows are cached independently. Default-window calls (no `from`/`to`) share a cache slot.
* The walk is parallelized — total bandwidth use is moderate even for huge markets.
# Market Volume
Source: https://docs.polynode.dev/api-reference/onchain/market-volume
GET /v2/onchain/markets/{token_id}/volume
Lifetime onchain volume statistics for any market token.
Returns lifetime trading volume statistics for a specific market token, sourced directly from onchain settlement data. Enriched with market metadata.
## Request
```
GET /v2/onchain/markets/{token_id}/volume
```
| Parameter | Type | Location | Description |
| ---------- | ------ | -------- | ----------------------------- |
| `token_id` | string | path | CTF token ID (numeric string) |
## Response
```json theme={null}
{
"token_id": "21912724974096796009916816278814088615574660931588091764221331842149572809887",
"source": "onchain",
"found": true,
"market": "Bitcoin Up or Down - March 28, 9:55PM-10:00PM ET",
"slug": "btc-updown-5m-1774749300",
"outcome": "Up",
"image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/BTC+fullsize.png",
"condition_id": "0xb91dbc2ba7f0d33c71fca765909e98b2023af855f1d69b82d5fd59732085700d",
"total_trades": 2886,
"buys": 2609,
"sells": 277,
"volume_usdc": 31054.394523,
"buy_volume_usdc": 29482.48427,
"sell_volume_usdc": 1571.910253
}
```
| Field | Type | Description |
| ------------------ | ------- | ----------------------------------- |
| `found` | boolean | Whether the token was found onchain |
| `market` | string | Market question |
| `slug` | string | Market slug |
| `outcome` | string | Outcome label for this token |
| `image` | string | Market image URL |
| `condition_id` | string | Market condition ID |
| `total_trades` | number | Lifetime number of fills |
| `buys` | number | Number of buy fills |
| `sells` | number | Number of sell fills |
| `volume_usdc` | number | Total volume in USDC |
| `buy_volume_usdc` | number | Buy-side volume in USDC |
| `sell_volume_usdc` | number | Sell-side volume in USDC |
## Example
```bash theme={null}
curl "https://api.polynode.dev/v2/onchain/markets/21912724974096796009916816278814088615574660931588091764221331842149572809887/volume" \
-H "x-api-key: YOUR_KEY"
```
# Positions (All)
Source: https://docs.polynode.dev/api-reference/onchain/positions
GET /v2/onchain/positions
Query every position on Polymarket. Filter by wallet, market, status, or minimum size. Full history with cursor pagination.
Search and filter all Polymarket positions across every wallet. Each position is enriched with market metadata and includes realized P\&L, average entry price, and current size. Sourced directly from onchain settlement data.
## Request
```
GET /v2/onchain/positions
```
### Query parameters
| Parameter | Type | Required | Description |
| ---------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `wallet` | string | No | Filter by wallet address |
| `market_slug` | string | No | Filter by market slug (e.g. `will-zohran-mamdani-win-the-2025-nyc-mayoral-election`) |
| `condition_id` | string | No | Filter by condition ID (0x-prefixed, 64 hex chars) |
| `token_id` | string | No | Filter by outcome token ID |
| `status` | string | No | `open` (size > 0), `closed` (size = 0), or `all` (default) |
| `min_size` | number | No | Minimum position size in shares |
| `limit` | integer | No | Results per page (1-500, default 100) |
| `order` | string | No | Sort direction: `desc` (default, most recent first) or `asc`. When `wallet` is set, results are ordered by the wallet's most recent on-chain activity per position. Otherwise, ordered by position ID. |
| `pagination_key` | string | No | Cursor from a previous response to fetch the next page. Not used for wallet queries (see Pagination). |
### Identifying markets
* **`market_slug`** — human-readable URL slug from Polymarket. Returns positions for all outcomes.
* **`condition_id`** — unique condition identifier. Returns positions for all outcomes.
* **`token_id`** — specific outcome token. Returns positions for only that outcome.
## Response
```json theme={null}
{
"count": 2,
"pagination": {
"limit": 2,
"has_more": true,
"pagination_key": "0xa973ae12882a1c24a75da7a3b52cb01500b2f639-99881503238784655670984673100655147508393351942279611133016622634826369070119"
},
"positions": [
{
"wallet": "0xa973ae12882a1c24a75da7a3b52cb01500b2f639",
"token_id": "99949662063403569895321097192043694236016212986254553767097648841549016857591",
"size": 0.012785,
"avg_price": 0.859999,
"realized_pnl": 0.16,
"unrealized_pnl": 0.02,
"current_price": 0.88,
"market_status": "live",
"total_bought": 1.16,
"market": "Will FC Bayern Munchen win on 2026-04-07?",
"market_slug": "ucl-rma1-bay1-2026-04-07-bay1",
"outcome": "Yes",
"condition_id": "0x5f5c8d8fa28d77b5552562f32393ac199fb6b92ed1c1e2239e29c09f7e4eb3f5",
"image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/champions-league-pic-QIUFsL8vaDdq.png"
},
{
"wallet": "0xa973ae12882a1c24a75da7a3b52cb01500b2f639",
"token_id": "99881503238784655670984673100655147508393351942279611133016622634826369070119",
"size": 0,
"avg_price": 0.98,
"realized_pnl": 0.1,
"unrealized_pnl": 0,
"current_price": null,
"market_status": "closed",
"total_bought": 5,
"market": "Solana Up or Down - December 30, 10:30PM-10:45PM ET",
"market_slug": "sol-updown-15m-1767151800",
"outcome": "Down",
"condition_id": "0xa7f50f1bd65e8fd1790117b3b162dc03489df7b89cd35f4b1093792691b8a327",
"image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/SOL+fullsize.png"
}
]
}
```
### Position fields
| Field | Type | Description |
| ----------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `wallet` | string | Wallet address holding the position |
| `token_id` | string | Outcome token ID |
| `size` | number | Current position size in shares. `0` means the position is fully exited. |
| `avg_price` | number | Average entry price (0 to 1) |
| `realized_pnl` | number | Realized profit/loss in USD. Reflects gains/losses from closed portions of the position and from onchain redemptions of resolved markets. Stays `0` for resolved-but-not-yet-redeemed positions; check `redeemable` to detect those. |
| `unrealized_pnl` | number | Unrealized profit/loss in USD on the remaining open shares. For resolved markets, uses the final settlement price (`1.0` for winners, `0.0` for losers). For live markets, uses the current market price. Returns `0` when `size = 0`. Byte-identical to Polymarket's `cashPnl` field for any position covered by both. |
| `current_price` | number \| null | Price used to compute `unrealized_pnl`. `1.0` or `0.0` for resolved markets (derived deterministically from on-chain `payoutNumerators`), the live market price for active markets, and `null` when `size = 0` or no price is available. |
| `market_status` | string | One of: `"live"` (market still trading), `"resolved-win"` (market resolved, this outcome won), `"resolved-loss"` (market resolved, this outcome lost), or `"closed"` (position fully exited, `size = 0`). A fifth value `"resolved-unknown"` may appear briefly for very old markets while resolution data is catching up. Never `"live"` when `resolved_at` is set. |
| `won` | boolean \| undefined | Present only on `resolved-win` / `resolved-loss` rows. `true` when this outcome won, `false` when it lost. Derived from on-chain payouts. |
| `winning_outcome_index` | number \| undefined | Present only on resolved rows. Numeric index of the outcome that won (0 or 1 for binary markets). Pair with `outcome_index` to know whether this row is the winning side. |
| `outcome_index` | number \| undefined | Numeric index of this row's outcome within the market's outcomes array (0 or 1 for binary markets). Stable across the API regardless of how the UI labels the outcome (`Yes`/`No`, team names, etc.). Use this for cross-row joins instead of parsing `outcome` strings. |
| `total_bought` | number | Total amount bought in USD over the lifetime of the position. |
| `initial_value` | number \| undefined | Cost basis in USD of the currently held shares (`size × cost_per_share`). Use this if you need the basis number that matches Polymarket `cashPnl` and what users see in the Polymarket UI. |
| `redeemable` | boolean \| undefined | `true` when the market has resolved and the user can call redeem on the CTF contract to claim payout (or accept loss). Useful for detecting "resolved-but-not-redeemed" positions: filter `market_status = "resolved-loss"` AND `redeemable = true` for unclaimed losses, or `market_status = "resolved-win"` AND `redeemable = true` for unclaimed wins. |
| `opposite_asset` | string \| undefined | Token ID of the OPPOSITE outcome on the same market (the binary counterpart). Useful for fetching the matching position on the other side without re-resolving the condition. |
| `market` | string | Market question text |
| `market_slug` | string | Market URL slug |
| `outcome` | string | Outcome label (e.g. "Yes", "No") |
| `condition_id` | string | Market condition ID |
| `image` | string \| null | Market image URL. `null` for some delisted or niche markets. |
| `event_slug` | string \| null | Parent **event** slug, distinct from the per-row market `slug`. For multi-market events (NBA games with several lines, election markets with several candidates, FIFA World Cup with one market per team), this is the parent the markets share. For single-market events, equals the market slug. `null` when event metadata is not yet known. |
| `last_activity` | number \| undefined | Unix timestamp of the wallet's most recent fill on this position. Present only when querying by `wallet`. Positions with no fill history (e.g. acquired purely via split/merge/redemption) omit this field and sort to the end. |
| `last_trade_at` | number \| null | Unix seconds. Latest fill on this token across V1 + V2 exchanges. Same data as `last_activity` but always present (`null` instead of omitted) for consistent shape. Use either; `last_trade_at` is preferred for new integrations. |
| `closed_at` | number \| null | Unix seconds. Latest moment this wallet redeemed any outcome of the market for collateral. `null` when the wallet has not redeemed (open positions, positions sold to zero pre-resolution, or wins held but not yet redeemed). |
| `resolved_at` | number \| null | Unix seconds. Moment the market was resolved on-chain (when payouts became redeemable). `null` for markets that resolved before polynode began tracking, or for markets that have not yet resolved. Recent markets are fully covered. |
### Pagination fields
| Field | Type | Description |
| --------------------------- | ------- | ---------------------------------------------------------------------- |
| `count` | number | Number of positions in this response |
| `pagination.limit` | number | Requested page size |
| `pagination.has_more` | boolean | `true` if more results exist beyond this page |
| `pagination.pagination_key` | string | Pass this as `pagination_key` in the next request to get the next page |
## Examples
### All positions for a wallet
```bash cURL theme={null}
curl "https://api.polynode.dev/v2/onchain/positions?wallet=0xa973ae12882a1c24a75da7a3b52cb01500b2f639&limit=100" \
-H "x-api-key: YOUR_KEY"
```
```javascript Node.js theme={null}
const resp = await fetch(
"https://api.polynode.dev/v2/onchain/positions?wallet=0xa973ae12882a1c24a75da7a3b52cb01500b2f639&limit=100",
{ headers: { "x-api-key": "YOUR_KEY" } }
);
const data = await resp.json();
console.log(data.positions);
```
```python Python theme={null}
import requests
resp = requests.get(
"https://api.polynode.dev/v2/onchain/positions",
params={"wallet": "0xa973ae12882a1c24a75da7a3b52cb01500b2f639", "limit": 100},
headers={"x-api-key": "YOUR_KEY"},
)
data = resp.json()
print(data["positions"])
```
### Open positions only
Use `status=open` to get only positions with a non-zero size.
```bash theme={null}
curl "https://api.polynode.dev/v2/onchain/positions?wallet=0xa973ae12882a1c24a75da7a3b52cb01500b2f639&status=open&limit=100" \
-H "x-api-key: YOUR_KEY"
```
```json theme={null}
{
"count": 2,
"pagination": {
"limit": 2,
"has_more": true,
"pagination_key": "0xa973ae12882a1c24a75da7a3b52cb01500b2f639-99504617..."
},
"positions": [
{
"wallet": "0xa973ae12882a1c24a75da7a3b52cb01500b2f639",
"token_id": "99949662063403569895321097192043694236016212986254553767097648841549016857591",
"size": 0.012785,
"avg_price": 0.859999,
"realized_pnl": 0.16,
"unrealized_pnl": 0.02,
"current_price": 0.88,
"market_status": "live",
"total_bought": 1.16,
"market": "Will FC Bayern Munchen win on 2026-04-07?",
"market_slug": "ucl-rma1-bay1-2026-04-07-bay1",
"outcome": "Yes",
"condition_id": "0x5f5c8d8fa28d77b5552562f32393ac199fb6b92ed1c1e2239e29c09f7e4eb3f5",
"image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/champions-league-pic-QIUFsL8vaDdq.png"
}
]
}
```
### Who holds a market
Use `market_slug` to see all wallets with positions on a specific market.
```bash theme={null}
curl "https://api.polynode.dev/v2/onchain/positions?market_slug=will-zohran-mamdani-win-the-2025-nyc-mayoral-election&status=open&limit=50" \
-H "x-api-key: YOUR_KEY"
```
```json theme={null}
{
"count": 2,
"pagination": {
"limit": 2,
"has_more": true,
"pagination_key": "0xfffe254008792df0c325325a75a3c6e7aaed436a-10583236..."
},
"positions": [
{
"wallet": "0xffff3840fbf40fd2e193c01cc299cdf1262cffaf",
"token_id": "33945469250963963541781051637999677727672635213493648594066577298999471399137",
"size": 0.009534,
"avg_price": 0.947999,
"realized_pnl": -0.54,
"unrealized_pnl": -5.07,
"current_price": 0.416,
"market_status": "live",
"total_bought": 539.03,
"market": "Will Zohran Mamdani win the 2025 NYC mayoral election?",
"market_slug": "will-zohran-mamdani-win-the-2025-nyc-mayoral-election",
"outcome": "Yes",
"condition_id": "0xebddfcf7b4401dade8b4031770a1ab942b01854f3bed453d5df9425cd9f211a9",
"image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/will-zohran-mamdani-win-the-2025-nyc-mayoral-election-EscSJQTT6hWg.jpg"
}
]
}
```
### Large positions
Use `min_size` to find positions above a threshold.
```bash theme={null}
curl "https://api.polynode.dev/v2/onchain/positions?min_size=100&limit=20" \
-H "x-api-key: YOUR_KEY"
```
```json theme={null}
{
"count": 2,
"pagination": {
"limit": 2,
"has_more": true,
"pagination_key": "0xffffffe1e093aacd21e4e281e66d543fb0b23455-98495912..."
},
"positions": [
{
"wallet": "0xffffffe1e093aacd21e4e281e66d543fb0b23455",
"token_id": "98813479054803844837498343855179110721772056323529222930302029314504656450267",
"size": 1100,
"avg_price": 0.003636,
"realized_pnl": 0,
"unrealized_pnl": -3.96,
"current_price": 0,
"market_status": "resolved-loss",
"total_bought": 1100,
"market": "Bitcoin Up or Down - February 5, 9:45AM-10:00AM ET",
"market_slug": "btc-updown-15m-1770302700",
"outcome": "Up",
"condition_id": "0x3ec09263f6fb247e65635d52c2787dc0f46806f153124e138f42ad03411198b4",
"image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/BTC+fullsize.png"
}
]
}
```
### Combined filters
Filter by wallet, market, and status at once.
```bash theme={null}
curl "https://api.polynode.dev/v2/onchain/positions?wallet=0xa973ae12882a1c24a75da7a3b52cb01500b2f639&market_slug=ucl-rma1-bay1-2026-04-07-bay1&status=open&limit=10" \
-H "x-api-key: YOUR_KEY"
```
### Pagination
**Wallet queries** return every position for the wallet in a single response (up to 500), sorted by most recent on-chain activity. No pagination needed — request `limit=500` and read all results. `has_more` is always `false` and no cursor is returned. Wallets with more than 500 lifetime positions are capped at 500.
**Non-wallet queries** (filtering by `market_slug`, `condition_id`, `token_id`, or `min_size` alone) use cursor-based pagination for iterating through large result sets.
```bash theme={null}
# First page
curl "https://api.polynode.dev/v2/onchain/positions?min_size=100&limit=100" \
-H "x-api-key: YOUR_KEY"
# Next page
curl "https://api.polynode.dev/v2/onchain/positions?min_size=100&limit=100&pagination_key=CURSOR_FROM_PREVIOUS" \
-H "x-api-key: YOUR_KEY"
```
## Error responses
| Status | Response | Condition |
| ------ | -------------------------------------------------------------------------------------- | --------------------------------- |
| 400 | `{"error": "market_slug not found"}` | Invalid or unknown `market_slug` |
| 400 | `{"error": "condition_id not found"}` | Invalid or unknown `condition_id` |
| 401 | `{"error": "API key required. Pass via ?key= or x-api-key header."}` | Missing API key |
| 403 | `{"error": "V2 endpoints require a paid plan. See polynode.dev/pricing for details."}` | Free tier key |
| 429 | `{"error": "Rate limited. N req/s for your tier.", "retryAfterMs": ...}` | Rate limited |
## Notes
* Position data is sourced from onchain settlement records. Every position that was ever opened on Polymarket is included.
* The `realized_pnl` field reflects actual profit/loss from closed portions of the position, including onchain redemptions of resolved markets. For open positions, it reflects any partial closes.
* The `unrealized_pnl` field shows the paper profit/loss on the remaining open shares. For resolved markets, this uses the final settlement price ($1 for winners, $0 for losers). For active markets, the current market price is used.
* `market_status` lets clients distinguish live-tradable positions from positions on resolved markets that the wallet never redeemed. Resolved-and-never-redeemed positions still have `size > 0` and will show the correct terminal `unrealized_pnl`.
* Market metadata (`market`, `market_slug`, `outcome`, `condition_id`, `image`) is enriched from our index. Very old or delisted markets may not have metadata.
* When querying by `wallet`, the sort key is the timestamp of the most recent fill for each position, not the position's close date. A position bought months ago and held to resolution (closed only via redemption) will sort by its original buy date.
## Verifying parity with Polymarket
Per-position fields on this endpoint match Polymarket's `data-api.polymarket.com/positions` byte-for-byte. Anyone can verify directly with the standalone Node script below — no internal access required.
### Field map
| polynode field | Polymarket field |
| ---------------- | ---------------- |
| `token_id` | `asset` |
| `condition_id` | `conditionId` |
| `size` | `size` |
| `avg_price` | `avgPrice` |
| `realized_pnl` | `realizedPnl` |
| `unrealized_pnl` | `cashPnl` |
| `current_price` | `curPrice` |
| `outcome` | `outcome` |
### Self-test script (Node 18+, no dependencies)
```javascript theme={null}
// Save as parity_demo.mjs and run with:
// POLYNODE_KEY=pn_live_xxx node parity_demo.mjs
const KEY = process.env.POLYNODE_KEY;
const WALLET = process.argv[2];
const [pm, us] = await Promise.all([
fetch(`https://data-api.polymarket.com/positions?user=${WALLET}&sizeThreshold=0&limit=2000`,
{ headers: { 'User-Agent': 'Mozilla/5.0' } }).then(r => r.json()),
fetch(`https://api.polynode.dev/v2/onchain/positions?wallet=${WALLET}&status=all&limit=2000`,
{ headers: { 'x-api-key': KEY } }).then(r => r.json()).then(j => Array.isArray(j) ? j : (j.positions ?? [])),
]);
const pmByPid = new Map(pm.map(p => [String(p.asset), p]));
const usByPid = new Map(us.map(p => [String(p.token_id), p]));
const shared = [...pmByPid.keys()].filter(k => usByPid.has(k));
let exact = 0, sub_dollar = 0;
for (const pid of shared) {
const p = pmByPid.get(pid), u = usByPid.get(pid);
const avgDiff = Math.abs(Number(p.avgPrice) - Number(u.avg_price));
const pnlDiff = Math.abs(Number(p.realizedPnl) - Number(u.realized_pnl));
if (avgDiff < 0.005 && pnlDiff < 0.01) exact++;
if (pnlDiff < 1.0) sub_dollar++;
console.log(` ${pid.slice(0,8)}… PM realPnl=${Number(p.realizedPnl).toFixed(2).padStart(10)} Us realPnl=${Number(u.realized_pnl).toFixed(2).padStart(10)} Δ=${pnlDiff.toFixed(4)}`);
}
console.log(`\n byte-perfect: ${exact}/${shared.length} sub-$1: ${sub_dollar}/${shared.length}`);
```
Run against any wallet to confirm. Validated 2026-04-30 across diverse wallets at 100 % byte-perfect match on every shared open position.
### What this proves
If you trust Polymarket's positions page, you can trust ours — they're computing the same thing from the same on-chain events. Use this script as ongoing regression coverage in your own integration if PnL accuracy is critical to your product.
# Trade History (All)
Source: https://docs.polynode.dev/api-reference/onchain/trades
GET /v2/onchain/trades
Query every trade ever executed on Polymarket. Filter by wallet, market, time range, or minimum size. Full history with cursor pagination.
Search and filter the complete history of Polymarket trades. Every CLOB fill is enriched with market metadata. Supports filtering by wallet, market (via slug, condition ID, or token ID), time range, and minimum trade size.
When no filters are provided, returns the most recent trades from the last 24 hours. Add any filter to query the full history.
## Request
```
GET /v2/onchain/trades
```
### Query parameters
| Parameter | Type | Required | Description |
| ---------------- | ------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `wallet` | string | No | Filter by wallet address (matches both maker and taker sides) |
| `market_slug` | string | No | Filter by market slug (e.g. `will-zohran-mamdani-win-the-2025-nyc-mayoral-election`) |
| `condition_id` | string | No | Filter by condition ID (0x-prefixed, 64 hex chars) |
| `token_id` | string | No | Filter by outcome token ID |
| `start_time` | integer | No | Unix timestamp — only return trades after this time |
| `end_time` | integer | No | Unix timestamp — only return trades before this time |
| `min_total` | number | No | Minimum trade size in USD |
| `limit` | integer | No | Results per page (1-500, default 100) |
| `order` | string | No | Sort by timestamp: `desc` (newest first, default) or `asc` (oldest first). Using `asc` without any filter returns an error. |
| `sort_by` | string | No | Set to `order_hash` to cluster fills by order. All fills from the same limit order appear adjacent, sorted by time within each group. Groups ordered by most recent fill. Default sort is by timestamp. |
| `group_by` | string | No | Set to `order_hash` to aggregate fills into one row per order. Each row contains `total_amount_usd`, `total_shares`, `avg_price`, `fill_count`, `first_fill_at`, `last_fill_at`, and `tx_hashes`. See [Group by order](#group-by-order) below. |
| `pagination_key` | string | No | Cursor from a previous response to fetch the next page |
### Identifying markets
There are three ways to filter trades by market:
* **`market_slug`** — the human-readable URL slug from Polymarket (e.g. `will-zohran-mamdani-win-the-2025-nyc-mayoral-election`). Easiest to use.
* **`condition_id`** — the unique condition identifier for a market. Returns trades for all outcomes (Yes and No).
* **`token_id`** — the specific outcome token. Returns trades for only that outcome.
You can combine market filters with `wallet` to find a specific wallet's trades on a specific market.
## Response
```json theme={null}
{
"count": 2,
"pagination": {
"limit": 2,
"has_more": true,
"pagination_key": "0xfffff7ed9080f46d7975f905e7f9358c436a1b177c6e19e54510c3ba6dcfddc4_0x09c8f6c7a148a83dfc968b1d9aece6ddb43a7a0e1bc2b1cb4b38356bb32220f0"
},
"trades": [
{
"timestamp": 1775668111,
"tx_hash": "0xfffff7ed9080f46d7975f905e7f9358c436a1b177c6e19e54510c3ba6dcfddc4",
"order_hash": "0xd808640f8d06bd5d1fa6f86c149d2a66753a6c86a4580c616a9e170f57f96b02",
"maker": "0xa973ae12882a1c24a75da7a3b52cb01500b2f639",
"taker": "0xc5d563a36ae78145c45a50134d48a1215220f80a",
"token_id": "3486411672082301675645165272859578340300389072626261580506086464973862637410",
"price": 0.92,
"amount_usd": 1,
"fee_usd": 0.01,
"shares": 1.08695,
"market": "Will Goztepe SK win on 2026-04-08?",
"market_slug": "tur-goz-gal-2026-04-08-goz",
"outcome": "No",
"condition_id": "0x8c7ec1f6e3694c402bd204a25756d55b34bb294f28677b624e1068450f1dfb1a",
"image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/tur.png"
},
{
"timestamp": 1775668111,
"tx_hash": "0xfffff7ed9080f46d7975f905e7f9358c436a1b177c6e19e54510c3ba6dcfddc4",
"order_hash": "0x09c8f6c7a148a83dfc968b1d9aece6ddb43a7a0e1bc2b1cb4b38356bb32220f0",
"maker": "0x84ad9c5c547a82ec9a08547b94bd922446e5bfb7",
"taker": "0xa973ae12882a1c24a75da7a3b52cb01500b2f639",
"token_id": "107137984184508009127443282317786484656951337636215782600474106740257845898368",
"price": 0.08,
"amount_usd": 0.09,
"fee_usd": 0.11,
"shares": 1.08695,
"market": "Will Goztepe SK win on 2026-04-08?",
"market_slug": "tur-goz-gal-2026-04-08-goz",
"outcome": "Yes",
"condition_id": "0x8c7ec1f6e3694c402bd204a25756d55b34bb294f28677b624e1068450f1dfb1a",
"image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/tur.png"
}
]
}
```
### Trade fields
| Field | Type | Description |
| -------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `timestamp` | number | Unix timestamp of the fill |
| `tx_hash` | string | Polygon transaction hash |
| `order_hash` | string | CLOB order hash that was filled |
| `maker` | string | Maker wallet address |
| `taker` | string | Taker wallet address |
| `token_id` | string | Outcome token ID |
| `price` | number | Fill price (0 to 1) |
| `amount_usd` | number | Trade size in USD |
| `fee_usd` | number | Fee paid in USD |
| `shares` | number | Number of outcome shares traded |
| `side` | string | Exchange role: `"maker"` or `"taker"`. With `wallet` filter, this is the queried wallet's role on the fill. Without `wallet`, always `"maker"` (see "Direction & side semantics" below). Always present. |
| `direction` | string | Trader intent: `"BUY"` or `"SELL"`. With `wallet` filter, from the queried wallet's perspective (`BUY` when the wallet contributed USDC, `SELL` when it contributed outcome tokens). Without `wallet`, from the maker's perspective. Always present. |
| `market` | string | Market question text |
| `market_slug` | string | Market URL slug |
| `outcome` | string | Outcome label (e.g. "Yes", "No") |
| `condition_id` | string | Market condition ID |
| `image` | string \| null | Market image URL. `null` for some delisted or niche markets. |
### Direction & side semantics
`direction` and `side` answer two different questions and are always populated:
* **`direction` (`"BUY"` or `"SELL"`)** — what the subject of the row did, economically. They contributed USDC and received outcome shares (`BUY`) or contributed outcome shares and received USDC (`SELL`).
* **`side` (`"maker"` or `"taker"`)** — the subject's role on the fill. The `maker` placed a resting limit order; the `taker` aggressed the spread.
The "subject" of the row depends on the filter:
| Query | Subject of `direction` / `side` |
| ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `?wallet=W` (with or without other filters) | The wallet `W`. `side` reflects whether `W` was the maker or taker on each fill. |
| Any market-wide filter (no `wallet`): `?condition_id=…`, `?token_id=…`, `?market_slug=…`, or no filter | The **maker** of each fill. On Polymarket's CLOB the maker is always the user who signed the limit order, so `direction` is always a real user's BUY/SELL. `side` is therefore always `"maker"` in this case. |
This means a row from a wallet-filtered query and the same row from a market-wide query may report different `direction` values — both are correct, just anchored on different parties (the queried wallet vs. the maker).
### Pagination fields
| Field | Type | Description |
| --------------------------- | ------- | ---------------------------------------------------------------------- |
| `count` | number | Number of trades in this response |
| `pagination.limit` | number | Requested page size |
| `pagination.has_more` | boolean | `true` if more results exist beyond this page |
| `pagination.pagination_key` | string | Pass this as `pagination_key` in the next request to get the next page |
## Examples
### Recent trades
Returns the most recent trades from the last 24 hours.
```bash cURL theme={null}
curl "https://api.polynode.dev/v2/onchain/trades?limit=2" \
-H "x-api-key: YOUR_KEY"
```
```javascript Node.js theme={null}
const resp = await fetch(
"https://api.polynode.dev/v2/onchain/trades?limit=2",
{ headers: { "x-api-key": "YOUR_KEY" } }
);
const data = await resp.json();
console.log(data.trades);
```
```python Python theme={null}
import requests
resp = requests.get(
"https://api.polynode.dev/v2/onchain/trades",
params={"limit": 2},
headers={"x-api-key": "YOUR_KEY"},
)
data = resp.json()
print(data["trades"])
```
### Filter by wallet
Returns all trades where the wallet was either maker or taker. The `side` field indicates which role the wallet played.
```bash theme={null}
curl "https://api.polynode.dev/v2/onchain/trades?wallet=0xa973ae12882a1c24a75da7a3b52cb01500b2f639&limit=1" \
-H "x-api-key: YOUR_KEY"
```
```json theme={null}
{
"count": 1,
"pagination": {
"limit": 1,
"has_more": true,
"pagination_key": "0xfffff7ed...d808640f..."
},
"trades": [
{
"timestamp": 1775668111,
"tx_hash": "0xfffff7ed9080f46d7975f905e7f9358c436a1b177c6e19e54510c3ba6dcfddc4",
"order_hash": "0xd808640f8d06bd5d1fa6f86c149d2a66753a6c86a4580c616a9e170f57f96b02",
"maker": "0xa973ae12882a1c24a75da7a3b52cb01500b2f639",
"taker": "0xc5d563a36ae78145c45a50134d48a1215220f80a",
"token_id": "34864116720823016756451652728595783403003890726262615805060864649738626374410",
"price": 0.92,
"amount_usd": 1,
"fee_usd": 0.01,
"shares": 1.08695,
"side": "maker",
"market": "Will Goztepe SK win on 2026-04-08?",
"market_slug": "tur-goz-gal-2026-04-08-goz",
"outcome": "No",
"condition_id": "0x8c7ec1f6e3694c402bd204a25756d55b34bb294f28677b624e1068450f1dfb1a",
"image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/tur.png"
}
]
}
```
### Filter by market
Use `market_slug` to get all trades on a specific market across all wallets.
```bash theme={null}
curl "https://api.polynode.dev/v2/onchain/trades?market_slug=will-zohran-mamdani-win-the-2025-nyc-mayoral-election&limit=1" \
-H "x-api-key: YOUR_KEY"
```
```json theme={null}
{
"count": 1,
"pagination": {
"limit": 1,
"has_more": true,
"pagination_key": "0xffffffeb...00063fc7..."
},
"trades": [
{
"timestamp": 1759503806,
"tx_hash": "0xffffffeb7983b64a9394a0485c897113ad17ffca0fde8f3fa16aa045396d0582",
"order_hash": "0x00063fc790381b8d017b868f153aeed48152c8c7012e33cb4793b3f5c8989b3a",
"maker": "0xe05d8c348aee0323cf115c18006a35db54ba2685",
"taker": "0xc5d563a36ae78145c45a50134d48a1215220f80a",
"token_id": "33945469250963963541781051637999677727672635213493648594066577298999471399137",
"price": 0.897,
"amount_usd": 100,
"fee_usd": 0,
"shares": 111.482719,
"market": "Will Zohran Mamdani win the 2025 NYC mayoral election?",
"market_slug": "will-zohran-mamdani-win-the-2025-nyc-mayoral-election",
"outcome": "Yes",
"condition_id": "0xebddfcf7b4401dade8b4031770a1ab942b01854f3bed453d5df9425cd9f211a9",
"image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/will-zohran-mamdani-win-the-2025-nyc-mayoral-election-EscSJQTT6hWg.jpg"
}
]
}
```
You can also filter by `condition_id` to get both outcomes of a market:
```bash theme={null}
curl "https://api.polynode.dev/v2/onchain/trades?condition_id=0xebddfcf7b4401dade8b4031770a1ab942b01854f3bed453d5df9425cd9f211a9&limit=10" \
-H "x-api-key: YOUR_KEY"
```
### Large trades
Use `min_total` to find trades above a USD threshold.
```bash theme={null}
curl "https://api.polynode.dev/v2/onchain/trades?min_total=100&limit=10" \
-H "x-api-key: YOUR_KEY"
```
```json theme={null}
{
"count": 1,
"pagination": {
"limit": 1,
"has_more": true,
"pagination_key": "0xffffffeb...a0017d41..."
},
"trades": [
{
"timestamp": 1759503806,
"tx_hash": "0xffffffeb7983b64a9394a0485c897113ad17ffca0fde8f3fa16aa045396d0582",
"order_hash": "0xa0017d41adb20f9cb729243b8d8464218d3f2a9e8d24c8e53f36c744c9f250a7",
"maker": "0x12d6cccfc7470a3f4bafc53599a4779cbf2cf2a8",
"taker": "0xe05d8c348aee0323cf115c18006a35db54ba2685",
"token_id": "33945469250963963541781051637999677727672635213493648594066577298999471399137",
"price": 0.897,
"amount_usd": 100,
"fee_usd": 0,
"shares": 111.482719,
"market": "Will Zohran Mamdani win the 2025 NYC mayoral election?",
"market_slug": "will-zohran-mamdani-win-the-2025-nyc-mayoral-election",
"outcome": "Yes",
"condition_id": "0xebddfcf7b4401dade8b4031770a1ab942b01854f3bed453d5df9425cd9f211a9",
"image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/will-zohran-mamdani-win-the-2025-nyc-mayoral-election-EscSJQTT6hWg.jpg"
}
]
}
```
### Time range
Use `start_time` and `end_time` (Unix timestamps) to query a specific window.
```bash theme={null}
curl "https://api.polynode.dev/v2/onchain/trades?start_time=1775600000&end_time=1775700000&limit=10" \
-H "x-api-key: YOUR_KEY"
```
### Combined filters
Combine any filters. For example, a wallet's trades on a specific market:
```bash theme={null}
curl "https://api.polynode.dev/v2/onchain/trades?wallet=0xa973ae12882a1c24a75da7a3b52cb01500b2f639&market_slug=tur-goz-gal-2026-04-08-goz&limit=10" \
-H "x-api-key: YOUR_KEY"
```
### Pagination
Pass the `pagination_key` from the previous response to get the next page.
```bash theme={null}
# First page
curl "https://api.polynode.dev/v2/onchain/trades?limit=100" \
-H "x-api-key: YOUR_KEY"
# Next page
curl "https://api.polynode.dev/v2/onchain/trades?limit=100&pagination_key=CURSOR_FROM_PREVIOUS" \
-H "x-api-key: YOUR_KEY"
```
### Sort by order
Use `sort_by=order_hash` to cluster fills from the same limit order together. A single limit order can be filled across many transactions over time. This sort groups all those partial fills adjacently, sorted by timestamp within each order. Orders are ranked by their most recent fill.
Every fill is still returned individually with all fields. No data is collapsed or aggregated.
```bash theme={null}
curl "https://api.polynode.dev/v2/onchain/trades?wallet=0xa973ae12882a1c24a75da7a3b52cb01500b2f639&sort_by=order_hash&limit=100" \
-H "x-api-key: YOUR_KEY"
```
### Group by order
Use `group_by=order_hash` to aggregate all fills from the same limit order into a single row. Each row contains summed totals, a VWAP price, fill count, time range, and a list of transaction hashes.
```bash theme={null}
curl "https://api.polynode.dev/v2/onchain/trades?wallet=0xa973ae12882a1c24a75da7a3b52cb01500b2f639&group_by=order_hash&limit=100" \
-H "x-api-key: YOUR_KEY"
```
```json theme={null}
{
"count": 76,
"source": "onchain-v2-grouped",
"trades": [
{
"order_hash": "0x7f69046bdbb2a595667e7ffe24ea285f87f95f7d146333dbc8a7e02948bbe52b",
"fill_count": 12,
"total_amount_usd": 208.76,
"total_shares": 353.82,
"avg_price": 0.59,
"total_fee_usd": 0.0,
"first_fill_at": 1778409433,
"last_fill_at": 1778414041,
"timestamp": 1778414041,
"token_id": "102860925866954003158841129327041711410011624382962867117087237151352260776640",
"maker": "0x6ac5bb06a9eb05641fd5e82640268b92f3ab4b6e",
"side": "maker",
"direction": "BUY",
"market": "Pistons vs. Cavaliers",
"market_slug": "nba-det-cle-2026-05-11",
"outcome": "Cavaliers",
"condition_id": "0x52f953bd31997875e5f782fbed7f85b8707aa8fefe95b6854cf5b8c69370b3f0",
"tx_hashes": ["0xa7fdeb87...", "0xbb9092bc...", "..."]
}
]
}
```
#### Grouped trade fields
| Field | Type | Description |
| ------------------ | -------------- | ----------------------------------------------------------------- |
| `order_hash` | string | The CLOB order hash (grouping key) |
| `fill_count` | number | Number of individual fills aggregated into this row |
| `total_amount_usd` | number | Sum of all fill sizes in USD |
| `total_shares` | number | Sum of all outcome shares traded |
| `avg_price` | number | Volume-weighted average price (`total_amount_usd / total_shares`) |
| `total_fee_usd` | number | Sum of all fees in USD |
| `first_fill_at` | number | Unix timestamp of the earliest fill |
| `last_fill_at` | number | Unix timestamp of the most recent fill |
| `timestamp` | number | Same as `last_fill_at` (for compatibility with default sort) |
| `tx_hashes` | string\[] | List of unique transaction hashes for all fills |
| `token_id` | string | Outcome token ID |
| `maker` | string | Maker address (from the first fill) |
| `side` | string | Exchange role (from the first fill) |
| `direction` | string | Trade direction (from the first fill) |
| `market` | string | Market question text |
| `market_slug` | string | Market URL slug |
| `outcome` | string | Outcome label |
| `condition_id` | string | Market condition ID |
| `image` | string \| null | Market image URL |
### Oldest first
Use `order=asc` to get trades in chronological order. Requires at least one filter.
```bash theme={null}
curl "https://api.polynode.dev/v2/onchain/trades?wallet=0xa973ae12882a1c24a75da7a3b52cb01500b2f639&order=asc&limit=10" \
-H "x-api-key: YOUR_KEY"
```
## Error responses
| Status | Response | Condition |
| ------ | ------------------------------------------------------------------------------------------------------------------- | --------------------------------- |
| 400 | `{"error": "order=asc requires at least one filter (wallet, token_id, condition_id, market_slug, or start_time)."}` | `order=asc` without any filter |
| 400 | `{"error": "market_slug not found"}` | Invalid or unknown `market_slug` |
| 400 | `{"error": "condition_id not found"}` | Invalid or unknown `condition_id` |
| 401 | `{"error": "API key required. Pass via ?key= or x-api-key header."}` | Missing API key |
| 403 | `{"error": "V2 endpoints require a paid plan. See polynode.dev/pricing for details."}` | Free tier key |
| 429 | `{"error": "Rate limited. N req/s for your tier.", "retryAfterMs": ...}` | Rate limited |
## Notes
* Without filters, defaults to the last 24 hours for performance. Add `start_time` to query further back.
* The `side` field is only included when filtering by `wallet`. Without a wallet filter, there is no perspective to assign a side from.
* Market metadata (`market`, `market_slug`, `outcome`, `condition_id`, `image`) is enriched from our index. Very old or delisted markets may not have metadata.
* Results are sourced from onchain settlement data. Every fill that settled on Polygon is included.
# Wallet Activity
Source: https://docs.polynode.dev/api-reference/onchain/wallet-activity
GET /v2/onchain/wallets/{address}/activity
Onchain activity for a wallet: splits, merges, and multi-outcome conversions.
Returns all onchain activity for a wallet including token splits (minting outcome tokens from collateral), merges (burning outcome tokens back to collateral), and neg-risk conversions (multi-outcome market hedging). Each event is enriched with market metadata. This data is not available through Polymarket's API.
## Request
```
GET /v2/onchain/wallets/{address}/activity?limit=100&offset=0
```
| Parameter | Type | Location | Description |
| --------- | ------- | -------- | ------------------------------------------ |
| `address` | string | path | Wallet address (0x-prefixed, 40 hex chars) |
| `limit` | integer | query | Max results (default 100, max 1000) |
| `offset` | integer | query | Skip first N results for pagination |
## Response
```json theme={null}
{
"wallet": "0x2f5653a3761f65c5a299f9839eadbd4d4d679ffa",
"source": "onchain",
"count": 1,
"offset": 0,
"splits": 0,
"merges": 1,
"conversions": 0,
"events": [
{
"type": "merge",
"id": "0x583037e505fc38851d81fc129d2eef0cb57a81270d7b5cb9f500dca9a3f898e9_0x5fd",
"timestamp": 1774715707,
"condition": "0xe84334d8b082ddf50a6aa659bb19283ac94edfd52a5357fb7547c0371bb18084",
"amount": 5,
"market": "Bitcoin Up or Down - March 28, 12:30PM-12:35PM ET",
"slug": "btc-updown-5m-1774715400",
"image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/BTC+fullsize.png"
}
]
}
```
| Field | Type | Description |
| ----------------------------- | ------ | -------------------------------------------------- |
| `splits` | number | Count of split events in this page |
| `merges` | number | Count of merge events in this page |
| `conversions` | number | Count of neg-risk conversion events in this page |
| `events[].type` | string | `"split"`, `"merge"`, or `"neg_risk_conversion"` |
| `events[].timestamp` | number | Unix timestamp |
| `events[].condition` | string | Condition ID |
| `events[].amount` | number | USDC amount |
| `events[].market` | string | Market question |
| `events[].slug` | string | Market slug |
| `events[].image` | string | Market image URL |
| `events[].neg_risk_market_id` | string | Neg-risk market ID (conversions only) |
| `events[].index_set` | string | Index set (conversions only) |
| `events[].question_count` | number | Questions in the neg-risk event (conversions only) |
## Event Types
**Split** — Collateral deposited and split into outcome tokens. This is how positions are entered via the CTF contract (as opposed to buying on the orderbook).
**Merge** — Outcome tokens burned back into collateral. The inverse of a split. Often used when exiting a hedged position.
**Neg-risk conversion** — Converting between outcome tokens in a multi-outcome market (e.g., "Who will win the election?" with 5+ candidates). Used for hedging and arbitrage.
## Example
```bash theme={null}
curl "https://api.polynode.dev/v2/onchain/wallets/0x2f5653a3761f65c5a299f9839eadbd4d4d679ffa/activity?limit=50" \
-H "x-api-key: YOUR_KEY"
```
# Wallet Redemptions
Source: https://docs.polynode.dev/api-reference/onchain/wallet-redemptions
GET /v2/onchain/wallets/{address}/redemptions
All onchain redemptions for a wallet. See who cashed out, when, and how much.
Returns every onchain redemption for a wallet. A redemption is when a user claims their payout after a market resolves. This data is not available through Polymarket's API. Each redemption is enriched with market metadata.
## Request
```
GET /v2/onchain/wallets/{address}/redemptions?limit=100&offset=0
```
| Parameter | Type | Location | Description |
| --------- | ------- | -------- | ------------------------------------------ |
| `address` | string | path | Wallet address (0x-prefixed, 40 hex chars) |
| `limit` | integer | query | Max results (default 100, max 1000) |
| `offset` | integer | query | Skip first N results for pagination |
## Response
```json theme={null}
{
"wallet": "0x2f5653a3761f65c5a299f9839eadbd4d4d679ffa",
"source": "onchain",
"count": 1,
"offset": 0,
"total_payout": 5,
"redemptions": [
{
"id": "0x655555bc5091e07a453667c20afe4af5df2e099311670abf86a718dc48998b53_0x678",
"timestamp": 1774749134,
"condition": "0xedb2d159f7917cedf2ca9fe5b2fe70ba5db7fee23eff951ce8d7f96983cf6a66",
"index_sets": ["1", "2"],
"payout": 5,
"market": "Bitcoin Up or Down - March 28, 9:45PM-9:50PM ET",
"slug": "btc-updown-5m-1774748700",
"image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/BTC+fullsize.png"
}
]
}
```
| Field | Type | Description |
| -------------------------- | ------ | ------------------------------------------------- |
| `total_payout` | number | Sum of all payout amounts (USDC) in this page |
| `redemptions[].timestamp` | number | Unix timestamp of the redemption |
| `redemptions[].condition` | string | Condition ID of the resolved market |
| `redemptions[].index_sets` | array | Outcome index sets redeemed |
| `redemptions[].payout` | number | USDC payout amount (0 = losing position redeemed) |
| `redemptions[].market` | string | Market question |
| `redemptions[].slug` | string | Market slug |
| `redemptions[].image` | string | Market image URL |
## Example
```bash theme={null}
curl "https://api.polynode.dev/v2/onchain/wallets/0x2f5653a3761f65c5a299f9839eadbd4d4d679ffa/redemptions?limit=10" \
-H "x-api-key: YOUR_KEY"
```
# Trade History (Wallet)
Source: https://docs.polynode.dev/api-reference/onchain/wallet-trades
GET /v2/onchain/wallets/{address}/trades
Complete onchain trade fill history for any wallet. Every fill, every counterparty, every fee.
Returns every onchain trade fill for a wallet — both maker and taker sides, every fill, every counterparty, every fee. Pulled directly from onchain settlement data, never misses a fill. Each trade is enriched with market metadata (question, slug, outcome, image).
**Want the entire wallet's history in one call?** Use [Trade History (Wallet) — Bulk](/api-reference/onchain/wallet-trades-all) (Growth plan and above). Returns up to 250K trades in a single response, no pagination.
## Quick start — get the most recent 100 trades
```bash theme={null}
curl "https://api.polynode.dev/v2/onchain/wallets/{address}/trades?limit=100" \
-H "x-api-key: YOUR_KEY"
```
Default `limit=100`, max `limit=1000`. That's all most apps need.
## Get every trade for the wallet (full history)
To walk the entire history, add `?cursor=` (empty value) to the first request, then **for each next page, copy `pagination.cursor` from the response back into the URL**. Stop when `pagination.has_more` is `false`.
Three complete copy-paste scripts that walk an entire wallet:
```python Python theme={null}
import requests
API_KEY = "YOUR_KEY"
WALLET = "0xc944d399164b4460a4a79184fc6f419027891e0a"
def get_all_trades(wallet):
cursor = "" # empty string = first page
all_trades = []
while True:
r = requests.get(
f"https://api.polynode.dev/v2/onchain/wallets/{wallet}/trades",
params={"limit": 1000, "cursor": cursor},
headers={"x-api-key": API_KEY},
).json()
all_trades.extend(r["trades"])
if not r["pagination"]["has_more"]:
break
cursor = r["pagination"]["cursor"] # feed this into the next request
return all_trades
trades = get_all_trades(WALLET)
print(f"got {len(trades)} trades")
```
```javascript JavaScript theme={null}
const API_KEY = "YOUR_KEY";
const WALLET = "0xc944d399164b4460a4a79184fc6f419027891e0a";
async function getAllTrades(wallet) {
let cursor = ""; // empty string = first page
const allTrades = [];
while (true) {
const url = `https://api.polynode.dev/v2/onchain/wallets/${wallet}/trades`
+ `?limit=1000&cursor=${encodeURIComponent(cursor)}`;
const r = await fetch(url, { headers: { "x-api-key": API_KEY } }).then(r => r.json());
allTrades.push(...r.trades);
if (!r.pagination.has_more) break;
cursor = r.pagination.cursor; // feed this into the next request
}
return allTrades;
}
const trades = await getAllTrades(WALLET);
console.log(`got ${trades.length} trades`);
```
```bash bash theme={null}
KEY="YOUR_KEY"
WALLET="0xc944d399164b4460a4a79184fc6f419027891e0a"
CURSOR=""
TOTAL=0
while true; do
RESP=$(curl -s "https://api.polynode.dev/v2/onchain/wallets/$WALLET/trades?limit=1000&cursor=$CURSOR" \
-H "x-api-key: $KEY")
COUNT=$(echo "$RESP" | python3 -c "import json,sys;print(json.load(sys.stdin)['count'])")
HAS_MORE=$(echo "$RESP" | python3 -c "import json,sys;print(json.load(sys.stdin)['pagination']['has_more'])")
TOTAL=$((TOTAL + COUNT))
echo "got $COUNT trades, total: $TOTAL"
[ "$HAS_MORE" = "False" ] && break
CURSOR=$(echo "$RESP" | python3 -c "import json,sys;print(json.load(sys.stdin)['pagination']['cursor'])")
done
```
Each page takes \~1 second regardless of how many trades the wallet has. The loop just keeps running until `has_more` is `false`.
## Request
```
GET /v2/onchain/wallets/{address}/trades?limit=100
GET /v2/onchain/wallets/{address}/trades?limit=1000&cursor=
```
| Parameter | Type | Location | Description |
| --------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `address` | string | path | Wallet address (0x-prefixed, 40 hex chars) |
| `limit` | integer | query | Max results per page (default 100, max 1000) |
| `cursor` | string | query | Pass empty string `?cursor=` for first page, then echo back `pagination.cursor` from each response. Required to walk past the first page reliably on heavy wallets. |
| `offset` | integer | query | Alternative to cursor: skip first N. Fast for the first \~25K results then degrades. Use `cursor` for anything bigger. |
## Response
```json theme={null}
{
"wallet": "0xc944d399164b4460a4a79184fc6f419027891e0a",
"source": "onchain",
"count": 1,
"pagination": {
"limit": 1000,
"has_more": false
},
"trades": [
{
"tx_hash": "0x3611178d615caeab8f1fb32bc6232a32aaf93fc1e2f5bc5c9914ce2ddb9a13c3",
"order_hash": "0x8676e03168a3cbf39bc1c695e9419127605bd070ff26e7cd628b662c363a82b5",
"timestamp": 1777503244,
"maker": "0xc944d399164b4460a4a79184fc6f419027891e0a",
"taker": "0xe111180000d2663c0091e4f400237545b87b996b",
"maker_asset_id": "0",
"taker_asset_id": "110959653450933276250915064669875552310439627880508793089816880777942697720191",
"maker_amount": 4723.199764,
"taker_amount": 4737.412,
"fee": 0,
"side": "maker",
"direction": "BUY",
"market": "US x Iran ceasefire extended by April 22, 2026?",
"slug": "us-x-iran-ceasefire-extended-by-april-22-2026",
"outcome": "No",
"image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/us-x-iran-ceasefire-by-Cgmx3GCuOwjs.jpg"
}
]
}
```
When `pagination.has_more` is `true`, the response also includes a `pagination.cursor` string — pass that as `?cursor=` for the next request.
| Field | Type | Description |
| ------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `count` | integer | Number of trades in this response |
| `pagination.has_more` | boolean | `true` if more pages available — keep paginating |
| `pagination.cursor` | string | Pass back as `?cursor=` for the next page (only present when `has_more` is `true`) |
| `trades[].tx_hash` | string | Transaction hash |
| `trades[].timestamp` | number | Unix timestamp of the fill |
| `trades[].order_hash` | string | Order hash that was filled |
| `trades[].maker` | string | Maker wallet address |
| `trades[].taker` | string | Taker wallet address |
| `trades[].maker_asset_id` | string | Asset the maker provided (`"0"` = USDC) |
| `trades[].taker_asset_id` | string | Asset the taker provided (CTF token ID) |
| `trades[].maker_amount` | number | Amount maker provided (USDC) |
| `trades[].taker_amount` | number | Amount taker provided (tokens) |
| `trades[].fee` | number | Fee paid on this fill (USDC) |
| `trades[].side` | string | `"maker"` or `"taker"` relative to the queried wallet — exchange role on this fill |
| `trades[].direction` | string | `"BUY"` or `"SELL"` from the queried wallet's perspective. `BUY` = wallet contributed USDC and received outcome tokens. `SELL` = wallet contributed outcome tokens and received USDC. |
| `trades[].market` | string | Market question |
| `trades[].slug` | string | Market slug |
| `trades[].outcome` | string | Outcome label (e.g. "Yes", "Up", "Trump") |
| `trades[].image` | string | Market image URL |
## `side` vs `direction`
Both fields are populated on every row, and they answer different questions:
* **`side`** = was the wallet the **maker** (resting limit order) or **taker** (crossing the spread) on this fill?
* **`direction`** = did the wallet **BUY** outcome shares (gave USDC) or **SELL** them (received USDC)?
You'll see all four combinations in real wallets — a maker can be a buyer or seller depending on which side of the orderbook they posted to. Same for takers.
# Trade History (Wallet) — Bulk
Source: https://docs.polynode.dev/api-reference/onchain/wallet-trades-all
GET /v2/onchain/wallets/{address}/trades/all
Return every trade for a wallet in a single response. Growth plan or above.
**Growth plan or above only.** This endpoint returns the **entire** trade history for a wallet in **one response** — no client-side pagination, no offset bookkeeping. Because a single call can return tens of thousands of fills (and tens of MB of JSON), it's gated to the **Growth plan and above**. Starter and free tier requests receive `403`.
* Hard cap: **250,000 trades per call** — beyond that, response includes `"partial": true` with `"partial_reason": "hard_cap_250000"`
* Wall-clock budget: **180 seconds** — first cold-cache call on a heavy wallet may take 1-3 minutes
* Cache: full results cached **5 minutes**. Repeat calls return in under 1 second
* Typical payload: 1-50 MB. Worst case (capped): 100-200 MB JSON
Returns every onchain trade fill for a wallet in a single response — both maker and taker sides, every counterparty, every fee, complete history. The server walks the full trade record with parallel time-bucketed queries server-side; you don't need to paginate.
This is the right endpoint for **bulk-export** of a wallet for analysis, indexing, or backtesting. For interactive UIs that just need the most recent N trades, use the standard [Wallet Trade History](/api-reference/onchain/wallet-trades) instead.
## Request
```
GET /v2/onchain/wallets/{address}/trades/all?from=&to=
```
| Parameter | Type | Location | Description |
| --------- | ------- | -------- | --------------------------------------------------------------------------------------------- |
| `address` | string | path | Wallet address (0x-prefixed, 40 hex chars) |
| `from` | integer | query | Optional start of window in unix seconds. Default: 2024-01-01 (covers all Polymarket history) |
| `to` | integer | query | Optional end of window in unix seconds. Default: now |
## Response
```json theme={null}
{
"wallet": "0x2f5653a3761f65c5a299f9839eadbd4d4d679ffa",
"from_ts": 1704067200,
"to_ts": 1777600000,
"count": 8421,
"partial": false,
"partial_reason": null,
"fetched_in_ms": 7813,
"cache": "miss",
"trades": [
{
"tx_hash": "0x2fc63f3efc794d13c747d089d3b42bb9b4539b22216ccb2e6b2ea039ca5bb9ca",
"order_hash": "0x84f21e21f5c2a1df3264694a33ec9a8a292b83e163cbda4efe63333243aa08ef",
"timestamp": 1774749310,
"maker": "0x2f5653a3761f65c5a299f9839eadbd4d4d679ffa",
"taker": "0x198098d9c6c1dcb843314b9da212c44396c9a1d0",
"token_id": "21912724974096796009916816278814088615574660931588091764221331842149572809887",
"direction": "BUY",
"price": 0.625,
"shares": 4.88,
"usd": 3.05,
"fee": 0
}
]
}
```
## Response fields
| Field | Type | Description |
| ------------------- | -------------- | ---------------------------------------------------------------------------------------------------------- |
| `wallet` | string | The wallet address requested (lowercased) |
| `from_ts` / `to_ts` | integer | Resolved time window in unix seconds |
| `count` | integer | Total trades returned (≤ 250,000) |
| `partial` | boolean | `true` if either the 250K hard cap or the 180s wall-clock budget was hit before the full window was walked |
| `partial_reason` | string \| null | `"hard_cap_250000"` or `"wall_clock_180s"` when partial; otherwise `null` |
| `fetched_in_ms` | integer | Server-side fetch time. Cached calls show the original miss time |
| `cache` | string | `"hit"` if served from Redis cache, `"miss"` if freshly walked |
| `trades` | array | All trades, sorted by `timestamp` descending (newest first) |
## Trade fields
| Field | Type | Description |
| ------------ | ------- | ------------------------------------------------ |
| `tx_hash` | string | Polygon transaction hash |
| `order_hash` | string | Order hash from the exchange |
| `timestamp` | integer | Unix seconds when the fill settled |
| `maker` | string | Maker address |
| `taker` | string | Taker address |
| `token_id` | string | The outcome token id traded in this fill |
| `direction` | string | `"BUY"` or `"SELL"` from the maker's perspective |
| `price` | number | USDC per share, 4-decimal precision |
| `shares` | number | Outcome tokens traded |
| `usd` | number | USDC notional |
| `fee` | number | Maker fee paid (USDC) |
## Example: full lifetime trade history of a wallet
```bash theme={null}
curl "https://api.polynode.dev/v2/onchain/wallets/0x2f5653a3761f65c5a299f9839eadbd4d4d679ffa/trades/all?key=$YOUR_KEY"
```
## Example: only trades from the last 30 days
```bash theme={null}
NOW=$(date +%s); FROM=$((NOW - 2592000))
curl "https://api.polynode.dev/v2/onchain/wallets/0x2f5653a3761f65c5a299f9839eadbd4d4d679ffa/trades/all?from=$FROM&to=$NOW&key=$YOUR_KEY"
```
## Errors
`403 Tier required`:
```json theme={null}
{ "error": "/trades/all requires the Growth plan or above. Your tier: starter. See polynode.dev/pricing." }
```
`401 No API key`:
```json theme={null}
{ "error": "API key required. Pass via ?key= or x-api-key header." }
```
## Notes
* `partial: true` results are **not cached**. Subsequent calls re-attempt the full walk. Tighten the time window with `from`/`to` if you keep hitting the cap.
* Cache key is `(wallet, from_ts, to_ts)`. Different windows cached independently.
* For wallets that consistently exceed the 250K cap, slice into multiple windowed calls and concatenate client-side. Each window cached separately for 5 minutes.
* Trades include both maker AND taker sides — every fill the wallet was involved in. No double-counting.
# Midpoint Price
Source: https://docs.polynode.dev/api-reference/orderbook/midpoint
GET /v1/midpoint/{token_id}
Returns the midpoint price for a token, calculated as the average of the best bid and best ask.
If the book is one-sided (only bids or only asks), the missing side is treated as 0. Response is enriched with PolyNode market metadata.
Returns the midpoint price for a token or market, calculated as the average of the best bid and best ask.
## Identifier types
The `{token_id}` parameter accepts three types:
| Identifier | Format | Returns |
| ---------------- | --------------------- | --------------------------------------- |
| **Token ID** | All digits, 70+ chars | Single midpoint (Polymarket-compatible) |
| **Condition ID** | Starts with `0x` | Midpoints for both outcomes |
| **Slug** | Human-readable string | Midpoints for all outcomes in the event |
### By token ID
```bash theme={null}
curl "https://api.polynode.dev/v1/midpoint/114694726451307654528948558967898493662917070661203465131156925998487819889437" \
-H "x-api-key: YOUR_KEY"
```
```json theme={null}
{
"mid": "0.465",
"token_id": "114694726451307654528948558967898493662917070661203465131156925998487819889437",
"question": "Netanyahu out by end of 2026?",
"slug": "netanyahu-out-before-2027-684-719-226"
}
```
### By condition ID
```bash theme={null}
curl "https://api.polynode.dev/v1/midpoint/0xd1796c09d0d6f876f8580086ae9808ec991784e3a74b25a1830a25de71a78c96" \
-H "x-api-key: YOUR_KEY"
```
```json theme={null}
{
"condition_id": "0xd1796c09d0d6f876f8580086ae9808ec991784e3a74b25a1830a25de71a78c96",
"question": "Netanyahu out by end of 2026?",
"event_title": "Netanyahu out by...?",
"outcomes": [
{"outcome": "Yes", "token_id": "11469472645...", "mid": "0.465"},
{"outcome": "No", "token_id": "66255671088...", "mid": "0.535"}
]
}
```
## Calculation
```
midpoint = (best_bid + best_ask) / 2
```
If the book is one-sided (only bids or only asks), the missing side is treated as 0. This matches Polymarket's behavior.
## Notes
* The `mid` value is a decimal string (e.g. `"0.465"`).
* Returns a 404 if no book exists for the identifier.
# Order Book
Source: https://docs.polynode.dev/api-reference/orderbook/order-book
GET /v1/orderbook/{token_id}
Returns the full order book for a token, including all bid and ask price levels, market properties, and PolyNode metadata enrichment.
The response format is compatible with Polymarket's CLOB API. Data is served from PolyNode's live orderbook engine, which maintains real-time state via WebSocket deltas from Polymarket.
Returns the full order book for a Polymarket token or market.
## Identifier types
The `{token_id}` parameter accepts three identifier types. The response format depends on which type you use:
| Identifier | Format | Returns |
| ---------------- | --------------------- | --------------------------------------------------- |
| **Token ID** | All digits, 70+ chars | Single book for one outcome (Polymarket-compatible) |
| **Condition ID** | Starts with `0x` | Both outcomes for one market (Yes + No) |
| **Slug** | Human-readable string | All outcomes across all markets in the event |
### By token ID (single outcome)
Best when you already have a specific token ID and want the exact Polymarket-compatible format.
```bash theme={null}
curl "https://api.polynode.dev/v1/orderbook/114694726451307654528948558967898493662917070661203465131156925998487819889437" \
-H "x-api-key: YOUR_KEY"
```
Response matches Polymarket's CLOB `/book` format exactly, plus a `metadata` field:
```json theme={null}
{
"market": "0xd1796c09d0d6f876f8580086ae9808ec991784e3a74b25a1830a25de71a78c96",
"asset_id": "114694726451307654528948558967898493662917070661203465131156925998487819889437",
"timestamp": "1773901991806",
"hash": "b811dc1233a0f250a60018bc94f377955c9a513e",
"bids": [
{"price": "0.01", "size": "1130220"},
{"price": "0.02", "size": "76910"},
{"price": "0.46", "size": "18"}
],
"asks": [
{"price": "0.99", "size": "24292"},
{"price": "0.98", "size": "2048"},
{"price": "0.47", "size": "687"}
],
"min_order_size": "5",
"tick_size": "0.01",
"neg_risk": false,
"last_trade_price": "0.540",
"metadata": {
"question": "Netanyahu out by end of 2026?",
"slug": "netanyahu-out-before-2027-684-719-226",
"outcomes": ["Yes", "No"],
"condition_id": "0xd1796c09d0d6f876f8580086ae9808ec991784e3a74b25a1830a25de71a78c96",
"image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/netanyahu-out-in-2025-Vc7bE4GtiJzM.jpg"
}
}
```
### By condition ID (both outcomes)
Best when you want both sides of a market at once. Condition IDs start with `0x`.
```bash theme={null}
curl "https://api.polynode.dev/v1/orderbook/0xd1796c09d0d6f876f8580086ae9808ec991784e3a74b25a1830a25de71a78c96" \
-H "x-api-key: YOUR_KEY"
```
Response wraps both outcomes:
```json theme={null}
{
"condition_id": "0xd1796c09d0d6f876f8580086ae9808ec991784e3a74b25a1830a25de71a78c96",
"question": "Netanyahu out by end of 2026?",
"event_title": "Netanyahu out by...?",
"outcomes": [
{
"outcome": "Yes",
"token_id": "114694726451307654528948558967898493662917070661203465131156925998487819889437",
"market": "0xd1796c09d0d6f876f8580086ae9808ec991784e3a74b25a1830a25de71a78c96",
"bids": [{"price": "0.01", "size": "1130220"}, "..."],
"asks": [{"price": "0.99", "size": "24292"}, "..."],
"min_order_size": "5",
"tick_size": "0.01",
"neg_risk": false,
"last_trade_price": "0.540"
},
{
"outcome": "No",
"token_id": "66255671088804707681511323064315150986307471908131081808279119719218775249892",
"market": "0xd1796c09d0d6f876f8580086ae9808ec991784e3a74b25a1830a25de71a78c96",
"bids": [{"price": "0.01", "size": "821530"}, "..."],
"asks": [{"price": "0.99", "size": "13720.65"}, "..."],
"min_order_size": "5",
"tick_size": "0.01",
"neg_risk": false,
"last_trade_price": "0.540"
}
]
}
```
### By slug (all outcomes across markets)
Best for getting a complete view of an event. Slugs are the human-readable event identifiers from Polymarket (e.g. `netanyahu-out-before-2027`). An event may have multiple markets, each with their own outcomes.
```bash theme={null}
curl "https://api.polynode.dev/v1/orderbook/netanyahu-out-before-2027" \
-H "x-api-key: YOUR_KEY"
```
Response includes all outcomes across all markets in the event. Same format as the condition ID response, but with `slug` instead of `condition_id` at the top level.
## Fields (single-token response)
| Field | Type | Description |
| ------------------ | ------- | ------------------------------------------------------------------- |
| `market` | string | Market condition ID (hex) |
| `asset_id` | string | Token ID |
| `timestamp` | string | Unix timestamp in milliseconds |
| `hash` | string | 40-character hex hash of the book state |
| `bids` | array | Bid price levels, sorted ascending by price |
| `asks` | array | Ask price levels, sorted descending by price |
| `min_order_size` | string | Minimum order size for this market |
| `tick_size` | string | Minimum price increment (e.g. `"0.01"` or `"0.001"`) |
| `neg_risk` | boolean | Whether this is a neg-risk market |
| `last_trade_price` | string | Last traded price |
| `metadata` | object | PolyNode enrichment: question, slug, outcomes, condition\_id, image |
## Notes
* `tick_size` and `min_order_size` vary per market. They are sourced from the Gamma API, not hardcoded.
* Bids are sorted ascending by price (best bid is the last element). Asks are sorted descending (best ask is the last element). This matches Polymarket's sort order.
* If no book exists for the requested identifier, returns a 404 with `{"error": "No orderbook exists for the requested token id"}`.
# Bid-Ask Spread
Source: https://docs.polynode.dev/api-reference/orderbook/spread
GET /v1/spread/{token_id}
Returns the bid-ask spread for a token, calculated as the difference between the best ask and best bid price.
For one-sided books, the spread equals the best price on the existing side. Response is enriched with PolyNode market metadata.
Returns the bid-ask spread for a token or market, calculated as the difference between the best ask and best bid.
## Identifier types
The `{token_id}` parameter accepts three types:
| Identifier | Format | Returns |
| ---------------- | --------------------- | ------------------------------------- |
| **Token ID** | All digits, 70+ chars | Single spread (Polymarket-compatible) |
| **Condition ID** | Starts with `0x` | Spreads for both outcomes |
| **Slug** | Human-readable string | Spreads for all outcomes in the event |
### By token ID
```bash theme={null}
curl "https://api.polynode.dev/v1/spread/114694726451307654528948558967898493662917070661203465131156925998487819889437" \
-H "x-api-key: YOUR_KEY"
```
```json theme={null}
{
"spread": "0.01",
"token_id": "114694726451307654528948558967898493662917070661203465131156925998487819889437",
"question": "Netanyahu out by end of 2026?",
"slug": "netanyahu-out-before-2027-684-719-226"
}
```
### By condition ID
```bash theme={null}
curl "https://api.polynode.dev/v1/spread/0xd1796c09d0d6f876f8580086ae9808ec991784e3a74b25a1830a25de71a78c96" \
-H "x-api-key: YOUR_KEY"
```
```json theme={null}
{
"condition_id": "0xd1796c09d0d6f876f8580086ae9808ec991784e3a74b25a1830a25de71a78c96",
"question": "Netanyahu out by end of 2026?",
"event_title": "Netanyahu out by...?",
"outcomes": [
{"outcome": "Yes", "token_id": "11469472645...", "spread": "0.01"},
{"outcome": "No", "token_id": "66255671088...", "spread": "0.01"}
]
}
```
## Calculation
```
spread = best_ask - best_bid
```
For one-sided books (only bids or only asks), the spread equals the best price on the existing side.
## Notes
* The `spread` value is a decimal string (e.g. `"0.01"`).
* Returns a 404 if no book exists for the identifier.
# API Reference
Source: https://docs.polynode.dev/api-reference/overview
Start here for PolyNode REST APIs: V3 Polymarket data, wallet analytics, market data, orderbook snapshots, profiles, search, and legacy endpoints.
PolyNode exposes authenticated REST APIs for Polymarket market data, wallet analytics, positions, trades, builders, orderbooks, profiles, search, and supporting tools.
## Core APIs
| API | Use it for |
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| [V3 Data API](/data/overview) | Polymarket markets, positions, trades, wallet P\&L, builders, combos, rewards, tags, fees, and global stats. |
| [Wallet P\&L](/data/wallets/summary) | Wallet summaries, realized P\&L, P\&L events, open positions, and wallet activity. |
| [Markets & Search](/data/markets/search) | Market discovery, condition lookups, market-level positions, prices, trades, and candles. |
| [Orderbook](/api-reference/orderbook/order-book) | Current orderbook, midpoint, and spread snapshots. |
| [Profiles](/api-reference/polymarket-profiles/overview) | Polymarket profile lookup and username setup flows. |
| [Onchain V2](/api-reference/onchain/trades) | Legacy onchain V2 trade, position, redemption, volume, and candle endpoints. |
## Base URLs
| Surface | Base URL |
| ----------------- | --------------------------------- |
| REST API | `https://api.polynode.dev` |
| WebSocket streams | `wss://polynode.dev/ws` |
| Orderbook stream | `wss://polynode.dev/orderbook/ws` |
## Authentication
Send your API key with either `x-api-key`, `Authorization: Bearer `, or `?key=`.
## Rate Limits
REST and streaming limits vary by plan. See [Rate Limits](/guides/rate-limits) for the current public limits, including the separate bucket for heavy trade-history endpoints.
# Complete Username Action
Source: https://docs.polynode.dev/api-reference/polymarket-profiles/complete-username
POST /v3/polymarket/profiles/username/complete
Submit wallet signatures and create or change the user's Polymarket username.
```text theme={null}
POST /v3/polymarket/profiles/username/complete
```
Completes a previously created username challenge. This endpoint consumes the challenge atomically, verifies both wallet signatures, logs in to Polymarket, creates the profile if needed, and sets the username.
Call this from your backend with the same PolyNode API key that created the challenge. Do not call it directly from the browser.
## Request
```json theme={null}
{
"challenge_id": "pmprof_652e85a9839fe3e97348a72ff5018ef5",
"address": "0xC3579b0C908F43d50C63951A30C6f72fCEA4F6A0",
"username": "alice123",
"polymarket_signature": "0x...",
"consent_signature": "0x..."
}
```
| Field | Type | Required | Description |
| ---------------------- | ------ | -------- | ----------------------------------------------------------------- |
| `challenge_id` | string | Yes | Challenge id returned by the challenge endpoint |
| `address` | string | Yes | Same EOA address used in the challenge |
| `username` | string | Yes | Same username used in the challenge |
| `polymarket_signature` | string | Yes | Signature of `challenge.polymarket.message` using `personal_sign` |
| `consent_signature` | string | Yes | Signature of `challenge.consent` using `eth_signTypedData_v4` |
## Example
```bash theme={null}
curl -X POST "https://api.polynode.dev/v3/polymarket/profiles/username/complete" \
-H "content-type: application/json" \
-H "x-api-key: pn_live_..." \
-d '{
"challenge_id": "pmprof_652e85a9839fe3e97348a72ff5018ef5",
"address": "0xC3579b0C908F43d50C63951A30C6f72fCEA4F6A0",
"username": "alice123",
"polymarket_signature": "0x...",
"consent_signature": "0x..."
}'
```
## Response
```json theme={null}
{
"status": "success",
"action": "set_username",
"username": "alice123",
"address": "0xc3579b0c908f43d50c63951a30c6f72fcea4f6a0",
"deposit_wallet": "0xf6d30d58add6c6814d1b086ec543a16ab6cc9a31",
"safe_address": "0xba2ef45d0fe68cee6351420b60fb554bbe91bf02",
"gamma_user_id": "8313180",
"gamma_profile_id": "8391726",
"created_profile": true,
"changed_username": true,
"profile_url": "https://polymarket.com/profile/0xf6d30d58add6c6814d1b086ec543a16ab6cc9a31"
}
```
| Field | Type | Description |
| ------------------ | -------------- | ------------------------------------------------------- |
| `status` | string | `success` |
| `action` | string | `set_username` |
| `username` | string | Final requested username |
| `address` | string | EOA address, lowercase |
| `deposit_wallet` | string | Deterministic Polymarket deposit wallet |
| `safe_address` | string | Deterministic Safe address checked during challenge |
| `gamma_user_id` | string or null | Polymarket Gamma user id if returned |
| `gamma_profile_id` | string or null | Polymarket Gamma profile id if returned |
| `created_profile` | boolean | `true` if PolyNode created a new Gamma profile |
| `changed_username` | boolean | `true` if PolyNode changed the profile name |
| `profile_url` | string | Polymarket profile URL using the deposit wallet address |
## Server checks
The endpoint:
* loads and consumes the challenge atomically
* requires the same PolyNode API key hash that created the challenge
* requires exact address, username, action, and challenge id match
* verifies the challenge is not expired
* recovers the signer from the Polymarket SIWE signature
* recovers the signer from the PolyNode consent typed-data signature
* requires both recovered addresses to equal the challenge EOA
* re-checks username availability unless the current profile already has that username
## Errors
| Status | Code | Meaning |
| ------ | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| `400` | `challenge_mismatch` | API key, address, username, action, or challenge id did not match |
| `400` | `challenge_expired` | Challenge is older than 10 minutes |
| `400` | `signature_invalid` | A signature was malformed or recovered to the wrong address |
| `404` | `challenge_not_found` | Challenge was missing, already consumed, or expired from Redis |
| `409` | `username_taken` | Username became unavailable before completion |
| `409` | `safe_already_deployed` | Derived Safe is deployed but Gamma did not return a user for the session |
| `403` | `polymarket_compliance_blocked` | Polymarket rejected the action for compliance or eligibility reasons |
| `429` | `rate_limited` | Profile-specific rate limit exceeded |
| `502` | `polymarket_login_failed`, `polymarket_profile_create_failed`, `polymarket_profile_update_failed`, or `upstream_unavailable` | Upstream action failed |
# Create Username Challenge
Source: https://docs.polynode.dev/api-reference/polymarket-profiles/create-challenge
POST /v3/polymarket/profiles/username/challenge
Create the wallet-signing challenge for a Polymarket username create or update.
```text theme={null}
POST /v3/polymarket/profiles/username/challenge
```
Creates a one-time challenge for a user-owned EOA. The response contains:
* a Polymarket SIWE message to sign with `personal_sign`
* a PolyNode consent typed-data payload to sign with `eth_signTypedData_v4`
* derived deposit wallet and Safe addresses
The challenge expires after 10 minutes.
## Request
```json theme={null}
{
"address": "0xC3579b0C908F43d50C63951A30C6f72fCEA4F6A0",
"username": "alice123",
"action": "set_username"
}
```
| Field | Type | Required | Description |
| ---------- | ------ | -------- | --------------------------------------------------- |
| `address` | string | Yes | User EOA address |
| `username` | string | Yes | Desired username |
| `action` | string | No | Must be `set_username`. Defaults to `set_username`. |
## Example
```bash theme={null}
curl -X POST "https://api.polynode.dev/v3/polymarket/profiles/username/challenge" \
-H "content-type: application/json" \
-H "x-api-key: pn_live_..." \
-d '{
"address": "0xC3579b0C908F43d50C63951A30C6f72fCEA4F6A0",
"username": "alice123",
"action": "set_username"
}'
```
## Response
```json theme={null}
{
"challenge_id": "pmprof_652e85a9839fe3e97348a72ff5018ef5",
"address": "0xC3579b0C908F43d50C63951A30C6f72fCEA4F6A0",
"username": "alice123",
"action": "set_username",
"expires_at": "2026-05-26T13:03:06Z",
"polymarket": {
"signature_type": "personal_sign",
"message": "polymarket.com wants you to sign in with your Ethereum account:\n0xC3579b0C908F43d50C63951A30C6f72fCEA4F6A0\n\nWelcome to Polymarket! Sign to connect.\n\nURI: https://polymarket.com\nVersion: 1\nChain ID: 137\nNonce: 4b7d...\nIssued At: 2026-05-26T12:53:06.000Z\nExpiration Time: 2026-06-02T12:53:06.000Z",
"fields": {
"domain": "polymarket.com",
"address": "0xC3579b0C908F43d50C63951A30C6f72fCEA4F6A0",
"statement": "Welcome to Polymarket! Sign to connect.",
"uri": "https://polymarket.com",
"version": "1",
"chainId": 137,
"nonce": "4b7d...",
"issuedAt": "2026-05-26T12:53:06.000Z",
"expirationTime": "2026-06-02T12:53:06.000Z"
}
},
"consent": {
"signature_type": "typed_data_v4",
"domain": {
"name": "Polynode",
"version": "1",
"chainId": 137
},
"types": {
"EIP712Domain": [
{ "name": "name", "type": "string" },
{ "name": "version", "type": "string" },
{ "name": "chainId", "type": "uint256" }
],
"PolymarketProfileAction": [
{ "name": "action", "type": "string" },
{ "name": "address", "type": "address" },
{ "name": "username", "type": "string" },
{ "name": "challengeId", "type": "string" },
{ "name": "expiresAt", "type": "uint256" }
]
},
"primaryType": "PolymarketProfileAction",
"message": {
"action": "set_username",
"address": "0xc3579b0c908f43d50c63951a30c6f72fcea4f6a0",
"username": "alice123",
"challengeId": "pmprof_652e85a9839fe3e97348a72ff5018ef5",
"expiresAt": 1779800586
}
},
"derived": {
"deposit_wallet": "0xf6d30d58add6c6814d1b086ec543a16ab6cc9a31",
"safe_address": "0xba2ef45d0fe68cee6351420b60fb554bbe91bf02",
"safe_deployed": false
}
}
```
## Server checks
The endpoint:
* validates address and username format
* checks Polymarket username availability
* fetches a Gamma nonce
* builds the exact Polymarket SIWE message
* builds PolyNode consent typed data
* derives deposit wallet and Safe addresses
* checks whether the derived Safe is deployed on Polygon
* stores a one-time challenge under a 10 minute TTL
## Errors
| Status | Code | Meaning |
| ------ | --------------------------------------- | ---------------------------------------- |
| `400` | `invalid_address` or `invalid_username` | Request validation failed |
| `400` | `challenge_mismatch` | Unsupported action |
| `409` | `username_taken` | Username is not available |
| `429` | `rate_limited` | Profile-specific rate limit exceeded |
| `502` | `upstream_unavailable` | Polymarket, Gamma, or Polygon RPC failed |
# Get Public Profile
Source: https://docs.polynode.dev/api-reference/polymarket-profiles/get-profile
GET /v3/polymarket/profiles/{address}
Read public Polymarket profile state for an EOA address.
```text theme={null}
GET /v3/polymarket/profiles/{address}
```
Returns normalized public profile information from Polymarket for an EOA address.
## Path parameters
| Parameter | Type | Required | Description |
| --------- | ------ | -------- | ----------- |
| `address` | string | Yes | EOA address |
## Example
```bash theme={null}
curl "https://api.polynode.dev/v3/polymarket/profiles/0xC3579b0C908F43d50C63951A30C6f72fCEA4F6A0" \
-H "x-api-key: pn_live_..."
```
```json theme={null}
{
"address": "0xc3579b0c908f43d50c63951a30c6f72fcea4f6a0",
"proxy_wallet": "0xf6d30d58add6c6814d1b086ec543a16ab6cc9a31",
"username": "alice123",
"pseudonym": "Glistening-Risk",
"display_username_public": true,
"profile_image": null,
"x_username": null,
"verified_badge": false,
"source": "polymarket"
}
```
## Response fields
| Field | Type | Description |
| ------------------------- | --------------- | ------------------------------------------- |
| `address` | string | EOA address requested, lowercase |
| `proxy_wallet` | string or null | Polymarket wallet address returned by Gamma |
| `username` | string or null | Public profile username |
| `pseudonym` | string or null | Polymarket-generated pseudonym |
| `display_username_public` | boolean or null | Whether the username is public |
| `profile_image` | string or null | Profile image URL if present |
| `x_username` | string or null | Linked X username if present |
| `verified_badge` | boolean or null | Polymarket verified badge state |
| `source` | string | `polymarket` |
## Errors
| Status | Code | Meaning |
| ------ | ---------------------- | ------------------------------------------ |
| `400` | `invalid_address` | Address is not a valid EVM address |
| `404` | `profile_not_found` | Polymarket did not return a public profile |
| `429` | `rate_limited` | Profile-specific rate limit exceeded |
| `502` | `upstream_unavailable` | Polymarket/Gamma profile lookup failed |
# Polymarket Profiles
Source: https://docs.polynode.dev/api-reference/polymarket-profiles/overview
Create or change a Polymarket username from a user-owned EOA wallet.
The Polymarket Profiles API is a secure username provisioning flow for apps that manage their own users and wallets.
Use it when your user has an EOA wallet and you want to let them set up a Polymarket username without sending private keys or long-lived Polymarket credentials to PolyNode.
## Base URL
```text theme={null}
https://api.polynode.dev
```
## Authentication
Every request requires a paid PolyNode API key.
```bash theme={null}
curl "https://api.polynode.dev/v3/polymarket/profiles/username-available?username=alice123" \
-H "x-api-key: pn_live_..."
```
Your backend should call these endpoints. Do not expose your API key to browser clients.
## Endpoints
| Method | Endpoint | Description |
| ------ | ----------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
| `GET` | [`/v3/polymarket/profiles/username-available`](/api-reference/polymarket-profiles/username-available) | Check whether a username is available |
| `POST` | [`/v3/polymarket/profiles/username/challenge`](/api-reference/polymarket-profiles/create-challenge) | Create the two wallet-signing payloads |
| `POST` | [`/v3/polymarket/profiles/username/complete`](/api-reference/polymarket-profiles/complete-username) | Submit signatures and create or change the username |
| `GET` | [`/v3/polymarket/profiles/{address}`](/api-reference/polymarket-profiles/get-profile) | Read public profile state |
## Flow
1. Check the username.
2. Create a challenge for `{address, username, action}`.
3. User signs the Polymarket SIWE message with `personal_sign`.
4. User signs the PolyNode consent typed data with `eth_signTypedData_v4`.
5. Complete the action from your backend.
See the [Polymarket Profile Setup guide](/guides/polymarket-profile-setup) for a complete frontend and backend integration.
## Error shape
```json theme={null}
{
"error": "challenge_mismatch",
"message": "challenge fields do not match completion request"
}
```
Common error codes:
| Code | Meaning |
| ------------------------------- | -------------------------------------------------------------------- |
| `invalid_address` | Invalid EVM address |
| `invalid_username` | Username failed local validation |
| `username_taken` | Username is unavailable |
| `challenge_not_found` | Challenge expired, already consumed, or missing |
| `challenge_expired` | Challenge is older than 10 minutes |
| `challenge_mismatch` | Challenge fields or API key do not match |
| `signature_invalid` | Wallet signature did not recover to the requested EOA |
| `polymarket_compliance_blocked` | Polymarket rejected the action for compliance or eligibility reasons |
| `upstream_unavailable` | Polymarket, Gamma, or Polygon RPC failed |
| `rate_limited` | Profile-specific rate limit exceeded |
# Check Username Availability
Source: https://docs.polynode.dev/api-reference/polymarket-profiles/username-available
GET /v3/polymarket/profiles/username-available
Check whether Polymarket will accept a username.
```text theme={null}
GET /v3/polymarket/profiles/username-available
```
Use this endpoint before creating a challenge. It calls Polymarket's username availability check and returns the upstream result.
## Query parameters
| Parameter | Type | Required | Description |
| ---------- | ------ | -------- | ------------------------------------------------------------------------------------------------------ |
| `username` | string | Yes | Desired username. Local validation is `^[A-Za-z0-9_]{3,32}$`. Polymarket is still the final authority. |
## Example
```bash theme={null}
curl "https://api.polynode.dev/v3/polymarket/profiles/username-available?username=alice123" \
-H "x-api-key: pn_live_..."
```
```json theme={null}
{
"username": "alice123",
"available": true,
"source": "polymarket"
}
```
Unavailable usernames return `200` with `available: false`:
```json theme={null}
{
"username": "alice_123",
"available": false,
"source": "polymarket"
}
```
## Errors
| Status | Code | Meaning |
| ------ | ---------------------- | ---------------------------------------- |
| `400` | `invalid_username` | Missing username or invalid local format |
| `401` | varies | Missing or invalid PolyNode API key |
| `402` | varies | Paid plan required |
| `429` | `rate_limited` | Profile-specific rate limit exceeded |
| `502` | `upstream_unavailable` | Polymarket availability check failed |
# Market Card
Source: https://docs.polynode.dev/api-reference/pricing/market-statistics
GET /v1/stats/{token_id}
One-shot consolidated summary for a token — condition, outcomes, neg-risk flag, end date, last trade time, current orderbook liquidity, 24h OHLCV, and last price. Saves you from making 3–4 separate calls when you just need a market overview tile.
# Online Search API
Source: https://docs.polynode.dev/api-reference/search-online
GET /v2/search/online
Search the public web and return normalized organic results for agent context, market research, and enrichment workflows.
**Beta.** This endpoint is designed for low-volume context enrichment. Results are best-effort, cached, and may be partial when upstream search engines throttle or fail.
## Endpoint
```http theme={null}
GET /v2/search/online
```
Search the public web and return normalized organic results. Responses are cached to keep the endpoint lightweight.
```bash theme={null}
curl "https://api.polynode.dev/v2/search/online?q=Cavaliers%20Knicks%20injury%20news&max_results=5" \
-H "x-api-key: YOUR_KEY"
```
## Query parameters
| Parameter | Description |
| ------------------- | ---------------------------------------------------------------------------------------------------------- |
| `q` | Search query. Required. |
| `max_results` | Number of normalized organic results. Defaults to `10`, capped at `20`. |
| `engines` | Comma-separated engine adapters. Defaults to `bing,duckduckgo`. `brave` is available as an opt-in adapter. |
| `language` | Search language. Defaults to `en-US`. |
| `time_range` | Optional freshness filter when supported: `day`, `week`, `month`, `year`. |
| `safe_search` | Safe-search mode: `0`, `1`, or `2`. Defaults to `0`. |
| `cache_ttl_seconds` | Cache TTL for this query. Defaults to `600`, capped at `3600`. |
Rate limit: `3` requests per `10` seconds per API key, in addition to your normal tier limit.
## Response
```json theme={null}
{
"query": "Cavaliers Knicks injury news",
"source": "search_online",
"adapter": "searxng",
"status": "ok",
"cached": false,
"engines_requested": ["bing", "duckduckgo"],
"unresponsive_engines": [],
"result_count": 2,
"results": [
{
"title": "Cleveland Cavaliers Injury Status - ESPN",
"url": "https://www.espn.com/nba/team/injuries/_/name/cle/cleveland-cavaliers",
"domain": "espn.com",
"snippet": "Visit ESPN for the current injury situation...",
"engines": ["duckduckgo"],
"score": 1,
"category": "general",
"published_at": null
}
],
"answers": [],
"suggestions": [],
"fetched_at": "2026-05-25T11:10:00.000Z",
"elapsed_ms": 1200
}
```
`status` may be `partial` when one engine fails but other engines still return results.
# Sports Markets
Source: https://docs.polynode.dev/api-reference/sports/sports-markets
PolyNode sports endpoints expose Polymarket-native sports market data: leagues, teams, games, markets, token IDs, current CLOB prices, and historical price series.
These endpoints replace the old sportsbook/OddsJam experiment. They do not return sportsbook odds. They return Polymarket market prices and metadata, normalized around sports games.
## Endpoints
| Endpoint | Description |
| --------------------------------------- | -------------------------------------------------------------------------------------------- |
| `GET /v2/sports/leagues` | Sports and esports league metadata from Polymarket Gamma |
| `GET /v2/sports/summary` | League-level active/live/date summary |
| `GET /v2/sports/live` | Backward-compatible live overview derived from `/sports/summary` |
| `GET /v2/sports/leagues/{league}/teams` | Teams for a league, with logos and records when available |
| `GET /v2/sports/leagues/{league}/games` | Games/events for a league, including markets, token IDs, and Gamma fallback metadata |
| `GET /v2/sports/games/{slug}` | Full game/event detail with normalized markets |
| `GET /v2/sports/games/{slug}/state` | Canonical game state: metadata, score fields, markets, current prices, and subscribe payload |
| `GET /v2/sports/games/{slug}/context` | Agent-friendly context bundle from generated game queries and optional external sources |
| `GET /v2/sports/games/{slug}/prices` | Current bid, ask, midpoint, and spread for game tokens |
| `GET /v2/sports/games/{slug}/history` | Batch CLOB price history for game tokens |
| `GET /v2/sports/market-types` | Polymarket sports market type codes with PolyNode labels/categories |
| `GET /v2/sports/search?q={query}` | Search sports events and teams |
## Game state
## League games
Use league games for schedule discovery, such as listing all active FIFA World Cup match markets:
```bash theme={null}
curl "https://api.polynode.dev/v2/sports/leagues/fifwc/games?status=active&sort=startDate&direction=asc&limit=50" \
-H "x-api-key: YOUR_KEY"
```
The response is filtered and sorted by PolyNode after resolving the league code to Polymarket's sports `series_id`. If Polymarket Gamma's primary event listing is unavailable, the response can still succeed through alternate Gamma sources and will include `fallback: true`. If Gamma is fully unavailable and PolyNode has a recent good response, it returns that cached response with `stale: true`.
Use this when you want one game-centric object instead of manually joining game metadata, CLOB prices, markets, and websocket subscription details.
```bash theme={null}
curl "https://api.polynode.dev/v2/sports/games/nba-cle-nyk-2026-05-31/state?price_limit_tokens=20" \
-H "x-api-key: YOUR_KEY"
```
```json theme={null}
{
"slug": "nba-cle-nyk-2026-05-31",
"title": "Cavaliers vs. Knicks",
"status": "scheduled",
"source": {
"metadata": "polymarket_gamma",
"prices": "polymarket_clob",
"history": null,
"score": "polymarket_gamma"
},
"score": {
"source": "gamma:event",
"score": null,
"period": "NS",
"live": null,
"freshness": "unknown"
},
"markets": [
{
"question": "Cavaliers vs. Knicks",
"sportsMarketType": "moneyline",
"clobTokenIds": ["..."],
"prices": [
{
"outcome": "Cavaliers",
"best_bid": "0.31",
"best_ask": "0.47",
"midpoint": "0.39",
"spread": "0.16"
}
]
}
],
"subscribe": {
"ws": "wss://ws.polynode.dev/ws",
"action": "subscribe",
"type": "orderbook",
"token_ids": ["..."]
}
}
```
Add `include_history=true` to include CLOB price history in the same response:
```bash theme={null}
curl "https://api.polynode.dev/v2/sports/games/nba-cle-nyk-2026-05-31/state?include_history=true&history_limit_tokens=2&interval=1d" \
-H "x-api-key: YOUR_KEY"
```
## Game context
Use this for agent workflows that need game-specific search context next to the normalized sports object. PolyNode generates a small set of matchup, injury, lineup, news, or market-specific queries from the Polymarket game record, then runs the requested sources.
```bash theme={null}
curl "https://api.polynode.dev/v2/sports/games/nba-cle-nyk-2026-05-31/context?sources=x,online&query_set=injuries&max_queries=2&max_per_query=5&include_state=true&price_limit_tokens=20" \
-H "x-api-key: YOUR_KEY"
```
```json theme={null}
{
"slug": "nba-cle-nyk-2026-05-31",
"title": "Cavaliers vs. Knicks",
"query_set": "injuries",
"generated_queries": [
{
"id": "matchup-injuries",
"query": "Cavaliers Knicks injury",
"reason": "Availability and injury context for both teams."
}
],
"sources_requested": ["x", "online"],
"sources": {
"x": {
"status": "ok",
"result_count": 5,
"queries": [
{
"query": "Cavaliers Knicks injury",
"tweets": []
}
]
},
"online": {
"status": "ok",
"source": "search_online",
"adapter": "searxng",
"queries": []
}
},
"state": {
"slug": "nba-cle-nyk-2026-05-31",
"markets": []
}
}
```
Parameters:
| Parameter | Description |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `sources` | Comma-separated sources. Defaults to `x`. Supported: `x`, `online`. `web`, `search`, `ddg`, and `ddg_instant` alias to `online`. |
| `query_set` | Query strategy. Supported: `default`, `injuries`, `lineups`, `news`, `social`, `markets`. |
| `max_queries` | Number of generated queries to run. Defaults to `3`, capped at `5`. |
| `max_per_query` | Per-source result count per query. Defaults to `5`, capped at `10`. |
| `include_state` | Include the `/state` response in the same payload. Defaults to `false`. |
| `price_limit_tokens` | Token cap used when `include_state=true`. Defaults to `20`, capped at `200`. |
The `x` source uses the same X Search beta quota and 1 request/second key-level rate limit as `/v2/x/search`. Each generated X query consumes one X Search quota unit. The `online` source uses `/v2/search/online` and returns normalized public web results.
## Current prices
```bash theme={null}
curl "https://api.polynode.dev/v2/sports/games/nba-nyk-cle-2026-05-25/prices?limit_tokens=10" \
-H "x-api-key: YOUR_KEY"
```
```json theme={null}
{
"slug": "nba-nyk-cle-2026-05-25",
"title": "Knicks vs. Cavaliers",
"source": "polymarket_clob",
"count": 10,
"total_tokens": 106,
"truncated": true,
"tokens": [
{
"token_id": "36872217940161491972120830074682109141065398170878601426051147635064624582652",
"outcome": "Knicks",
"question": "Knicks vs. Cavaliers",
"market_type": "moneyline",
"best_bid": "0.56",
"best_ask": "0.57",
"midpoint": "0.565",
"spread": "0.01"
}
],
"subscribe": {
"ws": "wss://ws.polynode.dev/ws",
"action": "subscribe",
"type": "orderbook",
"token_ids": ["..."]
}
}
```
## Price history
```bash theme={null}
curl "https://api.polynode.dev/v2/sports/games/nba-nyk-cle-2026-05-25/history?limit_tokens=2" \
-H "x-api-key: YOUR_KEY"
```
`limit_tokens` defaults to `20` and is capped at `20` because Polymarket's batch history endpoint caps requests at 20 market asset IDs. If you do not pass `interval`, `start_ts`, or `end_ts`, PolyNode requests the full available CLOB history with `interval=max`.
```json theme={null}
{
"slug": "nba-nyk-cle-2026-05-25",
"source": "polymarket_clob",
"count": 2,
"total_tokens": 106,
"truncated": true,
"tokens": [
{
"token_id": "36872217940161491972120830074682109141065398170878601426051147635064624582652",
"outcome": "Knicks",
"history": [
{ "t": 1779615604, "p": 0.555 }
]
}
]
}
```
## Notes
* `prices` fans out to Polymarket CLOB batch price, midpoint, and spread endpoints.
* `history` uses Polymarket CLOB batch price history. Bare `/history` calls default to `interval=max`; pass `interval=1d`, `interval=1w`, or an explicit `start_ts`/`end_ts` window when you want a smaller range.
* Scores are included only where Polymarket Gamma has score fields on the event. Use score fields as best-effort metadata, not guaranteed live scoreboard state.
* For live orderbook updates, use the returned `subscribe.token_ids` with PolyNode orderbook WebSocket.
# Generate API Key
Source: https://docs.polynode.dev/api-reference/system/generate-api-key
POST /v1/keys
Generates a new API key. Rate limited to 1 per IP per day. The key is returned only once and cannot be retrieved again.
# Health Check
Source: https://docs.polynode.dev/api-reference/system/health-check
GET /healthz
Liveness probe. Returns 200 OK if the server is running.
# Readiness Check
Source: https://docs.polynode.dev/api-reference/system/readiness-check
GET /readyz
Returns 200 if node is connected and Redis is reachable, 503 otherwise.
# System Status
Source: https://docs.polynode.dev/api-reference/system/system-status
GET /v1/status
Returns system status including node connectivity, uptime, stream lengths, and state engine summary.
# Top Traders (Market)
Source: https://docs.polynode.dev/api-reference/wallets/market-positions
GET /v1/markets/{id}/positions
Every wallet that holds (or has held) a position in a single market, grouped by outcome and sorted by P&L. The 'who's making money on this market' feed.
Returns every wallet with a position in a single market, broken out by outcome (e.g. Up/Down or Yes/No) and ranked by P\&L. The natural feed for "top traders on this market" leaderboards, top-X-by-PnL widgets, and per-market trader audits.
Path accepts either the **market slug** (e.g. `btc-updown-5m-1777179000`) or the **condition\_id** (e.g. `0xa7ae8a41...`). It does **not** accept an outcome token id — pass the market identifier, not the side.
Field shape is camelCase (`avgPrice`, `realizedPnl`, etc.) — distinct from polynode's V2 onchain endpoints which use snake\_case. Different schemas, different use cases. Default cap is 50 rows per outcome; configurable up to 500.
## Request
```
GET /v1/markets/{slug-or-condition-id}/positions
```
| Parameter | Type | Location | Description |
| --------------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | string | path | Market slug or condition\_id (`0x…`). **Not** an outcome token id. |
| `limit` | integer | query | Max rows per outcome. Default `50`, max `500`. |
| `offset` | integer | query | Skip first N rows. |
| `sortBy` | string | query | `TOTAL_PNL` (default), `REALIZED_PNL`, `CURRENT_VALUE`, `SIZE`, `INITIAL_VALUE`. |
| `sortDirection` | string | query | `DESC` (default) or `ASC`. |
| `status` | string | query | `OPEN`, `CLOSED`, or `ALL` (default). |
| `min_size` | number | query | Drop positions with `size < min_size` from each outcome. Use `min_size=0.0001` to filter to current holders only (excludes historical participants who closed out to zero). |
| `includeTrades` | boolean | query | When `true`, adds `firstTradeAt` / `lastTradeAt` (unix seconds) per row. Heavy — separate 20 req/min rate limit per key. |
| `user` | string | query | Filter to a single wallet, or up to 20 wallets comma-separated, scoped to this market. |
`TOTAL_PNL` and `REALIZED_PNL` are sorted by Polymarket's data-api directly. `CURRENT_VALUE`, `SIZE`, and `INITIAL_VALUE` are sorted in-process after the fetch (and after `min_size` if set), so they pair cleanly with the holder-filtering use case.
## Response
```json theme={null}
{
"condition_id": "0xa7ae8a4119fe00f231e693ed717339dd1e13da4617c79f3b1522ab1aee3965b6",
"market_title": "Bitcoin Up or Down - April 26, 12:50AM-12:55AM ET",
"slug": "btc-updown-5m-1777179000",
"image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/BTC+fullsize.png",
"outcome_names": ["Up", "Down"],
"outcomes": [
{
"token": "90684694382683135738693577247532961739938461809085935450142382916430459093778",
"positions": [
{
"proxyWallet": "0x9a1d392572d0e6bfefbf9302101b9e44c8ee86d6",
"name": "kq9000",
"profileImage": "",
"verified": false,
"asset": "90684694382683135738693577247532961739938461809085935450142382916430459093778",
"conditionId": "0xa7ae8a4119fe00f231e693ed717339dd1e13da4617c79f3b1522ab1aee3965b6",
"outcome": "Up",
"outcomeIndex": 0,
"size": 0,
"avgPrice": 0.4592,
"currPrice": 1,
"currentValue": 0,
"totalBought": 1633.1987,
"realizedPnl": 812.5754,
"cashPnl": 0,
"totalPnl": 812.5754,
"firstTradeAt": 1777179030,
"lastTradeAt": 1777179152
}
]
},
{ "token": "96824…", "positions": [ "…" ] }
]
}
```
### Top-level fields
| Field | Type | Description |
| ------------------------ | --------- | -------------------------------------------------------------- |
| `condition_id` | string | Market condition id |
| `market_title` | string | Human-readable market question |
| `slug` | string | Market slug |
| `image` | string | Market image URL |
| `outcome_names` | string\[] | Ordered outcome labels (`["Up","Down"]`, `["Yes","No"]`, etc.) |
| `outcomes[]` | array | One entry per outcome — each holds a `positions` array |
| `outcomes[].token` | string | Outcome token id |
| `outcomes[].positions[]` | array | Traders with positions in this outcome, sorted by `sortBy` |
### Per-trader fields (`outcomes[].positions[]`)
| Field | Type | Description |
| -------------- | ------- | ------------------------------------------------------------------- |
| `proxyWallet` | string | Trader address (Gnosis Safe proxy) |
| `name` | string | Display name (often empty or auto-generated) |
| `profileImage` | string | Profile image URL (often empty) |
| `verified` | boolean | Verified flag |
| `asset` | string | Outcome token id (same as `outcomes[].token`) |
| `conditionId` | string | Market condition id |
| `outcome` | string | Outcome label (`"Up"`, `"No"`, etc.) |
| `outcomeIndex` | number | 0-based position in `outcome_names` |
| `size` | number | Current token balance. `0` = fully exited or never held. |
| `avgPrice` | number | Volume-weighted average entry price |
| `currPrice` | number | Current market price (or terminal `1.0`/`0.0` for resolved markets) |
| `currentValue` | number | Mark-to-market value of remaining shares (`size × currPrice`) |
| `totalBought` | number | Lifetime tokens acquired |
| `realizedPnl` | number | Realized P\&L from sells + redemptions, in USDC |
| `cashPnl` | number | P\&L from cash flows (separate accounting axis) |
| `totalPnl` | number | Net portfolio P\&L for this position |
| `firstTradeAt` | number | Unix seconds — only with `?includeTrades=true` |
| `lastTradeAt` | number | Unix seconds — only with `?includeTrades=true` |
## Examples
### Top 10 traders on a market by total P\&L
```bash theme={null}
curl "https://api.polynode.dev/v1/markets/btc-updown-5m-1777179000/positions?limit=10&sortBy=TOTAL_PNL&sortDirection=DESC" \
-H "x-api-key: YOUR_KEY"
```
### By condition\_id, with first/last trade timestamps
```bash theme={null}
curl "https://api.polynode.dev/v1/markets/0xa7ae8a4119fe00f231e693ed717339dd1e13da4617c79f3b1522ab1aee3965b6/positions?includeTrades=true&limit=20" \
-H "x-api-key: YOUR_KEY"
```
### Drill into specific wallets within a market
```bash theme={null}
curl "https://api.polynode.dev/v1/markets/btc-updown-5m-1777179000/positions?user=0x9a1d392572d0e6bfefbf9302101b9e44c8ee86d6,0x44bd2993a69d8b569859ed8c0bf0b946f733f71a" \
-H "x-api-key: YOUR_KEY"
```
### Closed positions only
```bash theme={null}
curl "https://api.polynode.dev/v1/markets/btc-updown-5m-1777179000/positions?status=CLOSED&sortBy=REALIZED_PNL" \
-H "x-api-key: YOUR_KEY"
```
## Notes
* **Default cap of 50 rows** per outcome unless `limit` is set higher (max 500). On very large markets with deep tail traders, paginate with `offset`.
* **Sort ties on equal P\&L are non-deterministic** — two wallets at the same `totalPnl` may swap positions between calls. Use `proxyWallet` as a stable secondary key on the client side if you need consistent ordering.
* **`size = 0` is normal** on closed positions, redeemed positions, or fully-exited positions. Use `realizedPnl` and `totalBought` to detect history.
* **`firstTradeAt` / `lastTradeAt` require `?includeTrades=true`** and trip a separate heavy-endpoint rate limit (20 req/min per key). Don't request it on every refresh — fetch once and cache.
## When to use this vs. other position endpoints
| If you want | Use |
| ------------------------------------------------ | -------------------------------------------------------------------------------------- |
| Top traders ranked on a single market | **This endpoint** |
| One wallet's positions across all markets | [`GET /v2/wallets/{addr}/positions/onchain`](/api-reference/wallets/onchain-positions) |
| All trades on a market (chronological fill feed) | [`GET /v2/onchain/markets/{tokenId}/trades`](/api-reference/onchain/market-trades) |
# Positions & P&L (Wallet)
Source: https://docs.polynode.dev/api-reference/wallets/onchain-positions
GET /v2/wallets/{address}/positions/onchain
Every position a wallet has held — open and closed — with realized + unrealized P&L, per-position activity timestamps, market resolution timestamps, and parent event slugs. Optional time-window filtering.
Returns every position a wallet has ever held, including closed positions that no longer appear in the standard positions endpoint. Each position includes precomputed `realized_pnl`, `avg_price`, `total_bought`, plus per-position activity, redemption, and resolution timestamps that let you build leaderboards, trim to a 30-day or 90-day window, or filter to a parent event.
The standard `/v1/wallets/{addr}/positions` endpoint only returns open positions. Once a position is fully exited or a market resolves, it disappears. This endpoint fills that gap.
## Request
```
GET /v2/wallets/{address}/positions/onchain
```
| Parameter | Type | Description |
| ---------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `address` | path | Wallet address (0x-prefixed, 40 hex chars) |
| `since` | query (optional) | Unix seconds. Drop positions whose `last_trade_at` is before this timestamp. |
| `until` | query (optional) | Unix seconds. Drop positions whose `last_trade_at` is after this timestamp. |
| `tag_slug` | query (optional) | Keep only positions whose market carries this Polymarket event tag (e.g. `nba`, `politics`, `crypto`, `2026-fifa-world-cup-winner-595`). Matches against the per-market `tag_slugs` array. See [Filtering by tag](#filtering-by-tag). |
`since`, `until`, and `tag_slug` are all **opt-in**. When none are supplied, the response is identical to a request with no parameters at all — same shape, same aggregates over the full position set. When any is supplied, positions are filtered and aggregates are recomputed (see [Filtering](#filtering-by-activity-window)).
## Response
```json theme={null}
{
"wallet": "0xbddf61af533ff524d27154e589d2d7a81510c684",
"source": "onchain",
"count": 879,
"open_count": 340,
"closed_count": 288,
"total_realized_pnl": 17183579.48,
"total_unrealized_pnl": -14346997.32,
"total_pnl": 2836582.16,
"positions_with_pnl": 309,
"positions": [
{
"token_id": "100424505911492726143069668369640898890012859227903329249368753635028896002613",
"size": 0,
"avg_price": 0.63,
"realized_pnl": 147588.53019,
"unrealized_pnl": 0,
"current_price": null,
"market_status": "closed",
"total_bought": 398887.919433,
"market": "Will Fulham FC win on 2026-03-21?",
"slug": "epl-ful-bur-2026-03-21-ful",
"event_slug": "epl-ful-bur-2026-03-21",
"outcome": "Yes",
"image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/Repetitive-markets/premier+league.jpg",
"condition_id": "0xae68634cb01137acbe26e65790f7268a523ee1519601f2576bd7b067c7c9f9da",
"last_trade_at": 1742568134,
"closed_at": 1742573012,
"resolved_at": 1742572840
}
]
}
```
| Field | Type | Description |
| ---------------------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `wallet` | string | Queried wallet address (lowercased) |
| `source` | string | Always `"onchain"` |
| `count` | number | Total positions returned |
| `open_count` | number | Positions with `size > 0` |
| `closed_count` | number | Positions with `size = 0` and nonzero `realized_pnl` |
| `total_realized_pnl` | number | Sum of `realized_pnl` across all returned positions |
| `total_unrealized_pnl` | number | Sum of `unrealized_pnl` across all returned positions. Reflects open-size paper P\&L at current (or terminal) market prices. |
| `total_pnl` | number | `total_realized_pnl + total_unrealized_pnl`. Matches the net portfolio PnL shown on a Polymarket profile page. |
| `positions_with_pnl` | number | Positions where `realized_pnl != 0` |
| `filtered` | boolean | Present **only** when `since` or `until` was supplied. Always `true` in that case. Absent in default responses. |
| `applied_filters` | object | Present **only** when filtering. Echoes `{since, until}` (each is the supplied unix seconds or `null`). Absent in default responses. |
| `positions[].token_id` | string | CTF token ID (called `asset` on the [closed-positions](/api-reference/wallets/closed-positions) endpoint) |
| `positions[].size` | number | Current token balance (0 = fully exited) |
| `positions[].avg_price` | number | Volume-weighted average entry price |
| `positions[].realized_pnl` | number | Realized profit/loss in USDC |
| `positions[].unrealized_pnl` | number | Paper P\&L on remaining open shares. For resolved markets, uses the terminal settlement price (1.0 winner / 0.0 loser). For live markets, uses the current market price. `0` when `size = 0`. |
| `positions[].current_price` | number \| null | Price used for `unrealized_pnl`. `1.0` or `0.0` on resolved markets, live market price on active markets, `null` when `size = 0` or no price is available. |
| `positions[].market_status` | string | `"live"` / `"resolved-win"` / `"resolved-loss"` / `"resolved-unknown"` / `"closed"`. See [positions feed](/api-reference/onchain/positions#position-fields) for full enum. |
| `positions[].total_bought` | number | Total tokens acquired for this position |
| `positions[].market` | string | Market question |
| `positions[].slug` | string | **Market** slug — identifies the specific market within an event (e.g. `nba-bos-cle-2026-03-08-spread`). |
| `positions[].event_slug` | string \| null | **Event** slug — identifies the parent event a market belongs to (e.g. `nba-bos-cle-2026-03-08`). For single-market events, equals `slug`. For multi-market events (NBA games with several lines, election markets with several candidates, FIFA World Cup with one market per team, etc.) this is the parent the markets share. `null` when the event metadata is not yet known. |
| `positions[].outcome` | string | Outcome label (e.g. "Yes", "No", "Up") |
| `positions[].image` | string | Market image URL |
| `positions[].condition_id` | string | Market condition ID |
| `positions[].last_trade_at` | number \| null | Unix seconds. Latest fill (maker or taker) on this token across V1 + V2 exchanges. `null` when polynode has no fill history for the token (e.g. positions acquired purely via splits/merges or NegRisk conversion with no later trade). |
| `positions[].closed_at` | number \| null | Unix seconds. Latest moment this wallet redeemed any outcome of this market's `condition_id` for collateral. `null` when the wallet has not redeemed (positions sold to zero before resolution, or open positions, or wins held but not yet redeemed). |
| `positions[].resolved_at` | number \| null | Unix seconds. Moment the market was resolved on-chain (when payouts became redeemable). `null` for markets that resolved before polynode began tracking, or for markets that have not yet resolved. Recent markets are fully covered. |
| `positions[].tag_slugs` | string\[] \| null | Polymarket event-level tags applied to this market. Examples: `["NBA","Basketball","Sports","All"]` for an NBA game market, `["Crypto","Up or Down","Recurring","All"]` for a 5-min crypto market. Order is most-specific → most-general. Tags live on the parent event (markets sharing an `event_slug` share the same `tag_slugs`). `null` only on the rare event that doesn't carry any tag or hasn't been ingested yet. |
## Distinguishing the three timestamp fields
These three timestamp fields look similar but answer different questions. Choosing the right one matters when filtering:
| Field | Whose action | When |
| --------------- | ----------------------------------------------------------------- | --------------------------------------------------------------- |
| `last_trade_at` | The **wallet**'s most recent fill on this exact token | Each fill event (buy or sell) updates this |
| `closed_at` | The **wallet**'s most recent collateral redemption for the market | Set when the wallet calls `redeemPositions` on the CTF contract |
| `resolved_at` | The **market itself** (not the wallet) becoming redeemable | Set once per market, when the oracle reports payouts on-chain |
A position can have `resolved_at` populated and `closed_at` null — the market resolved but the wallet has not yet redeemed. A position can have `closed_at` set and `resolved_at` null — the wallet redeemed during a window polynode does not have full historical resolution coverage for, but the redemption itself was observed.
For windowing a leaderboard to "the last 30 days of trading activity," `last_trade_at` is the right field — it captures every position the wallet was active in. For "positions resolved in the last 90 days," `resolved_at` is the right one. For "positions the wallet has actually closed out," `closed_at` is the right one.
## Filtering by activity window
`since` and `until` are optional unix-seconds query parameters that filter positions by `last_trade_at`. Designed to support 30d / 90d / 365d leaderboard windows without fetching multi-year history just to trim it client-side.
### Behavior
* Both bounds are **inclusive**.
* Positions with `last_trade_at = null` are dropped under any active filter — there's no timestamp to compare against.
* All seven aggregates (`count`, `open_count`, `closed_count`, `total_realized_pnl`, `total_unrealized_pnl`, `total_pnl`, `positions_with_pnl`) **recompute over the filtered set**.
* The response gains two top-level keys to make the shift self-documenting: `filtered: true` and `applied_filters: { since, until }`. These keys are absent from default responses.
* Bad inputs return `400` with a descriptive error.
### 30-day window
```bash theme={null}
SINCE=$(( $(date -u +%s) - 86400 * 30 ))
curl "https://api.polynode.dev/v2/wallets/0xbddf61af533ff524d27154e589d2d7a81510c684/positions/onchain?since=$SINCE" \
-H "x-api-key: YOUR_KEY"
```
### 90-day window
```bash theme={null}
SINCE=$(( $(date -u +%s) - 86400 * 90 ))
curl "https://api.polynode.dev/v2/wallets/0xbddf61af533ff524d27154e589d2d7a81510c684/positions/onchain?since=$SINCE" \
-H "x-api-key: YOUR_KEY"
```
### Bounded range (e.g. Q1 2026)
```bash theme={null}
curl "https://api.polynode.dev/v2/wallets/0x.../positions/onchain?since=1735689600&until=1743465600" \
-H "x-api-key: YOUR_KEY"
```
**The default response is unaffected by these new params.** When you call this endpoint with no `since`/`until`/`tag_slug`, the response shape and every aggregate is identical to what you got before. The `filtered` and `applied_filters` keys are only added when you opt in.
## Filtering by tag
`tag_slug` is an optional query parameter that keeps only positions whose market carries the matching Polymarket event tag.
Tags come from Polymarket's event taxonomy — typical values include `nba`, `basketball`, `sports`, `politics`, `crypto`, `fed`, `world`, `2025-predictions`, and event-specific slugs like `2026-fifa-world-cup-winner-595`. Each market inherits the tag set of its parent event, so all 32 markets under "Will X win the World Cup?" share the same tag list. The full set is visible in the per-row `tag_slugs` array on every response.
### Behavior
* Match is case-sensitive and exact (`?tag_slug=nba` matches markets whose `tag_slugs` array contains `"nba"`).
* Positions with `tag_slugs = null` are dropped under an active filter — same logic as `last_trade_at` for `since`/`until` (cannot match what we don't know).
* Aggregates recompute over the filtered set.
* `applied_filters.tag_slug` echoes the value back so the response is self-documenting.
* Combine with `since` / `until` to get e.g. "all NBA positions in the last 30 days" — all three filters AND together.
### Examples
#### All NBA positions for a wallet
```bash theme={null}
curl "https://api.polynode.dev/v2/wallets/0x.../positions/onchain?tag_slug=nba" \
-H "x-api-key: YOUR_KEY"
```
#### All positions tagged to a specific event
```bash theme={null}
curl "https://api.polynode.dev/v2/wallets/0x.../positions/onchain?tag_slug=2026-fifa-world-cup-winner-595" \
-H "x-api-key: YOUR_KEY"
```
This collapses across every children market under the FIFA World Cup winner event.
#### Crypto positions in the last 90 days
```bash theme={null}
SINCE=$(( $(date -u +%s) - 86400 * 90 ))
curl "https://api.polynode.dev/v2/wallets/0x.../positions/onchain?tag_slug=crypto&since=$SINCE" \
-H "x-api-key: YOUR_KEY"
```
### Discovering available tags
Use the dedicated [`/v2/onchain/tags`](/api-reference/onchain/tags) endpoint for the full tag namespace (5,779 tags currently). Returns a flat slug array by default, or `?details=true` for per-tag market counts and timestamps:
```bash theme={null}
curl "https://api.polynode.dev/v2/onchain/tags?min_markets=100&details=true" \
-H "x-api-key: YOUR_KEY"
```
The values returned by `/v2/onchain/tags` are the exact strings to pass to `?tag_slug=` here.
For a wallet-specific view, you can also fetch the wallet's positions without a filter and read the union of `tag_slugs` across the response — that's the tag namespace the wallet has actually traded in.
## How `realized_pnl` Works
Each position's `realized_pnl` reflects the profit or loss after the position is closed (sold or redeemed). For a winning binary position bought at $0.40 that resolves at $1.00:
* `avg_price` = 0.40
* `total_bought` = 10,000 (tokens acquired)
* Cost basis = 10,000 × $0.40 = $4,000 USDC
* Payout at $1.00 = 10,000 × $1.00 = \$10,000 USDC
* `realized_pnl` = $10,000 - $4,000 = **\$6,000**
The formula: `realized_pnl = total_bought × (1 - avg_price)` for positions that resolve at \$1.
Per-position values (`realized_pnl`, `avg_price`, `total_bought`) come from the same onchain settlement data that Polymarket uses. Individual position P\&L matches what Polymarket shows on its portfolio page.
## Which total to use
The response returns three portfolio-level totals. Each answers a different question:
| Total | Answers | Matches |
| ---------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------ |
| `total_realized_pnl` | How much profit has this wallet actually taken off the table from closed + resolved positions? | Not shown on Polymarket profile |
| `total_unrealized_pnl` | What is the paper P\&L on the wallet's currently-held shares at current (or terminal) prices? | Not shown directly |
| `total_pnl` | What is the wallet's net portfolio P\&L right now? | The "Profit" number on a Polymarket profile page |
`total_pnl` is what most dashboards want — it's the single number users recognize from Polymarket. `total_realized_pnl` is useful for attribution (which markets made money vs which positions are still out). `total_unrealized_pnl` is useful for risk views (how much is still at stake).
`total_pnl = total_realized_pnl + total_unrealized_pnl`. Reach for `total_pnl` when you want a number that matches a Polymarket profile page. Reach for `total_realized_pnl` when you want only closed-out gains.
When you supply `since` or `until`, all three totals recompute over the filtered set — the same logic, just narrower input.
## Performance
Responses are cached for 5 minutes per wallet. First request for a new wallet takes 200ms-3s depending on position count (up to 20,000 positions supported). Cached responses return in under 50ms. Filter parameters are applied to the cached set; passing `since`/`until` does not invalidate the underlying cache.
| Positions | First request | Cached |
| ----------- | ------------- | ------- |
| \< 500 | \~1s | \< 50ms |
| 500-1,000 | \~1.5s | \< 50ms |
| 1,000-5,000 | \~3s | \< 50ms |
## Examples
### Default response (no filter)
```bash theme={null}
curl "https://api.polynode.dev/v2/wallets/0xbddf61af533ff524d27154e589d2d7a81510c684/positions/onchain" \
-H "x-api-key: YOUR_KEY"
```
Returns every position the wallet has held, all-time, with the full set of fields documented above. Default response shape — no `filtered` or `applied_filters` keys.
### Filter to the last 30 days
```bash theme={null}
SINCE=$(( $(date -u +%s) - 86400 * 30 ))
curl "https://api.polynode.dev/v2/wallets/0xbddf61af533ff524d27154e589d2d7a81510c684/positions/onchain?since=$SINCE" \
-H "x-api-key: YOUR_KEY"
```
### Filter to a parent event
`event_slug` is included on every row, so once you have a wallet's positions you can group them by parent event without an extra round-trip:
```bash theme={null}
curl "https://api.polynode.dev/v2/wallets/0x.../positions/onchain" \
-H "x-api-key: YOUR_KEY" \
| jq '.positions | group_by(.event_slug) | map({event_slug: .[0].event_slug, market_count: length, pnl: (map(.realized_pnl + .unrealized_pnl) | add)})'
```
This collapses a wallet's positions across the children of an event (e.g. all 32 World Cup winner markets, or the multiple lines of a single NBA game) into one row per event.
# Resolve Wallet
Source: https://docs.polynode.dev/api-reference/wallets/resolve
GET /v1/resolve/{query}
Instantly resolve any Polymarket wallet address, EOA, or username.
Look up the full identity for any Polymarket user: preferred trading wallet, EOA (externally owned account), username, and wallet type. Accepts any of the three identifiers as input and returns all fields.
Supports both Polymarket's original Safe proxy wallets and the newer deposit wallets. The response format is unchanged: `safe` is kept for backward compatibility, but it means the preferred Polymarket trading wallet.
If the controlling EOA has a deployed deposit wallet, `safe` returns that deposit wallet and `type` is `"deposit_wallet"` — even when you query the EOA, username, or older Safe wallet. If no deposit wallet is deployed, `safe` falls back to the existing Safe/proxy wallet.
One of:
* **Wallet address** — Polymarket trading wallet: Safe proxy, deposit wallet, or proxy wallet (`0x...`, 42 chars)
* **EOA address** — externally owned account (`0x...`, 42 chars)
* **Username** — Polymarket display name (case-insensitive)
## By wallet address (Safe, no deposit wallet)
Look up a Polymarket Safe proxy wallet to get the controlling EOA and username. If that EOA also has a deployed deposit wallet, the response returns the deposit wallet in `safe` instead.
```bash theme={null}
curl "https://api.polynode.dev/v1/resolve/0xfa85349327a63aa563029737f0492a79dca8f95d" \
-H "x-api-key: YOUR_KEY"
```
```json theme={null}
{
"safe": "0xfa85349327a63aa563029737f0492a79dca8f95d",
"eoa": "0xc2783891b1d2287345e30f75e0f1ecd189a967d0",
"username": "Letsgetit6969",
"type": "safe"
}
```
## By wallet address (deposit wallet)
Look up a Polymarket deposit wallet to get the controlling EOA. Deposit wallets are a newer wallet type introduced by Polymarket for new accounts.
```bash theme={null}
curl "https://api.polynode.dev/v1/resolve/0x8b60bf0f650bf7a0d93f10d72375b37de18f8c40" \
-H "x-api-key: YOUR_KEY"
```
```json theme={null}
{
"safe": "0x8b60bf0f650bf7a0d93f10d72375b37de18f8c40",
"eoa": "0xa60601a4d903af91855c52bfb3814f6ba342f201",
"username": null,
"type": "deposit_wallet"
}
```
## By EOA address
Look up an EOA to find its preferred Polymarket trading wallet and username. Deposit wallets are prioritized when deployed.
```bash theme={null}
curl "https://api.polynode.dev/v1/resolve/0xc2783891b1d2287345e30f75e0f1ecd189a967d0" \
-H "x-api-key: YOUR_KEY"
```
```json theme={null}
{
"safe": "0x8b60bf0f650bf7a0d93f10d72375b37de18f8c40",
"eoa": "0xa60601a4d903af91855c52bfb3814f6ba342f201",
"username": null,
"type": "deposit_wallet"
}
```
## By wallet address (magic-link proxy)
Resolve a Polymarket magic-link wallet — the wallet type used by accounts created with email or social login. The endpoint returns the controlling EOA, recovered automatically.
```bash theme={null}
curl "https://api.polynode.dev/v1/resolve/0x16cbe223607a6513ae76d1e3751c78e4eabc2704" \
-H "x-api-key: YOUR_KEY"
```
```json theme={null}
{
"safe": "0x16cbe223607a6513ae76d1e3751c78e4eabc2704",
"eoa": "0xbe5ba588ab7173b34efc0706b881794951014293",
"username": "MRF",
"type": "proxy"
}
```
## By username
Look up a Polymarket username to find the preferred trading wallet. Case-insensitive.
```bash theme={null}
curl "https://api.polynode.dev/v1/resolve/Fredi9999" \
-H "x-api-key: YOUR_KEY"
```
```json theme={null}
{
"safe": "0x1f2dd6d473f3e824cd2f8a89d9c69fb96f6ad0cf",
"eoa": null,
"username": "Fredi9999",
"type": "safe"
}
```
## Response fields
Preferred Polymarket trading wallet address. The field name is kept for backward compatibility. If the controlling EOA has a deployed deposit wallet, this is the deposit wallet. Otherwise it is the existing Safe/proxy wallet.
Externally owned account that controls the trading wallet. This is the signer address. May be `null` for some magic-link wallets where the EOA cannot be determined.
Polymarket display name. `null` if the user has no profile set.
Wallet type for the address returned in `safe`. One of:
* `"safe"` — Gnosis Safe proxy wallet (most existing Polymarket accounts)
* `"deposit_wallet"` — deposit wallet (newer Polymarket accounts)
* `"proxy"` — magic-link wallet (accounts created via email or social login)
## Error responses
**404 — Not found**
```json theme={null}
{"error": "Address not found."}
```
```json theme={null}
{"error": "Username not found."}
```
## Coverage
The resolver supports every Polymarket wallet type on Polygon, including Safe proxy wallets and the newer deposit wallets. Cached lookups are sub-millisecond. Uncached lookups resolve on-chain in real time, then prefer a deployed deposit wallet when one exists for the resolved EOA.
## Use cases
* **Database enrichment** — bulk-resolve trading wallets to EOAs for analytics pipelines
* **Profile lookups** — show usernames alongside wallet addresses in your UI
* **Cross-referencing** — match on-chain activity (EOA) to the preferred Polymarket trading wallet
* **Whale tracking** — identify the EOA behind a trading wallet to track activity across protocols
* **Wallet type detection** — use the `type` field to determine how a user's account is set up
# X Search API
Source: https://docs.polynode.dev/api-reference/x-search
GET /v2/x/search
Beta endpoint for live X (Twitter) search and account-timeline data. Drop social context next to your prediction market data without building your own X integration.
**Beta.** Response shape and quota numbers are stable. We may add optional fields, but no breaking changes. Email `josh@polynode.dev` if you're building something on top of this and need direct support.
## What this is
Two read-only endpoints for live X data:
* **Search** any text across X. Full operator support (`from:`, `since:`, `min_faves:`, hashtags, exact phrases).
* **Account timeline** for any public X handle — most-recent tweets, replies, retweets, and quoted content.
Most teams already using polynode for prediction-market data also want to know what's being said on X about a market, an event, or a specific account. Setting up your own X feed is rate-limit hell and a maintenance treadmill — we just do it for you, and you query it like any other polynode endpoint.
## Who this is for
* Prediction-market dashboards layering social sentiment over on-chain data.
* AI agents that need to factor live X posts into their reasoning about an event.
* Research and analytics products that want clean JSON without building their own X integration.
## Base URL
```
https://api.polynode.dev/v2/x
```
## Authentication
Pass your polynode API key on every request — either header works:
```http theme={null}
x-api-key: pn_live_...
Authorization: Bearer pn_live_...
```
* Free-tier keys are rejected with `402 Payment Required`. Paid plans only.
* Hard rate limit: **1 request per second per key**. Two requests on the same key in the same second → the second one gets `429`.
* Each successful response (HTTP 200) increments your monthly quota counter. Rejected requests do not.
## Tier quotas
| Tier | Searches per month |
| ---------- | ------------------ |
| starter | 500 |
| growth | 1,000 |
| enterprise | 5,000 |
Quotas reset midnight UTC on the 1st of each month. Track your remaining usage in real time via response headers:
```
X-Quota-Used: 47
X-Quota-Limit: 500
```
## Endpoints
### `GET /v2/x/search`
Search recent X posts by query.
**Query params:**
| Param | Type | Default | Notes |
| ----- | ------ | -------- | --------------------------------------------------------------- |
| `q` | string | required | The search string. URL-encode operators and special characters. |
| `max` | int | `20` | Number of tweets to return. Min `1`, max `50`. |
**Operator examples:**
```
q=polymarket
q=from:Polymarket
q=election odds since:2026-04-20
q=#prediction min_faves:100
q="exact phrase" lang:en
q=trump until:2026-05-01
```
**Example request:**
```bash theme={null}
curl -H "x-api-key: $PN_KEY" \
"https://api.polynode.dev/v2/x/search?q=polymarket&max=5"
```
**Example response:**
```json theme={null}
{
"query": "polymarket",
"tweets": [
{
"id": "2048234907965526306",
"text": "Polymarket is showing 65% on Trump for 2024",
"created_at": "Sun Apr 26 03:21:58 +0000 2026",
"public_metrics": {
"retweet_count": 244,
"reply_count": 38,
"like_count": 1450,
"quote_count": 22,
"bookmark_count": 87,
"impression_count": "184302"
},
"author": {
"id": "987654321",
"name": "Polymarket",
"username": "Polymarket"
},
"conversation_id": "2048234907965526306",
"urls": [
{
"url": "https://t.co/...",
"expanded_url": "https://polymarket.com/event/...",
"display_url": "polymarket.com/event/...",
"title": "Will Trump win 2024?",
"description": "..."
}
],
"media": [
{ "type": "photo", "url": "https://pbs.twimg.com/media/...", "expanded_url": "..." }
]
}
],
"result_count": 5,
"fetched_at": "2026-04-26T03:35:00Z"
}
```
### `GET /v2/x/user/{handle}/tweets`
Get the latest tweets from a specific X account's timeline.
**Path params:**
| Param | Type | Notes |
| -------- | ------ | ----------------------------------------------------------- |
| `handle` | string | The X handle without `@`. Letters, digits, underscore only. |
**Query params:**
| Param | Type | Default | Notes |
| ----- | ---- | ------- | ------------------------------------ |
| `max` | int | `30` | Number of tweets. Min `1`, max `50`. |
**Example request:**
```bash theme={null}
curl -H "x-api-key: $PN_KEY" \
"https://api.polynode.dev/v2/x/user/Polymarket/tweets?max=10"
```
The response shape is identical to `/v2/x/search`, except the top-level field is `handle` instead of `query`.
## Errors
| HTTP | Meaning |
| --------- | ---------------------------------------------------------------------------------------------------------- |
| 400 / 422 | Invalid params: empty `q`, `max` out of range, or handle contains invalid characters. |
| 401 | Missing or invalid API key. |
| 402 | Free-tier key; this endpoint requires a paid plan. |
| 404 | Handle does not exist (timeline endpoint only). |
| 429 | Either the 1 req/sec rate cap, or your monthly quota is exhausted. Check `X-Quota-Used` / `X-Quota-Limit`. |
| 502 | Transient upstream issue. Retry after a few seconds. We're alerted automatically. |
## Tweet shape reference
Every tweet object can include these fields. Optional fields are only present when relevant.
| Field | Type | Always present? | Notes |
| ----------------- | ------ | --------------- | ------------------------------------------------------------------------------------------------------------- |
| `id` | string | yes | Tweet's numeric ID as a string. |
| `text` | string | yes | The tweet body. `t.co` shortlinks are pre-expanded to their final destination URLs. |
| `created_at` | string | yes | X's native timestamp format (e.g. `Sun Apr 26 03:21:58 +0000 2026`). |
| `public_metrics` | object | yes | Counters: `retweet_count`, `reply_count`, `like_count`, `quote_count`, `bookmark_count`, `impression_count`. |
| `author` | object | yes | `{ id, name, username }`. |
| `conversation_id` | string | yes | The root tweet of the thread this tweet belongs to. |
| `urls` | array | optional | Embedded link metadata: `{ url, expanded_url, display_url, title, description }`. |
| `media` | array | optional | Photos, videos, GIFs: `{ type, url, expanded_url }` (videos include `video_url` for the highest-bitrate MP4). |
| `card` | object | optional | Link preview cards: `{ name, url, title, description, domain, thumbnail_url }`. |
| `quoted_tweet` | object | optional | Full tweet object of any quoted post (recursive). |
| `article` | string | optional | Long-form X post content rendered as Markdown. |
## Quirks worth knowing
* **`created_at` is X's native string format**, not ISO 8601. Parse with `new Date(tweet.created_at)` in JS or `dateutil.parser.parse` in Python.
* **`impression_count` is a string**, not an int — X returns it as a string and we pass it through verbatim. Cast on your side if you need a number.
* **`text` is post-expansion.** All `t.co` shortlinks are already replaced with their target URLs. The `urls` array also gives you the full expanded metadata (title, description) per link, which is the easiest way to surface link previews in your UI.
* **No pagination cursor** in the beta. The `max` cap is 50 per call. To go beyond that, run multiple queries narrowed with `since:` / `until:` operators.
* **Search prefers recency.** Live results are ranked newest-first. Combine with `min_faves:N` or `min_retweets:N` if you want to floor on engagement.
## FAQ
**Why is search slower than timeline?**
Search and timeline take different paths. Timeline is typically \~1-2 seconds. Search is typically 5-7 seconds because the underlying X data path is heavier. This is normal and stable.
**Can I get my own quota lifted?**
Yes — email `josh@polynode.dev` if you're hitting the cap and want a custom limit.
**Will the response shape change?**
We may add optional fields (e.g. new metrics X exposes). We will not rename or remove existing fields without versioning the endpoint. Code defensively against optional fields, never against missing ones.
# Authentication
Source: https://docs.polynode.dev/authentication
API key generation, authentication methods, and security.
All API endpoints (except `/healthz`, `/readyz`, and `POST /v1/keys`) require an API key.
## Passing the key
Two methods are supported:
```bash theme={null}
curl -H "x-api-key: pn_live_YOUR_KEY" https://api.polynode.dev/v1/markets
```
```bash theme={null}
curl "https://api.polynode.dev/v1/markets?key=pn_live_YOUR_KEY"
```
For WebSocket connections, use the query parameter:
```
wss://ws.polynode.dev/ws?key=pn_live_YOUR_KEY
```
## Key format
API keys use the prefix `pn_live_` followed by a random string. Legacy keys with `qm_live_` prefix are also accepted.
## Generating a key
```bash theme={null}
curl -s -X POST https://api.polynode.dev/v1/keys \
-H "Content-Type: application/json" \
-d '{"name": "my-app"}'
```
| Field | Type | Description |
| ------ | ----------------- | ----------------------------------------- |
| `name` | string (optional) | Label for the key. Defaults to "unnamed". |
Key generation is rate limited to **1 per IP per day**.
The raw API key is returned only once. It cannot be retrieved after creation — store it securely.
## Security
* API keys are **SHA-256 hashed** before storage. The raw key is never persisted.
* All traffic should use HTTPS in production.
* Rotate keys by generating a new one and decommissioning the old one.
## Error responses
| Status | Error | Fix |
| ------ | --------------------------- | ------------------------------------------------------- |
| 401 | Missing or invalid API key | Include your key as `x-api-key` header or `?key=` param |
| 403 | Invalid or inactive API key | Verify your key is correct, or generate a new one |
| 429 | Rate limit exceeded | Reduce request frequency. Free tier: 60 req/min |
# Changelog
Source: https://docs.polynode.dev/changelog
All notable changes to the PolyNode API and SDKs.
## 2026-06-22 - V3 wallet accounting summary
Added an optional all-time accounting summary to the V3 wallet summary endpoint.
What's new:
* `GET /v3/wallets/{address}?include_accounting_summary=true` now returns `accounting_summary`.
* `accounting_summary.fees_paid.amount` is the exact all-time trader-paid fee total from indexed onchain `OrderFilled` rows.
* `accounting_summary.fees_paid.raw` exposes the exact 6-decimal raw amount.
* Maker rebates and rewards are represented with explicit status fields. `not_loaded` means the source projection has not been loaded; clients should not treat those values as zero.
Existing default responses are unchanged. The accounting object is included only when requested.
***
## 2026-06-15 - PM2 AutoRedeemer redemptions in WebSocket streams
PM2 AutoRedeemer redemptions are now surfaced in both the combo stream and the standard redemption stream.
What's new:
* AutoRedeemer `Redemption`, `BinaryRedemption`, and `NegRiskRedemption` logs emit customer-facing `combo_lifecycle` events with the wallet, exact raw payout, normalized payout, module name, and receipt log index. Binary and negative-risk redemptions also include the condition ID directly.
* Confirmed `combo_status_update` messages can include `lifecycle_events[]` for receipt-only AutoRedeemer lifecycle activity.
* The same AutoRedeemer payout is also bridged into the standard `redemption` event class, attributed to the user wallet instead of the AutoRedeemer contract.
* New `type: "redemptions"` WebSocket preset subscribes directly to `redemption` events. Existing `settlements` or `wallets` subscriptions with `event_types: ["redemption"]` continue to work.
No subscription change is required for existing redemption consumers.
***
## 2026-06-12 - Faster V3 builder trade time filters
Improved reliability and latency for time-filtered builder trade lookups.
What's improved:
* `GET /v3/builders/{code}/trades?after=...` now returns consistently for high-volume builders instead of timing out on large builder histories.
* `after` / `before` filters now work efficiently with broad `tag_slug` or `category` filters when a time bound is provided.
* Time windows outside a builder's activity range return a normal empty response instead of an internal timeout.
* Invalid millisecond timestamps now return a structured `400` with a hint to use Unix seconds.
No response fields or pagination semantics changed.
***
## 2026-06-11 - Polymarket combos: V3 API and WebSocket event references
Added public documentation for Polymarket combo / combinatorial position support.
What's new:
* New combo API group in the API Reference.
* `GET /v3/combos/markets` for observed combo markets and legs.
* `GET /v3/combos/activity` for combo lifecycle activity by market, condition, position, or wallet.
* `GET /v3/wallets/{address}/combos/positions` for combo-only wallet positions.
* `GET /v3/wallets/{address}/combos/trades` for combo-only wallet trades.
* `GET /v3/wallets/{address}/combos/activity` for combo-only wallet lifecycle activity.
* `GET /v3/wallets/{address}/combos/summary` for combo-only wallet P\&L summary.
* `include_combos=true` on wallet summary, wallet P\&L, wallet positions, and wallet trades for additive standard + combo responses.
* New WebSocket event reference pages for `combo_execution`, `combo_status_update`, `combo_lifecycle`, and `combo_approval`.
Notes:
* Existing wallet endpoints keep their default response behavior. Combo data is included only when explicitly requested or when using dedicated combo endpoints.
* If a wallet has no combo exposure, `include_combos=true` returns a normal successful response with a zero combo contribution.
***
## 2026-06-09 - Python SDK v0.10.6 WebSocket resilience
Released `polynode==0.10.6` for Python WebSocket consumers.
What's new:
* Event-stream subscriptions preserve frames that arrive before the subscribe acknowledgement, so handlers attached immediately after `await ...send()` still receive initial snapshot/live events.
* Default event WebSocket subscribe timeout is now 30s and configurable with `WsOptions(subscribe_timeout=...)`.
* Orderbook `subscribe()` now resolves only after the server acknowledges the subscription, and rejects promptly on server errors, disconnects, or timeout.
* Orderbook `unsubscribe(token_ids)` can remove a subset of tokens; `unsubscribe()` with no arguments remains the full unsubscribe behavior.
* Added protocol regression tests for snapshot-before-ack, pending-subscribe errors, timeout handling, and partial unsubscribe behavior.
Upgrade:
```bash theme={null}
pip install --upgrade polynode==0.10.6
```
***
## 2026-06-09 - TypeScript SDK v0.10.17 WebSocket resilience
Released `polynode-sdk@0.10.17` for Node/TypeScript WebSocket consumers.
What's new:
* Event-stream subscriptions preserve frames that arrive before the subscribe acknowledgement, so handlers attached immediately after `await subscribe(...).send()` still receive initial snapshot/live events.
* Default WebSocket subscribe timeout is now 30s and configurable with `subscribeTimeoutMs`.
* Orderbook `subscribe()` now resolves only after the server acknowledges the subscription.
* Orderbook reconnects replay the active token list without replacing existing handlers.
* Orderbook `unsubscribe(tokenIds)` can remove a subset of tokens; `unsubscribe()` with no arguments remains the full unsubscribe behavior.
* Added SDK protocol tests and a live orderbook soak test for multi-token subscribe, partial unsubscribe/resubscribe, snapshots, live batches, protocol errors, unexpected unsubscribe frames, and disconnects.
Upgrade:
```bash theme={null}
npm install polynode-sdk@0.10.17 ws
```
***
## 2026-05-27 - V3 wallet P\&L alignment, fees, rebates, rewards, and PolyUSD flows
Updated V3 wallet accounting surfaces.
What's new:
* Default/all-time `GET /v3/wallets/{address}/pnl/events` now returns a single all-time bucket so it agrees with `GET /v3/wallets/{address}/pnl`.
* Explicit time-windowed P\&L events (`period`, `after`, or `before`) return realized P\&L buckets and ignore timestamp-0 artifacts.
* Added `GET /v3/wallets/{address}/fees-paid` for fee-bearing wallet fills.
* Added `GET /v3/wallets/{address}/polyusd-flows` for wallet PolyUSD deposits and withdrawals.
* Added `GET /v3/wallets/{address}/rebates` for maker rebates.
* Added public reward-market configuration at `GET /v3/rewards/markets` and `GET /v3/rewards/markets/{condition_id}`.
Notes:
* Fees paid and maker rebates are intentionally separate surfaces. LP reward market configuration is public; per-wallet earned LP rewards require authenticated Polymarket data and are not exposed as an arbitrary-wallet lookup in the public docs.
* `GET /v3/wallets/{address}/activity` remains the conditional-token activity feed; PolyUSD deposits and withdrawals now live in the separate wallet PolyUSD flows endpoint.
***
## 2026-05-26 — Webhooks beta: first observed position opens
Added `position_status_change` support for watched-wallet CTF position opens and exits.
What's new:
* `position_status_change` now detects first observed `none` -> `open` transitions for watched wallets from CTF ERC-1155 transfer deltas.
* Existing wallet positions are seeded when a wallet is first watched, so new webhooks emit future transitions without backfilling historical opens.
* Each watched wallet now gets its own seed cursor, so adding a new wallet does not replay stale source rows from before that wallet's baseline.
* Payloads now include `previous_amount`, `new_amount`, `delta_amount`, `transfer_amount`, `source`, `source_event_id`, block/transaction fields, and market enrichment.
* Filters now include `wallet_addresses`, `token_ids`, `condition_ids`, `event_slugs`, `tags`, `status_from`, and `status_to`.
Beta note:
* `wallet_addresses` is required for this beta. Unfiltered/global position-status webhooks are intentionally not enabled yet.
Docs updated:
* See [`position_status_change`](/webhooks/events/position-status-change) for the exact payload and copyable first-open registration example.
***
## 2026-05-26 — TypeScript SDK v0.10.13: V3 parity, resolver, and profile helpers
Released a TypeScript SDK parity update for the current REST API surface.
What's new:
* `pn.v3` namespace for the V3 historical data API while preserving existing top-level SDK behavior.
* Typed V3 methods for wallet summaries, wallet P\&L, P\&L events, wallet trades, wallet positions, global trades, global positions, market search, market price, market trades, market positions, leaderboard, tags, builders, builder trades, fees, resolutions, conditions, and token info.
* Builder trade filters in the SDK: `tokenId`, `conditionId`, `marketSlug`, `eventSlug`, `side`, `category`, `tagSlug`, `minAmount`, `after`, `before`, `limit`, and `offset`.
* `pn.resolve(query)` for wallet / EOA / username resolution, returning `{ safe, eoa, username, type }`.
* Fully typed `pn.walletOnchainPositions()` response including `condition_id`, `unrealized_pnl`, `current_price`, `market_status`, `market`, `slug`, `event_slug`, `outcome`, `won`, `winning_outcome_index`, `last_trade_at`, `closed_at`, `resolved_at`, and top-level `total_unrealized_pnl`.
* V3 Polymarket profile helpers: `polymarketUsernameAvailable`, `createPolymarketUsernameChallenge`, `completePolymarketUsername`, and `polymarketProfile`.
Docs updated:
* Added [TypeScript — V3 Data & Profiles](/sdks/ts/v3-data) with copyable SDK examples.
* Updated [TypeScript — REST API](/sdks/ts/rest-api), [TypeScript — Types](/sdks/ts/types), and [Polymarket Profile Setup](/guides/polymarket-profile-setup) with SDK-specific examples and the exact signing flow.
Notes:
* Existing `pn.market()`, `pn.wallet()`, `pn.leaderboard()`, and other top-level methods keep their historical paths and response shapes.
* Event-by-ID lookup and a fully filterable events listing remain backend/API work, not just SDK wrapper work. Current SDK support reflects the endpoints that exist publicly today.
***
## 2026-06-04 — `/resolve` prefers deployed deposit wallets
`GET /v1/resolve/{query}` keeps the same response fields: `safe`, `eoa`, `username`, and `type`.
The `safe` field now returns the preferred Polymarket trading wallet. If the resolved EOA has a deployed deposit wallet, `safe` is that deposit wallet and `type` is `"deposit_wallet"`. If no deposit wallet is deployed, `safe` falls back to the existing Safe/proxy wallet.
This applies to address, EOA, and cached username lookups.
***
## 2026-05-22 — V3 grouped trade total alias
Added `total_amount_usd` to v3 `group_by=order_hash` trade responses. It is an additive alias of `total_usd` / `amount_usd`, so clients that already use the v2 grouped trade field can read the same total field on v3.
Affected endpoints:
* `GET /v3/wallets/{address}/trades?group_by=order_hash`
* `GET /v3/trades?group_by=order_hash`
Existing fields are unchanged.
***
## 2026-05-22 — Builder metadata and filtered builder trades
Builder analytics now include public builder profile metadata on all builder endpoints:
* `GET /v3/builders`
* `GET /v3/builders/{code}`
* `GET /v3/builders/{code}/trades`
New additive fields:
* `builder_code`
* `builder_name`
* `builder_logo`
* `builder_verified`
`/v3/builders/{code}/trades` also now supports builder-specific filters for `token_id`, `condition_id`, `market_slug`, `event_slug`, `side=buy|sell`, `min_amount`, `after`, `before`, `limit`, and `offset`.
`category` and `tag_slug` filters are available with guardrails. Very broad category/tag requests return a structured `400` asking you to add `market_slug`, `condition_id`, `token_id`, or time bounds.
```bash theme={null}
curl "https://api.polynode.dev/v3/builders/0x4898df15.../trades?market_slug=dota2-tundra-xtreme-2026-05-22-game1&tag_slug=sports&limit=10"
```
Existing fields and existing pagination behavior are unchanged.
***
## 2026-05-18 — PnL methodology note
For users comparing our PnL numbers to what the Polymarket frontend displays: the two are computed differently and will diverge for wallets that hold a lot of settled-but-not-redeemed positions.
**Polymarket's `pnl`** (leaderboard, profile chart) is a **mark-to-market equity curve delta**: `equity(end_of_window) − equity(start_of_window)`. Resolved markets are booked into the curve at settlement, regardless of whether the wallet has called `redeemPositions`.
**Our `net_realized_pnl`** counts realized closes only — CLOB sells (the wallet sold their shares back) plus V2 adapter events (redemptions, merges, NRC conversions). Unrealized gains on settled-but-unredeemed positions stay in `unrealized_pnl`, not `net_realized_pnl`.
**Empirically verified** against PM's `/closed-positions[].realizedPnl` on a representative wallet: per-position realized matches PM within 1%. The "lifetime pnl" headline diverges by the value of settled-not-redeemed positions in the wallet.
`total_pnl` (= `net_realized_pnl + unrealized_pnl`) is the closer comparison to PM's curve delta — it includes both closed and still-open positions priced at current market.
***
## 2026-05-18 — New endpoint: wallet P\&L time series
Added `GET /v3/wallets/{address}/pnl/events` — returns realized P\&L bucketed by `hour`, `day`, `week`, or `month` so you can plot a wallet's P\&L over time without pulling every trade. Each bucket includes `realized_pnl`, a running `cumulative_pnl`, gross profit/loss, win/loss counts, and event count. Supports `?period=1d|7d|30d|1y` shortcuts or explicit `after` / `before` Unix-second timestamps.
Most full-history queries return sub-second.
Other quality-of-life:
* `?after` / `?before` outside the wallet's active window now correctly return zero buckets instead of an error.
* All `/v3/*` paths now accept an optional trailing slash (`/v3/positions/` is equivalent to `/v3/positions`).
* Unknown `/v3/*` paths return a structured 404 with a `hint` pointing to the docs, rather than HTML.
* Invalid `sort` and `order` values now return `400` with the list of valid options instead of silently falling back to a default — typos surface immediately.
### Doc / API parity pass (later same day)
Round 2 of the parity work — pure alignment between the API and the public reference, no breaking changes:
* **`/v3/markets/condition/{condition_id}/positions`** — response field names now match `/v3/markets/{token_id}/positions`. Was returning abbreviated keys (`amt`, `avgp`, `bought`, `rpnl`, `tid`); now returns the long, documented names (`amount`, `avg_price`, `total_bought`, `realized_pnl`, `token_id`). Old keys are gone — update any client code using them.
* **`/v3/resolutions`** — `payout_numerators` is now a JSON array of integers (e.g. `[0, 1]`), matching `/v3/conditions/{cid}`. Was returning quoted strings. `payout_denominator` is also an integer now.
* **`/v3/builders`, `/v3/tags`, `/v3/tokens/{token_id}`** — response envelopes now include `offset` and `limit` like every other list endpoint.
* **Param validation tightened** — invalid `?order=`, `?side=`, `?status=`, `?group_by=`, `?sort_by=` on trades / fees / resolutions / positions endpoints now return `400` with the valid-values list, instead of silently falling back to a default. Valid values still pass through unchanged.
* **Token price scale** — `/v3/markets/{token_id}/price.price` and `/v3/tokens/{token_id}.data[].price` are JSON numbers in USD (range `0`–`1`), not 6-decimal strings. Reference page text updated accordingly.
***
## 2026-05-16 — V3 API goes live + performance fixes
All `/v3/*` endpoints are now generally available (no longer "Coming Soon"). Several queries that timed out or ran slowly have been rewritten to use index-friendly query plans:
* `/v3/wallets/{addr}/positions` — default sort no longer times out, returns in \<100ms
* `/v3/markets/slug/{slug}/trades` — fixed timeout, now \<100ms
* `/v3/tags/{slug}/leaderboard` — small/medium tags are now sub-second; tags larger than 4,000 markets return a structured "too large" hint pointing to the global leaderboard
* `/v3/tags/{slug}` — drops from 4.8s to \~1s
* `/v3/wallets/{addr}/trades` — drops from 2.3s to \~200ms
Tag slugs are now matched case-insensitively and accept URL-style hyphens: `/v3/tags/us-election/leaderboard` resolves to the stored tag `US Election`.
Several doc examples that previously showed a `columns/rows` envelope have been corrected to the `data: [{...}]` shape that the API actually returns.
***
## 2026-05-14 — SDK fix: deposit wallet deploy + approve
Fixed the deposit wallet (V2 / POLY\_1271) onboarding flow across all three SDKs. The relayer's `/submit` endpoint now requires builder authentication for WALLET-type submissions. All SDKs have been updated to include the required auth headers.
If you previously hit `401 invalid authorization` or `API key and signer mismatch` errors when deploying a deposit wallet or setting approvals, update to the latest SDK version:
```bash theme={null}
npm install polynode-sdk@0.10.7 # TypeScript
cargo update -p polynode # Rust (0.13.4)
pip install polynode==0.10.4 # Python
```
`ensureReady()` handles the full flow automatically — deploy, approve, create credentials. If your wallet is already deployed, it skips to approvals. If approvals are already set, it skips to credentials.
***
## 2026-05-10 — Sort and group trades by order hash
`/v2/onchain/trades` now supports two new query parameters for working with limit order fills:
**`sort_by=order_hash`** — reorders the response so all fills from the same limit order are adjacent, sorted by time within each order. Every fill is still returned individually. No data is collapsed.
```bash theme={null}
curl "https://api.polynode.dev/v2/onchain/trades?wallet=0x...&sort_by=order_hash&limit=100" \
-H "x-api-key: YOUR_KEY"
```
**`group_by=order_hash`** — aggregates all fills from the same limit order into a single row with `total_amount_usd`, `total_shares`, `avg_price` (VWAP), `fill_count`, time range (`first_fill_at` / `last_fill_at`), and a `tx_hashes` array.
```bash theme={null}
curl "https://api.polynode.dev/v2/onchain/trades?wallet=0x...&group_by=order_hash&limit=100" \
-H "x-api-key: YOUR_KEY"
```
A single limit order can be filled across hundreds of transactions over hours or days. These parameters let you view that activity at either granularity.
***
## 2026-05-05 — Magic-link wallet EOA recovery in `/resolve`
`/v1/resolve` now returns the controlling EOA for Polymarket magic-link wallets — accounts created with email or social login. Previously these returned `eoa: null` because Polymarket's profile API does not expose the signer for this wallet type.
```json theme={null}
{
"safe": "0x16cbe223607a6513ae76d1e3751c78e4eabc2704",
"eoa": "0xbe5ba588ab7173b34efc0706b881794951014293",
"username": "MRF",
"type": "proxy"
}
```
**No code changes required.** The response shape is unchanged. The `eoa` field is now populated for the vast majority of `type: "proxy"` wallets. A small number may still return `eoa: null` where the EOA cannot be determined.
Repeat queries are sub-100ms.
***
## 2026-05-02 — Deposit wallet support
Polymarket is rolling out deposit wallets as a new account type for new users. This release adds full support across the API, all three SDKs, and the copy trading engine. **Existing integrations are fully backward compatible** — no code changes required unless you want to take advantage of the new features.
Read the full [Deposit Wallets guide](/guides/deposit-wallets) for details, code examples, and FAQ.
### What's new
**`/resolve` endpoint** — now returns a `type` field (`"safe"`, `"deposit_wallet"`, or `"proxy"`) in all responses. Resolves deposit wallets in both directions: wallet address to EOA and EOA to wallet address. Existing `safe`, `eoa`, and `username` fields unchanged.
```json theme={null}
{
"safe": "0x8b60bf0f650bf7a0d93f10d72375b37de18f8c40",
"eoa": "0xa60601a4d903af91855c52bfb3814f6ba342f201",
"username": null,
"type": "deposit_wallet"
}
```
**SDK — address derivation and detection** — `deriveDepositWalletAddress(eoa)` computes the deterministic deposit wallet address for any EOA. `detectWalletType()` and `ensureReady()` automatically detect whether an EOA has a Safe, proxy, or deposit wallet. Safe is checked first, so existing users keep their existing wallet type.
**SDK — order signing** — when `signatureType` is `POLY_1271` (3), the SDK wraps the EIP-712 signature using ERC-7739 TypedDataSign format automatically. `order()` / `signV2Order()` / `create_signed_order_v2()` detect the signature type from stored credentials and sign correctly. No code changes needed on your side.
**SDK — onboarding** — `ensureReady()` handles the full flow for deposit wallet users: derives the wallet, deploys via `WALLET-CREATE` if needed, and sets V2 approvals via a signed `WALLET` batch. Same one-call pattern as Safe wallets.
**Copy trading** — the copy trading engine generates the correct typed data for deposit wallet followers (`sig_type: 3`). Your signing code should use `signV2Order()` from the SDK.
### Install
```bash theme={null}
cargo add polynode@0.13.3 # Rust
npm install polynode-sdk@0.10.6 # TypeScript
pip install polynode==0.10.3 # Python
```
### Do I need to update?
* **If you only read data** (positions, trades, resolve): no changes needed. The `type` field is additive.
* **If you place orders via the SDK**: update to the versions above. Your code stays the same.
* **If you build order signatures yourself**: you need to implement ERC-7739 wrapping for `signatureType: 3`. See the [guide](/guides/deposit-wallets#how-poly_1271-signing-works).
***
## 2026-05-01 — Cursor pagination on `/v2/onchain/*/trades`
The two trade-history endpoints now support **cursor pagination** in addition to offset:
* `GET /v2/onchain/markets/{token_id}/trades?cursor=:`
* `GET /v2/onchain/wallets/{address}/trades?cursor=:`
**Why it matters:** offset pagination slows down the deeper you page — page 500 means the database walks 50,000 rows of wasted work to find your starting point. Cursor pagination loads page 1000 just as fast as page 1 — typically \~1 second per page, regardless of how deep you go.
**How to use:**
1. First page: pass `?cursor=` (empty value) instead of `?offset=0`.
2. Response includes a `pagination` block with `cursor` and `has_more`.
3. Subsequent pages: pass that `cursor` back as `?cursor=`.
4. Stop when `has_more` is `false`.
**Available on every paid tier** — no Growth restriction.
**Compatibility:** purely additive. The existing `?offset=` mode is unchanged and remains fully supported. Existing integrations don't need to change anything — switch to cursor when you next touch the integration.
See the updated docs for full examples: [Trade History (Market)](/api-reference/onchain/market-trades) · [Trade History (Wallet)](/api-reference/onchain/wallet-trades).
***
## 2026-05-01 — Bulk trade-export endpoints (Growth plan and above)
Two new endpoints return the **entire** trade history for a market token or wallet in a **single response** — no client-side pagination required:
* `GET /v2/onchain/markets/{token_id}/trades/all`
* `GET /v2/onchain/wallets/{address}/trades/all`
The endpoint walks the full history for you. Optional `from` and `to` query parameters narrow the window.
**Limits:**
* Hard cap **250,000 trades** per call. Beyond that, response includes `"partial": true` with `"partial_reason": "hard_cap_250000"`.
* Wall-clock budget **180 seconds**. First cold-cache calls on busy markets/heavy wallets may take **1-3 minutes**. Subsequent calls hit Redis cache and return in **under 1 second**.
* Typical payload **1-50 MB**. Worst case (capped): **100-200 MB JSON**.
* Cache TTL **5 minutes** per `(scope, from, to)` combination. Partial results are **not cached**.
**Tier requirement:** **Growth plan (\$200/mo) or above.** Starter and free tier requests receive `403`. The bulk endpoints are gated to paid plans because of their large query and payload sizes.
**Compatibility:** purely additive. The standard paginated `/trades` endpoints are unchanged and remain available on all paid tiers.
Updated the request timeout to 180 seconds so cold-cache calls complete cleanly.
***
## 2026-05-01 — Backtest Copy PnL: new `total_realized_pnl_usdc` field
Added `total_realized_pnl_usdc` to `/v2/copy-pnl/{wallet}` responses. This is the **signed sum of WAVG-realized PnL across every position the matcher touched in the window** — the closest single number to "did this wallet make money trading?" and the recommended primary "PnL" field for customer-facing UI.
Existing `actual_pnl_usdc` is unchanged but is now documented as a **cashflow** metric (USDC delta in/out of the wallet over the window), which is intentionally different from realized PnL — see the [endpoint docs](/api-reference/backtesting/copy-pnl#understanding-cashflow-pnl-vs-realized-pnl) for the worked example.
Also fixed a long-standing bug in the cashflow accounting where neg-risk-conversion events were over-counting `settlement_in` (treating share counts as USDC). NRC-active wallets see corrected numbers across all four periods (`14d`, `30d`, `60d`, `180d`) on the next BYOB cycle refresh.
***
## 2026-04-30 — Backtest Copy PnL: per-position metrics now match Polymarket byte-for-byte
The new fields added earlier today (`avg_entry_prob_weighted`, `positions_closed` on `/v2/copy-pnl/*`) now produce values that match Polymarket's own data-api position math.
Validated against `data-api.polymarket.com/positions` `realizedPnl` across 30 positions on diverse wallets (standard CTF + neg-risk markets):
* **97 %** sub-penny match
* **100 %** sub-\$1 match
* **100 %** sub-\$10 match
No API changes — same field names, same response shape. Existing integrations automatically benefit. Cached values progressively refresh as background scoring cycles complete.
***
## 2026-04-30 — BYOB Snapshot: every wallet × every period in one read
New endpoint `GET /v2/copy-pnl/snapshot` returns every wallet in your tracked-wallet pool with backtest copy-PnL scores across all six time windows (`7d`, `14d`, `30d`, `60d`, `90d`, `180d`) in a single response. Built for stats-card UIs and dashboards that need the whole pool at once instead of issuing one request per period.
* **One request** — no per-period round trips, no client-side stitching.
* **Sub-second cached** — 100 wallets × 6 periods returns in \~70 ms / \~150 KB. 1000 wallets × 6 periods in \~500 ms / \~1.5 MB.
* **Optional period subset** via `?periods=7d,30d` to drop payload when you only need one window.
* **Per-wallet error reporting** — heavy whales that errored on the most recent refresh surface their failure reason inline, so your UI can render "X wallets retrying" instead of hiding gaps.
```bash theme={null}
curl -H "x-api-key: $YOUR_KEY" \
"https://api.polynode.dev/v2/copy-pnl/snapshot"
```
See [BYOB — Snapshot](/api-reference/backtesting/byob-snapshot) for the full response shape and examples.
***
## 2026-04-30 — `/v2/onchain/markets/{token}/volume` faster
Backend optimization for the market volume endpoint. Response shape and values are unchanged — same `buys`, `sells`, `total_trades`, `buy_volume_usdc`, `sell_volume_usdc`, `volume_usdc`, `found` fields. Light and empty markets see roughly halved latency. No integration changes required.
***
## 2026-04-30 — Wallet activity & redemptions: deep pagination unlocked
Heavy wallets returning more than 1000 records on `/v2/onchain/wallets/{wallet}/redemptions` and `/v2/onchain/wallets/{wallet}/activity` now return the complete dataset:
* **`offset` past 1000 now works.** Previously, pagination beyond the first 1000 records returned empty results regardless of how many records existed. You can now walk a heavy redeemer's full history with `offset=1000`, `offset=2000`, etc.
* **No more silent truncation.** Wallets with >1000 redemptions, splits, merges, or neg-risk conversions previously had results capped at 1000. The full record set is now returned.
No SDK update required. No integration changes. Same URLs, same query parameters, same response shapes — your existing code automatically benefits.
```bash theme={null}
# Walk a heavy redeemer's full history:
curl "https://api.polynode.dev/v2/onchain/wallets/{wallet}/redemptions?limit=1000&offset=0" \
-H "x-api-key: $YOUR_KEY"
curl "https://api.polynode.dev/v2/onchain/wallets/{wallet}/redemptions?limit=1000&offset=1000" \
-H "x-api-key: $YOUR_KEY"
```
***
## 2026-04-30 — TypeScript SDK 0.9.7
Pluggable storage for Bun/Deno/edge runtime compatibility:
* **`storage` config option** — pass `'memory'` for an in-memory backend (no native dependencies), a file path for SQLite (default), or your own `TradingStorage` implementation.
* **Auto-fallback** — if `better-sqlite3` isn't available (Bun, Deno, edge runtimes), the SDK automatically falls back to in-memory storage instead of crashing.
* **`BunSqliteBackend`** — persistent SQLite storage using Bun's built-in `bun:sqlite`. Zero native dependencies. Same schema, same persistence as the Node backend.
* **Auto-detection** — if you don't pass a `storage` option, the SDK tries `better-sqlite3` first (Node), then `bun:sqlite` (Bun), then falls back to in-memory. Just works in both runtimes.
* **Exported interface** — `TradingStorage`, `InMemoryStorage`, and `BunSqliteBackend` are all exported so you can use them directly or build custom adapters.
```typescript theme={null}
// Bun — auto-detected, no config needed:
const trader = new PolyNodeTrader({ /* storage auto-detects bun:sqlite */ });
// Bun — explicit:
import { PolyNodeTrader, BunSqliteBackend } from 'polynode-sdk';
const trader = new PolyNodeTrader({
storage: new BunSqliteBackend('./trading.db'),
});
```
Upgrade:
```bash theme={null}
npm install polynode-sdk@0.9.7
```
***
## 2026-04-30 — TypeScript SDK 0.9.5
Bug fix release for the trading module:
* **Fixed `signer.getAddress is not a function` crash** — `ensureReady()` and `linkWallet()` could throw this error with viem WalletClient signers when Polymarket packages expect an ethers-style `getAddress()` method. The SDK now ensures `getAddress()` is always available on the normalized signer, regardless of which `@polymarket/clob-client` version you have installed.
* **Fixed signer detection logic** — a duplicate guard condition was allowing incomplete signer objects through, causing crashes instead of clean error messages.
* **Hardened relay signer adapter** — address extraction now handles additional wallet shapes during Safe deployment and approval flows.
Upgrade:
```bash theme={null}
npm install polynode-sdk@0.9.5
```
***
## 2026-04-30 — BYOB (Bring Your Own Backtest) — precomputed leaderboards
The on-demand backtest endpoints answer "score this wallet" synchronously. **BYOB inverts that** — you hand us a private wallet pool, we precompute scores in the background across all six period presets, you query the resulting leaderboard with sub-second latency.
Four new endpoints under `/v2/copy-pnl/`:
```
POST /v2/copy-pnl/wallets body: {addresses[]} add to pool (max 1000)
DELETE /v2/copy-pnl/wallets body: {addresses[]} remove from pool
GET /v2/copy-pnl/wallets list pool
GET /v2/copy-pnl/leaderboard ?period=&sort_by=&order=&limit=&offset=&min_trade_count=&exclude_toxic=
```
Newly-added wallets are scored within \~30s of being added. The full pool refreshes daily in the background. Each result row includes `computed_at` so you can render freshness in your UI.
Per-tenant isolation: each API key has its own private pool keyed on the SHA256 of the key. Your tracked wallets are never visible to other customers.
References: [Backtesting Overview](/api-reference/backtesting/overview) · [Add Wallets](/api-reference/backtesting/byob-add-wallets) · [Remove Wallets](/api-reference/backtesting/byob-remove-wallets) · [List Wallets](/api-reference/backtesting/byob-list-wallets) · [Leaderboard](/api-reference/backtesting/byob-leaderboard)
***
## 2026-04-30 — On-chain wallet/market trades: deep pagination unlocked
`/v2/onchain/wallets/{w}/trades` and `/v2/onchain/markets/{token}/trades` now properly paginate across the full trade history of any wallet or market, no matter how active. Previously these endpoints silently truncated heavy traders to \~2000 fills lifetime — deep `?offset=` queries returned empty.
Customer-visible behavior changes:
* Heavy wallets (e.g. 1M+ fills) now return correct, complete trade history
* Deep `?offset=` queries return real data instead of empty arrays
* No SDK or query-shape changes required — same `?limit=&offset=` parameters
* Lower latency on most queries
***
## 2026-04-30 — Backtest Copy PnL: batch endpoint
Score up to 100 wallets in one call:
```
POST /v2/copy-pnl/batch
{ "addresses": ["0x...", ...], "from": "...", "to": "..." }
```
Same math + response shape as the single-wallet endpoint. Per-wallet errors don't fail the batch — the bad slot returns `{"wallet": "...", "error": "..."}` and the rest still come back.
Reference: [Batch endpoint](/api-reference/backtesting/batch)
***
## 2026-04-30 — Backtest Copy PnL endpoint
New paid endpoint for scoring any wallet's copy-trade quality:
```
GET /v2/copy-pnl/{wallet}?period=30d
```
Walks every fill in the requested window, applies a realistic 2% slippage on buys and sells (capped at \$1.00 per share for buys), settles redemptions / merges / splits at face value, and returns:
* `actual_pnl_usdc` — the wallet's cashflow PnL over the window
* `backtest_copy_pnl_usdc` — what a copier would have earned with friction
* `slippage_amount_usdc` — the dollar gap
* `slippage_cost_rate_pct` — friction as a percentage of actual PnL
* `toxic_for_copying` — `true` when the rate exceeds 15% (wallet's profit relies on execution speed; copier won't replicate)
* `trade_count`, `applied_filters`, `sources`, optional `trades[]` drill-down
**Time window options**: `?period=7d|14d|30d|60d|90d|180d` (default 30d), or `?from=&to=` with `YYYY-MM-DD` or unix seconds.
**Note on PnL definition**: this returns **cashflow** PnL (real dollars moved), not Polymarket's website PnL (which marks open positions at current price). The response includes a `pnl_definition: "cashflow"` field to make this explicit. For PM-website style PnL use [Trader PnL Series](/api-reference/enriched/trader-pnl).
**Limits**: paid tier required, 1 request per 5s per key, 30s server-side budget. Validated on wallets up to \~1.6M fills in the window.
Full reference: [Backtest Copy PnL](/api-reference/backtesting/copy-pnl) · [Backtesting Overview](/api-reference/backtesting/overview)
***
## 2026-04-27 — Positions: 100% Polymarket parity, deterministic resolved-market mark, six new fields, real cursor pagination
`/v2/onchain/positions` now matches Polymarket's `data-api` to the cent on every shared position. Verified across six wallets, 627 total shared positions, 100.00% sub-penny match on `unrealized_pnl`.
What's new on each position row:
* **`initial_value`** — cost basis in USD of currently held shares. Use this when you need the basis number that matches Polymarket `cashPnl`.
* **`redeemable`** — `true` when the market has resolved and the user can call redeem to claim payout / accept loss. Filter on this with `market_status` to detect resolved-but-not-yet-redeemed positions.
* **`outcome_index`** — numeric index of this row's outcome (0 or 1 for binary markets). Stable across the API regardless of UI label.
* **`won`** / **`winning_outcome_index`** — present on `resolved-win` / `resolved-loss` rows. Explicit booleans / ints so consumers don't have to parse outcome label strings.
* **`opposite_asset`** — token ID of the binary counterpart outcome on the same market.
What's fixed on the math:
* **`current_price` is deterministic on resolved markets.** Reads settlement values directly from the CTF contract on Polygon. Returns exactly `1.0` for the winning outcome and `0.0` for the loser, the moment the market settles on chain.
* **`market_status` never lies.** Was previously stuck on `"live"` for fresh resolutions when our metadata was behind. Now uses the chain timestamp as the authoritative resolution signal: `"live"`, `"resolved-win"`, `"resolved-loss"`, `"resolved-unknown"`, or `"closed"`.
* **`unrealized_pnl` is byte-identical to Polymarket `cashPnl`** for any position covered by both sources.
`/v2/onchain/trades` cursor pagination now reports truthful `has_more`. Previously `has_more: false` was returned even when more rows existed beyond the requested `limit`. Walk pages by passing `pagination.pagination_key` back as `?pagination_key=…`.
```bash theme={null}
# returns has_more: true with pagination_key now
curl "https://api.polynode.dev/v2/onchain/trades?wallet=0x…&limit=2" -H "x-api-key: …"
# walk to the next page
curl "https://api.polynode.dev/v2/onchain/trades?wallet=0x…&limit=2&pagination_key=2" -H "x-api-key: …"
```
Time filters on `/v2/onchain/trades` use **`start_time`** and **`end_time`** (unix seconds), not `from` / `to`. Documented and verified.
***
## 2026-04-27 — Trades: unified schema, `direction` & `side` always present
`/v2/onchain/trades`, `/v2/onchain/wallets/{w}/trades`, and `/v2/onchain/markets/{tok}/trades` now return the same `direction` and `side` fields on every row, regardless of which filter combination is passed. Previously these fields appeared only when a `?wallet=` anchor was supplied — market-wide queries (`?condition_id=…`, `?token_id=…`, no filter) omitted both, and consumers had to render two different schemas.
```bash theme={null}
# customer's market-wide query — now returns direction+side on every row
curl "https://api.polynode.dev/v2/onchain/trades?condition_id=0xb778…&limit=3" \
-H "x-api-key: YOUR_KEY"
```
```json theme={null}
{
"side": "maker",
"direction": "BUY",
"maker": "0xdf0d2c…",
"taker": "0xb323fd…"
}
```
**Anchor rule:**
* **With `?wallet=`** — `direction` and `side` are wallet-relative. `direction` is `BUY` when the wallet contributed USDC and received outcome shares, `SELL` when it contributed outcome shares and received USDC. `side` is `maker` when the wallet placed the resting order, `taker` when it crossed the spread.
* **Without `?wallet=`** — `direction` is anchored on the **maker** of each fill (the user who signed the limit order; on Polymarket's CLOB this is always a user EOA). `side` is therefore always `"maker"` in this case.
This means a single fill viewed from a wallet-filtered query and from a market-wide query may report different `direction` values — both are correct, just anchored on different parties. See the ["Direction & side semantics"](/api-reference/onchain/trades#direction--side-semantics) section on the trades reference page.
Strictly additive — calls that previously returned `direction`/`side` continue to return the exact same values.
***
## 2026-04-26 — New endpoint: `GET /v2/onchain/tags` for tag discovery
Lists every tag slug in the polynode taxonomy — currently 5,779 tags. Lightweight by default (just slug strings, 73 KB / 43 ms cold for the full list) or `?details=true` for per-tag enrichment (markets, events, first/last seen).
```bash theme={null}
# all tags
curl "https://api.polynode.dev/v2/onchain/tags" -H "x-api-key: YOUR_KEY"
# top 700 mainstream tags with stats
curl "https://api.polynode.dev/v2/onchain/tags?min_markets=100&details=true" \
-H "x-api-key: YOUR_KEY"
# discover NBA-related tags
curl "https://api.polynode.dev/v2/onchain/tags?prefix=nba&details=true" \
-H "x-api-key: YOUR_KEY"
```
Values returned here are exactly what to pass to `?tag_slug=` on the [wallet positions](/api-reference/wallets/onchain-positions) and [unified positions](/api-reference/onchain/positions) endpoints. Use it to populate filter dropdowns or autocomplete inputs. The underlying materialized view refreshes hourly so newly-added Polymarket tags appear within an hour.
***
## 2026-04-26 — Wallet Positions: `tag_slugs` field + `?tag_slug=` filter
Every row of [`GET /v2/wallets/{address}/positions/onchain`](/api-reference/wallets/onchain-positions) now includes a `tag_slugs` array carrying the Polymarket event-level tags that apply to that market — typical values like `nba`, `basketball`, `sports`, `politics`, `crypto`, `fed`, `2025-predictions`, plus event-specific slugs like `2026-fifa-world-cup-winner-595`. Order is most-specific to most-general.
A new optional `?tag_slug=` query parameter filters the response to only positions whose market carries that tag. Composes with `since` / `until`, so you can ask for e.g. "all NBA positions traded in the last 30 days" in one request:
```bash theme={null}
SINCE=$(( $(date -u +%s) - 86400 * 30 ))
curl "https://api.polynode.dev/v2/wallets/0x.../positions/onchain?tag_slug=nba&since=$SINCE" \
-H "x-api-key: YOUR_KEY"
```
When the filter is supplied, aggregates recompute over the filtered set and the response gains `filtered: true` plus `applied_filters: { since, until, tag_slug }` so the behavior is self-documenting. Strictly additive — when no filter param is supplied, the response shape is byte-identical to before.
Coverage is effectively universal: tags are populated on every event Polymarket indexes (sampled empirically at 100% in our backfill audit), so any non-test market should return a populated `tag_slugs` array.
***
## 2026-04-26 — Unified Trades: additive `direction` (BUY/SELL) field on `/v2/onchain/trades`
[`GET /v2/onchain/trades`](/api-reference/onchain/trades) now also returns `direction` (`"BUY"` / `"SELL"`) on every row when filtered by `wallet` — same semantics as the wallet-trades endpoint, applied to the unified-trades flow. Independent of the existing `side` field. When no wallet anchor is supplied, `direction` is omitted (no perspective to derive against). Strictly additive: every other field is byte-identical to prior responses, and queries without `wallet` are unchanged.
Verified live: 1,214 trades cross-checked against Polymarket data-api `side` — zero drift.
***
## 2026-04-26 — Wallet Trades: additive `direction` (BUY/SELL) field
Every row of [`GET /v2/onchain/wallets/{address}/trades`](/api-reference/onchain/wallet-trades) now carries a `direction` field with values `"BUY"` or `"SELL"`, derived from whether the queried wallet contributed USDC or outcome tokens on the fill.
This is independent of the existing `side` field, which remains the exchange role (`"maker"` / `"taker"`) and is unchanged. The two answer different questions:
* `side` — did the wallet provide liquidity or take it?
* `direction` — did the wallet enter (BUY) or exit (SELL) outcome shares?
All four combinations (`maker`+`BUY`, `maker`+`SELL`, `taker`+`BUY`, `taker`+`SELL`) appear in real responses. Strictly additive — every other field is byte-identical to prior responses.
The information `direction` carries was always derivable from `maker_asset_id` / `taker_asset_id` (exactly one is `"0"` on every fill, that's the buyer). This change just makes the derivation explicit so analytics integrations don't have to compute it client-side.
***
## 2026-04-26 — Wallet Positions: per-position timestamps, parent event slug, opt-in window filter
Four new fields on every row of [`GET /v2/wallets/{address}/positions/onchain`](/api-reference/wallets/onchain-positions) (and the unified [`GET /v2/onchain/positions`](/api-reference/onchain/positions) feed). Two new optional query parameters for server-side window filtering. Every change is **strictly additive** — calls with no new parameters return responses byte-identical to before.
**New per-position fields:**
* **`last_trade_at`** — unix seconds. Latest fill on this token across V1 + V2 exchanges. The right field for "active in the last 30 / 90 days" leaderboard windows.
* **`closed_at`** — unix seconds. Latest moment the wallet redeemed any outcome of the market for collateral. Distinct from `resolved_at` — `resolved_at` is when the market itself resolved on-chain; `closed_at` is when this specific wallet actually redeemed.
* **`resolved_at`** — unix seconds. Moment the market became redeemable (oracle reported payouts on-chain). Recent markets are fully covered; markets that resolved before polynode began tracking return `null`.
* **`event_slug`** — the parent **event** slug, distinct from the per-row `slug` (which is the per-market slug). For multi-market events (NBA games with several lines, election markets with multiple candidates, FIFA World Cup with one market per team), `event_slug` is the parent the markets share. For single-market events, `event_slug` equals `slug`. Lets you group positions by event without an extra metadata round-trip.
**New optional query parameters** — opt-in only:
* **`?since=`** — keep positions whose `last_trade_at >= since`.
* **`?until=`** — keep positions whose `last_trade_at <= until`.
When either is supplied, all aggregates (`count`, `open_count`, `closed_count`, `total_realized_pnl`, `total_unrealized_pnl`, `total_pnl`, `positions_with_pnl`) recompute over the filtered set, and the response gains two top-level keys to make the shift self-documenting: `filtered: true` and `applied_filters: { since, until }`. These keys are absent from default responses, so existing integrations see zero behavior change.
```bash theme={null}
# 30-day window — leaderboard-style request
SINCE=$(( $(date -u +%s) - 86400 * 30 ))
curl "https://api.polynode.dev/v2/wallets/0x.../positions/onchain?since=$SINCE" \
-H "x-api-key: YOUR_KEY"
```
See the [Positions & P\&L (Wallet)](/api-reference/wallets/onchain-positions) reference for the full field catalog and filtering rules.
***
## 2026-04-26 — X Search API beta
New paid endpoints for live X (Twitter) search and account-timeline data. Aimed at teams adding social context next to their prediction-market data — sentiment around a market, replies on a leader's post, what's being said about an event.
* **`GET /v2/x/search?q=…&max=…`** — search by query, with full operator support (`from:`, `since:`, `min_faves:`, hashtags, `lang:`).
* **`GET /v2/x/user/{handle}/tweets?max=…`** — most-recent tweets from any public X account.
Available on starter, growth, and enterprise tiers with monthly quotas of 500 / 1,000 / 5,000. Hard rate cap of 1 request per second per key. Track usage live via `X-Quota-Used` / `X-Quota-Limit` response headers.
See the [X Search API guide](/api-reference/x-search) for the full schema and examples.
***
## 2026-04-25 — Pricing section retired: candles deduplicated, "Market Card" introduced
The "Pricing" sidebar section has been removed. It contained two endpoints:
* **`GET /v1/stats/{token_id}`** — kept, renamed to **Market Card**, moved further down the sidebar (just above "System"). This one is genuinely useful: a single call returns the condition\_id, outcomes, neg-risk flag, end date, current orderbook liquidity, 24h OHLCV summary, and last price. Saves you from chaining 3–4 calls when you just need a market overview tile.
* **`GET /v1/candles/{token_id}`** — removed from documentation. It was an in-memory rolling buffer with no historical depth, no pagination, no VWAP, and no buy/sell split. Use **`GET /v2/onchain/candles/{token_id}`** instead — same OHLCV plus VWAP, volume in shares, trade count, full history, and pagination. The endpoint itself still responds on the API for backward compatibility.
***
## 2026-04-25 — Wallet endpoints reorganized: V1 wallet endpoints removed from documentation
The "Wallets" and "Onchain" sidebar sections have been merged into a single section called **Wallets / Positions / Onchain**. All wallet, position, and trade endpoints now live in one place — the V2 onchain endpoints.
**What changed in the docs:**
The seven legacy V1 wallet/market endpoints have been removed from the documentation sidebar. They were inferior duplicates of the V2 onchain equivalents — three of them returned empty or error responses, and the rest lacked PnL, average-price, current-price, and market-status fields that the V2 versions provide directly from chain data.
| Removed from docs | Use instead |
| ------------------------------------------ | ---------------------------------------------------- |
| `GET /v1/wallets/{addr}/positions` | `GET /v2/onchain/positions?wallet=…` |
| `GET /v1/wallets/{addr}/closed-positions` | `GET /v2/onchain/positions?wallet=…&status=closed` |
| `GET /v1/wallets/positions` (multi-wallet) | Loop `GET /v2/onchain/positions?wallet=…` per wallet |
| `GET /v1/wallets/{addr}/trades` | `GET /v2/onchain/wallets/{address}/trades` |
| `GET /v1/markets/{id}/positions` | `GET /v2/onchain/positions?token_id=…` |
| `GET /v1/markets/{id}/trades` | `GET /v2/onchain/markets/{token_id}/trades` |
| `GET /v1/resolve/{query}` | `GET /v2/resolve/{query}` |
**Important — backward compatibility preserved:**
The V1 endpoints **still respond on the API**. We have not turned them off. If you have integrations hitting the legacy paths, they continue to work exactly as before — we just stopped surfacing them in the documentation because the V2 onchain replacements are strictly better. You should migrate, but you don't have to migrate today.
**Why V2 onchain is better:**
V1 wallet endpoints returned mostly social-flavored fields (bio, profile picture, pseudonym). V2 onchain endpoints return rich trading-relevant fields: `avg_price`, `realized_pnl`, `unrealized_pnl`, `current_price`, `total_bought`, `market_status`, `order_hash`, `fee`, `maker_amount`, `taker_amount`, `condition_id`, and more. Same data your equity curve, leaderboard, and PnL endpoints already use.
***
## 2026-04-25 — Equity Curve endpoint: faster default + four new query parameters
The `/v2/trader/{wallet}/equity` endpoint is faster, more flexible, and more accurate.
**Default is now realized-only.** The curve is built from completed P\&L by default — the locked-in result of every closed position, partial sell, and resolution. Same query, same numbers, every time. Whales return in 1–2 seconds (was 8+).
If you want mark-to-market on currently-open positions (the old behavior), pass `include_unrealized=1`. That path is still supported, just opt-in now because it adds latency and makes responses non-deterministic as live prices move.
**Four new optional query parameters:**
| Param | Description |
| ---------------------- | ----------------------------------------------------------- |
| `from=YYYY-MM-DD` | Start date (also accepts unix seconds). |
| `to=YYYY-MM-DD` | End date (same format). |
| `max_markets=N` | Keep only the most recent N markets the wallet has touched. |
| `include_unrealized=1` | Mark-to-market open positions (was the old default). |
All four compose. Example — fast realized-only curve over the wallet's last 50 markets in early April:
```bash theme={null}
curl "https://api.polynode.dev/v2/trader/{wallet}/equity?from=2026-04-01&to=2026-04-15&max_markets=50&normalize=1" \
-H "Authorization: Bearer YOUR_API_KEY"
```
**New response fields:** `markets_count` (distinct outcome groups in the curve), `applied_filters` (echoes what was applied, useful for debugging), and `partial` (true if the request hit the 10-second server-side cap).
**Coverage fix.** Wallets with more than 500 lifetime trades previously had positions whose first-trade timestamp wasn't found, causing those positions to be incorrectly stamped at "now" on the curve. The endpoint now walks all relevant trade history and falls back to sibling-token and redemption timestamps for positions acquired through USDC splits. Curves are dramatically more accurate for active traders.
Full reference: [/api-reference/enriched/equity-curve](/api-reference/enriched/equity-curve).
***
## 2026-04-24 — Fee Escrow V2 live on Polygon (pUSD collateral, V2 CLOB)
We've deployed a V2 variant of the Fee Escrow contract so platforms building on the V2 CLOB can charge fees in pUSD (V2's native collateral) instead of USDC.e. Both V1 and V2 are live and fully supported — there is no migration. If you're already running V1, nothing changes.
**FeeEscrow V2 (new):**
| Field | Value |
| -------------- | -------------------------------------------------------------- |
| Address | `0x3A43D88ef8Aae4dF5a50B3abf67122CAAeEF7c9F` |
| Collateral | pUSD `0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB` (6 decimals) |
| EIP-712 domain | `name="PolyNodeFeeEscrowV2"`, `version="2"`, chainId 137 |
**FeeEscrow V1 (unchanged):**
| Field | Value |
| -------------- | ------------------------------------------------------ |
| Address | `0xa11D28433B79D0A88F3119b16A090075752258EA` |
| Collateral | USDC.e `0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174` |
| EIP-712 domain | `name="PolyNodeFeeEscrow"`, `version="1"`, chainId 137 |
The contracts have byte-identical calldata, identical function signatures, and identical semantics. The only functional differences are the collateral token and the domain name / version. The `FeeAuth` typed-data struct is unchanged.
**Opting into V2 today:**
The cosigner accepts an `escrow_contract` field in `fee_auth`. Absent or set to the V1 address = V1 (default). Set to the V2 address = V2. Sign the `FeeAuth` against the V2 domain when using V2.
```jsonc theme={null}
"fee_auth": {
"escrow_contract": "0x3A43D88ef8Aae4dF5a50B3abf67122CAAeEF7c9F",
"escrow_order_id": "0x...",
"payer": "0xSafe...",
"signer": "0xEOA...",
"fee_amount": "27500",
"deadline": 1777000000,
"nonce": 0,
"signature": "0x...", // signed against the V2 domain
"affiliate": "0xPartnerWallet...",
"affiliate_share_bps": 10000
}
```
**SDKs:** V2 fee escrow is wired up in all three SDKs. Set `exchangeVersion` to V2 in your `TraderConfig` and the SDK automatically signs `FeeAuth` against the V2 domain, reads nonces from the V2 contract, approves pUSD against the V2 FeeEscrow during `ensureReady()`, and tells the cosigner to route to the V2 operator. Install:
```bash theme={null}
npm install polynode-sdk@0.9.2
pip install "polynode==0.9.2"
cargo add polynode@0.12.2
```
V1 customers: no changes, V1 stays the default. Omit `exchangeVersion` (or set it to V1) and everything keeps working exactly as before.
**Validation:** 8/8 end-to-end fork tests pass against the deployed V2 contract, exercising pullFee + distribute (30/70 treasury/affiliate split) + refund + 72-hour claimRefund + every revert path (expired deadline, wrong nonce, tampered signature). The test impersonates a real on-chain pUSD whale to fund the payer. Contract bytecode is a strict 2-change diff from V1 (collateral address + domain strings).
Full guide: [/guides/fee-escrow](/guides/fee-escrow).
***
## 2026-04-21 (late) — Unrealized P\&L and market status on positions
The `/v2/onchain/positions` and `/v2/wallets/{address}/positions/onchain` endpoints now return unrealized P\&L, the current market price, and a status flag per position — so you don't have to fetch prices separately or special-case resolved markets in client code.
**New per-position fields:**
* `unrealized_pnl` — paper P\&L on the held size at the current price. For resolved markets, uses the settlement price (1.0 for winners, 0.0 for losers). For live markets, uses the current market price. Returns 0 when `size = 0`.
* `current_price` — the price used to compute `unrealized_pnl`. `null` when `size = 0` or no price is available.
* `market_status` — one of `live`, `resolved-win`, `resolved-loss`, or `closed`. Lets you tell apart a live-tradable holding from a resolved-but-never-redeemed position without looking up the market separately.
**New portfolio totals** on `/v2/wallets/{address}/positions/onchain`:
* `total_unrealized_pnl`
* `total_pnl` — `total_realized_pnl + total_unrealized_pnl`. This is the number that matches the "Profit" value shown on a Polymarket profile page.
Drop-in — every existing field keeps its name and type.
***
## 2026-04-21 (late) — Gasless Safe wrap/unwrap via the Polymarket relayer
Patch release. `wrapToPolyUsd` / `unwrapFromPolyUsd` (and their Python/Rust equivalents) now route through the Polymarket relayer automatically when called on a Safe or Proxy wallet — the flow `ensureReady` already uses. Previously these helpers sent the transaction directly from the EOA, which failed for Safe wallets because the funds live in the Safe, not on the EOA, and the EOA has no MATIC for gas.
**Install:**
```bash theme={null}
npm install polynode-sdk@0.9.1 # TypeScript
pip install "polynode>=0.9.1" # Python
cargo add polynode@0.12.1 # Rust
```
**Verified end-to-end with real Safe-via-relayer transactions:**
* TS: wrap `0xde6c02c3...`, unwrap `0x006cd3dd...`
* Python: wrap `0x7de53811...`, unwrap `0xccf9cf32...`
* Rust: wrap `0xd80af0f4...`, unwrap `0xd21d6ad6...`
All six txs mined on Polygon with `status: 0x1`, funded by third-party relayer EOAs — your EOA only signs the Safe `execTransaction` EIP-712 payload.
**Requirements for Safe wrap/unwrap:** pass `builderCredentials` in `TraderConfig`. The SDK uses your Polymarket builder HMAC key to authenticate the relayer submit. Mint one at [polymarket.com/settings?tab=builder](https://polymarket.com/settings?tab=builder).
**Tip — avoid stale-cache order rejection after wrap:** after `wrapToPolyUsd()`, the V2 CLOB's cached balance view is stale for a few seconds. If you place an order immediately, the CLOB can return a cryptic `"error parsing fee rate bps"` instead of the actual "balance not yet visible". Call `await trader.refreshBalanceAllowance()` before the order, or wait \~3 seconds. This is why we don't auto-refresh in wrap — not every wrap is followed by an immediate order.
**Also in this patch:**
* **Rust:** fixed `encode_unwrap` selector (was `0x39f47693`, correct value is `0x8cc7104f`). All Rust V2 unwraps prior to 0.12.1 hit the wrong contract function and reverted on-chain.
* **Rust:** `get_polyusd_balance()` / `get_usdce_balance()` now read the funder address (Safe) instead of the EOA. Previously they always returned 0 for Safe-wallet users.
* **Python:** `relayer.py` module added with a minimal Python port of `@polymarket/builder-relayer-client`. Implements EIP-712 Safe tx hashing, EIP-191 personal\_sign via `eth_account`, builder HMAC headers, and `/submit` + `/transaction` polling.
***
## 2026-04-21 — SDK V2 order flow hardening (all three SDKs)
End-to-end V2 order placement verified live on `clob-v2.polymarket.com` across all three SDKs. Matched fills tagged with your builder code now appear in the V2 CLOB's `/builder/trades` attribution feed.
**Install:**
```bash theme={null}
npm install polynode-sdk@0.9.0 # TypeScript
pip install "polynode>=0.9.0" # Python
cargo add polynode@0.12.0 # Rust
```
**What changed:**
* **V2 approvals now include the V1 `NegRiskAdapter`** in the spender list. The V2 CLOB's balance-allowance check monitors this address alongside the two V2 exchanges — orders without this approval were rejected with `"not enough balance / allowance"`.
* **New methods: `refreshBalanceAllowance()` / `getBalanceAllowance()`.** The V2 CLOB maintains a cached balance-allowance view per API key; `ensureReady()` now auto-refreshes it after setting V2 approvals so the first order after onboarding goes through.
* **Amount math hardened against float-precision edge cases.** Raw-units now derived via string-based decimal conversion instead of `Math.trunc(x * 1e6)` (TS) / `(x * 1_000_000.0) as u64` (Rust), which could truncate to `N-1` for prices that round-trip badly through float.
* **V2 order body now matches `@polymarket/clob-client-v2` exactly** — adds `taker`, `postOnly`, `deferExec` fields to the POST payload.
* **`checkBalance()` is V2-aware** — reads pUSD on V2, USDC.e on V1. Previously always read USDC.e.
* **Market neg-risk detection fix (TS).** The old check treated every market as neg-risk (truthiness of the response object rather than its `neg_risk` field), which signed orders against the wrong exchange on standard markets.
**Migration:** drop-in. `trader.order(...)` signature is unchanged; if you're on V2 and your orders were going through before, they still will. If you had a custom `ensureReady`-equivalent flow, you may now drop the manual `/balance-allowance/update` call — `ensureReady()` handles it on fresh V2 approvals.
The full V2 wire format, required approvals, fee math, and common failure modes are documented in each SDK at `src/trading/V2_ORDER_FLOW.md` (TS, Rust) / `polynode/trading/V2_ORDER_FLOW.md` (Python).
**End-to-end verification (from each public registry, fresh sandbox, real V2 orders against `clob-v2.polymarket.com`):**
| SDK | Installed from | V2 order placed | Cancelled |
| -------------------- | ------------------------------- | -------------------- | --------- |
| `polynode-sdk@0.9.0` | `npm install polynode-sdk` | `0xb088a702...` LIVE | ✓ |
| `polynode 0.9.0` | `pip install polynode[trading]` | `0x4906757c...` LIVE | ✓ |
| `polynode 0.12.0` | `cargo add polynode@0.12` | `0x6d3623c6...` LIVE | ✓ |
Each sandbox pulled from the public registry, imported the SDK, placed a real V2 GTC BUY with builder attribution, confirmed `status: "live"` from the V2 CLOB, then cancelled. No local source overrides, no pre-compiled artifacts — same install path a new user follows.
**Docs fix also shipped today:** the V2 section of `/sdks/trading` now inlines `ensureReady` / `ensure_ready` so a V2-first reader who pastes only that block hits a copy-paste runnable flow. Previously the snippet silently assumed the reader had already gone through the Quick Start section.
***
## 2026-04-20 — CLOB v2 API launched
New paid REST namespace `/clobv2/*` exposing every Polymarket CLOB v2 fill, position, builder, and neg-risk event.
**11 endpoints, all enriched with market metadata (question, slug, outcome, image, condition\_id) inline:**
* `GET /clobv2/trades` — global fills, filterable by wallet/builder/market/time/size
* `GET /clobv2/positions` — every open/closed v2 position
* `GET /clobv2/candles/{token_id}` — OHLCV at 1m/5m/15m/1h/4h/1d with VWAP + buy/sell split
* `GET /clobv2/wallets/{address}/trades` — per-wallet fill history with `side`
* `GET /clobv2/markets/{token_id}/{trades,volume,orderbook}` — per-outcome aggregates
* `GET /clobv2/builders` — v2 builder leaderboard (v2-exclusive)
* `GET /clobv2/neg-risk/events{,/{parent_id}/children}` — multi-outcome event drilldown
* `GET /clobv2/volume/hourly` — platform-wide hourly volume buckets
Every list response includes `count`, `pagination.{limit,offset,has_more}`, and rate-limit headers. Paid tier required; free-tier keys receive `402`. See the [overview](/api-reference/clobv2/overview) for auth + numeric-precision conventions.
**`/v2/onchain/*` enrichment restored**
Trades / wallet-trades / market-trades / market-volume / candles on the legacy `/v2/onchain/*` endpoints now carry `market`, `market_slug`, `outcome`, `image`, and `condition_id` inline again. These fields had been quietly lost during a backend refactor.
***
## 2026-04-19 — Expanded orderbook wire format + Rust SDK 0.11.0
The orderbook WebSocket now delivers PM's full per-level payload so you can maintain a tick-accurate local book without REST polling. Verified at **100% best-bid / 100% best-ask** across 50 concurrent markets.
**What's new in the `price_change` message**
Every entry in the `assets` array now includes four extra fields:
* `size` — the absolute size at that level after the change. `"0"` means the level was removed.
* `side` — `"BUY"` (affects bids) or `"SELL"` (affects asks)
* `best_bid`, `best_ask` — best bid/ask on the asset after the change
Old clients parsing only `{asset_id, price}` are unaffected — the new fields are additive. Multiple distinct levels on the same asset may now appear in one batch; repeat hits on the same `(asset_id, price, side)` within the 250ms coalesce window still merge to last-write-wins.
See the [orderbook message reference](/orderbook/messages) for the full field list.
**Rust SDK 0.11.0**
* `PriceChangeAsset` now carries `size`, `side`, `best_bid`, `best_ask`
* New `LocalOrderbook::apply_price_change(&PriceChange)` — apply incremental updates directly to your local book
* `OrderbookEngine` automatically applies `price_change` events to its shared state so every `EngineView` stays tick-accurate
* `OrderbookUpdate::LastTradePrice(BookTrade)` — executed-trade events now deserialize cleanly (previously dropped)
**Install:**
```bash theme={null}
cargo add polynode@0.11.0
```
**Breaking:** none. Enum variants added are additive under existing serde routing. Explicit exhaustive matches on `OrderbookUpdate` will need to handle the new `LastTradePrice` variant — add a `_ => {}` arm or match it explicitly.
***
## 2026-04-19 — V2-ready SDKs released (Rust 0.9.0, TypeScript 0.8.0, Python 0.8.0)
All three polynode trading SDKs are now wire-compatible with the Polymarket V2 exchange ahead of the **April 28, 2026 cutover**. Upgrade before then. V1 orders submitted after cutover will be rejected with `order_version_mismatch`.
**What changed (all three SDKs):**
* V2 CLOB POST body now includes `"expiration": "0"` to match `@polymarket/clob-client-v2`'s wire format
* `OrderParams` now exposes a `builder` field (bytes32). Pass your builder code on each order to attribute trades on-chain. Defaults to zero (no attribution). Mint your V2 builder code at [polymarket.com/settings?tab=builder](https://polymarket.com/settings?tab=builder)
* All V2 order placement paths live-verified against `clob-v2.polymarket.com`
**Install:**
```bash theme={null}
cargo add polynode@0.9.0 # Rust
npm install polynode-sdk@^0.8.0 # TypeScript
pip install "polynode>=0.8.0" # Python
```
**Breaking (Rust only):** `OrderParams` added a required struct field. If you construct `OrderParams { ... }` explicitly without `..Default::default()`, add `builder: None` to your literal. No code change required if you use struct update syntax.
See the [V2 Migration Guide](/guides/v2-migration) for the full cutover checklist, the single-line switch, and a troubleshooting reference for common V2 errors.
***
## 2026-04-18 — Rust SDK 0.8.1: batch orderbook API
### New: batch + all-tracked queries
Every per-token orderbook method on `OrderbookEngine` and `EngineView` now has a batch and an all-tracked variant. One read lock, one round-trip, results returned as a `HashMap` keyed by token ID.
```rust theme={null}
let mids = engine.midpoints(&ids).await; // requested tokens
let mids = engine.midpoints_all().await; // every tracked token
// Same shape for: spreads / best_bids / best_asks / books
let books = engine.books_all().await; // full L2 for everything
```
Tokens not in local state are silently skipped, so callers can pass mixed lists without pre-filtering.
### New: detect inactive markets
Each tracked token now records when its local copy was last touched. Use it to find markets that have stopped moving.
```rust theme={null}
use std::time::Duration;
if let Some(ts) = engine.last_change("token_a").await {
println!("last update {:?} ago", ts.elapsed());
}
let stale = engine.inactive_since(Duration::from_secs(60)).await;
```
### New: direct state access
`engine.state()` returns the underlying `Arc>` so callers can hold the lock and walk the full state themselves — useful for snapshotting all tokens at one consistent moment or building custom views.
```rust theme={null}
let state = engine.state();
let guard = state.read().await;
let mids = guard.midpoints_all();
let books = guard.books_all();
```
### Install
```bash theme={null}
cargo add polynode@0.8.1
```
All existing per-token methods (`engine.midpoint(id)`, `engine.book(id)`, etc.) are unchanged. This release is purely additive.
***
## 2026-04-17 — Unrealized P\&L + redemption fix
### New: `unrealized_pnl` field on positions
Every position returned by `/v2/onchain/positions` now includes an `unrealized_pnl` field showing the paper profit or loss on remaining open shares.
* **Resolved markets**: uses the final settlement price ($1 for winners, $0 for losers)
* **Active markets**: uses the current market price, refreshed every 15 minutes
### Fixed: realized P\&L for redeemed positions
Positions that were closed via onchain payout redemption (rather than selling on the orderbook) now correctly reflect their realized P\&L. Previously, these showed `realized_pnl: 0` even when the position had a gain or loss.
***
## 2026-04-15 — Trade-based candles endpoint
### New: `GET /v2/onchain/candles/{token_id}`
Server-built OHLCV candles from real onchain fills. Each candle includes open, high, low, close, total volume in USD and shares, separate buy and sell volume, trade count, and VWAP.
Pagination is anchor-based: each request returns up to 1000 trades worth of candles, anchored at a timestamp, block number, or transaction hash. Walk older history by passing the cursor from the previous response. Same model used by major exchange APIs.
```bash theme={null}
curl "https://api.polynode.dev/v2/onchain/candles/$TOKEN_ID?resolution=1h" \
-H "x-api-key: YOUR_KEY"
```
Resolutions: `1m`, `5m`, `15m`, `1h`, `4h`, `1d`. Optional `gap_fill=true` for charting libraries that expect a continuous time axis. Full reference at [/api-reference/onchain/candles](/api-reference/onchain/candles).
***
## 2026-04-14 — Positions sort fix + `last_activity` field
### Fixed: `/v2/onchain/positions` wallet queries now sort by recency
When querying positions for a specific `wallet`, results are now ordered by the most recent on-chain activity for each position. Previously the `order=desc` parameter did not reflect recency, causing newer positions to appear below older ones.
The most recently traded positions now appear at the top. Positions with no fill history (rare — acquired purely via split, merge, or redemption) are placed at the end.
### New: `last_activity` response field
Wallet queries now include a `last_activity` field on each position, containing the unix timestamp of the most recent fill for that position. Use it to display "last traded" times in your UI.
```json theme={null}
{
"wallet": "0x4aefd329896464da0ffb16c4ebcd083a4360c181",
"token_id": "99969794444523158222638792918531145371262906249733117976401662588024805483979",
"size": 0,
"status": "closed",
"market": "Suns vs. Lakers",
"market_slug": "nba-phx-lal-2026-04-10",
"outcome": "Lakers",
"last_activity": 1776232830
}
```
### Pagination behavior
Wallet queries return every position for the wallet in a single response (up to 500), ordered by recency. `has_more` is always `false` and no cursor is returned — request `limit=500` once and read all results. Wallets with more than 500 lifetime positions are capped.
Non-wallet queries (filtering by `market_slug`, `condition_id`, `token_id`, or `min_size` alone) continue to use cursor-based pagination.
***
## 2026-04-12 — SDK v0.7.0: Fee Escrow + Order History
### New: Per-Order Fee Collection
The trading SDK now supports optional per-order fee collection with on-chain escrow. Platforms can charge fees on trades with a single config option.
```bash theme={null}
npm install polynode-sdk@0.7.0
```
```typescript theme={null}
const trader = new PolyNodeTrader({
polynodeKey: 'pn_live_...',
feeConfig: { feeBps: 50 }, // 0.5% fee
});
const result = await trader.order({
tokenId: '...',
side: 'BUY',
price: 0.55,
size: 100,
});
console.log(result.feeEscrowTxHash); // on-chain TX hash
console.log(result.feeAmount); // "0.0275" USDC
```
* **Automatic fee handling** -- fees are pulled into escrow before the order, distributed on fill, refunded on cancel
* **Cancel auto-refund** -- cancelling an order automatically returns the fee to the user's wallet, inline in the same request
* **Affiliate revenue sharing** -- split fees with partners on a per-order basis via `affiliate` and `affiliateShareBps`
* **72-hour safety net** -- if the operator doesn't settle, users can self-refund on-chain
* **Zero overhead when disabled** -- `feeBps: 0` (default) skips the escrow entirely with no behavior change
* **7th approval** -- added to `ensureReady()` batch, gasless for Safe wallets
### Improved: Order History
Local order history now persists fee escrow data (`feeAmountRaw`, `escrowOrderId`, `feeEscrowTxHash`). Existing databases auto-migrate on first open. No action required.
### SDK Releases
* **TypeScript** `polynode-sdk@0.7.1` on [npm](https://www.npmjs.com/package/polynode-sdk) — adds `FeeConfig` export
* **Rust** `polynode@0.7.1` on [crates.io](https://crates.io/crates/polynode) — fixes compile error in 0.7.0
* **Python** `polynode@0.7.2` on [PyPI](https://pypi.org/project/polynode/) — fixes `__version__` reporting
See the [Fee Escrow Guide](/guides/fee-escrow) for the full architecture, security model, and code examples.
***
## 2026-04-11 — polynode-charts v0.1.5
### New: `createShortFormOverlay`
One-liner to add price-to-beat overlays to any live chart. Renders interval buttons (5m/15m/1h), auto-discovers Polymarket short-form markets, draws a dashed price line, and shows live odds with a countdown timer.
```typescript theme={null}
import { createChart, createShortFormOverlay } from 'polynode-charts'
const chart = createChart('#btc')
const series = chart.addLineSeries({ color: '#f7931a' })
series.setLive(true)
chart.timeScale().goLive()
createShortFormOverlay(chart, series, { coin: 'btc' })
```
### Improvements
* **Multi-outcome event discovery** now prioritizes genuine multi-outcome markets (`neg_risk: true`) over resolved binary matches
* **`MarketInfo.outcome`** field identifies which side of a binary market each token represents ("Yes" or "No")
* **Price-to-beat badge** renders correctly on the right axis for all price ranges
* **Orderbook tooltip** no longer stays stuck after data refresh
* **Tighter chart padding** for a denser, more professional look
***
## 2026-04-11 — polynode-charts v0.1.0
New npm package for interactive charting in the browser. Zero dependencies, Canvas 2D rendering at 60fps, purpose-built for prediction market data and live crypto price streams.
### Install
```bash theme={null}
npm install polynode-charts
```
### What's included
* **Candlestick, line, area, and volume** series types with smooth animation
* **Live streaming mode** with lerp animation, pulsing dots, and auto-scroll
* **Orderbook visualization** with depth chart and spread display
* **Short-form market overlays** — price-to-beat lines with live odds rotation for 5m/15m/1h crypto markets
* **Multi-outcome support** for prediction markets (up to 20+ outcomes with auto-assigned colors)
* **Built-in data providers** — `PolynodeProvider` (REST), `PolynodeOBProvider` (WebSocket orderbook), `ShortFormProvider` (market discovery + rotation)
* **Interactive** pan, zoom, scroll, pinch-to-zoom on mobile
* **Crosshair** with snap-to-candle and OHLC tooltip
### Quick start
```typescript theme={null}
import { createChart, PolynodeProvider } from 'polynode-charts'
const provider = new PolynodeProvider({ apiKey: 'pn_...' })
const chart = createChart('#chart', {
rightPriceScale: { mode: 'probability' },
})
const series = chart.addCandleSeries({
upColor: '#22c55e',
downColor: '#ef4444',
})
const data = await provider.candles(tokenId, '1h')
series.setData(data)
```
### Documentation
Full docs at [Charts SDK Overview](/charts/overview), including API reference, data providers, series types, examples, and configuration.
`polynode-charts` is the **browser visualization** companion to `polynode-sdk` (the Node.js data SDK). They are separate npm packages. Building a frontend? Install both.
***
## 2026-04-09 — Confirmed Fills on Status Updates
`status_update` events now include a `confirmed_fills` array with exact execution data from on-chain `OrderFilled` receipt logs. This is the same data source Polymarket's own APIs read.
### What changed
* `status_update` events now carry a `confirmed_fills` field — an array of per-fill objects with exact price, size, fee, order hash, maker, taker, and token ID
* Each fill is decoded directly from `OrderFilled` receipt logs on the CTF Exchange and NegRisk CTF Exchange contracts
* Prices match Polymarket's activity data exactly (verified across 1,300+ fills with zero price discrepancies)
### Why this matters
polynode detects settlements 3-5 seconds before on-chain confirmation by decoding transaction calldata from the mempool. For single-maker fills, calldata prices are exact. For multi-maker fills (\~5% of trades), the calldata only has aggregate amounts, so per-maker prices are estimated within 0.01-0.04.
With `confirmed_fills`, you now get both: the fast pre-confirmation signal AND the exact on-chain execution data in a single subscription. Use whichever fits your use case.
### Use cases
* **Copy trading**: Use the pending `settlement` event (speed matters, price difference is negligible)
* **Analytics and bookkeeping**: Use `confirmed_fills` from the `status_update` (exact prices, fees, and sizes)
* **Full lifecycle**: Track both to compare pre-confirmation estimates against final execution
### Size difference vs Polymarket
`confirmed_fills` reports gross token amounts from the OrderFilled event. Polymarket's activity API reports sizes net of fees. The `fee` field on each fill lets you compute the net amount if needed.
### Subscribe
No subscription changes needed. If you already subscribe to `settlements`, you're already receiving `status_update` events with `confirmed_fills`.
```json theme={null}
{"action": "subscribe", "type": "settlements"}
```
### Documentation
* [Status Update Event Reference](/websocket/events/status-update) — updated with `confirmed_fills` schema and examples
* [Trade Tracking Guide](/guides/trade-tracking) — when to use pending settlements vs confirmed fills
***
## 2026-04-08 — Redemption Event Streaming
New `redemption` event type in the WebSocket stream. Every time a user redeems their outcome tokens after a market resolves, you get a real-time event with the redeemer address, payout amount, condition ID, and full market metadata enrichment.
### WebSocket Stream
* New event type: `redemption` — available in the firehose and via `event_types` filter
* Decoded from `PayoutRedemption` logs on the Conditional Tokens contract
* Includes payout amount in USDC, redeemer wallet, outcome slots redeemed
* Enriched with market title, slug, image, tokens map, neg\_risk flag
* Filter with `min_size` to see only winning redemptions, or track specific wallets
### Subscribe
```json theme={null}
{"action": "subscribe", "type": "settlements", "filters": {"event_types": ["redemption"]}}
```
### Documentation
* [Redemption Event Reference](/websocket/events/redemption) — full field reference, examples, and use cases
***
## 2026-04-07 — Polymarket V2 Exchange Support
polynode now supports the Polymarket V2 exchange system alongside V1. All V2 contracts have been identified, decoded, and verified against live mainnet data.
### Settlement Stream
* V2 settlements are detected and streamed automatically — no subscription changes needed
* Same event types: `settlement`, `status_update`, `trade`, `deposit`
* PolyUSD wrapping/unwrapping events now included in deposit stream
* V2 detection is automatic alongside V1 (both supported simultaneously)
### Trading SDK (Rust v0.6.0, TypeScript v0.6.0, Python v0.7.0)
* New `ExchangeVersion::V2` / `exchangeVersion: "v2"` config option
* V2 order signing with updated EIP-712 domain (version "2")
* `wrapToPolyUsd()` and `unwrapFromPolyUsd()` helper methods
* `getPolyUsdBalance()` and `getUsdceBalance()` balance checking
* V2 approval management (PolyUSD to V2 exchange contracts)
* One config change to switch from V1 to V2 — everything else stays the same
### Documentation
* [V2 Migration Guide](/guides/v2-migration) — what changed, what stayed the same, SDK examples
* [PolyUSD Guide](/guides/polyusd) — how to wrap/unwrap PolyUSD with code examples
* [V2 Technical Details](/guides/v2-details) — contract addresses, order struct, event signatures (subscribers only)
* Deposit and settlement event pages updated with V2 notes
### What We Verified
* V2 order placement tested live on the V2 CLOB
* EIP-712 order hash matches the live V2 exchange contract on Polygon mainnet
* PolyUSD wrapping detected through our event pipeline in real time
* All existing market data, token IDs, and enrichment identical between V1 and V2
***
## 2026-04-05 — Short-Form Docs: Full Field Reference + Orderbook Guide
Updated the [Short-Form Markets](/sdks/short-form) documentation with the complete `ShortFormMarket` field reference. All 14 fields are now documented, including `conditionId`, `clobTokenIds`, `windowStart`, `windowEnd`, `outcomes`, and `outcomePrices` which were previously undocumented.
Added a new **"Connect the Orderbook to Crypto Markets"** section showing how to pass `clobTokenIds` from a short-form rotation event directly to `OrderbookEngine` for real-time depth data on crypto prediction markets. Includes TypeScript and Rust examples.
No SDK changes. All fields were already available in the SDK — this is a docs-only update.
***
## 2026-04-04 — Crypto Price Fix (All SDKs)
Fixed stale price-to-beat values in short-form crypto markets. Polymarket changed the variant parameter on their crypto-price endpoint for 5-minute and hourly markets. The SDK now sends the correct values, matching what Polymarket's own frontend uses.
**Affected intervals:** 5-minute and hourly. 15-minute was unaffected.
```bash theme={null}
npm install polynode-sdk@0.5.9 # TypeScript
cargo add polynode@0.5.8 # Rust
pip install polynode==0.6.2 # Python
```
***
## 2026-04-04 — TypeScript SDK v0.5.8
Fixed a bug where `price_feed` events from Chainlink subscriptions were silently dropped in the TypeScript SDK. The `.on('price_feed', ...)` handler now fires correctly.
```typescript theme={null}
const sub = await pn.ws
.subscribe('chainlink')
.feeds(['BTC/USD'])
.send();
sub.on('price_feed', (event) => {
console.log(`${event.feed}: $${event.price}`);
});
```
Update with `npm install polynode-sdk@0.5.8`.
***
## 2026-04-02 — Position Management (All SDKs)
Split, merge, and convert positions directly from the SDK. No need to interact with smart contracts manually.
**TypeScript** — gasless execution via the Polymarket relayer:
```typescript theme={null}
await trader.split({ conditionId: '0x...', amount: 100 });
await trader.merge({ conditionId: '0x...', amount: 100 });
await trader.convert({ marketId: '0x...', outcomeIndices: [0, 1], amount: 100 });
```
**Rust & Python** — transaction builders that return ready-to-submit calldata:
```python theme={null}
tx = build_split_txn("0x...", 100.0, neg_risk=True)
tx = build_convert_txn("0x...", [0, 1], 100.0)
```
* [Position Management guide](/guides/position-management) — full walkthrough with examples
* Convert is unique to neg-risk multi-outcome markets (e.g. "Who will win the World Cup?")
* TypeScript SDK handles neg-risk vs standard market routing automatically
***
## 2026-04-02 — Positions Converted Event (WebSocket API)
New `positions_converted` event type for tracking position conversions on neg-risk multi-outcome markets.
When a trader converts NO positions into USDC + YES positions on complementary outcomes (via the NegRiskAdapter `convertPositions` function), this event fires with the full decoded details.
```json theme={null}
{
"data": {
"event_type": "positions_converted",
"event_title": "Hungary Parliamentary Election Winner",
"stakeholder": "0x30cecdf29f069563ea21b8ae94492e41e53a6b2b",
"converted_outcomes": ["Fidesz-KDNP", "TISZA"],
"amount": 98,
"market_id": "0x355e7310dd6e18ef5fa456de7ce1331bd8c7540c...",
"index_set": "0x...0021",
"neg_risk": true
}
}
```
* `converted_outcomes` decodes the bitmask into human-readable outcome names
* `event_title` shows the parent multi-outcome event
* Included by default in `wallets` and `markets` subscription types
* Add explicitly via `event_types: ["positions_converted"]` for other subscription types
* Fires \~13 times per minute across all active neg-risk markets
***
## 2026-04-01 — Builder Credentials (All SDKs)
### TypeScript SDK v0.5.7 / Rust SDK v0.5.7 / Python SDK v0.6.1
Platforms can now pass their own Polymarket builder credentials for order attribution. All volume gets credited to your builder profile on the [Builder Leaderboard](https://builders.polymarket.com).
```typescript theme={null}
const trader = new PolyNodeTrader({
polynodeKey: 'pn_live_...',
builderCredentials: {
key: process.env.POLY_BUILDER_API_KEY,
secret: process.env.POLY_BUILDER_SECRET,
passphrase: process.env.POLY_BUILDER_PASSPHRASE,
},
});
```
* Get your builder credentials at [polymarket.com/settings?tab=builder](https://polymarket.com/settings?tab=builder)
* polynode never stores your builder credentials. They're used per-request for HMAC signing only.
* When omitted, polynode's default builder attribution is used (orders still go through).
Install:
```bash theme={null}
npm install polynode-sdk@0.5.7 # TypeScript
cargo add polynode@0.5.7 # Rust
pip install polynode==0.6.1 # Python
```
***
## 2026-03-31 — TypeScript SDK v0.5.6 (Cache Crash Recovery)
### TypeScript SDK v0.5.6
Fixes a crash that could occur when stopping the cache mid-backfill, and adds proper crash recovery for interrupted backfills.
**Graceful shutdown**: `cache.stop()` now waits for any in-flight backfill operation to complete before closing the database. Previously, stopping during an active backfill could crash with a `Cannot read properties of null` error on the SQLite handle.
**Crash recovery**: on startup, any backfills that were interrupted by a previous crash or kill are automatically detected and retried. The cache logs exactly what's happening:
```
# Restart after crash — resumes incomplete backfills only
[PolyNodeCache] Reset 1 interrupted backfill(s) from previous session.
[PolyNodeCache] Backfilling 2 entities (1 page of 500 each) — ETA: ~1s
# Clean restart — all data persisted, no network calls needed
[PolyNodeCache] All 4 entities already backfilled, skipping.
```
**Accurate logging**: the startup log now only reports entities that actually need backfilling, instead of the total watchlist count.
```bash theme={null}
npm install polynode-sdk@0.5.6
```
***
## 2026-03-31 — Equity Curve Endpoint
### New: Trader Equity Curve
Full equity curve for any Polymarket wallet. Returns a time-ordered series of cumulative profit and loss across every position the wallet has ever taken.
**Endpoint:** `GET /v2/trader/{wallet}/equity?period=all&normalize=1`
**Features:**
* Raw P\&L curve with realized gains/losses and mark-to-market on open positions
* \$1-normalized curve for copy-trade analysis: each position scaled to \$1 of risk, showing expected returns per dollar deployed
* Period filtering: `7d`, `30d`, `90d`, `1y`, `all`
* Accuracy within 2-3% of Polymarket's own P\&L calculations (variance from real-time price differences on open positions only)
**\$1 normalization** answers the question: "If I copy every trade this wallet makes but only risk \$1 per position, what are my returns?" Useful for evaluating trader skill independent of position sizing.
```bash theme={null}
curl "https://api.polynode.dev/v2/trader/0x.../equity?period=all&normalize=1" \
-H "x-api-key: YOUR_KEY"
```
```json theme={null}
{
"positions_count": 278,
"open_count": 83,
"raw": {
"final_pnl": -395.90,
"curve": [{"t": 1774555427, "pnl": 10.56}, ...]
},
"normalized": {
"bet_size": 1,
"final_pnl": -3.19,
"curve": [{"t": 1774555427, "pnl": 0.07}, ...]
}
}
```
See the [full documentation](/api-reference/enriched/equity-curve) for methodology details and response field reference.
### Fix: Market Price Fetching
Fixed an issue where current market prices were not being fetched correctly for open positions across multiple endpoints. Price accuracy is now significantly improved for any endpoint that reports unrealized P\&L.
***
## 2026-03-29 — Rust SDK v0.5.6 (Order Placement Fix)
### Rust SDK v0.5.6
Critical fixes for order placement. The trading module now passes full end-to-end testing: wallet onboarding, order placement, cancellation, and order history.
**Order signing fixes:**
* Fixed EIP-712 struct hashing (domain type filtering)
* Fixed signature `v` normalization for CLOB compatibility
* Fixed amount rounding to match tick-size-based precision rules
* Fixed salt generation range for CLOB compatibility
**Authentication fixes:**
* Fixed HMAC base64 decoding for URL-safe encoded secrets
* Fixed wallet address checksumming (EIP-55) in all auth headers and order payloads
**Privy integration fixes:**
* Fixed address formatting in Privy wallet API calls
* Fixed signature normalization in Privy response extraction
```toml theme={null}
[dependencies]
polynode = { version = "0.5.6", features = ["trading"] }
```
***
## 2026-03-29 — Python SDK v0.6.0
### Python SDK v0.6.0
Trading module fixes and Privy server-side wallet support.
**Trading fixes:**
* Fixed CLOB credential creation (endpoint, auth headers, EIP-712 message structure)
* Fixed order signing (domain name, signature format, payload serialization)
* Fixed HMAC authentication for URL-safe base64 secrets
* Fixed `fetch_tick_size` and `fetch_neg_risk` response parsing
* Full end-to-end verified: wallet generation, onboarding, order placement, cancellation, gasless flow
**New: Privy signer**
* `PrivySigner` for server-side trading with Privy-managed wallets
* No private key needed, signing via Privy's wallet API
* Works with all trading methods (`ensure_ready`, `order`, `cancel_all`, etc.)
**Other fixes:**
* `markets_by_category()` now correctly filters via query parameter
* Fixed subscription filters code example in docs
```bash theme={null}
pip install polynode==0.6.0
# With trading support:
pip install "polynode[trading]==0.6.0"
```
***
## 2026-03-29 — Rust SDK v0.5.0 (Feature Parity)
### Rust SDK v0.5.0
Major release bringing the Rust SDK to feature parity with the TypeScript SDK.
**New REST endpoints (12 methods):**
* Orderbook REST: `orderbook_rest()`, `midpoint()`, `spread()`
* Enriched data: `leaderboard()`, `trending()`, `activity()`, `movers()`
* Trader analytics: `trader_profile()`, `trader_pnl()`
* Events: `event()`, `search_events()`, `markets_by_category()`
**Trading module** (`--features trading`):
* `PolyNodeTrader` for full order lifecycle on Polymarket
* Wallet generation, onboarding (auto-detect Safe/Proxy), order placement, cancellation
* Native EIP-712 signing via `TradingSigner` trait + `PrivateKeySigner`
* CREATE2 address derivation for Safe and Proxy wallets
* CLOB authentication with L2 HMAC headers
* Builder attribution via co-signer proxy
* Local SQLite storage for credentials and order history
* On-chain approval and balance checks
**Privy integration** (`--features privy`):
* `PrivySigner` for server-side trading with Privy-managed wallets
* Pure HTTP-based, no native SDK dependency
**Redemption watcher:**
* Monitor wallets for redeemable positions after oracle resolution
* Fires alerts with winner detection and payout estimation
```toml theme={null}
[dependencies]
polynode = "0.5"
# With trading:
# polynode = { version = "0.5", features = ["trading"] }
```
Full documentation: [Rust SDK](/sdks/rust)
***
## 2026-03-29 — Trading Module v0.5.5 (TypeScript SDK)
### TypeScript SDK v0.5.5
* **EOA auto-approvals:** `ensureReady()` now automatically sends approval transactions for EOA wallets with proper nonce management. Requires ~~0.05 MATIC in the wallet (~~\$0.01).
* **Fix:** `createPrivySigner`, `createPrivyClient`, and other Privy helpers are now correctly exported from the package root. In v0.5.2 these were only reachable through non-public module paths.
* **Privy server-auth integration:** `createPrivySigner()` and `createPrivyClient()` for server-side trading with Privy-managed wallets.
* **Unlimited gasless operations:** Paid tiers get unlimited gasless on-chain operations. Free tier gets basic gasless onboarding.
* **ethers v5/v6 compatibility:** Safe transaction signing works with both ethers v5 and v6.
```bash theme={null}
npm install polynode-sdk@0.5.5 viem better-sqlite3 @polymarket/clob-client @polymarket/builder-relayer-client @polymarket/builder-signing-sdk
```
Full documentation: [Trading](/sdks/trading)
***
## 2026-03-29 — Trading Module (TypeScript SDK v0.5.0)
### TypeScript SDK — Order Placement on Polymarket
Place and manage orders on Polymarket through the polynode SDK. One function call handles wallet setup, credential creation, approvals, and order placement.
```bash theme={null}
npm install polynode-sdk@0.5.0 viem better-sqlite3 @polymarket/clob-client @polymarket/builder-relayer-client @polymarket/builder-signing-sdk
```
**One-call onboarding:**
```typescript theme={null}
import { PolyNodeTrader } from 'polynode-sdk';
const trader = new PolyNodeTrader({ polynodeKey: 'pn_live_...' });
// Auto-detects your wallet type. Deploys Safe, sets approvals, creates credentials.
const status = await trader.ensureReady('0xYOUR_PRIVATE_KEY');
const result = await trader.order({
tokenId: '...',
side: 'BUY',
price: 0.55,
size: 100,
});
await trader.cancelOrder(result.orderId);
```
**Key features:**
* **Auto-detection:** Pass your private key. The SDK checks if you have a Safe, Proxy, or EOA on-chain and uses the right one. No wallet type guessing.
* **Wallet generation:** `PolyNodeTrader.generateWallet()` for users starting from scratch.
* **Gasless onboarding:** Safe deployment + 6 token approvals in one call, \~10 seconds, zero gas.
* **Local credential custody:** CLOB credentials stored in local SQLite. Export, import, and back up freely.
* **Builder attribution:** Orders route through polynode's relay for affiliate tracking. If the relay is down, orders fall back to direct CLOB submission.
* **All three wallet types:** EOA (type 0), Proxy (type 1, legacy Magic Link), Safe (type 2, browser wallets).
* **Dome drop-in:** Same `@polymarket/clob-client` signing under the hood. Import existing Dome credentials with `linkCredentials()`.
Full documentation: [Trading](/sdks/trading) | [Dome Migration](/dome-migration#order-placement)
***
## 2026-03-28 — Onchain Data Endpoints + Market Metadata Enrichment
### Enrichment: All Onchain Endpoints Now Include Market Metadata
Every onchain endpoint now returns enriched data with human-readable market context. Responses include `market` (question text), `slug`, `outcome` label, `image`, and `condition_id` alongside the raw onchain data. Powered by a full backfill of all 706K+ Polymarket markets into a local metadata store that refreshes every 5 minutes.
### New: Onchain Data Section
Seven new endpoints for blockchain settlement data. These provide complete, accurate data that never times out or drops records.
All onchain endpoints are under `/v2/onchain/` and documented in the new [Onchain](/api-reference/onchain/wallet-trades) section of the API reference.
#### Wallet endpoints
* **`GET /v2/onchain/wallets/{addr}/trades`** — Complete trade fill history for any wallet. Every fill, every counterparty, every fee.
* **`GET /v2/onchain/wallets/{addr}/redemptions`** — All redemptions with payout amounts. See who cashed out, when, and how much. Not available anywhere else.
* **`GET /v2/onchain/wallets/{addr}/activity`** — Splits, merges, and multi-outcome conversions. Shows how wallets interact with the CTF contract beyond simple trading.
#### Market endpoints
* **`GET /v2/onchain/markets/{tokenId}/trades`** — Complete trade tape for any market token.
* **`GET /v2/onchain/markets/{tokenId}/volume`** — Lifetime volume stats: total trades, buys, sells, USDC volume with buy/sell breakdown.
* **`GET /v2/onchain/markets/{conditionId}/oi`** — Per-market open interest in USDC.
* **`GET /v2/onchain/oi`** — Global platform open interest (currently \~\$469M).
All endpoints support `limit` and `offset` pagination where applicable. Responses are cached for 1-5 minutes depending on the endpoint.
***
### Wallet P\&L: Complete Position History + Accurate Realized P\&L
#### What changed
The existing `GET /v1/wallets/{addr}/positions` endpoint only returns **open** positions. Once a market resolves or a position is fully sold, it disappears from the response. The existing `GET /v1/wallets/{addr}/trades` endpoint silently drops trades for high-volume wallets (we verified one wallet showing 403 trades through the API vs 4,405 actual onchain trades).
This made it impossible to compute accurate P\&L through our API. That's fixed now.
Two new v2 endpoints return complete position history with accurate P\&L sourced from onchain settlement data. Per-position `realized_pnl` values come from the same onchain settlement data Polymarket uses and match individual position P\&L to the penny. The `total_realized_pnl` aggregate measures total realized gains, which is different from Polymarket's profile PnL (see [docs](/api-reference/wallets/onchain-positions#total_realized_pnl-vs-polymarket-profile-pnl) for a full breakdown).
#### `GET /v2/wallets/{address}/positions/onchain`
**One call. No pagination. All positions (open + closed).**
Unlike the v1 positions endpoint which requires the SDK to paginate through trades page by page, this endpoint returns the complete picture in a single request with up to 20,000 positions.
```bash theme={null}
curl "https://api.polynode.dev/v2/wallets/0xbddf61af533ff524d27154e589d2d7a81510c684/positions/onchain" \
-H "x-api-key: YOUR_KEY"
```
```json theme={null}
{
"wallet": "0xbddf61af533ff524d27154e589d2d7a81510c684",
"source": "onchain",
"count": 873,
"open_count": 334,
"closed_count": 288,
"total_realized_pnl": 17183579.48,
"positions_with_pnl": 309,
"positions": [
{
"token_id": "3415885798119615...",
"size": 0,
"avg_price": 0.316481,
"realized_pnl": 447182.95,
"total_bought": 654236.31
}
]
}
```
| Field | Description |
| -------------------------- | ---------------------------------------------- |
| `count` | Total positions (open + closed) |
| `open_count` | Positions still held (`size > 0`) |
| `closed_count` | Fully exited positions with nonzero P\&L |
| `positions_with_pnl` | Number of positions with nonzero realized P\&L |
| `total_realized_pnl` | Sum of all `realized_pnl` values |
| `positions[].realized_pnl` | Profit or loss for this position in USDC |
| `positions[].avg_price` | Volume-weighted average entry price |
| `positions[].total_bought` | Total tokens acquired |
Responses are cached for 5 minutes. First request takes 200ms-3s depending on position count. Cached responses return in under 50ms.
#### `GET /v2/wallets/{address}/closed-positions`
Closed positions with full metadata (title, outcome, slug). Paginated.
```bash theme={null}
curl "https://api.polynode.dev/v2/wallets/0xbddf61af533ff524d27154e589d2d7a81510c684/closed-positions?limit=3&sortBy=REALIZEDPNL&sortDirection=DESC" \
-H "x-api-key: YOUR_KEY"
```
Parameters: `limit` (max 50), `offset`, `sortBy` (REALIZEDPNL, AVGPRICE, PRICE, TITLE, TIMESTAMP), `sortDirection` (ASC/DESC).
#### SDK: Automatic onchain P\&L backfill
The TypeScript (`polynode-sdk@0.4.10`) and Rust (`polynode@0.4.3`) SDKs now fetch onchain position data automatically during backfill. No code changes needed if you're already using `cache.start()`. The backfill makes one additional API call per wallet (the onchain positions endpoint above) and stores the results in your local SQLite database.
```bash theme={null}
npm install polynode-sdk@0.4.10
```
**Before:** The SDK backfilled trades page by page (up to 6 requests per wallet) and computed P\&L from incomplete trade history. Wallets with dropped trades got wrong numbers.
**After:** One call per wallet returns all positions with precomputed P\&L. The SDK stores this in SQLite and uses it as the source of truth for all P\&L queries.
```typescript theme={null}
const cache = new PolyNodeCache(client);
await cache.start();
// P&L from local cache — no API calls
const pnl = cache.computeRealizedPnl("0xbddf61af533ff524d27154e589d2d7a81510c684");
```
```json theme={null}
{
"wallet": "0xbddf61af533ff524d27154e589d2d7a81510c684",
"total_realized_pnl": 17183579.48,
"total_unrealized_pnl": 3030.53,
"total_pnl": 17186610.01,
"confidence": "full",
"trades_analyzed": 403,
"tokens": [
{
"token_id": "3415885798119615...",
"market_title": "Nuggets vs. Warriors",
"outcome": "Warriors",
"realized_pnl": 447182.95,
"avg_cost": 0.3170,
"remaining_size": 0
}
]
}
```
`trades_analyzed` reflects cached trades from the REST API (403 in this case). The `total_realized_pnl` and per-token `realized_pnl` values come from onchain settlement data and are accurate regardless of cached trade count.
`walletDashboard()` now includes `realized_pnl`, `pnl_confidence`, and `token_pnl` fields:
```typescript theme={null}
const dash = cache.walletDashboard("0xbddf61af533ff524d27154e589d2d7a81510c684");
// dash.realized_pnl → 17183579.48
// dash.pnl_confidence → "full"
// dash.token_pnl → per-token P&L breakdown
// dash.total_pnl → realized + unrealized combined
```
***
### Crypto Price Streaming (WebSocket)
Real-time cryptocurrency prices are now available on the same WebSocket connection you already use for settlements and oracle events. Seven feeds, each updating roughly once per second:
**BTC/USD**, **ETH/USD**, **SOL/USD**, **BNB/USD**, **XRP/USD**, **DOGE/USD**, **HYPE/USD**
Subscribe with `{"action": "subscribe", "type": "chainlink"}`. Optionally filter to specific feeds:
```json theme={null}
{
"action": "subscribe",
"type": "chainlink",
"filters": { "feeds": ["BTC/USD", "ETH/USD"] }
}
```
Each price update includes bid, ask, and mid price:
```json theme={null}
{
"type": "price_feed",
"feed": "BTC/USD",
"timestamp": 1774675338,
"data": {
"feed": "BTC/USD",
"price": 66240.86,
"bid": 66234.85,
"ask": 66243.30,
"timestamp": 1774675338
}
}
```
BTC/USD includes actual bid/ask spread. Other feeds currently report bid and ask equal to the mid price.
See the [full documentation](/crypto/overview) for connection examples, feed details, and event format reference.
***
### Crypto REST Endpoints
Five new REST endpoints for crypto prediction market data:
* `GET /v1/crypto/markets` — All crypto prediction markets with volume, liquidity, and open interest (3 min cache)
* `GET /v1/crypto/candles?symbol=BTC` — \~30 five-minute OHLC candles (30s cache)
* `GET /v1/crypto/price?symbol=BTC&window={epoch}` — Open/close price for a specific market window (10s cache)
* `GET /v1/crypto/active` — Currently active 5-minute up-or-down markets across all 7 coins with live odds (30s cache)
* `GET /v1/crypto/series` — Recurring crypto market series across timeframes (5 min cache)
```bash theme={null}
curl "https://api.polynode.dev/v1/crypto/candles?symbol=ETH" \
-H "x-api-key: YOUR_KEY"
```
Supported symbols: `BTC`, `ETH`, `SOL`, `BNB`, `XRP`, `DOGE`, `HYPE`.
See the [REST API reference](/crypto/rest-api) for full parameter details and response samples.
***
## 2026-03-26
### API — Wallet Resolver
New endpoint that instantly resolves any Polymarket wallet identity. Pass a proxy wallet address, EOA address, or username and get back all three.
```bash theme={null}
curl "https://api.polynode.dev/v1/resolve/DTCahill" \
-H "x-api-key: YOUR_KEY"
```
```json theme={null}
{
"safe": "0x0ecdd241ec1bc84a40b8142bebe65787aee97514",
"eoa": "0xddd57cce99ca962fe23aa2da95b139f86a241459",
"username": "DTCahill"
}
```
Covers \~3 million Polymarket wallets and is updated daily. Lookups are sub-millisecond. Accepts any of the three identifiers as input:
* `GET /v1/resolve/{safe_address}` — proxy wallet to EOA + username
* `GET /v1/resolve/{eoa_address}` — EOA to proxy wallet + username
* `GET /v1/resolve/{username}` — username to both addresses (case-insensitive)
See the [full documentation](/api-reference/wallets/resolve) for details.
***
### API — `order_hash` on Settlements and Trades
Every settlement trade and confirmed trade event now includes an `order_hash` field — the EIP-712 hash that uniquely identifies a limit order on Polymarket's order book.
**Why this matters:** A single limit order can be partially filled across multiple transactions. The `order_hash` stays the same across all of them, letting you track order lifecycle, group partial fills, and build order-level analytics.
* **Settlement events:** `order_hash` appears on each trade inside the `trades[]` array. Available on pending (pre-chain) settlements, computed from transaction calldata before block confirmation.
* **Trade events:** `order_hash` appears as a top-level field, extracted directly from the on-chain `OrderFilled` event log.
```json theme={null}
{
"type": "trade",
"data": {
"order_hash": "0x916fa5c2728c93e19ea5d7c254b07234815ad01e59597573c09d852fe53ee564",
"maker": "0x4b8cf80092c60b9b23d22133eb63eb4508fe4d31",
"price": 0.71,
"size": 3.0
}
}
```
No SDK update required — the field is automatically included in the WebSocket stream.
### API — `order_hashes` on Status Updates
The `status_update` event now includes an `order_hashes` field containing every EIP-712 order hash from the original pending settlement. This lets you correlate confirmed settlements back to specific limit orders without needing to store state from the initial pending event.
```json theme={null}
{
"type": "status_update",
"data": {
"tx_hash": "0xbb26c8cd...",
"order_hashes": [
"0xafc3488df3505c4dbba8797b8b6c645630c8ee9decf811a0acc4d63fdecfdd36",
"0x9b5a06de7196bde87b4d8baa6cb80681151338d263fac0e643a2d53c4497e27f"
],
"maker_wallets": ["0xd408...", "0x9443..."],
"latency_ms": 3959
}
}
```
Purely additive. Existing clients are unaffected.
***
### TypeScript SDK — RedemptionWatcher Memory Management
The `RedemptionWatcher` now automatically evicts resolved conditions and zero-size positions from its local watch set. This keeps memory bounded to only active, non-zero positions regardless of how long the watcher runs or how many wallets it tracks.
Previously, resolved markets and fully-sold positions accumulated in memory indefinitely. For long-running services tracking thousands of wallets, this could grow to hundreds of megabytes over weeks.
**What changed:**
* Resolved conditions are removed from the index immediately after alerts fire
* Positions that drop to zero size (full sell) are removed automatically
* `refreshInterval` default changed from `0` (disabled) to `300_000` (5 minutes). This periodic REST refresh acts as a safety net, re-populating any positions missed during brief WebSocket disconnections.
**No breaking changes.** Alert behavior is identical. If you were explicitly setting `refreshInterval: 0`, that still works.
```bash theme={null}
npm install polynode-sdk@latest
```
See [RedemptionWatcher — Memory Management](/sdks/redemption-watcher#memory-management) for details.
***
### Trader Profile — EOA Wallet Resolution
The `/v1/trader/{wallet}` endpoint now returns an `eoaWallet` field that resolves the underlying EOA (externally owned account) for Polymarket proxy wallets. This is derived onchain from the Gnosis Safe contract.
```json theme={null}
{
"wallet": "0xc2e7800b5af46e6093872b177b7a5e7f0563be51",
"eoaWallet": "0xb49e5499562a4bc3345c1a1f2db13a5360dfddac",
"name": "beachboy4",
...
}
```
For older lightweight proxy wallets, `eoaWallet` returns `null`.
### Tier-Aware Rate Limits on Data-Heavy Endpoints
A handful of data-heavy endpoints now have their own per-key rate limits that scale with your plan:
| Tier | Limit |
| ---------- | ----------- |
| Free | 1 req/sec |
| Starter | 30 req/sec |
| Growth | 50 req/sec |
| Enterprise | 100 req/sec |
Affected endpoints: `/v1/trader/{wallet}`, `/v1/trader/{wallet}/pnl`, `/v1/leaderboard`, `/v1/trending`, `/v1/activity`, `/v1/event/{slug}`, `/v1/movers`, `/v2/markets/{category}`.
All other REST endpoints use your plan's standard rate limit. See [Rate Limits](/guides/rate-limits) for full details.
***
## 2026-03-25
### TypeScript SDK — RedemptionWatcher
New high-level class that monitors wallets and fires alerts the instant any position becomes redeemable on-chain.
```bash theme={null}
npm install polynode-sdk@latest
```
```typescript theme={null}
import { RedemptionWatcher } from 'polynode-sdk';
const watcher = new RedemptionWatcher({ apiKey: 'pn_live_...' });
watcher.on('alert', (alert) => {
if (alert.isWinner) {
console.log(`${alert.wallet} can redeem "${alert.marketTitle}" — ~$${alert.estimatedPayoutUsd}`);
}
});
await watcher.start(['0xabc...', '0xdef...']);
```
**What it does:** Fetches wallet positions via REST, indexes by `condition_id`, subscribes to the oracle stream for `condition_resolution` events, and cross-references in real-time. Optional live position tracking via the wallets stream keeps sizes accurate as users trade.
**Key features:**
* Runtime `addWallets()` / `removeWallets()` with automatic re-subscription
* Callback (`.on('alert', ...)`) and async iterator (`for await`) consumption
* Winner detection with payout estimates
* Full market metadata on every alert (title, slug, image, outcomes)
See the [full documentation](/sdks/redemption-watcher) for lifecycle, configuration, and a production example.
***
### Oracle Stream — `condition_resolution` Event
The oracle WebSocket stream now includes `condition_resolution` events. This is the on-chain signal that positions are redeemable on the Conditional Tokens contract.
Previously, the stream included `resolution` events (outcome decided on the UMA adapter) but not the separate on-chain step where `reportPayouts()` is called on the CTF contract. For neg-risk markets (the majority on Polymarket), there is a \~2-3 minute gap between these two events. `condition_resolution` closes that gap.
**Subscribe and filter:**
```json theme={null}
{
"action": "subscribe",
"type": "oracle",
"event_types": ["condition_resolution"]
}
```
**Payload includes:** `resolved_price`, `payouts`, `condition_id`, `question_id`, and full market metadata (title, outcomes, token IDs, image).
**Use case:** Trigger redemption workflows the instant positions become redeemable, instead of polling or waiting for the Polymarket UI to update.
**SDK support:** Available in Rust SDK `polynode 0.4` via `OracleEventType::ConditionResolution`. TypeScript SDK update coming soon.
***
### TypeScript SDK v0.4.8 — Composable Leaderboard Builder
The local cache now supports composable leaderboards with multi-metric ranking, market filtering, slug pattern matching, wallet scoping, and time windows.
**New:** Call `cache.leaderboard()` with no arguments to get a `LeaderboardBuilder`:
```typescript theme={null}
const rows = cache.leaderboard()
.metrics(['total_pnl', 'volume', 'win_rate'])
.slugs(['*election*'])
.since(weekAgo)
.limit(10)
.build();
// Each row: { wallet, label, rank, metrics: { total_pnl, volume, win_rate } }
```
**7 new metrics** (11 total): `roi`, `realized_pnl`, `volume`, `avg_trade_size`, `largest_win`, `largest_loss`, `market_count`.
**Builder methods:**
* `.metric()` / `.metrics()` — single or multi-metric per row
* `.sortBy()` / `.sort('ASC' | 'DESC')` — control ranking
* `.markets([conditionIds])` — filter by market
* `.slugs([patterns])` — glob match on market slugs (`*election*`, `btc-*`)
* `.category({ name, slugs })` — reusable named slug groups
* `.wallets([addrs])` — rank a wallet subset instead of full watchlist
* `.since(ts)` / `.until(ts)` — time window for trade metrics
* `.limit(n)` — top N
Backward compatible. Existing `cache.leaderboard('total_pnl')` still returns the same `LeaderboardEntry[]` format.
```bash theme={null}
npm install polynode-sdk@0.4.8
```
Full documentation: [Local Cache — Leaderboard Builder](/sdks/local-cache#leaderboard-builder)
***
### WebSocket — Application-Level Keepalive
Cloud platform reverse proxies (Railway, Render, Heroku, AWS ALB, fly.io) can intercept WebSocket Ping/Pong control frames, preventing the server from detecting that your client is still alive. This caused connections to drop after 1-2 minutes with an empty error and no close frame.
**What changed:**
* The server now accepts any incoming message (not just Pong frames) as proof the client is alive
* New `{"action": "ping"}` message returns `{"type": "pong", "ts": ...}` as an application-level keepalive
* Stale connection timeout extended from 90 seconds to 5 minutes
* Stale disconnects now include a close frame with a reason, instead of a silent drop
**If you're running on a cloud platform**, add a periodic ping to your connection:
```python Python theme={null}
async def keepalive():
while True:
await asyncio.sleep(30)
await ws.send(json.dumps({"action": "ping"}))
ping_task = asyncio.create_task(keepalive())
```
```javascript Node.js theme={null}
setInterval(() => ws.send(JSON.stringify({ action: "ping" })), 30000);
```
If you're running on bare metal or a VM with no reverse proxy in front of your client, this doesn't affect you. Standard WS Ping/Pong still works as before.
See [WebSocket Overview](/websocket/overview#application-level-keepalive) for full connection examples.
***
### WebSocket — Reconnect Gap-Fill with `since`
Subscribe messages now accept an optional `since` filter (UNIX milliseconds). When set, the initial snapshot returns all events after that timestamp instead of just the most recent N, letting you fill gaps after a disconnect or cold start without missing data.
```json theme={null}
{
"action": "subscribe",
"type": "settlements",
"filters": { "since": 1774412600000 }
}
```
The response is the same `snapshot` message you already handle. Existing clients are unaffected — `since` is fully optional.
**Lookback windows by tier:**
| Tier | Max lookback |
| ---------- | ------------ |
| Free | 30 seconds |
| Starter | 2 minutes |
| Growth | 5 minutes |
| Enterprise | 5 minutes |
If `since` is older than your tier's window, it's automatically clamped. Within the window, there is no event count limit — you get everything that matches your filters.
***
## 2026-03-24
### API — Onchain Redemption State on Wallet Positions
The `/v1/wallets/{addr}/positions` and `/v1/wallets/positions` endpoints now include onchain redemption data for resolved markets. Three new fields are returned on positions where `redeemable` is `true`:
* **`redeemed`** — `true` if the wallet has burned their tokens onchain (claimed USDC payout), `false` if tokens are still held.
* **`unredeemed_balance`** — Number of tokens still held onchain. Tells you exactly how much hasn't been claimed.
* **`unredeemed_usd`** — USD value of unclaimed tokens (\$1 per token on winning positions).
This data is not available from Polymarket's API. polynode checks the Conditional Token Framework contract onchain in a single batched call, adding \~130ms only when redeemable positions are present. Active (unresolved) positions are unaffected.
```bash theme={null}
curl "https://api.polynode.dev/v1/wallets/0x62d2.../positions" \
-H "x-api-key: pn_live_..."
```
```json theme={null}
{
"title": "Lana Del Rey divorce in 2025?",
"redeemable": true,
"redeemed": false,
"unredeemed_balance": 418.73,
"unredeemed_usd": 418.73,
"size": 418.73
}
```
***
## 2026-03-23
### API — Event Search & Token IDs
Two changes to event endpoints:
* **New:** `GET /v1/events/search?q=...&limit=N` — search events by text query. Returns events with all sub-markets, each including `tokenId` (YES token for CLOB price history via `/v1/candles`) and current `price`. Multi-outcome events like "How many Fed rate cuts in 2026?" return as a single result with all outcomes.
* **Updated:** `GET /v1/event/{slug}` — now includes `tokenId` on every market in the response. Previously, token IDs were not available on this endpoint.
```bash theme={null}
curl "https://api.polynode.dev/v1/events/search?q=recession&limit=5" \
-H "x-api-key: pn_live_..."
```
Rate limit: 1 request per second per API key (shared with other enriched data endpoints).
### TypeScript SDK v0.4.6
* **New:** `searchEvents(query, { limit })` — typed method for event search, returns `EventSearchResponse`
* **New types:** `EventSearchResponse`, `EventSearchResult`, `EventSearchMarket`
* **Updated:** `EventMarket` now includes optional `tokenId` field
```bash theme={null}
npm install polynode-sdk@0.4.6
```
```typescript theme={null}
const results = await pn.searchEvents('Fed rate', { limit: 5 });
for (const event of results.events) {
console.log(event.title, `(${event.markets.length} outcomes)`);
for (const m of event.markets) {
// Use tokenId to fetch price history
const candles = await pn.candles(m.tokenId, { resolution: '1h' });
}
}
```
***
## 2026-03-21
### TypeScript SDK v0.4.4 — Enriched Data Methods
Eight new typed methods on the `PolyNode` client for all enriched data endpoints:
* `leaderboard()` — top 20 traders by profit or volume, with period filtering
* `trending()` — carousel, breaking markets, hot topics, featured events, and biggest movers
* `activity()` — platform-wide trade feed (50 most recent)
* `movers()` — markets with largest 24h price swings
* `traderProfile(wallet)` — full trader stats: PnL, volume, trades, largest win
* `traderPnl(wallet, { period })` — cumulative PnL time series
* `event(slug)` — full event detail with all sub-markets
* `marketsByCategory(category)` — browse markets by category
All methods are fully typed with dedicated response interfaces. Install:
```bash theme={null}
npm install polynode-sdk@0.4.4
```
***
### TypeScript SDK v0.4.3 — Cache UI Primitives
Four new features for building dashboards on top of the local cache.
* **View methods** — `watchlistSummary()`, `walletDashboard()`, `leaderboard()`, `marketOverview()`. Pre-shaped data for common dashboard patterns. No SQL, no aggregation.
* **Reactive subscriptions** — `onChange()` and `onWalletChange()` fire callbacks when new trades or settlements land from the live WebSocket stream. Returns an `unsub()` function for cleanup.
* **Export helpers** — `exportCSV()`, `exportJSON()`, `exportRows()` dump filtered data for charting libraries, spreadsheets, or custom analysis.
* **Query builder** — Chainable fluent API: `.wallet()`, `.side()`, `.since()`, `.market()`, `.minPnl()`, `.limit()`, `.run()`. Complex filters without writing SQL.
```bash theme={null}
npm install polynode-sdk@0.4.3
```
Full documentation: [Local Cache](/sdks/local-cache)
***
## 2026-03-21
### API — Enriched Data Endpoints
Eight new REST endpoints for trader analytics, market discovery, and platform trends.
* `GET /v1/leaderboard` — Top 20 traders ranked by profit or volume (daily, weekly, monthly, all-time)
* `GET /v1/trader/{wallet}` — Full trader profile: PnL, volume, trade count, largest win, portfolio value
* `GET /v1/trader/{wallet}/pnl` — PnL time series at 4 resolutions (1D, 1W, 1M, ALL)
* `GET /v1/trending` — Carousel highlights, breaking markets, hot topics, featured events, biggest movers
* `GET /v1/activity` — Platform-wide live trade feed (last 50 trades with tx hashes)
* `GET /v1/event/{slug}` — Full event data with all sub-markets, outcome prices, condition IDs
* `GET /v1/movers` — Markets with the largest 24h price changes
* `GET /v2/markets/{category}` — Category market listings with counts (crypto, politics, sports, etc.)
Rate limit: 1 request per second per API key. Responses cached 1-3 minutes.
### TypeScript SDK v0.4.1
* **Fix:** ESM imports now work correctly. v0.4.0 crashed on `cache.start()` when using `import` syntax.
* **Fix:** Eliminated `MODULE_TYPELESS_PACKAGE_JSON` Node.js warning.
* **New:** `getActiveTestWallet()` / `getActiveTestWallets()` — returns known-active wallet addresses for testing and examples.
```bash theme={null}
npm install polynode-sdk@0.4.1
```
### API — Wallet positions fix
* Fixed `firstTradeAt` / `lastTradeAt` returning `null` for certain wallets on `GET /v1/markets/{id}/positions?includeTrades=true`.
* Added fallback query path for wallets where the primary trade lookup returns empty.
### API — Per-endpoint rate limiting
* `includeTrades=true` on market positions now has its own 20 req/min rate limit per key.
* Separate bucket from standard rate limit — doesn't consume your normal quota.
***
## 2026-03-21
### TypeScript SDK v0.4.0
* **New:** Local Cache — SQLite-backed local storage for instant offline queries.
* Backfill wallet history in seconds (1 request per wallet, up to 500 trades).
* Live WebSocket stream keeps the cache up to date automatically.
* Query trades, positions, and settlements locally with zero API calls.
* Watchlist file with hot-reload and runtime add/remove API.
* Full documentation at [Local Cache](/sdks/local-cache).
### Rust SDK v0.4.0
* **New:** Local Cache ported from TypeScript. Same architecture, same SQL schema, same query methods.
* Builder pattern: `PolyNodeCache::builder(client).db_path(...).build()?`
* `rusqlite` (bundled) + `notify` as optional dependencies behind `cache` feature flag.
***
## 2026-03-20
### Web Frontend
* Speed comparison test redesigned with 3-second warm-up period, win % scoreboard, and visual win ratio bar.
* Updated "Run this test yourself" code snippet to match new logic.
***
## 2026-03-19
### TypeScript SDK v0.3.0
* **New:** `OrderbookEngine` — higher-level orderbook client with shared state and filtered views per component.
* **New:** `LocalOrderbook` — local orderbook state management.
* **New:** `ShortFormStream` — condensed event stream for bandwidth-constrained environments.
### API
* Orderbook REST endpoints: `/v1/orderbook/{token}`, `/v1/midpoint/{token}`, `/v1/spread/{token}`.
* Wallet trades `limit` parameter now allows up to 1,000 (was 500).
***
## 2026-03-15
### API v1 — Initial Public Release
* **WebSocket streaming:** settlements, trades, prices, blocks, wallets, markets, large trades, oracle, chainlink
* **REST API:** markets, search, candles, stats, settlements, wallets
* **Orderbook streaming** via `ob.polynode.dev`
* **RPC endpoint** via `rpc.polynode.dev` — JSON-RPC with TX #1 delivery
* **CLI** (`pn`) — stream events, query markets, manage API keys from the terminal
# API Reference
Source: https://docs.polynode.dev/charts/api-reference
Complete API documentation for Chart, Series, TimeScale, PriceScale, and Orderbook
# API Reference
## createChart
```typescript theme={null}
import { createChart } from 'polynode-charts'
const chart = createChart(container, options?)
```
| Parameter | Type | Description |
| ----------- | ----------------------- | --------------------------- |
| `container` | `HTMLElement \| string` | DOM element or CSS selector |
| `options` | `ChartOptions` | Optional configuration |
### ChartOptions
```typescript theme={null}
interface ChartOptions {
width?: number // fixed width (px)
height?: number // fixed height (px)
autoSize?: boolean // auto-resize to container (default: true)
layout?: LayoutOptions
grid?: GridOptions
crosshair?: CrosshairOptions
timeScale?: TimeScaleOptions
rightPriceScale?: PriceScaleOptions
}
```
### LayoutOptions
```typescript theme={null}
interface LayoutOptions {
background?: string // default: '#080d16'
textColor?: string // default: '#556'
fontFamily?: string // default: '"SF Mono", "Fira Code", monospace'
fontSize?: number // default: 10
}
```
### GridOptions
```typescript theme={null}
interface GridOptions {
vertLines?: { color?: string; visible?: boolean }
horzLines?: { color?: string; visible?: boolean }
}
```
### PriceScaleOptions
```typescript theme={null}
interface PriceScaleOptions {
mode?: 'normal' | 'probability' // 'probability' clamps axis to 0-100%
autoScale?: boolean
scaleMargins?: { top?: number; bottom?: number }
}
```
***
## Chart
### Series Methods
```typescript theme={null}
chart.addCandleSeries(opts?: CandleSeriesOptions): CandleSeries
chart.addLineSeries(opts?: LineSeriesOptions): LineSeries
chart.addAreaSeries(opts?: AreaSeriesOptions): AreaSeries
chart.addVolumeSeries(opts?: VolumeSeriesOptions): VolumeSeries
chart.removeSeries(series): void
```
### Scale Access
```typescript theme={null}
chart.timeScale(): TimeScale
```
### Events
```typescript theme={null}
// Subscribe to crosshair movement — returns unsubscribe function
const unsub = chart.subscribeCrosshairMove((params) => {
console.log(params.time, params.point)
})
unsub() // stop listening
// Subscribe to chart clicks
const unsub2 = chart.subscribeClick((params) => {
console.log('Clicked at time:', params.time)
})
```
**CrosshairMoveParams:**
```typescript theme={null}
interface CrosshairMoveParams {
time: number | null
point: { x: number; y: number } | null
seriesData: Map
}
```
**ClickParams:**
```typescript theme={null}
interface ClickParams {
time: number | null
point: { x: number; y: number }
}
```
### Utility
```typescript theme={null}
chart.getContainer(): HTMLElement // returns the chart's parent DOM element
```
### Lifecycle
```typescript theme={null}
chart.resize(width, height): void // manual resize
chart.remove(): void // cleanup everything
```
***
## Series
All series share these common methods:
```typescript theme={null}
series.setData(data[]): void // replace all data
series.update(item): void // append or update last point
series.setMaxLength(n): void // cap buffer size (drops oldest)
```
### CandleSeries
```typescript theme={null}
const series = chart.addCandleSeries({
upColor: '#22c55e',
downColor: '#ef4444',
wickUpColor: '#22c55e', // optional, defaults to upColor
wickDownColor: '#ef4444', // optional, defaults to downColor
borderVisible: true,
})
```
**Data shape:**
```typescript theme={null}
interface OhlcData {
time: number // epoch milliseconds
open: number
high: number
low: number
close: number
volume?: number
}
```
### LineSeries
```typescript theme={null}
const series = chart.addLineSeries({
color: '#f7931a',
lineWidth: 2,
smooth: true, // bezier curve interpolation
showDot: true, // pulsing dot at last point
dotColor: '#f7931a',
})
```
**Data shape:**
```typescript theme={null}
interface LineData {
time: number // epoch milliseconds
value: number
}
```
**Live mode:**
```typescript theme={null}
series.setLive(true) // phantom point + smooth lerp
series.isLive() // check live state
```
**Price line overlay:**
```typescript theme={null}
series.setPriceLine(72822.50, {
color: '#3b82f6',
dash: [6, 4],
label: 'PRICE TO BEAT',
labelColor: '#fff',
})
series.clearPriceLine()
series.getPriceLine() // returns PriceLineConfig | null
```
The price line renders as a dashed horizontal line across the chart with a label on the left and a price badge on the right axis. The Y-axis auto-expands to keep the price line visible.
### AreaSeries
Extends `LineSeries` with a gradient fill beneath the line.
```typescript theme={null}
const series = chart.addAreaSeries({
color: '#6ea8fe', // line color
topColor: '#6ea8fe33', // gradient top
bottomColor: '#6ea8fe00', // gradient bottom (transparent)
lineWidth: 2,
smooth: true,
})
```
Uses the same `LineData` shape as LineSeries.
### VolumeSeries
Automatically creates a separate pane at 22% of chart height.
```typescript theme={null}
const series = chart.addVolumeSeries({
upColor: 'rgba(34, 197, 94, 0.4)',
downColor: 'rgba(239, 68, 68, 0.4)',
opacity: 0.4,
})
```
**Data shape:**
```typescript theme={null}
interface VolumeData {
time: number
value: number
color?: string // override per-bar color
}
```
***
## TimeScale
```typescript theme={null}
const ts = chart.timeScale()
```
| Method | Description |
| ----------------------------- | ------------------------------------------------------------------------ |
| `goLive()` | Enable live mode. Auto-scrolls to newest data. Snaps back after 4s idle. |
| `isLive()` | Returns `true` if in live mode |
| `setVisibleRange(start, end)` | Set visible time range (epoch ms) |
| `animateToRange(start, end)` | Animated zoom to range |
| `zoomAt(factor, mouseXFrac)` | Zoom around a point (scroll wheel) |
| `getVisibleStart()` | Left edge time (epoch ms) |
| `getVisibleEnd()` | Right edge time (epoch ms) |
| `getVisibleWindow()` | Duration of visible range (ms) |
**Live mode behavior:** When live, the chart auto-scrolls to show the latest data. If the user pans or zooms away, the chart remembers and snaps back to live after 4 seconds of idle. This gives users freedom to explore history while keeping the default view current.
***
## Orderbook
```typescript theme={null}
import { createOrderbook } from 'polynode-charts'
const ob = createOrderbook('#orderbook', {
colorBid: '#22c55e',
colorAsk: '#ef4444',
depthFillOpacity: 0.08,
labelCount: 8,
})
ob.update({
bids: [{ price: 0.54, size: 1200 }, { price: 0.53, size: 800 }],
asks: [{ price: 0.55, size: 900 }, { price: 0.56, size: 1500 }],
})
```
### OrderbookOptions
```typescript theme={null}
interface OrderbookOptions {
colorBid?: string // default: '#22c55e'
colorAsk?: string // default: '#ef4444'
depthFillOpacity?: number // default: 0.08
labelCount?: number // price labels on axis
background?: string
textColor?: string
}
```
### BookData
```typescript theme={null}
interface BookData {
bids: PriceLevel[]
asks: PriceLevel[]
}
interface PriceLevel {
price: number
size: number
}
```
| Method | Description |
| ----------------- | ------------------------------- |
| `ob.update(data)` | Replace book data and re-render |
| `ob.destroy()` | Remove DOM and stop rendering |
***
## createShortFormOverlay
One-liner to add Polymarket short-form price-to-beat overlays to any live chart. Adds interval buttons, auto-discovers markets, draws a dashed price-to-beat line, and shows live odds with a countdown timer.
```typescript theme={null}
import { createChart, createShortFormOverlay } from 'polynode-charts'
const chart = createChart('#btc', { layout: { background: '#0c1220' } })
const series = chart.addLineSeries({ color: '#f7931a', showDot: true })
series.setLive(true)
chart.timeScale().goLive()
// One line: buttons + discovery + price line + odds + countdown
const overlay = createShortFormOverlay(chart, series, { coin: 'btc' })
```
### ShortFormOverlayOptions
```typescript theme={null}
interface ShortFormOverlayOptions {
coin: ShortFormCoin // 'btc' | 'eth' | 'sol' | 'xrp' | 'doge' | 'hype'
intervals?: ShortFormInterval[] // default: ['5m', '15m', '1h']
defaultInterval?: ShortFormInterval // if set, auto-starts on creation
onRotation?: (event: RotationEvent) => void // callback on each poll/rotation
}
```
### ShortFormOverlay (return type)
| Method | Description |
| ----------------------------- | ---------------------------------------------- |
| `overlay.setInterval('15m')` | Switch to a different interval |
| `overlay.stop()` | Stop polling, remove price line, clear display |
| `overlay.destroy()` | Full cleanup: stop + remove DOM |
| `overlay.getActiveInterval()` | Returns current interval or `null` |
### What it renders
* **Interval buttons** (5m, 15m, 1h) — click to toggle on/off
* **Price-to-beat line** — horizontal dashed blue line on the chart
* **Odds display** — "24% up · 77% down · 1m35s" with live countdown
* **Auto-rotation** — discovers the next market window when the current one expires
***
## PolynodeProvider — MarketInfo
The `outcome` field tells you which side of a binary market this token represents.
```typescript theme={null}
interface MarketInfo {
token_id: string
question: string // the market question (e.g. "US x Iran ceasefire by April 7?")
slug: string
image: string
last_price: number
volume_24h: number
outcomes: string[] // e.g. ['Yes', 'No']
outcome: string // which outcome THIS token is ('Yes' or 'No')
condition_id?: string
neg_risk?: boolean // true for multi-outcome events (elections, championships)
}
```
The `neg_risk` field indicates a multi-outcome event on Polymarket. When `provider.event()` searches for multi-outcome markets, it prioritizes `neg_risk: true` markets to find genuine events (elections, World Cup, NBA Finals) rather than binary matches.
# Data Providers
Source: https://docs.polynode.dev/charts/data-providers
Built-in providers for polynode REST API, orderbook streaming, and short-form market discovery
# Data Providers
The Charts SDK includes three data providers that connect to polynode endpoints. All are optional. The chart itself works with any data source.
## PolynodeProvider
Full-featured REST provider for market data, candles, search, events, and multi-outcome charts.
```typescript theme={null}
import { PolynodeProvider } from 'polynode-charts'
const provider = new PolynodeProvider({
apiKey: 'pn_live_...',
baseUrl: 'https://api.polynode.dev', // default
})
```
### Top Markets
```typescript theme={null}
const markets = await provider.markets(10)
// Returns MarketInfo[] sorted by 24h volume
```
**Response:**
```json theme={null}
[
{
"token_id": "82855088893985825781350...",
"question": "US x Iran ceasefire by April 7?",
"slug": "us-x-iran-ceasefire-by-april-7",
"image": "https://polymarket-upload.s3...",
"last_price": 0.999,
"volume_24h": 19542289.32,
"outcomes": ["Yes", "No"],
"condition_id": "0x4c5701bc..."
}
]
```
### Search Markets
```typescript theme={null}
const results = await provider.search('bitcoin', 5)
// Returns MarketInfo[] matching query, sorted by volume
```
### Fetch Candles
```typescript theme={null}
const candles = await provider.candles(tokenId, '1h')
// Returns OhlcData[] — ready for series.setData()
const series = chart.addCandleSeries()
series.setData(candles)
```
The first call fetches trades and caches them locally. After that, switching resolution is instant — the SDK rebuilds candles client-side from the cached trades.
**Supported resolutions:** `'1m'`, `'5m'`, `'15m'`, `'1h'`, `'4h'`, `'1d'`
**Options:**
```typescript theme={null}
// Fetch more history (default: 30 days)
const candles = await provider.candles(tokenId, '1h', { days: 90 })
```
**Response shape (`OhlcData[]`):**
```json theme={null}
[
{ "time": 1775826000000, "open": 0.54, "high": 0.55, "low": 0.52, "close": 0.54, "volume": 1200 }
]
```
### Price History (Line/Area Charts)
For simple `{time, value}` points (faster than candles — one HTTP call, no pagination):
```typescript theme={null}
const history = await provider.priceHistory(tokenId, '7d')
// Returns LineData[]
const series = chart.addAreaSeries({ color: '#22c55e' })
series.setData(history)
```
**Supported ranges:** `'1h'`, `'6h'`, `'1d'`, `'7d'`, `'30d'`, `'all'`
`candles()` fetches trades and builds OHLCV locally. Best for candlestick charts where you need resolution switching.
`priceHistory()` hits the CLOB prices-history endpoint directly. Best for line/area charts where you just need price points over time.
### Get Event (Multi-Outcome)
```typescript theme={null}
const event = await provider.event('presidential election')
```
Returns `EventInfo | null`. The method searches your loaded markets, groups by shared question pattern, and returns all outcomes with prices and colors.
**Response:**
```json theme={null}
{
"question": "the 2028 US Presidential Election",
"slug": "presidential-election-winner-2028",
"condition_id": "0x...",
"image": "https://...",
"volume_24h": 2847291,
"outcomes": [
{ "name": "Gavin Newsom", "token_id": "98250...", "price": 0.159, "color": "#4378FF" },
{ "name": "Kamala Harris", "token_id": "70663...", "price": 0.032, "color": "#22c55e" },
{ "name": "Ron DeSantis", "token_id": "10448...", "price": 0.021, "color": "#FDC503" }
]
}
```
Colors are assigned automatically from `OUTCOME_COLORS` (a 20-color palette).
### Multi-Outcome Price History (Batch)
Fetch price history for all outcomes in parallel:
```typescript theme={null}
const event = await provider.event('presidential election')
const priceMap = await provider.outcomePrices(event.outcomes, '7d', { maxOutcomes: 5 })
for (const outcome of event.outcomes) {
const data = priceMap.get(outcome.token_id)
if (!data) continue
const series = chart.addLineSeries({ color: outcome.color })
series.setData(data)
}
```
`outcomePrices()` is much faster than fetching candles per outcome — one HTTP call each, no trade pagination.
For candlestick data per outcome, use `outcomeCandles()` instead:
```typescript theme={null}
const candleMap = await provider.outcomeCandles(event.outcomes, '4h', { days: 30, maxOutcomes: 5 })
```
### Orderbook Snapshot
```typescript theme={null}
const book = await provider.book(tokenId)
// Returns { bids: PriceLevel[], asks: PriceLevel[] }
```
### Poll for Live Updates
```typescript theme={null}
const stop = provider.pollPrices(tokenId, (point) => {
series.update(point) // { time, value }
}, { intervalMs: 5000 })
// Stop polling
stop()
```
For multi-outcome polling:
```typescript theme={null}
const stop = provider.pollOutcomePrices(event.outcomes, (tokenId, point) => {
seriesMap.get(tokenId)?.update(point)
}, { intervalMs: 10000, maxOutcomes: 5 })
```
### Market Resolution
All methods accept flexible market identifiers:
* **token\_id** — direct lookup (fastest)
* **slug** — matched against loaded markets (e.g. `'bitcoin-above-64k'`)
* **condition\_id** — hex string starting with `0x`
### Clear Cache
```typescript theme={null}
provider.clearCache() // clears trade cache for all markets
```
### MarketInfo
```typescript theme={null}
interface MarketInfo {
token_id: string
question: string
slug: string
image: string
last_price: number // 0-1
volume_24h: number // USD
outcomes: string[] // ["Yes", "No"] or ["Up", "Down"]
condition_id?: string
}
```
***
## PolynodeOBProvider
WebSocket-based orderbook streaming with automatic zlib decompression (uses the native `DecompressionStream` API, no dependencies).
```typescript theme={null}
import { PolynodeOBProvider } from 'polynode-charts'
const ob = new PolynodeOBProvider({
wsUrl: 'wss://ob.polynode.dev/ws', // default
})
ob.subscribe(tokenId, (book) => {
orderbook.update(book) // pass to Orderbook renderer
})
```
### Methods
| Method | Description |
| ------------------------------ | --------------------------------------------------- |
| `subscribe(tokenId, callback)` | Subscribe to live book updates |
| `unsubscribe(tokenId)` | Stop receiving updates for a token |
| `connect()` | Manually connect (auto-connects on first subscribe) |
| `disconnect()` | Close WebSocket and stop reconnecting |
The provider handles reconnection automatically with exponential backoff (1s to 30s).
### Message Types
The provider processes three message types from the orderbook WebSocket:
* `snapshot_batch` — initial full book for all subscribed tokens
* `book_snapshot` — full book replacement for a single token
* `book_update` — incremental update (add/remove/change levels)
All are normalized to `BookData` before calling your callback.
***
## ShortFormProvider
Discovers short-form crypto markets (5-minute, 15-minute, and hourly up/down markets) and provides live odds rotation. HTTP-only, no WebSocket required.
```typescript theme={null}
import { ShortFormProvider } from 'polynode-charts'
const sf = new ShortFormProvider()
```
### One-Shot Discovery
```typescript theme={null}
const market = await sf.discover('btc', '15m')
if (market) {
console.log(market.priceToBeat) // 72822.50
console.log(market.upOdds) // 0.62
console.log(market.downOdds) // 0.38
console.log(market.windowEnd) // unix timestamp
}
```
### Auto-Rotation with Live Odds
`startRotation()` discovers the current window, emits immediately, then polls for updated odds every second and re-discovers at each window boundary.
```typescript theme={null}
const stop = sf.startRotation('btc', '15m', (event) => {
// Called on discovery + every odds update
series.setPriceLine(event.market.priceToBeat!, {
color: '#3b82f6',
label: 'PRICE TO BEAT',
})
console.log(`${(event.market.upOdds * 100).toFixed(0)}% up`)
console.log(`${event.timeRemaining}s remaining`)
}, { pollMs: 1000 })
// Stop rotation and polling
stop()
```
### Supported Coins
| Key | Coin |
| -------- | -------- |
| `'btc'` | Bitcoin |
| `'eth'` | Ethereum |
| `'sol'` | Solana |
| `'xrp'` | XRP |
| `'doge'` | Dogecoin |
| `'hype'` | Hype |
| `'bnb'` | BNB |
### Supported Intervals
| Key | Window |
| ------- | ---------- |
| `'5m'` | 5 minutes |
| `'15m'` | 15 minutes |
| `'1h'` | 1 hour |
### ShortFormMarket
```typescript theme={null}
interface ShortFormMarket {
coin: ShortFormCoin
slug: string
title: string
conditionId: string
windowStart: number // unix seconds
windowEnd: number // unix seconds
outcomes: string[]
outcomePrices: number[]
clobTokenIds: string[]
upOdds: number // 0-1
downOdds: number // 0-1
liquidity: number
volume24h: number
priceToBeat: number | null
}
```
### RotationEvent
```typescript theme={null}
interface RotationEvent {
interval: ShortFormInterval
market: ShortFormMarket
windowStart: number // unix seconds
windowEnd: number // unix seconds
timeRemaining: number // seconds until window closes
}
```
### Options
```typescript theme={null}
const sf = new ShortFormProvider({
apiBaseUrl: 'https://api.polynode.dev', // default
rotationBuffer: 3, // seconds after window end before re-discovering (default: 3)
})
```
# Examples
Source: https://docs.polynode.dev/charts/examples
Copy-paste examples for common chart setups
# Examples
## Prediction Market — Yes/No Outcome
```typescript theme={null}
import { createChart, PolynodeProvider } from 'polynode-charts'
const provider = new PolynodeProvider({ apiKey: 'pn_...' })
const chart = createChart('#chart', {
rightPriceScale: { mode: 'probability' },
})
const series = chart.addAreaSeries({
color: '#22c55e',
topColor: '#22c55e22',
bottomColor: '#22c55e00',
smooth: true,
})
const history = await provider.priceHistory(tokenId, '7d')
series.setData(history)
```
The `probability` price scale mode clamps the Y-axis to 0-100% with appropriate tick marks.
## Multi-Outcome Event (e.g., Election)
```typescript theme={null}
import { createChart, PolynodeProvider, OUTCOME_COLORS } from 'polynode-charts'
const provider = new PolynodeProvider({ apiKey: 'pn_...' })
const event = await provider.event('presidential election')
const chart = createChart('#chart', {
rightPriceScale: { mode: 'probability' },
})
// Fetch all outcome price histories in one call
const priceMap = await provider.outcomePrices(event.outcomes, '7d', { maxOutcomes: 8 })
for (const outcome of event.outcomes) {
const data = priceMap.get(outcome.token_id)
if (!data || data.length === 0) continue
const series = chart.addLineSeries({
color: outcome.color,
lineWidth: 2,
smooth: true,
})
series.setData(data)
}
```
`OUTCOME_COLORS` provides a 20-color palette optimized for dark backgrounds. Colors are auto-assigned by `provider.event()`.
## Candlestick + Volume
```typescript theme={null}
import { createChart, PolynodeProvider } from 'polynode-charts'
const provider = new PolynodeProvider({ apiKey: 'pn_...' })
const chart = createChart('#chart')
const candles = chart.addCandleSeries({
upColor: '#22c55e',
downColor: '#ef4444',
})
const volume = chart.addVolumeSeries({
upColor: 'rgba(34, 197, 94, 0.3)',
downColor: 'rgba(239, 68, 68, 0.3)',
})
const data = await provider.candles(tokenId, '1h')
candles.setData(data)
// Volume is included in OhlcData when built from trades
volume.setData(data.map(d => ({
time: d.time,
value: d.volume || 0,
color: d.close >= d.open ? '#22c55e66' : '#ef444466',
})))
```
### Instant Resolution Switching
After the first `candles()` call, trades are cached locally. Switching resolution rebuilds candles client-side with zero network calls:
```typescript theme={null}
// First call fetches trades (~2s)
await provider.candles(tokenId, '1h')
// These are instant — trades are already cached
await provider.candles(tokenId, '5m')
await provider.candles(tokenId, '1d')
```
## Live Crypto with Short-Form Overlay
```typescript theme={null}
import { createChart, ShortFormProvider } from 'polynode-charts'
const chart = createChart('#chart', {
layout: { background: '#0c1220' },
})
const series = chart.addLineSeries({
color: '#f7931a',
lineWidth: 2,
smooth: true,
showDot: true,
dotColor: '#f7931a',
})
series.setLive(true)
series.setMaxLength(600)
chart.timeScale().goLive()
// WebSocket price feed
const ws = new WebSocket('wss://ws.polynode.dev/ws?key=pn_...')
ws.onopen = () => ws.send(JSON.stringify({ action: 'subscribe', type: 'chainlink' }))
ws.onmessage = (e) => {
const msg = JSON.parse(e.data)
if (msg.type === 'price_feed' && msg.feed === 'BTC/USD') {
series.update({ time: Date.now(), value: msg.data.price })
}
}
// Short-form price-to-beat overlay
const sf = new ShortFormProvider()
let stopRotation = null
function setInterval(interval) {
if (stopRotation) stopRotation()
stopRotation = sf.startRotation('btc', interval, (event) => {
const m = event.market
if (m.priceToBeat !== null) {
series.setPriceLine(m.priceToBeat, {
color: '#3b82f6',
label: 'PRICE TO BEAT',
})
}
updateUI({
upPct: (m.upOdds * 100).toFixed(0),
downPct: (m.downOdds * 100).toFixed(0),
timeRemaining: event.timeRemaining,
})
}, { pollMs: 1000 })
}
// Start with 15m interval
setInterval('15m')
```
## Interactive Crosshair
```typescript theme={null}
const chart = createChart('#chart')
const series = chart.addCandleSeries()
series.setData(data)
chart.subscribeCrosshairMove((params) => {
if (!params.time) {
tooltip.style.display = 'none'
return
}
tooltip.style.display = 'block'
tooltip.style.left = params.point.x + 'px'
tooltip.style.top = params.point.y + 'px'
tooltip.textContent = `Time: ${new Date(params.time).toLocaleString()}`
})
```
## Orderbook with Depth Chart
```typescript theme={null}
import { createOrderbook, PolynodeOBProvider } from 'polynode-charts'
const ob = createOrderbook('#orderbook', {
colorBid: '#22c55e',
colorAsk: '#ef4444',
depthFillOpacity: 0.08,
labelCount: 8,
})
const provider = new PolynodeOBProvider()
provider.subscribe(tokenId, (book) => {
ob.update(book)
// Calculate spread
const bestBid = book.bids[0]?.price || 0
const bestAsk = book.asks[0]?.price || 0
const spread = bestAsk - bestBid
document.getElementById('spread').textContent =
`Spread: ${(spread * 100).toFixed(1)}c`
})
```
## Poll Multiple Outcomes
```typescript theme={null}
import { createChart, PolynodeProvider } from 'polynode-charts'
const provider = new PolynodeProvider({ apiKey: 'pn_...' })
const event = await provider.event('presidential election')
const chart = createChart('#chart', {
rightPriceScale: { mode: 'probability' },
})
// Load initial history
const priceMap = await provider.outcomePrices(event.outcomes, '7d')
const seriesMap = new Map()
for (const outcome of event.outcomes) {
const data = priceMap.get(outcome.token_id)
if (!data) continue
const series = chart.addLineSeries({ color: outcome.color, smooth: true })
series.setData(data)
seriesMap.set(outcome.token_id, series)
}
// Poll for live updates
const stop = provider.pollOutcomePrices(event.outcomes, (tokenId, point) => {
seriesMap.get(tokenId)?.update(point)
}, { intervalMs: 10000 })
```
## Responsive Chart
The chart auto-sizes to its container by default. Just give the container a CSS height:
```html theme={null}
```
```typescript theme={null}
const chart = createChart('#chart')
// Chart resizes automatically on window/container resize
```
For mobile, the chart handles touch events natively: single-finger drag to pan, pinch to zoom. No configuration needed.
# Getting Started
Source: https://docs.polynode.dev/charts/getting-started
Create your first chart in under 5 minutes
# Getting Started
## Installation
```bash theme={null}
npm install polynode-charts
```
The package has **zero runtime dependencies**. It ships as ESM and CJS with full TypeScript declarations.
## Create a Chart
Every chart needs a container element with a defined height.
```html theme={null}
```
```typescript theme={null}
import { createChart } from 'polynode-charts'
const chart = createChart('#chart')
```
You can also pass an `HTMLElement` directly:
```typescript theme={null}
const el = document.getElementById('chart')
const chart = createChart(el)
```
## Add a Series
```typescript theme={null}
// Candlestick chart
const candles = chart.addCandleSeries({
upColor: '#22c55e',
downColor: '#ef4444',
})
candles.setData([
{ time: 1710000000000, open: 0.52, high: 0.55, low: 0.50, close: 0.54 },
{ time: 1710003600000, open: 0.54, high: 0.58, low: 0.53, close: 0.57 },
])
```
```typescript theme={null}
// Line chart
const line = chart.addLineSeries({
color: '#f7931a',
lineWidth: 2,
smooth: true,
})
line.setData([
{ time: 1710000000000, value: 72500 },
{ time: 1710000001000, value: 72510 },
])
```
## Chart Options
```typescript theme={null}
const chart = createChart('#chart', {
layout: {
background: '#0a0e17',
textColor: '#556',
fontFamily: '"SF Mono", monospace',
fontSize: 10,
},
grid: {
horzLines: { color: 'rgba(100, 120, 150, 0.06)' },
vertLines: { visible: false },
},
rightPriceScale: {
mode: 'probability', // clamps axis to 0-100%
},
})
```
## Live Streaming
For real-time data, use `update()` instead of `setData()`:
```typescript theme={null}
const series = chart.addLineSeries({
color: '#14f195',
showDot: true,
dotColor: '#14f195',
})
// Enable live mode — smooth phantom point between ticks
series.setLive(true)
// Auto-scroll to latest data
chart.timeScale().goLive()
// Push updates as they arrive
ws.onmessage = (e) => {
const { price } = JSON.parse(e.data)
series.update({ time: Date.now(), value: price })
}
```
In live mode, the chart adds a phantom leading point that lerps smoothly to the latest value, giving fluid motion even with infrequent ticks. A pulsing dot marks the current price.
## Max Data Length
For streaming use cases, cap the data buffer to prevent memory growth:
```typescript theme={null}
series.setMaxLength(600) // keep last 600 points (~10 min at 1/sec)
```
Older points are dropped from the front as new ones arrive.
## Volume Pane
Adding a volume series automatically creates a second pane at 22% height:
```typescript theme={null}
const volume = chart.addVolumeSeries({
upColor: 'rgba(34, 197, 94, 0.4)',
downColor: 'rgba(239, 68, 68, 0.4)',
})
volume.setData([
{ time: 1710000000000, value: 15000, color: '#22c55e' },
{ time: 1710003600000, value: 23000, color: '#ef4444' },
])
```
## Cleanup
```typescript theme={null}
chart.remove() // stops animation, detaches listeners, removes DOM
```
## Next Steps
* [API Reference](/charts/api-reference) — full Chart, Series, and Scale APIs
* [Data Providers](/charts/data-providers) — connect to polynode REST, WebSocket, and short-form endpoints
* [Live Streaming](/charts/live-streaming) — real-time crypto prices with price-to-beat overlays
# Live Streaming
Source: https://docs.polynode.dev/charts/live-streaming
Real-time crypto price charts with price-to-beat overlays and live odds
# Live Streaming
This guide covers building a real-time crypto price chart with WebSocket streaming, live mode, and short-form market overlays.
## Basic Live Chart
Connect a WebSocket feed to a line series with live mode enabled:
```typescript theme={null}
import { createChart } from 'polynode-charts'
const chart = createChart('#chart', {
layout: { background: '#0c1220', textColor: '#556' },
grid: {
horzLines: { color: 'rgba(100, 120, 150, 0.06)' },
vertLines: { visible: false },
},
})
const series = chart.addLineSeries({
color: '#f7931a',
lineWidth: 2,
smooth: true,
showDot: true,
dotColor: '#f7931a',
})
// Live mode: smooth phantom point + auto-scroll
series.setLive(true)
series.setMaxLength(600) // ~10 min at 1/sec
chart.timeScale().goLive()
// Connect to price feed
const ws = new WebSocket('wss://ws.polynode.dev/ws?key=pn_...')
ws.onopen = () => {
ws.send(JSON.stringify({ action: 'subscribe', type: 'chainlink' }))
}
ws.onmessage = (e) => {
const msg = JSON.parse(e.data)
if (msg.type === 'price_feed' && msg.feed === 'BTC/USD') {
series.update({ time: Date.now(), value: msg.data.price })
}
}
```
## How Live Mode Works
When `setLive(true)` is called:
1. **Phantom point** — a virtual leading data point is appended at `Date.now()` that smoothly lerps toward the latest real value. This creates fluid animation even if ticks arrive only once per second.
2. **Pulsing dot** — a glowing dot marks the current price at the phantom point, pulsing with a subtle sine-wave animation.
3. **Auto-scroll** — `goLive()` on the time scale keeps the visible range pinned to the latest data. If the user pans away, the chart returns to live after 4 seconds of inactivity.
4. **Continuous redraw** — the animation frame loop redraws the data layer every frame (for lerp + pulse), but background and overlay layers only redraw when dirty.
## Price-to-Beat Overlay
Overlay a horizontal dashed line showing the opening price for a short-form market window:
```typescript theme={null}
import { ShortFormProvider } from 'polynode-charts'
const sf = new ShortFormProvider()
const stop = sf.startRotation('btc', '15m', (event) => {
const m = event.market
// Draw dashed price line
if (m.priceToBeat !== null) {
series.setPriceLine(m.priceToBeat, {
color: '#3b82f6',
label: 'PRICE TO BEAT',
})
}
// Display odds
const upPct = (m.upOdds * 100).toFixed(0)
const downPct = (m.downOdds * 100).toFixed(0)
document.getElementById('odds').textContent =
`${upPct}% up · ${downPct}% down · ${event.timeRemaining}s`
}, { pollMs: 1000 })
```
The price line automatically stays visible. The Y-axis expands to include the price-to-beat value even if it's outside the current data range.
### What Happens at Window Boundaries
When a short-form window expires (e.g., the 15-minute window closes):
1. `startRotation` waits a buffer period (default 3 seconds)
2. Discovers the next window's market via the gamma proxy
3. Fetches the new price-to-beat from the crypto-price endpoint
4. Calls your `onRotation` callback with the new market
5. Resumes polling odds every `pollMs` for the new window
No manual intervention needed. The rotation handles the full lifecycle.
## Multiple Coins
Run independent charts for multiple coins, each with their own series and short-form rotation:
```typescript theme={null}
const coins = [
{ coin: 'btc', feed: 'BTC/USD', color: '#f7931a' },
{ coin: 'eth', feed: 'ETH/USD', color: '#627eea' },
{ coin: 'sol', feed: 'SOL/USD', color: '#14f195' },
]
for (const { coin, feed, color } of coins) {
const chart = createChart(`#chart-${coin}`, { /* ... */ })
const series = chart.addLineSeries({ color, smooth: true, showDot: true, dotColor: color })
series.setLive(true)
series.setMaxLength(600)
chart.timeScale().goLive()
// Price updates from WebSocket
ws.onmessage = (e) => {
const msg = JSON.parse(e.data)
if (msg.type === 'price_feed' && msg.feed === feed) {
series.update({ time: Date.now(), value: msg.data.price })
}
}
// Independent short-form rotation per coin
const sf = new ShortFormProvider()
sf.startRotation(coin, '15m', (event) => {
if (event.market.priceToBeat !== null) {
series.setPriceLine(event.market.priceToBeat, {
color: '#3b82f6',
label: 'PRICE TO BEAT',
})
}
})
}
```
## Orderbook + Chart Side-by-Side
Combine a candle chart with a live orderbook:
```typescript theme={null}
import { createChart, createOrderbook, PolynodeProvider, PolynodeOBProvider } from 'polynode-charts'
const provider = new PolynodeProvider({ apiKey: 'pn_...' })
// Chart
const chart = createChart('#chart')
const series = chart.addCandleSeries()
const candles = await provider.getCandles(tokenId, '1h')
series.setData(candles)
// Orderbook
const ob = createOrderbook('#orderbook', {
colorBid: '#22c55e',
colorAsk: '#ef4444',
})
const obProvider = new PolynodeOBProvider()
obProvider.subscribe(tokenId, (book) => ob.update(book))
```
## Reconnection
Both the WebSocket feed and the OB provider handle reconnection automatically with exponential backoff (1s up to 30s). No special handling is needed in your code.
For the `ShortFormProvider`, discovery retries 3 times with a 2-second delay between attempts before giving up on a window. The rotation timer then retries at the next buffer interval.
# Charts SDK
Source: https://docs.polynode.dev/charts/overview
High-performance Canvas 2D charting library built for prediction markets and live crypto prices
# polynode-charts
A zero-dependency **browser** charting library purpose-built for prediction market data and real-time crypto price streams. Renders on Canvas 2D with a 60fps animation loop, triple-layer dirty-flag rendering, and native touch/pinch support.
This is the **visualization** companion to `polynode-sdk`. The data SDK (`polynode-sdk`) handles REST API calls, WebSocket streaming, trading, and caching in Node.js. This package (`polynode-charts`) renders that data as interactive charts in the browser. They are separate npm packages.
```bash theme={null}
# Data layer (Node.js / server)
npm install polynode-sdk
# Visualization layer (browser)
npm install polynode-charts
```
`polynode-charts` also includes its own built-in data providers (REST, WebSocket, short-form discovery) so you can use it standalone without `polynode-sdk` if you only need charts.
## Features
* **Candlestick, line, area, and volume** series types
* **Live streaming mode** with smooth lerp animation between ticks
* **Orderbook visualization** with depth chart and spread display
* **Short-form market overlays** with price-to-beat lines and live odds
* **Multi-outcome support** for prediction markets (up to 20+ outcomes)
* **Auto-scaling** price axis with probability mode (0-100%)
* **Interactive** pan, zoom, scroll, pinch-to-zoom on mobile
* **Crosshair** with snap-to-candle and OHLC tooltip
## Install
```bash theme={null}
npm install polynode-charts
```
## Quick Start
```typescript theme={null}
import { createChart } from 'polynode-charts'
const chart = createChart('#my-chart', {
layout: { background: '#0a0e17', textColor: '#556' },
})
const series = chart.addCandleSeries({
upColor: '#22c55e',
downColor: '#ef4444',
})
series.setData([
{ time: 1710000000000, open: 0.52, high: 0.55, low: 0.50, close: 0.54 },
{ time: 1710003600000, open: 0.54, high: 0.58, low: 0.53, close: 0.57 },
// ...
])
```
## Architecture
The chart uses a triple-layer canvas stack per pane:
| Layer | Z-Index | Content |
| ---------- | ------- | ---------------------------------------- |
| Background | 1 | Grid lines, axis labels |
| Data | 2 | Series (candles, lines, areas, volumes) |
| Overlay | 3 | Crosshair, tooltips, interaction capture |
Each layer only redraws when its dirty flag is set, keeping GPU load minimal even at 60fps. The animation loop runs continuously for live mode (smooth phantom points, pulsing dots) but skips unchanged layers.
## Series Types
| Type | Method | Data Shape | Use Case |
| ------ | ------------------- | ------------ | ------------------------------------------- |
| Candle | `addCandleSeries()` | `OhlcData` | Market price history |
| Line | `addLineSeries()` | `LineData` | Live crypto prices, single-outcome tracking |
| Area | `addAreaSeries()` | `LineData` | Probability trends with gradient fill |
| Volume | `addVolumeSeries()` | `VolumeData` | Trading volume bars (auto-paned at 22%) |
## Data Providers
The SDK includes three built-in data providers for polynode endpoints:
* **PolynodeProvider** — REST + polling for market candles, search, events, multi-outcome charts
* **PolynodeOBProvider** — WebSocket orderbook streaming with zlib decompression
* **ShortFormProvider** — Short-form crypto market discovery with live odds rotation
All providers are optional. The chart itself is provider-agnostic and works with any data source.
## Live Demo
See the full interactive demo at [charts.polynode.dev](https://charts.polynode.dev).
# CLI
Source: https://docs.polynode.dev/cli/overview
Bloomberg terminal for prediction markets. Real-time TUI dashboards and JSON output for agents.
The PolyNode CLI (`pn`) gives you instant access to every PolyNode feature from the terminal. Watch live settlements scroll by, view full-depth orderbooks with bid/ask visualization, track short-form crypto markets with countdown timers, and monitor Chainlink price feeds. Every command also supports `--json` for programmatic use by agents and scripts.
## Install
```bash theme={null}
# From source (requires Rust)
cargo install --git https://github.com/joinQuantish/polynode-cli
```
The binary is called `pn`.
## Authentication
The CLI resolves your API key in this order:
1. `--key` flag: `pn markets --key pn_live_...`
2. `POLYNODE_API_KEY` environment variable
3. `~/.config/polynode/key` file (auto-saved when you run `pn key create`)
```bash theme={null}
# Generate a free API key (auto-saves to ~/.config/polynode/key)
pn key create
```
## Commands
| Command | Type | Description |
| ----------------------------- | -------- | ------------------------------ |
| `pn key create [name]` | REST | Generate a new API key |
| `pn status` | REST | System status and metrics |
| `pn markets [--count N]` | REST | Top markets by 24h volume |
| `pn search ` | REST | Full-text market search |
| `pn market ` | REST | Single market detail |
| `pn stream [type]` | Live TUI | Scrolling real-time event feed |
| `pn orderbook ` | Live TUI | Full-screen bid/ask depth view |
| `pn short-form <5m\|15m\|1h>` | Live TUI | Multi-coin crypto dashboard |
| `pn chainlink` | Live TUI | Live Chainlink price feeds |
Every command supports `--json` for machine-readable output.
***
## Static Commands
### Markets
View the top markets by 24h trading volume.
```bash theme={null}
pn markets --count 10
```
### Search
Full-text search across all market questions.
```bash theme={null}
pn search "bitcoin"
```
### Status
System health, connection count, and state metrics.
```bash theme={null}
pn status
```
### Market Detail
Look up a single market by slug or token ID.
```bash theme={null}
pn market will-bitcoin-reach-150k-in-march-2026
```
***
## Live TUI Commands
Live commands open a full-screen terminal UI that updates in real-time. Press `q` or `Esc` to exit.
### Stream
Watch every settlement, trade, block, or oracle event as it happens.
```bash theme={null}
# All settlements
pn stream settlements
# Large trades only (> $100)
pn stream settlements --min-size 100
# Filter by wallet
pn stream settlements --wallets 0xabc...
# Other event types
pn stream trades
pn stream blocks
pn stream oracle
pn stream global
```
Events are color-coded: yellow for pending, green for confirmed. The header shows event count and throughput rate.
### Orderbook
Full-depth live orderbook with proportional bid/ask bars. Green bars show bid depth growing left, red bars show ask depth growing right.
```bash theme={null}
pn orderbook btc-updown-5m-1774077000
pn orderbook will-bitcoin-reach-150k-in-march-2026
```
Use arrow keys to scroll deeper into the book. The footer shows midpoint, spread, total updates, and update rate.
### Short-Form Dashboard
Multi-coin dashboard for Polymarket's short-form crypto markets. Shows all 7 coins (BTC, ETH, SOL, XRP, DOGE, HYPE, BNB) with beat price, direction, odds, liquidity, volume, and a countdown timer. Auto-rotates to the next window when the current one expires.
```bash theme={null}
# 5-minute windows
pn short-form 5m
# 15-minute, specific coins
pn short-form 15m --coins btc,eth,sol
# Hourly
pn short-form 1h
```
### Chainlink Price Feeds
Live Chainlink data stream prices with bid/ask.
```bash theme={null}
pn chainlink
```
***
## JSON Mode (for Agents)
Every command supports `--json` for programmatic use. Static commands output pretty-printed JSON. Streaming commands output newline-delimited JSON (NDJSON), one object per line, with no TUI.
```bash theme={null}
# Static: pretty JSON
pn markets --count 5 --json
# Streaming: NDJSON (one event per line, no TUI)
pn stream settlements --json
# Pipe to jq
pn stream settlements --json | jq '.taker_size'
# Orderbook snapshots as JSON
pn orderbook btc-updown-5m-1774077000 --json
```
This makes `pn` composable with standard Unix tools and usable as a data source for automated trading agents.
***
## Key Bindings (TUI)
| Key | Action |
| ------------- | --------------------------- |
| `q` / `Esc` | Quit |
| `Ctrl-C` | Quit |
| `Up` / `Down` | Scroll orderbook depth |
| `Home` | Reset scroll to top of book |
# Event Format
Source: https://docs.polynode.dev/crypto/event-format
Price feed event schema and field reference.
## Event payload
```json theme={null}
{
"type": "price_feed",
"feed": "ETH/USD",
"timestamp": 1774672089,
"data": {
"feed": "ETH/USD",
"price": 1989.49,
"bid": 1989.49,
"ask": 1989.49,
"timestamp": 1774672089
}
}
```
## Fields
Always `"price_feed"`.
Feed name (e.g. `"BTC/USD"`, `"ETH/USD"`). Top-level convenience field, same value as `data.feed`.
Observation timestamp in Unix seconds. Top-level convenience field, same value as `data.timestamp`.
Feed name (e.g. `"BTC/USD"`).
Mid price in USD. This is the canonical price for this asset at this timestamp.
Bid price in USD.
Ask price in USD.
Observation timestamp in Unix seconds.
## Notes
* **Update rate**: approximately 1 update per second per feed.
* **No snapshot**: unlike settlement subscriptions, price feed subscriptions do not include an initial snapshot. The first event arrives with the next price observation (\~1 second after subscribing).
* **Deduplication**: if you subscribe to all feeds, BTC/USD may arrive at a slightly higher rate (\~2/second) due to multiple upstream sources. Your application should deduplicate by timestamp if needed.
# Available Feeds
Source: https://docs.polynode.dev/crypto/feeds
All crypto price feeds with live examples.
## Feed reference
All feeds update at approximately 1 tick per second. Prices are quoted in USD.
### BTC/USD
Bitcoin. The most liquid and widely traded crypto asset.
```json theme={null}
{
"type": "price_feed",
"feed": "BTC/USD",
"timestamp": 1774672089,
"data": {
"feed": "BTC/USD",
"price": 66236.61,
"bid": 66229.77,
"ask": 66237.94,
"timestamp": 1774672089
}
}
```
BTC/USD has bid/ask spread data. Other feeds currently report bid and ask equal to the mid price.
### ETH/USD
Ethereum.
```json theme={null}
{
"type": "price_feed",
"feed": "ETH/USD",
"timestamp": 1774672588,
"data": {
"feed": "ETH/USD",
"price": 1989.68,
"bid": 1989.68,
"ask": 1989.68,
"timestamp": 1774672588
}
}
```
### SOL/USD
Solana.
```json theme={null}
{
"type": "price_feed",
"feed": "SOL/USD",
"timestamp": 1774672588,
"data": {
"feed": "SOL/USD",
"price": 82.57,
"bid": 82.57,
"ask": 82.57,
"timestamp": 1774672588
}
}
```
### BNB/USD
BNB (Binance).
```json theme={null}
{
"type": "price_feed",
"feed": "BNB/USD",
"timestamp": 1774672588,
"data": {
"feed": "BNB/USD",
"price": 610.54,
"bid": 610.54,
"ask": 610.54,
"timestamp": 1774672588
}
}
```
### XRP/USD
XRP.
```json theme={null}
{
"type": "price_feed",
"feed": "XRP/USD",
"timestamp": 1774672588,
"data": {
"feed": "XRP/USD",
"price": 1.326,
"bid": 1.326,
"ask": 1.326,
"timestamp": 1774672588
}
}
```
### DOGE/USD
Dogecoin.
```json theme={null}
{
"type": "price_feed",
"feed": "DOGE/USD",
"timestamp": 1774672588,
"data": {
"feed": "DOGE/USD",
"price": 0.08991,
"bid": 0.08991,
"ask": 0.08991,
"timestamp": 1774672588
}
}
```
### HYPE/USD
Hyperliquid.
```json theme={null}
{
"type": "price_feed",
"feed": "HYPE/USD",
"timestamp": 1774672588,
"data": {
"feed": "HYPE/USD",
"price": 38.60,
"bid": 38.60,
"ask": 38.60,
"timestamp": 1774672588
}
}
```
## Filtering
Subscribe to specific feeds only:
```json theme={null}
{
"action": "subscribe",
"type": "chainlink",
"filters": {
"feeds": ["BTC/USD", "ETH/USD", "SOL/USD"]
}
}
```
Feed names are case-insensitive. `"btc/usd"` and `"BTC/USD"` both work.
## Subscribing to all feeds
Omit the `feeds` filter:
```json theme={null}
{"action": "subscribe", "type": "chainlink"}
```
You'll receive \~7 events per second (one per feed). At approximately 200 bytes per event, that's under 1.5 KB/s of bandwidth.
# Crypto Prices
Source: https://docs.polynode.dev/crypto/overview
Real-time crypto price streaming over WebSocket. 7 assets, ~1 update per second per feed.
Stream real-time crypto prices over WebSocket. 7 feeds, \~1 update per second each.
## Connection
```
wss://ws.polynode.dev/ws?key=YOUR_API_KEY
```
This is the same WebSocket endpoint used for settlements and orderbook. If you already have a connection open, you don't need a new one.
## Available feeds
| Feed | Pair | Update rate |
| ---------- | ----------------------- | ----------- |
| `BTC/USD` | Bitcoin / US Dollar | \~1/second |
| `ETH/USD` | Ethereum / US Dollar | \~1/second |
| `SOL/USD` | Solana / US Dollar | \~1/second |
| `BNB/USD` | BNB / US Dollar | \~1/second |
| `XRP/USD` | XRP / US Dollar | \~1/second |
| `DOGE/USD` | Dogecoin / US Dollar | \~1/second |
| `HYPE/USD` | Hyperliquid / US Dollar | \~1/second |
## Quick start
Connect and subscribe to all 7 feeds:
```javascript JavaScript theme={null}
const WebSocket = require("ws");
const ws = new WebSocket(
"wss://ws.polynode.dev/ws?key=YOUR_API_KEY"
);
ws.on("open", () => {
ws.send(JSON.stringify({
action: "subscribe",
type: "chainlink"
}));
});
ws.on("message", (data) => {
const msg = JSON.parse(data);
if (msg.type === "price_feed") {
console.log(`${msg.data.feed}: $${msg.data.price}`);
}
});
```
```python Python theme={null}
import asyncio
import json
import websockets
async def main():
uri = "wss://ws.polynode.dev/ws?key=YOUR_API_KEY"
async with websockets.connect(uri) as ws:
await ws.send(json.dumps({
"action": "subscribe",
"type": "chainlink"
}))
async for message in ws:
msg = json.loads(message)
if msg.get("type") == "price_feed":
d = msg["data"]
print(f"{d['feed']}: ${d['price']}")
asyncio.run(main())
```
```rust Rust theme={null}
use polynode::PolyNodeWS;
#[tokio::main]
async fn main() {
let mut ws = PolyNodeWS::connect("YOUR_API_KEY").await.unwrap();
ws.subscribe_price_feeds(None).await.unwrap(); // None = all feeds
while let Some(event) = ws.next_event().await {
println!("{}: ${}", event.feed, event.price);
}
}
```
```bash wscat theme={null}
wscat -c "wss://ws.polynode.dev/ws?key=YOUR_API_KEY"
# Then send:
{"action": "subscribe", "type": "chainlink"}
```
### Filter to specific feeds
Only receive the feeds you care about:
```json theme={null}
{
"action": "subscribe",
"type": "chainlink",
"filters": {
"feeds": ["BTC/USD", "ETH/USD"]
}
}
```
Omit `feeds` to get all 7.
## Event format
Every price update arrives as a `price_feed` event:
```json theme={null}
{
"type": "price_feed",
"feed": "BTC/USD",
"timestamp": 1774672588,
"data": {
"feed": "BTC/USD",
"price": 66225.49,
"bid": 66221.69,
"ask": 66229.46,
"timestamp": 1774672588
}
}
```
See [Event Format](/crypto/event-format) for the full field reference.
## Combining with other streams
Crypto prices run on the same WebSocket connection as settlements, orderbook, and oracle streams. Send multiple subscribe messages:
```javascript JavaScript theme={null}
// Settlements + crypto prices on the same connection
ws.on("open", () => {
ws.send(JSON.stringify({ action: "subscribe", type: "settlements" }));
ws.send(JSON.stringify({ action: "subscribe", type: "chainlink" }));
});
ws.on("message", (data) => {
const msg = JSON.parse(data);
if (msg.type === "price_feed") {
// crypto price tick
console.log(`${msg.data.feed}: $${msg.data.price}`);
} else if (msg.type === "settlement") {
// polymarket settlement
console.log(`${msg.data.market_title}: ${msg.data.outcome}`);
}
});
```
```python Python theme={null}
await ws.send(json.dumps({"action": "subscribe", "type": "settlements"}))
await ws.send(json.dumps({"action": "subscribe", "type": "chainlink"}))
async for message in ws:
msg = json.loads(message)
if msg.get("type") == "price_feed":
d = msg["data"]
print(f"Price: {d['feed']} ${d['price']}")
elif msg.get("type") == "settlement":
d = msg["data"]
print(f"Settlement: {d['market_title']}: {d['outcome']}")
```
Use the `type` field (`"price_feed"` vs `"settlement"`) to route events.
## REST Endpoints
In addition to the WebSocket stream, crypto data is available via REST. See the [REST API reference](/crypto/rest-api) for full details.
| Endpoint | Description |
| ------------------------------------------------------------- | -------------------------------------------------- |
| `GET /v1/crypto/markets` | All crypto prediction markets |
| `GET /v1/crypto/candles?symbol=BTC` | 5-min OHLC candles from Chainlink oracle |
| `GET /v1/crypto/ticks?symbol=BTC&from={unix_ms}&to={unix_ms}` | 1-second historical price ticks for chart backfill |
| `GET /v1/crypto/price?symbol=BTC&window={epoch}` | Open/close price for a market window |
| `GET /v1/crypto/active` | Currently active 5-minute markets for all 7 coins |
| `GET /v1/crypto/series` | Recurring crypto market series |
All endpoints require an API key via `?key=` or `x-api-key` header.
## Short-Form Crypto Markets
**These prices power prediction markets.** All 7 coins have active 5-minute, 15-minute, and 1-hour "Up or Down" prediction markets on Polymarket. The Chainlink price feed determines the opening price (price-to-beat), and the closing price at window end decides the outcome. Hundreds of trades happen every minute on these markets.
Here's how it works: a BTC 5-minute market opens at 10:00 with BTC at $66,800. If BTC is above $66,800 at 10:05, "Up" wins. If it's below, "Down" wins. A new market opens at 10:05 with the new price as the target. This repeats every 5 minutes, 15 minutes, and every hour, across all 7 coins.
### What you can do with this
| Stream | What it gives you | How to get it |
| -------------------------- | -------------------------------------------------- | --------------------------------------------------- |
| **Chainlink prices** | Underlying asset price at \~1/sec | `subscribe('chainlink')` on the main WS |
| **Historical price ticks** | 1-second chart backfill for an already-open window | `GET /v1/crypto/ticks` |
| **Short-form settlements** | Every trade on 5m/15m/1h markets | SDK `shortForm('15m')` auto-rotates between windows |
| **Short-form orderbook** | Bid/ask depth on crypto markets | `OrderbookEngine` with `clobTokenIds` from rotation |
| **Price-to-beat** | Chainlink opening price per window | Included in SDK rotation events |
| **Odds & liquidity** | Market-implied probabilities | Included in SDK rotation events |
### Quick example
Track BTC price alongside 15-minute market odds:
```javascript theme={null}
import { PolyNodeWS } from 'polynode-sdk';
const ws = new PolyNodeWS('pn_live_...', 'wss://ws.polynode.dev/ws');
// Live BTC price from Chainlink
const prices = await ws.subscribe('chainlink')
.feeds(['BTC/USD'])
.send();
prices.on('price_feed', (msg) => {
console.log(`BTC: $${msg.price}`);
});
// 15-minute market rotation + settlements
const stream = ws.shortForm('15m', { coins: ['btc'] });
stream.on('rotation', (r) => {
const m = r.markets[0];
console.log(`Window: beat $${m.priceToBeat} | ${(m.upOdds * 100).toFixed(0)}% up | ${r.timeRemaining}s left`);
});
stream.on('settlement', (e) => {
console.log(`Trade: ${e.outcome} $${e.taker_size}`);
});
```
The SDK handles market discovery, slug computation, token resolution, and automatic rotation between windows. See [Short-Form Markets](/sdks/short-form) for the full reference including the interactive slug calculator, all 14 market fields, and how to connect the orderbook.
### Market slug patterns
Slugs are deterministic, based on the window's Unix epoch timestamp:
| Interval | Pattern | Example |
| -------- | --------------------------- | ----------------------------------------- |
| 5 min | `{coin}-updown-5m-{epoch}` | `btc-updown-5m-1775399100` |
| 15 min | `{coin}-updown-15m-{epoch}` | `eth-updown-15m-1775397600` |
| 1 hour | Human-readable | `bitcoin-up-or-down-april-5-2026-10am-et` |
The epoch timestamp is always aligned to the interval boundary: `Math.floor(now / intervalSeconds) * intervalSeconds`.
## Use cases
* **Short-form market trading** -- track the underlying asset price alongside 5m/15m/1h crypto markets
* **Portfolio valuation** -- combine real-time crypto prices with Polymarket positions for live PnL
* **Hedging signals** -- correlate crypto price moves with prediction market settlements
* **Dashboards** -- display live crypto prices alongside prediction market data
* **Trading bots** -- trigger Polymarket trades based on crypto price thresholds
# REST API
Source: https://docs.polynode.dev/crypto/rest-api
REST endpoints for crypto market discovery, oracle candles, historical ticks, price-to-beat, and active short-form markets.
REST endpoints for crypto prediction market data. All endpoints require an API key.
## Authentication
Pass your API key via query parameter or header:
```bash theme={null}
# Query parameter
curl "https://api.polynode.dev/v1/crypto/markets?key=YOUR_API_KEY"
# Header
curl -H "x-api-key: YOUR_API_KEY" "https://api.polynode.dev/v1/crypto/markets"
```
***
## GET /v1/crypto/markets
All crypto prediction markets with liquidity, volume, and open interest.
**Cache:** 3 minutes
```bash theme={null}
curl "https://api.polynode.dev/v1/crypto/markets?key=YOUR_API_KEY"
```
```json theme={null}
{
"events": [
{
"id": "238474",
"slug": "what-price-will-bitcoin-hit-in-march-2026",
"title": "What price will Bitcoin hit in March?",
"startDate": "2026-03-01T05:20:27.791222Z",
"endDate": "2026-04-01T04:00:00Z",
"active": true,
"volume": 88886711.74,
"volume24hr": 5733750.73,
"liquidity": 6074250.39,
"openInterest": 12827944.10,
"seriesSlug": "bitcoin-hit-price-monthly",
"markets": [
{
"id": "1629442",
"question": "$100,000?",
"conditionId": "0x...",
"outcomes": ["Yes", "No"],
"outcomePrices": [0.045, 0.955],
"volume": 24084104.74,
"liquidity": 2028310.67,
"active": true,
"closed": false,
"groupItemTitle": "↑ 150,000"
}
]
}
]
}
```
***
## GET /v1/crypto/candles
5-minute OHLC candles from PolyNode's live Chainlink tick archive. Returns \~2.5 hours of history (30 candles).
**Cache:** \~5 seconds
| Parameter | Required | Description |
| --------- | -------- | ----------------------------------------------------------------------------------------------------- |
| `symbol` | Yes | Asset symbol or feed name: `BTC`, `ETH`, `SOL`, `BNB`, `XRP`, `DOGE`, `HYPE`, or names like `BTC/USD` |
```bash theme={null}
curl "https://api.polynode.dev/v1/crypto/candles?symbol=BTC&key=YOUR_API_KEY"
```
REST candle symbols accept both bare asset symbols like `BTC` and feed names like `BTC/USD`. WebSocket Chainlink subscriptions use feed names such as `BTC/USD`.
```json theme={null}
{
"candles": [
{
"time": 1774665300,
"open": 65954.03,
"high": 65992.01,
"low": 65947.14,
"close": 65949.69
}
]
}
```
***
## GET /v1/crypto/ticks
Historical 1-second ticks for the same crypto feeds available on the `price_feed` WebSocket stream. Use this to backfill a chart window before switching to live WebSocket updates.
**Range:** up to 24 hours per request
| Parameter | Required | Description |
| --------- | -------- | -------------------------------------------------------------------------- |
| `symbol` | Yes | Asset symbol: `BTC`, `ETH`, `SOL`, `BNB`, `XRP`, `DOGE`, `HYPE` |
| `from` | Yes | Start of range as Unix milliseconds |
| `to` | Yes | End of range as Unix milliseconds |
| `limit` | No | Max ticks to return. Default 10000, max 100000 |
| `source` | No | Optional source filter: `chainlink_data_streams` or `polymarket_chainlink` |
```bash theme={null}
curl -H "x-api-key: YOUR_API_KEY" \
"https://api.polynode.dev/v1/crypto/ticks?symbol=BTC&from=1780458961000&to=1780459082000&limit=5"
```
```json theme={null}
{
"symbol": "BTC",
"feed": "BTC/USD",
"from": 1780458961000,
"to": 1780459082000,
"limit": 5,
"count": 5,
"truncated": true,
"source": null,
"ticks": [
{
"id": "1780458962000-0",
"type": "price_feed",
"feed": "BTC/USD",
"timestamp": 1780458962,
"timestamp_ms": 1780458962000,
"source": "chainlink_data_streams",
"data": {
"feed": "BTC/USD",
"price": 65869.02124677686,
"bid": 65865.65936734258,
"ask": 65870.4120597498,
"timestamp": 1780458962
}
}
]
}
```
If `truncated` is `true`, request a narrower range or increase `limit`. BTC/USD can include more than one source for the same second; use the `source` parameter or deduplicate by `timestamp_ms` if your chart needs exactly one point per second.
***
## GET /v1/crypto/price
Open and close price for a specific crypto market window. Use this to get the "price to beat" for a short-form market.
**Cache:** 10 seconds
| Parameter | Required | Description |
| ---------- | -------- | --------------------------------------------------------------- |
| `symbol` | Yes | Asset symbol: `BTC`, `ETH`, `SOL`, `BNB`, `XRP`, `DOGE`, `HYPE` |
| `window` | Yes | Unix epoch timestamp of the market window start |
| `interval` | No | Market interval variant (e.g. `5m`, `15m`, `1h`, `4h`) |
```bash theme={null}
curl "https://api.polynode.dev/v1/crypto/price?symbol=BTC&window=1774674000&key=YOUR_API_KEY"
```
```json theme={null}
{
"openPrice": 66285.01,
"closePrice": 66238.61,
"timestamp": 1774674466843,
"completed": false,
"incomplete": true,
"cached": false
}
```
The `openPrice` is the oracle price at market open. `closePrice` updates in real time until the window completes. `completed: true` means the market has resolved.
***
## GET /v1/crypto/active
Currently active 5-minute up-or-down markets for all 7 coins. Returns live market data with token IDs, outcomes, and current odds.
**Cache:** 30 seconds
```bash theme={null}
curl "https://api.polynode.dev/v1/crypto/active?key=YOUR_API_KEY"
```
```json theme={null}
{
"markets": [
{
"id": "312746",
"slug": "btc-updown-5m-1774674300",
"title": "Bitcoin Up or Down - March 28, 1:05AM-1:10AM ET",
"active": true,
"closed": false,
"startDate": "2026-03-27T05:16:04.211285Z",
"endDate": "2026-03-28T05:10:00Z",
"markets": [
{
"question": "Bitcoin Up or Down - March 28, 1:05AM-1:10AM ET",
"conditionId": "0x...",
"outcomes": "[\"Up\", \"Down\"]",
"outcomePrices": [0.465, 0.535],
"tokenId": "12345...",
"active": true,
"closed": false
}
]
}
],
"count": 7,
"windowStart": 1774674300
}
```
The `windowStart` field is the epoch timestamp of the current 5-minute window. New markets rotate every 5 minutes.
Slug pattern is deterministic: `{coin}-updown-5m-{windowStart}`. You can compute the current window yourself with `Math.floor(Date.now() / 1000 / 300) * 300`. The SDK's `shortForm()` method handles this automatically with auto-rotation, enriched market data, and settlement streaming. See [Short-Form Markets](/sdks/short-form) for the interactive slug calculator and full reference.
***
## GET /v1/crypto/series
Recurring crypto market series (5m, 15m, 1h, 4h, daily, weekly, monthly patterns).
**Cache:** 5 minutes
```bash theme={null}
curl "https://api.polynode.dev/v1/crypto/series?key=YOUR_API_KEY"
```
```json theme={null}
{
"series": [
{
"id": "10422",
"slug": "xrp-up-or-down-15m",
"title": "XRP Up or Down 15m",
"recurrence": "15m",
"active": true,
"eventCount": null
},
{
"id": "10065",
"slug": "ethereum-neg-risk-weekly",
"title": "Ethereum Neg Risk Weekly",
"recurrence": "weekly",
"active": true,
"eventCount": null
}
],
"count": 7
}
```
# Builder Detail
Source: https://docs.polynode.dev/data/builders/detail
GET /v3/builders/{code}
Get stats and public profile metadata for a single builder by hex code.
Returns fill count, volume, fees, normalized side-aware volume, and public profile metadata for a specific builder.
## Request
```
GET /v3/builders/{code}
```
### Path parameters
| Parameter | Type | Description |
| --------- | ------ | -------------------------------------------- |
| `code` | string | Builder hex code (with or without 0x prefix) |
## Example
```bash theme={null}
curl https://api.polynode.dev/v3/builders/0xd1d9dd6983c40006b0dc8eab84a41ac9a4f27643296178479ffbebbc01ab7bde
```
```json theme={null}
{
"builder": "0xd1d9dd6983c40006b0dc8eab84a41ac9a4f27643296178479ffbebbc01ab7bde",
"builder_code": "0xd1d9dd6983c40006b0dc8eab84a41ac9a4f27643296178479ffbebbc01ab7bde",
"builder_logo": "https://polymarket-upload.s3.us-east-2.amazonaws.com/profile-image-5148537-b5dbf263-8d5f-41d7-bba1-387bfa17075c.png",
"builder_name": "MagicMarkets",
"builder_verified": true,
"buy_volume_e6": "8805777867302",
"buy_volume_usdc": "8805777.867302000000",
"elapsed_ms": 3,
"external_active_users": 3,
"external_rank": 23,
"external_volume": "18380636.129236996",
"fee_semantics": "Builder-attributed trader-paid fees. This is not a builder rebate payout.",
"fill_count": 23076,
"first_fill": "1777382110",
"last_fill": "1782307555",
"sell_volume_e6": "0",
"sell_volume_usdc": "0.000000000000000000000000",
"source": "builder_fee_stats",
"stats_checked_at": "2026-06-24 13:28:05.26204+00",
"stats_pending": false,
"stats_refreshed_at": "2026-06-24 13:28:03.133082+00",
"stats_source": "local_order_filled_event_index",
"total_fees": "116885095300",
"total_fees_usdc": "116885.095300000000",
"total_maker_volume": "8805777867302",
"total_maker_volume_usdc": "8805777.867302000000",
"total_taker_volume": "18512535904523",
"total_taker_volume_usdc": "18512535.904523000000",
"volume_e6": "8805777867302",
"volume_usdc": "8805777.867302000000"
}
```
## Response fields
Same fields as the [Builder Leaderboard](/data/builders/list) response, without `rank`.
| Field | Type | Description |
| ------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `builder` | string | Builder code (`0x`-prefixed hex, 32 bytes) |
| `builder_code` | string | Same builder code, included for profile-style naming consistency |
| `builder_name` | string \| null | Public builder profile name when available |
| `builder_logo` | string \| null | Public builder profile image URL when available |
| `builder_verified` | boolean \| null | Whether the builder profile is verified, when available |
| `fill_count` | integer | Total fills attributed to this builder |
| `first_fill` | string | Unix timestamp of first fill |
| `last_fill` | string | Unix timestamp of most recent fill |
| `source` | string | Legacy source label. `builder_fee_stats` means local builder stats are present; `polymarket_builder_metadata` means only profile metadata is present so far. |
| `stats_source` | string \| null | Local stats source, currently `local_order_filled_event_index` when local stats are populated |
| `total_fees` | string | Total fees generated in raw 6-decimal USDC units |
| `total_fees_usdc` | string \| null | Total fees normalized to decimal USDC |
| `total_maker_volume` | string | Sum of raw maker-side amount fields in 6-decimal units, kept for compatibility |
| `total_maker_volume_usdc` | string \| null | Maker-side total normalized to decimal USDC |
| `total_taker_volume` | string | Sum of raw taker-side amount fields in 6-decimal units, kept for compatibility |
| `total_taker_volume_usdc` | string \| null | Taker-side total normalized to decimal USDC |
| `volume_e6` | string \| null | Side-aware builder volume in raw 6-decimal USDC units. For buys this uses maker collateral; for sells this uses taker collateral. |
| `volume_usdc` | string \| null | `volume_e6` normalized to decimal USDC. Prefer this for displayed builder volume. |
| `buy_volume_e6` | string \| null | Buy-side normalized-volume numerator in raw 6-decimal USDC units |
| `buy_volume_usdc` | string \| null | Buy-side normalized volume in decimal USDC |
| `sell_volume_e6` | string \| null | Sell-side normalized-volume numerator in raw 6-decimal USDC units |
| `sell_volume_usdc` | string \| null | Sell-side normalized volume in decimal USDC |
| `external_rank` | integer \| null | Polymarket's public builder leaderboard rank, when available |
| `external_volume` | string \| null | Polymarket's public builder leaderboard volume, when available |
| `external_active_users` | integer \| null | Polymarket's public active-user count, when available |
| `stats_refreshed_at` | string \| null | Time PolyNode last rewrote local stats for this builder |
| `stats_checked_at` | string \| null | Time PolyNode last checked this builder, including no-op refreshes |
| `stats_pending` | boolean | `true` when profile metadata exists but the local stats row has not been populated yet |
| `fee_semantics` | string | Reminder that builder fees are trader-paid fees attributed to builder order flow, not a builder rebate payout |
| `elapsed_ms` | integer | Server-side query time in milliseconds |
## Related endpoints
| Endpoint | Description |
| -------------------------------- | --------------------------------- |
| `GET /v3/builders` | Builder leaderboard |
| `GET /v3/builders/{code}/trades` | Trades attributed to this builder |
# Builder Leaderboard
Source: https://docs.polynode.dev/data/builders/list
GET /v3/builders
Rank Polymarket builders by fill count, normalized volume, fees, and enriched builder profile metadata.
Polymarket V2 trades include a `builder` field identifying which order routing service executed the trade. This endpoint ranks builders by attributed fills and includes profile metadata when a builder has a public builder profile.
Builder profile metadata is synced from Polymarket's public builder metadata. Local fill, fee, and normalized volume stats come from PolyNode's indexed `OrderFilled` events. Builders can appear with metadata before their local stats row is refreshed; in that case `stats_pending` is `true` and normalized local volume fields are `null`.
## Request
```
GET /v3/builders
```
### Query parameters
| Parameter | Type | Default | Description |
| --------- | ------- | ------- | ---------------------------------- |
| `sort` | string | `fills` | Sort by: `fills`, `volume`, `fees` |
| `limit` | integer | 100 | Max 300 |
| `offset` | integer | 0 | Pagination offset |
## Example
```bash theme={null}
curl "https://api.polynode.dev/v3/builders?sort=volume&limit=1"
```
```json theme={null}
{
"builders": [
{
"builder": "0xd1d9dd6983c40006b0dc8eab84a41ac9a4f27643296178479ffbebbc01ab7bde",
"builder_code": "0xd1d9dd6983c40006b0dc8eab84a41ac9a4f27643296178479ffbebbc01ab7bde",
"builder_logo": "https://polymarket-upload.s3.us-east-2.amazonaws.com/profile-image-5148537-b5dbf263-8d5f-41d7-bba1-387bfa17075c.png",
"builder_name": "MagicMarkets",
"builder_verified": true,
"external_active_users": 3,
"external_rank": 23,
"external_volume": "18380636.129236996",
"fill_count": 23076,
"first_fill": "1777382110",
"last_fill": "1782307555",
"rank": 1,
"source": "local_order_filled_event_index",
"stats_checked_at": "2026-06-24 13:28:05.26204+00",
"stats_pending": false,
"stats_refreshed_at": "2026-06-24 13:28:03.133082+00",
"buy_volume_e6": "8805777867302",
"buy_volume_usdc": "8805777.867302000000",
"sell_volume_e6": "0",
"sell_volume_usdc": "0.000000000000000000000000",
"total_fees": "116885095300",
"total_fees_usdc": "116885.095300000000",
"total_maker_volume": "8805777867302",
"total_maker_volume_usdc": "8805777.867302000000",
"total_taker_volume": "18512535904523",
"total_taker_volume_usdc": "18512535.904523000000",
"volume_e6": "8805777867302",
"volume_usdc": "8805777.867302000000"
}
],
"elapsed_ms": 8,
"has_more": true,
"limit": 1,
"offset": 0,
"rows_returned": 1
}
```
## Response fields
| Field | Type | Description |
| ------------------------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `builder` | string | Builder code (`0x`-prefixed hex, 32 bytes) |
| `builder_code` | string | Same builder code, included for profile-style naming consistency |
| `builder_name` | string \| null | Public builder profile name when available |
| `builder_logo` | string \| null | Public builder profile image URL when available |
| `builder_verified` | boolean \| null | Whether the builder profile is verified, when available |
| `fill_count` | integer | Total fills attributed to this builder |
| `first_fill` | string | Unix timestamp of first fill |
| `last_fill` | string | Unix timestamp of most recent fill |
| `rank` | integer | Position in the leaderboard |
| `source` | string | `local_order_filled_event_index` when local stats are present, otherwise `polymarket_builder_metadata` |
| `total_fees` | string | Total fees generated in raw 6-decimal USDC units |
| `total_fees_usdc` | string \| null | Total fees normalized to decimal USDC |
| `total_maker_volume` | string | Sum of raw maker-side amount fields in 6-decimal units, kept for compatibility |
| `total_maker_volume_usdc` | string \| null | Maker-side total normalized to decimal USDC |
| `total_taker_volume` | string | Sum of raw taker-side amount fields in 6-decimal units, kept for compatibility |
| `total_taker_volume_usdc` | string \| null | Taker-side total normalized to decimal USDC |
| `volume_e6` | string \| null | Side-aware builder volume in raw 6-decimal USDC units. For buys this uses maker collateral; for sells this uses taker collateral. |
| `volume_usdc` | string \| null | `volume_e6` normalized to decimal USDC. Prefer this for displayed builder volume. |
| `buy_volume_e6` | string \| null | Buy-side normalized-volume numerator in raw 6-decimal USDC units |
| `buy_volume_usdc` | string \| null | Buy-side normalized volume in decimal USDC |
| `sell_volume_e6` | string \| null | Sell-side normalized-volume numerator in raw 6-decimal USDC units |
| `sell_volume_usdc` | string \| null | Sell-side normalized volume in decimal USDC |
| `external_rank` | integer \| null | Polymarket's public builder leaderboard rank, when available |
| `external_volume` | string \| null | Polymarket's public builder leaderboard volume, when available |
| `external_active_users` | integer \| null | Polymarket's public active-user count, when available |
| `stats_refreshed_at` | string \| null | Time PolyNode last rewrote local stats for this builder |
| `stats_checked_at` | string \| null | Time PolyNode last checked this builder, including no-op refreshes |
| `stats_pending` | boolean | `true` when profile metadata exists but the local stats row has not been populated yet |
`builder_name`, `builder_logo`, `builder_verified`, and the normalized local-volume fields are nullable. `external_volume` is Polymarket's public leaderboard value; `volume_usdc` is PolyNode's local indexed side-aware volume. Use `external_active_users` for the current public builder user-count signal.
## Related endpoints
| Endpoint | Description |
| -------------------------------- | ------------------------------------------------------------------ |
| `GET /v3/builders/{code}` | Stats for a single builder |
| `GET /v3/builders/{code}/trades` | Trades attributed to a builder, with market and profile enrichment |
# Builder Trades
Source: https://docs.polynode.dev/data/builders/trades
GET /v3/builders/{code}/trades
Get trades attributed to a specific builder, with builder profile and market enrichment.
Returns fills attributed to a specific builder, enriched with builder profile metadata and market context. Use token, market, event, side, size, and time filters to narrow the trade feed.
## Request
```
GET /v3/builders/{code}/trades
```
### Path parameters
| Parameter | Type | Description |
| --------- | ------ | -------------------------------------------- |
| `code` | string | Builder hex code (with or without 0x prefix) |
### Query parameters
| Parameter | Type | Default | Description |
| -------------- | ------- | ------- | ------------------------------------------------------------------------------------------ |
| `token_id` | string | -- | Filter trades involving this outcome token |
| `condition_id` | string | -- | Filter by market condition ID (resolves to token IDs) |
| `market_slug` | string | -- | Filter by market slug (resolves to token IDs) |
| `event_slug` | string | -- | Filter by event slug (resolves to token IDs across the event) |
| `side` | string | -- | `buy` or `sell`, based on the maker order side |
| `min_amount` | integer | -- | Minimum `maker_amount_filled` (raw 6-decimal) |
| `category` | string | -- | Filter by market category. Broad categories may return `400` asking for a narrower filter. |
| `tag_slug` | string | -- | Filter by market tag slug. Broad tags may return `400` asking for a narrower filter. |
| `after` | integer | 0 | Start of time range (Unix timestamp) |
| `before` | integer | -- | End of time range |
| `limit` | integer | 100 | Max 300 |
| `offset` | integer | 0 | Pagination offset |
## Example
```bash theme={null}
curl "https://api.polynode.dev/v3/builders/0x4898df15ec6590495dc6c0fedf951ade3e64001d47f9caf44a64e86fc11959df/trades?market_slug=dota2-tundra-xtreme-2026-05-22-game1&limit=1"
```
```json theme={null}
{
"builder": "0x4898df15ec6590495dc6c0fedf951ade3e64001d47f9caf44a64e86fc11959df",
"builder_code": "0x4898df15ec6590495dc6c0fedf951ade3e64001d47f9caf44a64e86fc11959df",
"builder_logo": "https://polymarket-upload.s3.us-east-2.amazonaws.com/profile-image-4043800-ced7ecde-0754-46a1-8a16-bb6c338fbf39.png",
"builder_name": "PolyCop",
"builder_verified": true,
"elapsed_ms": 12,
"has_more": true,
"limit": 1,
"offset": 0,
"rows_returned": 1,
"trades": [
{
"amount": 1.101,
"amount_usd": 1.08999,
"asset": "21578184864194020862792310253337084708609281967112790860038966988826118995357",
"builder": "0x4898df15ec6590495dc6c0fedf951ade3e64001d47f9caf44a64e86fc11959df",
"builder_code": "0x4898df15ec6590495dc6c0fedf951ade3e64001d47f9caf44a64e86fc11959df",
"builder_logo": "https://polymarket-upload.s3.us-east-2.amazonaws.com/profile-image-4043800-ced7ecde-0754-46a1-8a16-bb6c338fbf39.png",
"builder_name": "PolyCop",
"builder_verified": true,
"condition_id": "0x97269cbb95bd572b3dde34cdd619be278c8f13ba5e43e2e6c2d8639411128a86",
"direction": "BUY",
"fee": 0.00032,
"id": "0x0d57b8bd2cad026ccac902cee781becb358387c6fed98558802e0464434d4c9c_1060",
"image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/dota2-7ffacddb21.jpg",
"maker": "0x952d11ebff81d6bd3185e608ed3515b94618ab8a",
"maker_amount": 1.08999,
"maker_asset_id": "0",
"market": "Dota 2: Tundra Esports vs Xtreme Gaming - Game 1 Winner",
"order_hash": "0x52d42aa621bd4c451151e97eb9b8a9ec4154f76da440ba31b73cebe918331ac2",
"outcome": "Tundra Esports",
"outcome_index": 0,
"price": 0.99,
"role": "maker",
"side": 0,
"size": 1.101,
"slug": "dota2-tundra-xtreme-2026-05-22-game1",
"taker": "0xe111180000d2663c0091e4f400237545b87b996b",
"taker_amount": 1.101,
"taker_asset_id": "21578184864194020862792310253337084708609281967112790860038966988826118995357",
"timestamp": "1779448364",
"token_id": "21578184864194020862792310253337084708609281967112790860038966988826118995357",
"transaction_hash": "0x0d57b8bd2cad026ccac902cee781becb358387c6fed98558802e0464434d4c9c"
}
]
}
```
## Response fields
### Response envelope
| Field | Type | Description |
| ------------------ | --------------- | ---------------------------------------------------------------- |
| `builder` | string | Builder code requested in the path |
| `builder_code` | string | Same builder code, included for profile-style naming consistency |
| `builder_name` | string \| null | Public builder profile name when available |
| `builder_logo` | string \| null | Public builder profile image URL when available |
| `builder_verified` | boolean \| null | Whether the builder profile is verified, when available |
| `trades` | array | Trade rows attributed to this builder |
| `rows_returned` | integer | Number of trade rows returned |
| `has_more` | boolean | Whether another page is available |
| `offset` | integer | Pagination offset used |
| `limit` | integer | Page size used |
| `elapsed_ms` | integer | Server-side query time in milliseconds |
### Trade data
Each trade contains the same enriched fields as [Wallet Trades](/data/wallets/trades), including market context, computed price/size, order hash, and direction. Builder metadata is also repeated on each row for clients that stream or store trade rows independently.
| Field | Type | Description |
| ------------------ | --------------- | ------------------------------------------------------- |
| `builder_code` | string | Builder code for this trade row |
| `builder_name` | string \| null | Public builder profile name when available |
| `builder_logo` | string \| null | Public builder profile image URL when available |
| `builder_verified` | boolean \| null | Whether the builder profile is verified, when available |
## Filter examples
### Trades for a specific token
```bash theme={null}
curl "https://api.polynode.dev/v3/builders/0x4898df15.../trades?token_id=21578184864194020862792310253337084708609281967112790860038966988826118995357&limit=10"
```
### Trades for a specific market
```bash theme={null}
curl "https://api.polynode.dev/v3/builders/0x4898df15.../trades?market_slug=dota2-tundra-xtreme-2026-05-22-game1&limit=10"
```
### Maker-side buys or sells
```bash theme={null}
curl "https://api.polynode.dev/v3/builders/0x4898df15.../trades?side=buy&limit=10"
curl "https://api.polynode.dev/v3/builders/0x4898df15.../trades?side=sell&limit=10"
```
### Combine tag/category with a narrow market filter
```bash theme={null}
curl "https://api.polynode.dev/v3/builders/0x4898df15.../trades?market_slug=dota2-tundra-xtreme-2026-05-22-game1&tag_slug=sports&limit=10"
curl "https://api.polynode.dev/v3/builders/0x4898df15.../trades?market_slug=dota2-tundra-xtreme-2026-05-22-game1&category=Sports&limit=10"
```
Builder categories are derived from the first market tag. Broad `category` and `tag_slug` filters can be too large to run directly; add `market_slug`, `condition_id`, or `token_id` to use the indexed intersection path.
```json theme={null}
{
"error": "tag_slug filter too broad for builder trades; add market_slug, condition_id, token_id, or time bounds"
}
```
## Related endpoints
| Endpoint | Description |
| ------------------------- | ------------------------------------------- |
| `GET /v3/builders` | Builder leaderboard |
| `GET /v3/builders/{code}` | Stats and profile metadata for this builder |
# Combo Activity
Source: https://docs.polynode.dev/data/combos/activity
GET /v3/combos/activity
Query combo lifecycle activity by market, condition, position, or wallet.
Returns combo lifecycle activity across wallets. At least one of `market_id`, `condition_id`, or `position_id` is required. Add `wallet` to narrow results to a single wallet.
## Request
```
GET /v3/combos/activity
```
### Query parameters
| Parameter | Type | Default | Description |
| -------------- | ------- | ------- | ------------------------------------------------------------------------ |
| `market_id` | string | -- | Alias for `condition_id` |
| `condition_id` | string | -- | Combo condition ID |
| `position_id` | string | -- | Combo position ID |
| `wallet` | string | -- | Wallet address |
| `event_kind` | string | -- | Filter by event kind, for example `PositionsSplit` or `PositionRedeemed` |
| `limit` | integer | 100 | Results per page, max 300 |
| `offset` | integer | 0 | Pagination offset |
## Example
```bash theme={null}
curl "https://api.polynode.dev/v3/combos/activity?condition_id=0x03d98f6ac4c108c5ca66f79f34908a1d820000000000000000000000000000&limit=10"
```
```json theme={null}
{
"activity": [
{
"position_type": "combo",
"event_kind": "PositionsSplit",
"wallet_address": "0x63613e3b96f418332d43cd2af8dc321014d15907",
"combo_condition_id": "0x03d98f6ac4c108c5ca66f79f34908a1d820000000000000000000000000000",
"amount_usdc": "0.050750",
"tx_hash": "0xf3a8985f04bf869483ef4163a185f296c834eb827b5e5ae3db5bd44558121d51",
"block_number": 88276713,
"timestamp": 1781120055
}
],
"rows_returned": 10,
"has_more": true,
"position_type": "combo",
"source": "v3.combos.activity"
}
```
## Notes
* Use `/v3/wallets/{address}/combos/activity` when the wallet address is already known.
* Use this endpoint when you are exploring all lifecycle activity for a combo market or position.
* The activity feed is on-chain derived and can include split, merge, redemption, and transfer-related lifecycle rows as those events are observed.
* Public combo redemption rows are normalized to Polymarket's combo activity shape: router and auto-redeemer redemption paths are returned as `event_kind: "PositionRedeemed"` with `side: "Redeem"`, a module kind such as `Combinatorial`, `Binary`, or `NegRisk`, and `payout_usdc` when available.
* When leg metadata is available, resolved combo activity includes leg statuses such as `RESOLVED_WIN` or `RESOLVED_LOSS` and `leg_current_price` values.
# Combo Markets
Source: https://docs.polynode.dev/data/combos/markets
GET /v3/combos/markets
List observed Polymarket combo markets with legs and activity counters.
Returns observed combo markets. Each market includes its combo condition, status, activity counters, volume, and a `legs` array when leg metadata is available.
## Request
```
GET /v3/combos/markets
```
### Query parameters
| Parameter | Type | Default | Description |
| -------------- | ------- | ------- | ------------------------------------------------------------------------------------------------------------- |
| `market_id` | string | -- | Alias for `condition_id` |
| `condition_id` | string | -- | Combo condition ID |
| `status` | string | -- | `observed`, `open`, `closed`, `resolved`, `resolved_win`, `resolved_loss`, `redeemable`, `redeemed`, or `all` |
| `limit` | integer | 100 | Results per page, max 300 |
| `offset` | integer | 0 | Pagination offset |
## Example
```bash theme={null}
curl "https://api.polynode.dev/v3/combos/markets?limit=2"
```
```json theme={null}
{
"markets": [
{
"combo_condition_id": "0x03c58e66825a6501dfffbf7dd0e802c3470000000000000000000000000000",
"module_name": "CombinatorialModule",
"status": "observed",
"observed_execution_count": 10,
"observed_lifecycle_count": 16,
"observed_transfer_count": 32,
"volume_usdc": "427.01",
"last_activity_at": "2026-06-11T02:07:18+00:00",
"legs": [
{
"leg_index": 0,
"leg_position_id": "770259566746216134461634134886555299946416955842760322422283095048103919616",
"leg_status": "OPEN",
"market_slug": "fifwc-gha-pan-2026-06-17-spread-away-1pt5",
"market_title": "Panama (-1.5)",
"event_title": "Ghana vs. Panama - More Markets",
"leg_current_price": "0.0000"
}
]
}
],
"rows_returned": 2,
"has_more": true,
"position_type": "combo",
"source": "v3.combos.markets",
"elapsed_ms": 148
}
```
## Response fields
| Field | Type | Description |
| -------------------------- | ------- | ------------------------------------------------ |
| `combo_condition_id` | string | Combo condition ID |
| `module_name` | string | Combo module name, usually `CombinatorialModule` |
| `status` | string | Observed lifecycle status |
| `observed_execution_count` | integer | Observed combo execution count |
| `observed_lifecycle_count` | integer | Observed split/merge/lifecycle count |
| `observed_redeem_count` | integer | Observed combo redemption count |
| `observed_transfer_count` | integer | Observed combo position transfer count |
| `volume_usdc` | string | Observed combo volume formatted in USDC |
| `last_activity_at` | string | Last observed combo activity timestamp |
| `legs` | array | Market legs that define the combo |
Resolved leg metadata is refreshed from Polymarket combo metadata when available. Leg rows can include `leg_status` values such as `RESOLVED_WIN` or `RESOLVED_LOSS`, `leg_resolved_at`, and `leg_current_price`.
# Combos Overview
Source: https://docs.polynode.dev/data/combos/overview
Query Polymarket combo markets, combo wallet positions, combo trades, and combo lifecycle activity.
Combos are Polymarket's combinatorial positions: one position is defined by multiple market legs. PolyNode indexes those on-chain combo positions separately from standard CTF positions, then exposes them through dedicated combo endpoints and opt-in wallet aggregates.
Use the dedicated combo endpoints when you want combo-only data. Use `include_combos=true` on supported wallet endpoints when you want the wallet-level response to include both standard market data and combo data.
## Base URL
```
https://api.polynode.dev/v3
```
## Dedicated combo endpoints
| Endpoint | Description |
| -------------------------------------------- | -------------------------------------------------------------------------- |
| `GET /v3/combos/markets` | List observed combo markets with leg metadata |
| `GET /v3/combos/activity` | List combo lifecycle activity for a market, condition, position, or wallet |
| `GET /v3/wallets/{address}/combos/positions` | Combo positions for one wallet |
| `GET /v3/wallets/{address}/combos/trades` | Combo trades for one wallet |
| `GET /v3/wallets/{address}/combos/activity` | Combo lifecycle activity for one wallet |
| `GET /v3/wallets/{address}/combos/summary` | Combo-only P\&L summary for one wallet |
## Additive wallet support
These wallet endpoints support `include_combos=true`:
| Endpoint | Behavior |
| ------------------------------------- | -------------------------------------------------------------- |
| `GET /v3/wallets/{address}` | Adds combo P\&L/counts to the all-time wallet summary |
| `GET /v3/wallets/{address}/pnl` | Adds combo P\&L/counts to all-time wallet P\&L |
| `GET /v3/wallets/{address}/positions` | Appends combo positions to the position list |
| `GET /v3/wallets/{address}/trades` | Adds a `combo_trades` branch alongside the standard trade list |
If a wallet has no combo exposure, `include_combos=true` still returns `200` with the normal wallet response and a zero combo contribution or empty combo branch. Clients do not need to know ahead of time whether a wallet has traded combos.
## Identifier notes
| Identifier | Description |
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `combo_condition_id` | Combo condition ID. Returned as a hex string. |
| `position_id` / `combo_position_id` | ERC-1155 position ID for the combo leg or combo position. Returned as a string because it can exceed JavaScript integer precision. |
| `legs` | Array of market legs that define the combo position. Legs may include market slug/title, event slug/title, outcome label, current price, and leg status when metadata is available. |
## P\&L semantics
Combo P\&L uses the same high-level model as standard Polymarket positions: weighted-average cost basis, realized P\&L from closes/redeems/merges, and unrealized P\&L for open balances when a current price is available.
When a combo is redeemed, PolyNode settles the redeemed size against weighted-average entry basis. Fully redeemed winners return `RESOLVED_WIN` with zero open entry cost and realized P\&L from the redemption payout; fully redeemed losers return `RESOLVED_LOSS`. Combo redemption activity is normalized to Polymarket's public `PositionRedeemed` shape even when the on-chain path used the router or auto-redeemer.
All aggregate P\&L fields are returned as decimal USD numbers. Event-level combo amount fields use decimal strings such as `"0.050750"` for exact USDC-style display.
# Wallet Combo Activity
Source: https://docs.polynode.dev/data/combos/wallet-activity
GET /v3/wallets/{address}/combos/activity
List combo lifecycle activity for one wallet, including split and merge activity.
Returns combo lifecycle activity for a wallet. This is the combo-only counterpart to wallet splits, merges, redemptions, and lifecycle activity.
## Request
```
GET /v3/wallets/{address}/combos/activity
```
### Query parameters
| Parameter | Type | Default | Description |
| -------------- | ------- | ------- | ------------------------------------------------------------------------ |
| `market_id` | string | -- | Alias for `condition_id` |
| `condition_id` | string | -- | Combo condition ID |
| `position_id` | string | -- | Combo position ID |
| `event_kind` | string | -- | Filter by event kind, for example `PositionsSplit` or `PositionRedeemed` |
| `limit` | integer | 100 | Results per page, max 300 |
| `offset` | integer | 0 | Pagination offset |
## Example
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x63613e3b96f418332d43cd2af8dc321014d15907/combos/activity?limit=2"
```
```json theme={null}
{
"address": "0x63613e3b96f418332d43cd2af8dc321014d15907",
"activity": [
{
"position_type": "combo",
"event_kind": "PositionsSplit",
"side": "Split",
"user_address": "0x63613e3b96f418332d43cd2af8dc321014d15907",
"combo_condition_id": "0x03d98f6ac4c108c5ca66f79f34908a1d820000000000000000000000000000",
"combo_position_id": "1741334187009265192213210063949860811096650382021683265628751751539647840256",
"amount_usdc": "0.050750",
"tx_hash": "0xf3a8985f04bf869483ef4163a185f296c834eb827b5e5ae3db5bd44558121d51",
"log_index": 890,
"block_number": 88276713,
"timestamp": 1781120055,
"legs": [
{
"leg_index": 0,
"leg_position_id": "547325449395903582555711510844460161809002726131102493368605951180083822592"
}
]
}
],
"rows_returned": 2,
"has_more": true,
"position_type": "combo",
"source": "v3.wallet_combos.activity"
}
```
## Response fields
| Field | Type | Description |
| -------------------- | -------------- | ----------------------------------------------------------------- |
| `event_kind` | string | Public lifecycle event kind |
| `side` | string | Human-readable lifecycle side, such as `Split` or `Merge` |
| `combo_condition_id` | string | Combo condition ID |
| `combo_position_id` | string | Combo position ID |
| `amount_usdc` | string | Amount formatted in USDC |
| `payout_usdc` | string or null | Redemption payout formatted in USDC when the activity is a redeem |
| `tx_hash` | string | Polygon transaction hash |
| `block_number` | integer | Polygon block number |
| `timestamp` | integer | Unix timestamp in seconds |
| `legs` | array | Leg position IDs involved in the lifecycle event |
## Redemption semantics
Combo redemptions are normalized to match Polymarket's public combo activity shape. Router and auto-redeemer paths are returned as `event_kind: "PositionRedeemed"` with `side: "Redeem"` and the underlying module kind, such as `Combinatorial`, `Binary`, or `NegRisk`. The raw router/auto-redeemer contract path is used internally for indexing but is not the public event kind.
When leg metadata is available, redemption rows include resolved leg statuses and `leg_current_price` values in the `legs` array.
# Wallet Combo Positions
Source: https://docs.polynode.dev/data/combos/wallet-positions
GET /v3/wallets/{address}/combos/positions
List combo positions for one wallet with legs, entry basis, balances, and P&L.
Returns combo positions for a wallet. Each row includes balance, entry basis, realized/unrealized/total P\&L, status, and leg metadata when available.
For a combined standard + combo position list, call `GET /v3/wallets/{address}/positions?include_combos=true`.
## Request
```
GET /v3/wallets/{address}/combos/positions
```
### Query parameters
| Parameter | Type | Default | Description |
| -------------- | ------- | ------- | ------------------------------------------------------------------------------------------------------------- |
| `market_id` | string | -- | Alias for `condition_id` |
| `condition_id` | string | -- | Combo condition ID |
| `position_id` | string | -- | Combo position ID |
| `status` | string | -- | `open`, `closed`, `resolved`, `resolved_win`, `resolved_loss`, `redeemable`, `redeemed`, `observed`, or `all` |
| `limit` | integer | 100 | Results per page, max 300 |
| `offset` | integer | 0 | Pagination offset |
## Example
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x63613e3b96f418332d43cd2af8dc321014d15907/combos/positions?limit=1"
```
```json theme={null}
{
"address": "0x63613e3b96f418332d43cd2af8dc321014d15907",
"positions": [
{
"position_type": "combo",
"wallet_address": "0x63613e3b96f418332d43cd2af8dc321014d15907",
"combo_condition_id": "0x030002fa8781d9f445d838e60524d70bf30000000000000000000000000000",
"combo_position_id": "1356959103499736670017337806334234879289930581423500836189165811753797287936",
"shares_balance": "0.000100",
"entry_avg_price_usdc": "0.2285",
"entry_cost_usdc": "0.00",
"realized_pnl_usdc": "0.000000",
"total_pnl_usdc": "0.000000",
"status": "open",
"legs_total": 3,
"legs": [
{
"leg_index": 0,
"leg_outcome_label": "Yes",
"leg_status": "OPEN",
"market": {
"slug": "fifwc-ger-civ-2026-06-20",
"event_title": "Germany vs. Cote d'Ivoire"
}
}
]
}
],
"rows_returned": 1,
"has_more": true,
"position_type": "combo",
"source": "v3.wallet_combos.positions"
}
```
## Response fields
| Field | Type | Description |
| ---------------------- | -------------- | ------------------------------------------ |
| `combo_condition_id` | string | Combo condition ID |
| `combo_position_id` | string | Combo ERC-1155 position ID |
| `shares_balance` | string | Current combo share balance |
| `entry_avg_price_usdc` | string | Weighted-average entry price |
| `entry_cost_usdc` | string | Entry cost for current/lifetime accounting |
| `realized_pnl_usdc` | string | Realized P\&L |
| `unrealized_pnl_usdc` | string or null | Unrealized P\&L when available |
| `total_pnl_usdc` | string | Realized + unrealized P\&L |
| `status` | string | Current combo position status |
| `legs` | array | Leg metadata for the combo |
## Resolution semantics
Redeemed combo positions are settled into weighted-average cost basis as soon as PolyNode observes the on-chain combo redemption. A fully redeemed winning combo returns `status: "RESOLVED_WIN"`, `shares_balance: "0.000000"`, `entry_cost_usdc: "0.00"`, and realized/total P\&L equal to redemption payout minus entry basis. A fully redeemed losing combo returns `status: "RESOLVED_LOSS"` with zero open entry cost. `entry_avg_price_usdc` remains the historical weighted-average entry price after close.
Leg metadata is refreshed from Polymarket combo metadata. Resolved legs expose `leg_status` values such as `RESOLVED_WIN` or `RESOLVED_LOSS`, `leg_resolved_at`, and `leg_current_price` when available. `legs_resolved` and `legs_pending` are derived from those leg statuses.
# Wallet Combo Summary
Source: https://docs.polynode.dev/data/combos/wallet-summary
GET /v3/wallets/{address}/combos/summary
Get combo-only P&L and position counts for one wallet.
Returns a combo-only summary for a wallet. Use this endpoint when you want to inspect combo exposure separately from the standard wallet summary.
For an all-in wallet summary, call `GET /v3/wallets/{address}?include_combos=true`.
## Request
```
GET /v3/wallets/{address}/combos/summary
```
### Path parameters
| Parameter | Type | Description |
| --------- | ------ | -------------- |
| `address` | string | Wallet address |
## Example
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x63613e3b96f418332d43cd2af8dc321014d15907/combos/summary"
```
```json theme={null}
{
"address": "0x63613e3b96f418332d43cd2af8dc321014d15907",
"summary": {
"position_type": "combo",
"wallet_address": "0x63613e3b96f418332d43cd2af8dc321014d15907",
"open_combo_count": 1495,
"closed_combo_count": 19995,
"balance_count": 21492,
"realized_pnl_usdc": "113.510513",
"unrealized_pnl_usdc": null,
"total_pnl_usdc": "113.510513",
"last_activity_at": "2026-06-11T02:04:19.887367+00:00"
},
"rows_returned": 1,
"position_type": "combo",
"source": "v3.wallet_combos.summary",
"elapsed_ms": 13
}
```
## Response fields
| Field | Type | Description |
| --------------------- | -------------- | ----------------------------------------------- |
| `open_combo_count` | integer | Open combo positions |
| `closed_combo_count` | integer | Closed combo positions |
| `balance_count` | integer | Total combo balance rows tracked for the wallet |
| `realized_pnl_usdc` | string | Realized combo P\&L in USD |
| `unrealized_pnl_usdc` | string or null | Current unrealized combo P\&L when available |
| `total_pnl_usdc` | string | Realized + unrealized combo P\&L |
| `last_activity_at` | string | Last observed combo activity for this wallet |
# Wallet Combo Trades
Source: https://docs.polynode.dev/data/combos/wallet-trades
GET /v3/wallets/{address}/combos/trades
List combo trades for one wallet with side, size, price, role, counterparty, and transaction data.
Returns combo trades where the wallet participated as maker or taker. Use this endpoint for combo-only trade history.
## Request
```
GET /v3/wallets/{address}/combos/trades
```
### Query parameters
| Parameter | Type | Default | Description |
| -------------- | ------- | ------- | ------------------------- |
| `market_id` | string | -- | Alias for `condition_id` |
| `condition_id` | string | -- | Combo condition ID |
| `position_id` | string | -- | Combo position ID |
| `side` | string | -- | `BUY`, `SELL`, or `all` |
| `limit` | integer | 100 | Results per page, max 300 |
| `offset` | integer | 0 | Pagination offset |
## Example
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x63613e3b96f418332d43cd2af8dc321014d15907/combos/trades?limit=2"
```
```json theme={null}
{
"address": "0x63613e3b96f418332d43cd2af8dc321014d15907",
"trades": [
{
"position_type": "combo",
"wallet_role": "maker",
"wallet_address": "0x63613e3b96f418332d43cd2af8dc321014d15907",
"counterparty_address": "0xe3333700ca9d93003f00f0f71f8515005f6c00aa",
"position_id": "1741334187009265192213210063949860811096650382021683265628751751539647840256",
"side": "BUY",
"price": "0.0197",
"size": "0.050750",
"fee": "0.000020",
"tx_hash": "0xf3a8985f04bf869483ef4163a185f296c834eb827b5e5ae3db5bd44558121d51",
"log_index": 896,
"block_number": 88276713,
"timestamp": 1781120055
}
],
"rows_returned": 2,
"has_more": true,
"position_type": "combo",
"source": "v3.wallet_combos.trades",
"elapsed_ms": 23
}
```
## Response fields
| Field | Type | Description |
| ---------------------- | ------- | -------------------------------- |
| `wallet_role` | string | `maker` or `taker` |
| `counterparty_address` | string | Counterparty address |
| `position_id` | string | Combo position ID |
| `side` | string | `BUY` or `SELL` |
| `price` | string | Fill price |
| `size` | string | Combo shares traded |
| `fee` | string | Fee amount |
| `tx_hash` | string | Polygon transaction hash |
| `log_index` | integer | Log index within the transaction |
| `block_number` | integer | Polygon block number |
| `timestamp` | integer | Unix timestamp in seconds |
# Builder Fees
Source: https://docs.polynode.dev/data/fees/builder-fees
Rank builders by attributed fee generation and inspect their fee-bearing fills.
Builder fee data is based on the `builder` field on attributed fills. `total_fees` is protocol fee generated by attributed fills, not the builder's off-chain rev-share payout.
Canonical endpoint references:
* [Builder Leaderboard](/data/builders/list)
* [Builder Detail](/data/builders/detail)
* [Builder Trades](/data/builders/trades)
## Rank Builders by Fees
```bash theme={null}
curl "https://api.polynode.dev/v3/builders?sort=fees&limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
## Inspect Builder Trades
```bash theme={null}
curl "https://api.polynode.dev/v3/builders/0x4898df15ec6590495dc6c0fedf951ade3e64001d47f9caf44a64e86fc11959df/trades?limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
## Builder Trade Filters
| Parameter | Purpose |
| ----------------- | --------------------------- |
| `token_id` | One outcome token |
| `condition_id` | All outcomes in a condition |
| `market_slug` | Market URL slug |
| `event_slug` | All markets in an event |
| `side` | `buy` or `sell` |
| `min_amount` | Minimum raw maker amount |
| `category` | Market category |
| `tag_slug` | Market tag slug |
| `after`, `before` | Time range |
Builder codes are 32-byte hex tags, not wallet addresses.
# Fees by Receiver
Source: https://docs.polynode.dev/data/fees/fees-by-receiver
Fetch fee charged events received by one address.
Use this when you know the receiver address and want only fee events paid to that receiver.
Canonical endpoint reference: [Fee Events](/data/global/fees).
## Query
```bash theme={null}
curl "https://api.polynode.dev/v3/fees/0x115f48dc2a731aa16251c6d6e1befc42f92accc9?limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
## Filters
| Parameter | Purpose |
| --------- | ------------------ |
| `after` | Start timestamp |
| `before` | End timestamp |
| `limit` | Page size, max 300 |
| `offset` | Pagination offset |
## Returned Data
Rows match the global fee event shape, with the receiver implied by the path.
# Global Fee Events
Source: https://docs.polynode.dev/data/fees/global-fees
Browse fee charged events across Polymarket.
Use this when you want the global stream of fee charged events rather than wallet-owned fills.
Canonical endpoint reference: [Fee Events](/data/global/fees).
## Query
```bash theme={null}
curl "https://api.polynode.dev/v3/fees?limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
## Filters
| Parameter | Purpose |
| --------- | ------------------ |
| `after` | Start timestamp |
| `before` | End timestamp |
| `limit` | Page size, max 300 |
| `offset` | Pagination offset |
## Returned Data
Rows include the fee receiver, raw amount, transaction hash, and timestamp. Amounts are raw 6-decimal USDC values unless a page says otherwise.
# Maker Rebates
Source: https://docs.polynode.dev/data/fees/maker-rebates
Query Polymarket-reported maker rebate records for one maker/date.
Maker rebates are Polymarket accounting credits. They are not the same thing as trader-paid fill fees.
Canonical endpoint reference: [Wallet Maker Rebates](/data/wallets/rebates).
## Query
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x63ce342161250d705dc0b16df89036c8e5f9ba9a/rebates" \
-H "x-api-key: $POLYNODE_API_KEY"
```
## Optional Date
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x63ce342161250d705dc0b16df89036c8e5f9ba9a/rebates?date=2026-06-17" \
-H "x-api-key: $POLYNODE_API_KEY"
```
## Notes
* Rebate lookups are keyed by CLOB maker/signing address.
* Passing a Safe or proxy wallet can return an empty `rebates` array with a hint.
* This endpoint can return zero rows for a valid wallet/date.
# Fees Overview
Source: https://docs.polynode.dev/data/fees/overview
Where to query trader-paid fees, fee receiver events, builder-attributed fees, maker rebates, and reward market configuration.
PolyNode exposes fee data in a few different forms. Use this section when you know you need fee or rebate data but are not sure which endpoint matches the accounting question.
Fees, rebates, rewards, and builder payouts are separate concepts. Trader-paid fees come from executed fills. Fee events show receiver-level on-chain fee charges. Maker rebates and reward configuration are Polymarket accounting surfaces. Builder rev share is paid off-chain and is not the same as `total_fees`.
## Fee Surfaces
| Need | Endpoint | Use this when |
| --------------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------- |
| Wallet all-time fee summary | `GET /v3/wallets/{address}?include_accounting_summary=true` | You want the exact all-time trader-paid fee total without paging raw fills |
| Wallet fee-bearing fills | `GET /v3/wallets/{address}/fees-paid` | You want trades where a wallet paid a nonzero fill fee |
| Global fee events | `GET /v3/fees` | You want fee charged events across the platform |
| Fees by receiver | `GET /v3/fees/{receiver}` | You want fee events received by one address |
| Builder fee ranking | `GET /v3/builders?sort=fees` | You want builders ranked by fees generated on attributed fills |
| Builder fee trades | `GET /v3/builders/{code}/trades` | You want the underlying attributed fills, including per-fill `fee` |
| Maker rebates | `GET /v3/wallets/{address}/rebates` | You want Polymarket-reported maker rebates for one maker/date |
| Reward configs | `GET /v3/rewards/markets` | You want public reward-market configuration, not wallet-earned rewards |
## Quick Examples
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x63ce342161250d705dc0b16df89036c8e5f9ba9a?include_accounting_summary=true" \
-H "x-api-key: $POLYNODE_API_KEY"
```
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x63ce342161250d705dc0b16df89036c8e5f9ba9a/fees-paid?limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
```bash theme={null}
curl "https://api.polynode.dev/v3/fees?limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
```bash theme={null}
curl "https://api.polynode.dev/v3/builders?sort=fees&limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
## Related Pages
| Page | Canonical reference |
| --------------------------------------------------------- | --------------------------------------------- |
| [Wallet Summary](/data/wallets/summary) | [Wallet Summary](/data/wallets/summary) |
| [Wallet Fees Paid](/data/fees/wallet-fees-paid) | [Wallet Fees Paid](/data/wallets/fees-paid) |
| [Global Fee Events](/data/fees/global-fees) | [Fee Events](/data/global/fees) |
| [Fees by Receiver](/data/fees/fees-by-receiver) | [Fee Events](/data/global/fees) |
| [Builder Fees](/data/fees/builder-fees) | [Builder Leaderboard](/data/builders/list) |
| [Maker Rebates](/data/fees/maker-rebates) | [Wallet Maker Rebates](/data/wallets/rebates) |
| [Reward Market Configs](/data/fees/reward-market-configs) | [Reward Markets](/data/rewards/markets) |
# Reward Market Configs
Source: https://docs.polynode.dev/data/fees/reward-market-configs
Query public Polymarket reward-market configuration.
Reward market configuration describes markets with reward programs. It is not a per-wallet earned rewards ledger.
Canonical endpoint references:
* [Reward Markets](/data/rewards/markets)
* [Reward Market Detail](/data/rewards/market)
## List Reward Markets
```bash theme={null}
curl "https://api.polynode.dev/v3/rewards/markets" \
-H "x-api-key: $POLYNODE_API_KEY"
```
## One Condition
```bash theme={null}
curl "https://api.polynode.dev/v3/rewards/markets/0x0001cb8c0b39aeb614ab9a43867595317f06ede9c011661513065c638fbbefda" \
-H "x-api-key: $POLYNODE_API_KEY"
```
## Filters
| Parameter | Purpose |
| ------------- | ---------------------------------- |
| `sponsored` | Filter to sponsored reward markets |
| `next_cursor` | Provider cursor pagination |
Use this for public market reward configuration. Do not treat it as arbitrary-wallet earned LP rewards.
# Wallet Fees Paid
Source: https://docs.polynode.dev/data/fees/wallet-fees-paid
Query fee-bearing executed fills for one wallet.
Use this when you want the fills where a specific wallet paid a nonzero trader fee.
Canonical endpoint reference: [Wallet Fees Paid](/data/wallets/fees-paid).
For the exact all-time total, use [Wallet Summary](/data/wallets/summary) with `include_accounting_summary=true` and read `accounting_summary.fees_paid.amount`.
## Query
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x63ce342161250d705dc0b16df89036c8e5f9ba9a/fees-paid?limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
## Filters
| Parameter | Purpose |
| --------- | ----------------------------- |
| `after` | Start timestamp, Unix seconds |
| `before` | End timestamp, Unix seconds |
| `order` | `asc` or `desc` |
| `limit` | Page size, max 300 |
| `offset` | Pagination offset |
## Returned Data
The response includes a `fees` array with the same market-enriched fill fields as wallet trades, plus:
| Field | Meaning |
| ---------------------- | ----------------------------------------------------- |
| `fee` | Fee in USD |
| `fee_paid` | Same value as `fee`, named for fee-specific summaries |
| `fee_paid_raw` | Raw 6-decimal amount |
| `page_total_fees_paid` | Sum for the returned page only |
| `fee_semantics` | Reminder that this is trader-paid fill fees |
`page_total_fees_paid` is page-scoped. Use `GET /v3/wallets/{address}?include_accounting_summary=true` when you need the all-time total.
# Condition Detail
Source: https://docs.polynode.dev/data/global/conditions
GET /v3/conditions/{condition_id}
Get full market condition data including payouts, outcomes, metadata, and resolution status.
Returns full condition data for a market, including payout structure, outcomes, and enriched market metadata.
## Request
```
GET /v3/conditions/{condition_id}
```
### Path parameters
| Parameter | Type | Description |
| -------------- | ------ | --------------------------------- |
| `condition_id` | string | Market condition ID (0x-prefixed) |
## Example
```bash theme={null}
curl "https://api.polynode.dev/v3/conditions/0x6c4d221b3cf2c8d17467c70a8aa3c714e30299b6e57cd3e4269dc8a41d2b0cd8?key=YOUR_KEY"
```
```json theme={null}
{
"id": "0x6c4d221b3cf2c8d17467c70a8aa3c714e30299b6e57cd3e4269dc8a41d2b0cd8",
"position_ids": [
"30698649476690694814108493145041085718609914990967907518412053225237708492787",
"43362318061877762261164265810725209343672429810065542703856317173848260953021"
],
"payout_numerators": [0, 1],
"payout_denominator": "1",
"question": "Will Trump pardon Joe Exotic \"The Tiger King\" in 2025?",
"slug": "will-trump-pardon-joe-exotic-the-tiger-king-in-2025-784-928-365",
"outcomes": ["Yes", "No"],
"end_date": "2025-12-31 12:00:00+00",
"active": true,
"closed": true,
"neg_risk": false,
"event_slug": "who-will-trump-pardon-in-2025",
"event_title": "Who will Trump pardon in 2025?",
"category": null,
"liquidity": "0",
"volume": "99993.937724000003072433173656463623046875",
"tags": ["Politics", "Trump", "SBF", "cz", "Trump Presidency", "Ghislaine Maxwell"],
"winning_outcome_index": 1,
"elapsed_ms": 6
}
```
`position_ids` are **decimal strings**, not JSON numbers. Polymarket token IDs are 78-digit values that exceed IEEE 754 float precision — any JSON parser that decodes them as floats will corrupt the lower digits. Always treat them as strings.
`volume` and `liquidity` are returned at full database precision (up to \~50 digits after the decimal). Round them on the client; do not store as a JS `Number`.
## Response fields
| Field | Type | Nullable | Description |
| ----------------------- | ---------------- | -------- | ----------------------------------------------------------- |
| `id` | string | no | Condition ID |
| `position_ids` | array\ | no | Outcome token IDs (decimal-string-encoded — see note above) |
| `payout_numerators` | array\ | no | Payout numerators per outcome (non-zero after resolution) |
| `payout_denominator` | string | no | Payout denominator |
| `question` | string | no | Market question |
| `slug` | string | no | Market URL slug |
| `outcomes` | array\ | no | Outcome labels |
| `end_date` | string | yes | Market end date (Postgres timestamp with offset) |
| `active` | boolean | no | Currently active |
| `closed` | boolean | no | No longer accepting orders |
| `neg_risk` | boolean | no | Multi-outcome (neg-risk) market |
| `event_slug` | string | yes | Parent event slug |
| `event_title` | string | yes | Parent event title |
| `category` | string | yes | Optional category label |
| `liquidity` | string (decimal) | no | Current liquidity, full-precision string |
| `volume` | string (decimal) | no | All-time volume, full-precision string |
| `tags` | array\ | yes | Category tags in their canonical (Title Case) form |
| `winning_outcome_index` | integer | yes | Winning outcome (null if unresolved) |
# Fee Events
Source: https://docs.polynode.dev/data/global/fees
GET /v3/fees
Browse fee charged events across all Polymarket trades.
Returns fee events from the Polymarket exchange. Each fee event records the receiver, amount, and associated transaction.
## Request
```
GET /v3/fees
```
### Query parameters
| Parameter | Type | Default | Description |
| --------- | ------- | ------- | ------------------------------------ |
| `after` | integer | -- | Start of time range (Unix timestamp) |
| `before` | integer | -- | End of time range |
| `limit` | integer | 100 | Max 300 |
| `offset` | integer | 0 | Pagination offset |
### By receiver
```
GET /v3/fees/{receiver}
```
Returns fees received by a specific address.
## Example
```bash theme={null}
curl https://api.polynode.dev/v3/fees?limit=1
```
```json theme={null}
{
"data": [
{
"id": "0x1e20bc6a2107b61fa179b3ab17047b43569031e238f2b4ac92d9fa25a2822e3d_633",
"timestamp": "1778962419",
"receiver": "0x115f48dc2a731aa16251c6d6e1befc42f92accc9",
"amount": "318480",
"transaction_hash": "0x1e20bc6a2107b61fa179b3ab17047b43569031e238f2b4ac92d9fa25a2822e3d"
}
],
"rows_returned": 1,
"has_more": true,
"offset": 0,
"limit": 1,
"elapsed_ms": 3
}
```
When fetching `GET /v3/fees/{receiver}`, the `receiver` field is omitted from each row (it's redundant — every row matches the path parameter).
## Response fields
| Field | Type | Description |
| ------------------ | ------ | --------------------------- |
| `id` | string | Unique fee event ID |
| `timestamp` | string | Unix timestamp |
| `receiver` | string | Fee receiver address |
| `amount` | string | Fee amount (6-decimal USDC) |
| `transaction_hash` | string | On-chain transaction hash |
# Global Positions
Source: https://docs.polynode.dev/data/global/positions
GET /v3/positions
Browse the latest positions across all Polymarket wallets. Filter by market, status, or size.
Returns the most recent position changes across all wallets, enriched with market metadata and current prices. Defaults to most recent positions first.
## Request
```
GET /v3/positions
```
### Query parameters
| Parameter | Type | Default | Description |
| -------------- | ------- | ------- | --------------------------------------------------------------- |
| `status` | string | -- | Filter: `open`, `closed`, `redeemable`, `redeemed` |
| `sort` | string | recent | Sort by: `pnl`, `size`, `volume`. Default is most recent first. |
| `order` | string | `desc` | `asc` or `desc` |
| `token_id` | string | -- | Filter by outcome token ID |
| `condition_id` | string | -- | Filter by market condition ID |
| `market_slug` | string | -- | Filter by market slug |
| `min_size` | number | -- | Minimum position size in USD |
| `limit` | integer | 100 | Max 300 |
| `offset` | integer | 0 | Pagination offset |
### Position statuses
Global positions use the same lifecycle statuses as wallet positions:
| Status | Meaning |
| ------------ | --------------------------------------------------------------- |
| `open` | Wallet holds shares in an active (unresolved) market |
| `closed` | Position fully sold before market resolved |
| `redeemable` | Market resolved, wallet still holds shares that can be redeemed |
| `redeemed` | Market resolved and shares have been redeemed |
## Examples
### Latest position changes
```bash theme={null}
curl https://api.polynode.dev/v3/positions?limit=5
```
### Biggest open positions
```bash theme={null}
curl https://api.polynode.dev/v3/positions?sort=size&status=open&limit=5
```
### Who holds positions in a specific market
```bash theme={null}
curl "https://api.polynode.dev/v3/positions?condition_id=0xdd22472e...&sort=pnl&limit=10"
```
### Unclaimed redeemable positions
```bash theme={null}
curl "https://api.polynode.dev/v3/positions?status=redeemable&min_size=100&sort=size&limit=10"
```
## Response fields (per row in `positions`)
This endpoint returns a leaner field set than [`/v3/wallets/{addr}/positions`](/data/wallets/positions). For full enrichment (`last_trade_at`, `tag_slugs`, `event_slug`, `market_status`, `opposite_asset`, `price_source`, `redeemable_payout`, `won`, etc.), use the per-wallet endpoint.
| Field | Type | Nullable | Description |
| ---------------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------- |
| `wallet` | string | no | Wallet address |
| `token_id` | string | no | Outcome token ID (decimal-string-encoded — see [Identifiers](/data/overview#identifiers-and-large-numbers)) |
| `size` | number | no | Current shares held (USD) |
| `avg_price` | number | no | Weighted-average entry price (USD per outcome token) |
| `realized_pnl` | number | no | Realized P\&L (USD) |
| `unrealized_pnl` | number | no | Unrealized P\&L (USD) — `(current_price - avg_price) * size` |
| `total_pnl` | number | no | `realized_pnl + unrealized_pnl` (USD) |
| `current_price` | number | no | Latest token price (USD). Falls back to `0` if no price is known yet. |
| `total_bought` | number | no | Total USD spent buying this position |
| `resolved` | boolean | no | Whether the market has settled |
| `status` | string | no | `open`, `closed`, `redeemable`, or `redeemed` |
| `market` | string | yes | Market question (null if the token's metadata hasn't been backfilled) |
| `slug` | string | yes | Market URL slug |
| `outcome` | string | yes | Outcome label |
| `outcome_index` | integer | yes | Outcome position (0 or 1) |
| `image` | string | yes | Market image URL |
| `condition_id` | string | yes | Market condition ID |
| `neg_risk` | boolean | yes | Neg-risk (multi-outcome) market flag |
# Market Resolutions
Source: https://docs.polynode.dev/data/global/resolutions
GET /v3/resolutions
Browse recent market resolutions with payout data and market metadata.
Returns market resolution events, sorted by most recent. Each resolution includes the payout structure and enriched market metadata.
## Request
```
GET /v3/resolutions
```
### Query parameters
| Parameter | Type | Default | Description |
| --------- | ------- | ------- | ----------------- |
| `limit` | integer | 100 | Max 300 |
| `offset` | integer | 0 | Pagination offset |
## Example
```bash theme={null}
curl https://api.polynode.dev/v3/resolutions?limit=1
```
```json theme={null}
{
"data": [
{
"category": null,
"condition_id": "0xbd7f5df4a14a1b0ec35354ed910acf8c538d5e556c8389fcb58bb6a45c32e5b6",
"created_at": "2026-05-15 22:16:02.633047",
"outcomes": [
"Over",
"Under"
],
"payout_denominator": "1",
"payout_numerators": [
"0",
"1"
],
"question": "Chicago Cubs vs. Chicago White Sox: O/U 11.5",
"resolved_at": "1778883350000",
"slug": "mlb-chc-cws-2026-05-15-total-11pt5"
}
],
"elapsed_ms": 6,
"has_more": true,
"limit": 1,
"offset": 0,
"rows_returned": 1
}
```
## Response fields
| Field | Type | Description |
| -------------------- | ------ | --------------------------------------------------------------- |
| `condition_id` | string | Market condition ID |
| `payout_numerators` | array | Payout for each outcome (e.g. `["0", "1"]` means outcome 1 won) |
| `payout_denominator` | string | Payout denominator (usually `"1"`) |
| `resolved_at` | string | Resolution timestamp (milliseconds) |
| `created_at` | string | When the resolution was recorded |
| `question` | string | Market question |
| `slug` | string | Market slug |
| `outcomes` | array | Outcome labels (e.g. `["Yes", "No"]`) |
| `category` | string | Market category |
# Platform Stats
Source: https://docs.polynode.dev/data/global/stats
GET /v3/stats
Global aggregates for the entire Polymarket database. Total fills, wallets, volume, and P&L.
Returns platform-wide aggregates from the `global_stats` materialized view. Instant response.
## Request
```
GET /v3/stats
```
## Example
```bash theme={null}
curl https://api.polynode.dev/v3/stats
```
```json theme={null}
{
"total_fills": 1227599616,
"total_redemptions": 160535696,
"total_wallets": 2676595,
"total_positions": "227857354",
"total_realized_pnl": "2240414321986960",
"total_gross_profit": "13881389085858405",
"total_gross_loss": "-11640974763871445",
"total_volume": "2149019620148621138",
"elapsed_ms": 0
}
```
## Related global endpoints
| Endpoint | Description |
| ------------------------- | ------------------------------------------------------------------- |
| `GET /v3/trades` | Global trade feed. `?after=`, `?before=`, `?min_amount=`, `?order=` |
| `GET /v3/fees` | Fee events. `?after=`, `?before=` |
| `GET /v3/fees/{receiver}` | Fees by receiver address |
| `GET /v3/resolutions` | Recent market resolutions with metadata |
| `GET /v3/conditions/{id}` | Condition with payouts and market metadata |
# Global Trades
Source: https://docs.polynode.dev/data/global/trades
GET /v3/trades
Browse the full Polymarket trade history with market enrichment, computed prices, and order hashes.
Returns all Polymarket fills across all wallets, enriched with market metadata. Supports time range filtering, minimum amount, and token filtering.
## Request
```
GET /v3/trades
```
### Query parameters
| Parameter | Type | Default | Description |
| ------------ | ------- | ------- | --------------------------------------------- |
| `after` | integer | -- | Start of time range (Unix timestamp) |
| `before` | integer | -- | End of time range (Unix timestamp) |
| `token_id` | string | -- | Filter by outcome token |
| `builder` | string | -- | Filter by builder code (hex) |
| `min_amount` | integer | -- | Minimum maker\_amount\_filled (raw 6-decimal) |
| `order` | string | `desc` | `asc` or `desc` |
| `limit` | integer | 100 | Max 300 |
| `offset` | integer | 0 | Pagination offset |
## Example
```bash theme={null}
curl https://api.polynode.dev/v3/trades?limit=1
```
```json theme={null}
{
"trades": [
{
"id": "0xd4064c6d77afc87390ae9c371bb87d7f4687ac8f2cb1e600ad695928397c1dce_1483",
"maker": "0x84cfffc3f16dcc353094de30d4a45226eccd2f63",
"taker": "0xe2222d279d744050d28e00520010520000310f59",
"maker_asset_id": "0",
"taker_asset_id": "9593514921851392841100196009218771406639098519115981476479629056691973100726",
"maker_amount": 49.301,
"taker_amount": 70.43,
"fee": 0.4437,
"price": 0.7,
"size": 70.43,
"timestamp": "1778674687",
"transaction_hash": "0xd4064c6d77afc87390ae9c371bb87d7f4687ac8f2cb1e600ad695928397c1dce",
"order_hash": "0xad6a2b6ebf9537d72374fe77a41a7fc285906827a1adddda28948d37c09a6640",
"builder": "0x0000000000000000000000000000000000000000000000000000000000000000",
"side": 0,
"role": "maker",
"direction": "BUY",
"market": "Will Racing Club de Lens win on 2026-05-13?",
"slug": "fl1-rcl-psg-2026-05-13-rcl",
"outcome": "No",
"outcome_index": 1,
"image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/league-fl1.png",
"condition_id": "0x8e4ef7519b5dc20cebc4c19a07abae587e4abf889e415169ec6b1477d796e1b5"
}
],
"rows_returned": 1,
"has_more": true,
"offset": 0,
"limit": 1,
"elapsed_ms": 2
}
```
## Response fields
Each trade contains the same fields as [Wallet Trades](/data/wallets/trades), including market enrichment (market, slug, outcome, image, condition\_id), computed price and size in USD, order\_hash, and direction.
On global trades (no wallet context), `role` is always `"maker"` and `direction` reflects the maker's action. Use the [Wallet Trades](/data/wallets/trades) endpoint for wallet-specific perspective.
# Leaderboard
Source: https://docs.polynode.dev/data/leaderboard/global
GET /v3/leaderboard
Rank all Polymarket wallets by P&L, volume, profit, or win count. Supports one primary market filter dimension per request. All amounts in USD.
Ranks wallets by realized P\&L, total P\&L (realized + unrealized), gross profit, volume, or win count. All monetary values are in USD.
Leaderboard filters are designed around one primary dimension per request. Use one of `category`, `tags`/`tag_slug`, `market`/`market_slug`, `event_slug`, or `condition_id`. For compound analysis, run separate leaderboard calls or filter client-side.
## Request
```
GET /v3/leaderboard
```
### Query parameters
| Parameter | Type | Default | Description |
| -------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `sort` | string | `total` | Sort by: `total`, `total_pnl`, `realized`, `profit`, `loss`, `volume`, `wins`, `positions`, `unrealized` |
| `category` | string | -- | Filter to one market category, case-insensitive |
| `tags` | string | -- | Filter to one tag slug. Comma-separated multi-tag filters may be accepted for specific cases, but one tag is the recommended leaderboard shape. |
| `tag_slug` | string | -- | Alias for a single `tags` value |
| `market` | string | -- | Filter by condition ID or market slug |
| `market_slug` | string | -- | Filter by market slug |
| `event_slug` | string | -- | Filter by parent event slug |
| `condition_id` | string | -- | Filter by market condition ID |
| `limit` | integer | 100 | Max 300 |
| `offset` | integer | 0 | Pagination offset |
## Example
```bash theme={null}
curl https://api.polynode.dev/v3/leaderboard?limit=2&sort=total
```
### Filter by one dimension
```bash theme={null}
curl "https://api.polynode.dev/v3/leaderboard?category=crypto&limit=20"
curl "https://api.polynode.dev/v3/leaderboard?tag_slug=us-election&limit=20"
curl "https://api.polynode.dev/v3/leaderboard?market_slug=will-donald-trump-win-the-popular-vote-in-the-2024-presidential-election&limit=20"
curl "https://api.polynode.dev/v3/leaderboard?event_slug=presidential-election-popular-vote-winner-2024&limit=20"
```
```json theme={null}
{
"leaderboard": [
{
"rank": 1,
"wallet": "0x56687bf447db6ffa42ffe2204a05edaa20f55839",
"net_realized_pnl": 22053845.825455,
"gross_profit": 22057977.181649,
"gross_loss": -4131.356194,
"unrealized_pnl": 0.000688755,
"total_pnl": 22053845.826143753,
"wins": 18,
"losses": 4,
"position_count": 22,
"open_positions": 1,
"total_volume": 43013258.515682
}
],
"rows_returned": 1,
"has_more": true,
"offset": 0,
"limit": 2,
"elapsed_ms": 1028
}
```
## Response fields
| Field | Type | Description |
| ------------------ | ------- | ----------------------------------------- |
| `rank` | integer | Position in the leaderboard |
| `wallet` | string | Wallet address |
| `net_realized_pnl` | number | Net realized P\&L (USD) |
| `gross_profit` | number | Sum of winning positions (USD) |
| `gross_loss` | number | Sum of losing positions (USD, negative) |
| `unrealized_pnl` | number | Paper P\&L from open positions (USD) |
| `total_pnl` | number | `net_realized_pnl + unrealized_pnl` (USD) |
| `wins` | integer | Winning position count |
| `losses` | integer | Losing position count |
| `position_count` | integer | Total positions |
| `open_positions` | integer | Currently held positions |
| `total_volume` | number | Total volume traded (USD) |
# Tag Leaderboard
Source: https://docs.polynode.dev/data/leaderboard/tag-leaderboard
GET /v3/tags/{slug}/leaderboard
Rank wallets by P&L within a specific market category (tag). Works for niche tags.
Returns a P\&L leaderboard filtered to markets with a specific tag. This direct tag route is best for smaller/niche tags.
For broad tags or categories such as `crypto`, `sports`, or `politics`, use the projected global leaderboard with `?tags=` or `?category=`. It is the preferred route for high-cardinality leaderboard filters.
## Request
```
GET /v3/tags/{slug}/leaderboard
```
### Query parameters
| Parameter | Type | Default | Description |
| --------- | ------- | --------- | ---------------------------------------------------------- |
| `sort` | string | `net_pnl` | Sort by `net_pnl` (default), `volume`, `profit`, or `loss` |
| `limit` | integer | 100 | Max 300 |
| `offset` | integer | 0 | Pagination offset |
### Slug handling
The tag slug is matched case-insensitively. URL-style hyphens are also converted to spaces, so `/v3/tags/us-election/leaderboard` resolves to the stored tag `US Election`.
### Broad tag alternative
```bash theme={null}
curl "https://api.polynode.dev/v3/leaderboard?tags=crypto&limit=20"
curl "https://api.polynode.dev/v3/leaderboard?category=crypto&limit=20"
```
Use [`GET /v3/leaderboard`](/data/leaderboard/global) for broad tag/category rankings or pick a more specific tag for this route.
## Example
```bash theme={null}
curl https://api.polynode.dev/v3/tags/taylor-swift/leaderboard?limit=1
```
```json theme={null}
{
"data": [
{
"wallet": "0x4aec3e3c555ecbeec58adba532e8ea8b1c76e9b6",
"net_pnl": "60212.914000000000",
"gross_profit": "60212.914000000000",
"gross_loss": "0.000000000000000000",
"total_volume": "62721.000000000000",
"position_count": 3
}
],
"rows_returned": 1,
"has_more": true,
"offset": 0,
"limit": 1,
"elapsed_ms": 26
}
```
## Response fields (per row in `data`)
| Field | Type | Description |
| ---------------- | ---------------- | ----------------------------------------------- |
| `wallet` | string | Wallet address |
| `net_pnl` | string (decimal) | Net realized P\&L in this tag (USD) |
| `gross_profit` | string (decimal) | Sum of winning positions (USD) |
| `gross_loss` | string (decimal) | Sum of losing positions (USD, negative or zero) |
| `total_volume` | string (decimal) | Total volume traded in this tag (USD) |
| `position_count` | integer | Number of positions in markets with this tag |
# Tag Markets
Source: https://docs.polynode.dev/data/leaderboard/tag-markets
GET /v3/tags/{slug}
List markets that belong to a specific tag (category), ordered by all-time volume or other metrics.
Returns every market tagged with the given slug. Useful for building category landing pages or filtering search results.
## Request
```
GET /v3/tags/{slug}
```
### Path parameters
| Parameter | Type | Description |
| --------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `slug` | string | Tag slug. Matched case-insensitively. URL-style hyphens are converted to spaces, so `us-election` resolves to the stored tag `US Election`. |
### Query parameters
| Parameter | Type | Default | Description |
| --------- | ------- | ----------------- | -------------------------------------------------------- |
| `sort` | string | `volume_all_time` | Sort by `volume_all_time`, `liquidity`, or `created_at`. |
| `limit` | integer | 100 | Max 300 |
| `offset` | integer | 0 | Pagination offset |
## Example
```bash theme={null}
curl "https://api.polynode.dev/v3/tags/ufc?limit=2&key=YOUR_KEY"
```
```json theme={null}
{
"data": [
{
"condition_id": "0x61ef853c0ecc72a583da722cf6434681f9b92d10652cded44d8f889beb923418",
"question": "Dvalishvili vs. Sandhagen",
"slug": "ufc-dvalishvili-vs-sandhagen-2025-10-04",
"outcomes": ["Dvalishvili", "Sandhagen"],
"category": null,
"active": true,
"closed": true,
"neg_risk": false,
"volume_all_time": "999731.6550259999930858612060546875",
"liquidity": "0",
"end_date": "2025-10-11 18:00:00+00",
"event_slug": "ufc-320-ankalaev-vs-pereira-2",
"event_title": "UFC 320: Ankalaev vs. Pereira 2",
"winning_outcome_index": 0
}
],
"rows_returned": 1,
"has_more": true,
"offset": 0,
"limit": 1,
"elapsed_ms": 42
}
```
## Response fields (per row in `data`)
| Field | Type | Description |
| ----------------------- | ---------------- | ------------------------------------------------------ |
| `condition_id` | string | Market condition ID |
| `question` | string | Market question text |
| `slug` | string | Market URL slug |
| `outcomes` | array\ | Outcome labels |
| `category` | string \| null | Optional category label |
| `active` | boolean | Market is currently active |
| `closed` | boolean | Market is closed (no longer accepting orders) |
| `neg_risk` | boolean | Neg-risk (multi-outcome) market |
| `volume_all_time` | string (decimal) | All-time USD volume |
| `liquidity` | string (decimal) | Current liquidity in USD |
| `end_date` | string \| null | Market resolution deadline (ISO timestamp with offset) |
| `event_slug` | string \| null | Parent event slug |
| `event_title` | string \| null | Parent event title |
| `winning_outcome_index` | integer \| null | Winning outcome (null if unresolved) |
## Errors
If no tag matches the slug (case-insensitive, hyphen-converted), the endpoint returns `404` with:
```json theme={null}
{"error": "tag not found", "tag": "notarealtag"}
```
For a wallet-level P\&L leaderboard scoped to a tag, see [Tag Leaderboard](/data/leaderboard/tag-leaderboard).
# Tags
Source: https://docs.polynode.dev/data/leaderboard/tags
GET /v3/tags
List all market tags with the number of markets in each category.
Returns all market tag categories ranked by market count.
## Request
```
GET /v3/tags
```
### Query parameters
| Parameter | Type | Default | Description |
| --------- | ------- | ------- | ----------------- |
| `limit` | integer | 100 | Max 300 |
| `offset` | integer | 0 | Pagination offset |
## Example
```bash theme={null}
curl "https://api.polynode.dev/v3/tags?limit=3&key=YOUR_KEY"
```
```json theme={null}
{
"tags": [
{ "tag": "Sports", "market_count": 575318 },
{ "tag": "Games", "market_count": 522716 },
{ "tag": "Hide From New", "market_count": 447811 }
],
"rows_returned": 3,
"has_more": true,
"elapsed_ms": 1241
}
```
## Response fields
| Field | Type | Description |
| -------------- | ------- | ------------------------------------------------------------------------------------------------------------------------- |
| `tag` | string | Canonical tag label as stored in the database. Title Case, spaces preserved (e.g. `"US Election"`, `"Trump Presidency"`). |
| `market_count` | integer | Number of markets with this tag |
## Using tag values in URLs
The `tag` value above is the canonical (Title Case) form. When passing a tag in a URL path (e.g. [`/v3/tags/{slug}`](/data/leaderboard/tag-markets) or [`/v3/tags/{slug}/leaderboard`](/data/leaderboard/tag-leaderboard)), use a lowercase, hyphen-separated form — the API resolves it back to the canonical tag:
| URL form | Resolves to |
| ------------------- | ------------------- |
| `sports` | `Sports` |
| `us-election` | `US Election` |
| `primary-elections` | `primary elections` |
The match is case-insensitive and converts hyphens to spaces. If no tag matches, you get `404 {"error":"tag not found","tag":...}`.
# Market by Condition
Source: https://docs.polynode.dev/data/markets/by-condition
GET /v3/markets/condition/{condition_id}
Get market metadata, token IDs, and outcomes by condition ID.
Returns full market metadata for a condition, including all token IDs and their outcome labels.
## Request
```
GET /v3/markets/condition/{condition_id}
```
### Path parameters
| Parameter | Type | Description |
| -------------- | ------ | --------------------------------- |
| `condition_id` | string | Market condition ID (0x-prefixed) |
## Example
```bash theme={null}
curl "https://api.polynode.dev/v3/markets/condition/0x6c4d221b3cf2c8d17467c70a8aa3c714e30299b6e57cd3e4269dc8a41d2b0cd8?key=YOUR_KEY"
```
```json theme={null}
{
"data": [
{
"condition_id": "0x6c4d221b3cf2c8d17467c70a8aa3c714e30299b6e57cd3e4269dc8a41d2b0cd8",
"question": "Will Trump pardon Joe Exotic \"The Tiger King\" in 2025?",
"slug": "will-trump-pardon-joe-exotic-the-tiger-king-in-2025-784-928-365",
"outcomes": ["Yes", "No"],
"category": null,
"active": true,
"closed": true,
"neg_risk": false,
"volume_all_time": "99993.937724000003072433173656463623046875",
"liquidity": "0",
"end_date": "2025-12-31 12:00:00+00",
"event_slug": "who-will-trump-pardon-in-2025",
"event_title": "Who will Trump pardon in 2025?",
"tag_slugs": ["Politics", "Trump", "SBF", "cz", "Trump Presidency", "Ghislaine Maxwell"],
"token_ids": [
"30698649476690694814108493145041085718609914990967907518412053225237708492787",
"43362318061877762261164265810725209343672429810065542703856317173848260953021"
],
"token_outcomes": ["Yes", "No"],
"winning_outcome_index": 1
}
],
"rows_returned": 1,
"has_more": false,
"offset": 0,
"limit": 1,
"elapsed_ms": 3
}
```
The response is wrapped in the standard `data: [{...}]` envelope. If no condition matches, `data` is `[]` and `rows_returned` is `0`.
`volume_all_time` and `liquidity` are full-precision decimal strings — round on the client.
`token_ids` are decimal strings, not numbers — they are 78-digit values and lose precision if parsed as floats.
## Response fields (per row in `data`)
| Field | Type | Nullable | Description |
| ----------------------- | ---------------- | -------- | -------------------------------------------------------------- |
| `condition_id` | string | no | Market condition ID |
| `question` | string | no | Market question |
| `slug` | string | no | URL slug |
| `outcomes` | array\ | no | Outcome labels |
| `token_ids` | array\ | no | Outcome token IDs (decimal-string-encoded) |
| `token_outcomes` | array\ | no | Outcome label for each token, in the same order as `token_ids` |
| `active` | boolean | no | Currently active |
| `closed` | boolean | no | No longer accepting orders |
| `neg_risk` | boolean | no | Multi-outcome (neg-risk) market |
| `volume_all_time` | string (decimal) | no | Full-precision volume in USD |
| `liquidity` | string (decimal) | no | Full-precision current liquidity in USD |
| `end_date` | string | yes | Market end date (Postgres timestamp with offset) |
| `event_slug` | string | yes | Parent event slug |
| `event_title` | string | yes | Parent event title |
| `category` | string | yes | Optional category label |
| `tag_slugs` | array\ | yes | Category tags in their canonical (Title Case) form |
| `winning_outcome_index` | integer | yes | Which outcome won (null if unresolved) |
# Market by Slug
Source: https://docs.polynode.dev/data/markets/by-slug
GET /v3/markets/slug/{slug}/trades
Get trades and positions for a market by its URL slug.
Look up a market by its slug (the URL path on Polymarket) and get trades or positions across all outcomes.
## Trades by slug
```
GET /v3/markets/slug/{slug}/trades
```
### Query parameters
| Parameter | Type | Default | Description |
| --------- | ------- | ------- | ------------------------------------ |
| `after` | integer | -- | Start of time range (Unix timestamp) |
| `before` | integer | -- | End of time range |
| `limit` | integer | 100 | Max 300 |
| `offset` | integer | 0 | Pagination offset |
### Example
```bash theme={null}
curl "https://api.polynode.dev/v3/markets/slug/dota2-aur1-liquid-2026-05-13/trades?limit=1"
```
Returns the same enriched trade format as [Wallet Trades](/data/wallets/trades).
## Positions by slug
```
GET /v3/markets/slug/{slug}/positions
```
Returns every wallet holding (or that has ever held) any outcome of this market, across V1 + V2, with realized P\&L computed from the V2 adapter pipeline.
### Query parameters
| Parameter | Type | Default | Description |
| --------- | ------- | -------------- | ------------------------------------------------------------------------------------------ |
| `limit` | integer | 100 | Max 300 |
| `offset` | integer | 0 | Pagination offset |
| `sort` | string | `realized_pnl` | One of `amount`, `volume`, `realized_pnl`, `pnl` |
| `status` | string | -- | `open` (current holders only) or `closed` (zeroed-out positions only). Omit to return all. |
For this market-scoped endpoint, `status` is a holder-state filter on the current raw balance. `open` means `amount > 0`; `closed` means `amount = 0`. It is not the wallet/global lifecycle status split, where resolved nonzero positions are `redeemable` and resolved zero-balance positions are `redeemed`.
### Example
```bash theme={null}
curl "https://api.polynode.dev/v3/markets/slug/dota2-aur1-liquid-2026-05-13/positions?limit=1"
```
```json theme={null}
{
"data": [
{
"user": "0xca70278943df14029a0f819f9baf060f67860908",
"token_id": "2241710011255999173993483096156975147842635212627942653724495466212043852479",
"amount": "0",
"avg_price": "500000",
"total_bought": "2000000",
"realized_pnl": "998000"
}
],
"rows_returned": 1,
"has_more": true,
"offset": 0,
"limit": 1,
"elapsed_ms": 179
}
```
### Response fields
| Field | Type | Description |
| -------------- | ------ | ------------------------------------------------------------------------- |
| `user` | string | Wallet address (lowercased) |
| `token_id` | string | Outcome token ID this row is about |
| `amount` | string | Current balance in raw 6-decimal USDC (`0` if fully exited) |
| `avg_price` | string | Time-weighted average buy price in raw 6-decimal USDC (`500000` = \$0.50) |
| `total_bought` | string | Cumulative cost-basis of all purchases in raw 6-decimal USDC |
| `realized_pnl` | string | Realized P\&L on closed-out portion of the position in raw 6-decimal USDC |
# Market Candles
Source: https://docs.polynode.dev/data/markets/candles
Trade-built OHLCV candles for V3 Polymarket outcome tokens, condition IDs, and market slugs.
Build OHLCV candles from V3 Polymarket trade fills. Candles are bucketed by trade timestamp and include price, volume, VWAP, trade count, and first/last fill markers for each bucket.
Use this endpoint for V3 charting when you already have an outcome token ID, condition ID, or market slug.
## Endpoints
```http theme={null}
GET /v3/markets/{token_id}/candles
GET /v3/markets/condition/{condition_id}/candles
GET /v3/markets/slug/{slug}/candles
```
The token route returns one candle series for one outcome token. The condition and slug routes resolve the market's outcome tokens and return a `series` array, one entry per outcome token. Pass `token_id` on the condition or slug route to return only one outcome.
## Authentication
```bash theme={null}
curl "https://api.polynode.dev/v3/markets/{token_id}/candles?resolution=5m&limit=120" \
-H "x-api-key: $POLYNODE_API_KEY"
```
## Query parameters
| Parameter | Type | Default | Description |
| ------------ | ------: | -------: | ------------------------------------------------------------------------------------------------------------- |
| `resolution` | string | `1h` | Bucket size. One of `1m`, `5m`, `15m`, `1h`, `4h`, `1d`. |
| `limit` | integer | `120` | Maximum number of candle buckets in the requested window. Clamped by resolution. |
| `direction` | string | `before` | `before` returns the window ending at `anchor_ts` or now. `after` returns the window starting at `anchor_ts`. |
| `anchor_ts` | integer | now | Unix timestamp in seconds. Required when `direction=after`. |
| `start` | integer | -- | Explicit window start timestamp in seconds. Alias: `start_time`, `after`. |
| `end` | integer | -- | Explicit window end timestamp in seconds. Alias: `end_time`, `before`. |
| `order` | string | `asc` | Candle order. Use `desc` to return newest bucket first. |
| `gap_fill` | boolean | `false` | When true, empty buckets are filled with flat carry-forward candles after a prior price exists. |
| `token_id` | string | -- | Condition/slug routes only. Filters the resolved market to one outcome token. |
Timestamp parameters are Unix seconds, not milliseconds. If you pass an explicit range, the range must fit inside the resolution's bucket limit.
## Bucket limits
`limit` is a candle-bucket count, not a trade count.
| Resolution | Max candles per request |
| ---------- | ----------------------: |
| `1m` | 120 |
| `5m` | 180 |
| `15m` | 240 |
| `1h` | 300 |
| `4h` | 360 |
| `1d` | 500 |
For more history, walk backward with `pagination.older_end_ts`:
```bash theme={null}
# First page: latest 120 one-minute buckets
curl "https://api.polynode.dev/v3/markets/$TOKEN_ID/candles?resolution=1m&limit=120" \
-H "x-api-key: $POLYNODE_API_KEY"
# Older page: use pagination.older_end_ts from the previous response
curl "https://api.polynode.dev/v3/markets/$TOKEN_ID/candles?resolution=1m&limit=120&anchor_ts=$OLDER_END_TS" \
-H "x-api-key: $POLYNODE_API_KEY"
```
## Token route response
```json theme={null}
{
"token_id": "7936379438884929999943146941289093559746194477224187754527972073168717543990",
"resolution": "1m",
"count": 1,
"candles": [
{
"time": 1781659920,
"time_unix": 1781659920,
"time_ms": 1781659920000,
"open": 0.525,
"high": 0.535,
"low": 0.515,
"close": 0.53,
"volume": 233.135659,
"volume_usd": 233.135659,
"volume_shares": 444.019955,
"volume_buy": 121.504,
"volume_sell": 111.631659,
"trades": 13,
"vwap": 0.52504954,
"first_trade_ts": 1781659922,
"last_trade_ts": 1781659978,
"first_block": 88620000,
"last_block": 88620030,
"first_log_index": 320,
"last_log_index": 881,
"gap_filled": false
}
],
"question": "Spread: Argentina (-1.5)",
"slug": "fifwc-arg-alg-2026-06-16-spread-home-1pt5",
"outcome": "Argentina",
"image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/soccer ball-bba4025f77.png",
"condition_id": "0x1eb724a708f3720c2b7ef63d7ffc7483ecc14deed6df2e7e9dc1dfa724a98bd6",
"outcome_index": 0,
"window": {
"start_ts": 1781652840,
"end_ts": 1781659981,
"first_bucket": 1781652840,
"last_bucket": 1781659980,
"duration_seconds": 7142,
"bucket_count": 120,
"resolution_seconds": 60,
"older_end_ts": 1781652839,
"newer_start_ts": 1781660040,
"has_older": true,
"has_newer": true
},
"pagination": {
"limit": 120,
"max_limit": 120,
"direction": "before",
"order": "asc",
"older_end_ts": 1781652839,
"newer_start_ts": 1781660040,
"has_more": true,
"has_older": true,
"has_newer": true
},
"gap_fill": false,
"elapsed_ms": 27
}
```
Sparse markets can return `count: 0` and `candles: []` for a window with no trades. That is a valid empty chart window, not a missing market.
## Condition and slug responses
```bash theme={null}
curl "https://api.polynode.dev/v3/markets/condition/$CONDITION_ID/candles?resolution=5m&limit=2" \
-H "x-api-key: $POLYNODE_API_KEY"
```
```json theme={null}
{
"condition_id": "0x1eb724a708f3720c2b7ef63d7ffc7483ecc14deed6df2e7e9dc1dfa724a98bd6",
"resolution": "5m",
"series_count": 2,
"series": [
{
"token_id": "7936379438884929999943146941289093559746194477224187754527972073168717543990",
"resolution": "5m",
"count": 0,
"candles": [],
"question": "Spread: Argentina (-1.5)",
"slug": "fifwc-arg-alg-2026-06-16-spread-home-1pt5",
"outcome": "Argentina",
"image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/soccer ball-bba4025f77.png",
"condition_id": "0x1eb724a708f3720c2b7ef63d7ffc7483ecc14deed6df2e7e9dc1dfa724a98bd6",
"outcome_index": 0
}
],
"window": {
"start_ts": 1781668800,
"end_ts": 1781669399,
"first_bucket": 1781668800,
"last_bucket": 1781669100,
"duration_seconds": 600,
"bucket_count": 2,
"resolution_seconds": 300,
"older_end_ts": 1781668799,
"newer_start_ts": 1781669400,
"has_older": true,
"has_newer": true
},
"pagination": {
"limit": 2,
"max_limit": 180,
"direction": "before",
"order": "asc",
"older_end_ts": 1781668799,
"newer_start_ts": 1781669400,
"has_more": true,
"has_older": true,
"has_newer": true
},
"gap_fill": false,
"elapsed_ms": 205
}
```
For a single outcome from a condition or slug:
```bash theme={null}
curl "https://api.polynode.dev/v3/markets/condition/$CONDITION_ID/candles?token_id=$TOKEN_ID&resolution=15m&limit=96" \
-H "x-api-key: $POLYNODE_API_KEY"
```
If a condition or slug resolves to more than 50 outcome tokens, pass `token_id` to request one outcome series.
## Candle fields
| Field | Type | Description |
| ----------------- | --------------- | --------------------------------------------------------------------- |
| `time` | integer | Candle bucket start time in Unix seconds. |
| `time_unix` | integer | Same as `time`. |
| `time_ms` | integer | Candle bucket start time in Unix milliseconds for charting libraries. |
| `open` | number | First trade price in the bucket. |
| `high` | number | Highest trade price in the bucket. |
| `low` | number | Lowest trade price in the bucket. |
| `close` | number | Last trade price in the bucket. |
| `volume` | number | Total USD volume in the bucket. Alias of `volume_usd`. |
| `volume_usd` | number | Total USD volume in the bucket. |
| `volume_shares` | number | Total outcome-share volume in the bucket. |
| `volume_buy` | number | USD volume where the fill bought this outcome token. |
| `volume_sell` | number | USD volume where the fill sold this outcome token. |
| `trades` | integer | Number of fills in the bucket. |
| `vwap` | number | Volume-weighted average price. |
| `first_trade_ts` | integer or null | Timestamp of the first fill in the bucket. |
| `last_trade_ts` | integer or null | Timestamp of the last fill in the bucket. |
| `first_block` | integer or null | Polygon block of the first fill in the bucket. |
| `last_block` | integer or null | Polygon block of the last fill in the bucket. |
| `first_log_index` | integer or null | Log index of the first fill in the bucket. |
| `last_log_index` | integer or null | Log index of the last fill in the bucket. |
| `gap_filled` | boolean | `true` only for carry-forward buckets created by `gap_fill=true`. |
## Errors and guards
| Status | Condition |
| -----: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400` | Invalid `resolution`, invalid `direction`, invalid `order`, millisecond timestamp, range larger than the resolution limit, or `token_id` that does not belong to a condition/slug route. |
| `404` | Unknown token, condition, or slug. |
Unknown token IDs return:
```json theme={null}
{
"error": "market token not found",
"token_id": "0",
"hint": "candles are available for Polymarket outcome token IDs, not collateral or arbitrary asset IDs"
}
```
# Market Positions (Condition)
Source: https://docs.polynode.dev/data/markets/condition-positions
GET /v3/markets/condition/{condition_id}/positions
Get wallet positions across every outcome token in a market condition.
Returns wallet-position rows for every outcome token in a market condition. Use `sort=amount` to fetch the largest current holders first across all outcomes in the condition.
## Request
```
GET /v3/markets/condition/{condition_id}/positions
```
### Path parameters
| Parameter | Type | Description |
| -------------- | ------ | -------------------------------------------------------------------------------------------------------------------------- |
| `condition_id` | string | Market condition ID (`0x` + 64 hex characters). The response includes positions for every outcome token in this condition. |
### Query parameters
| Parameter | Type | Default | Description |
| --------- | ------- | ------- | ----------------------------------------------------------------------------------------------------- |
| `sort` | string | `pnl` | Sort order. One of `pnl` (realized P\&L), `amount` (current shares held), or `volume` (total bought). |
| `limit` | integer | 100 | Rows per page. Requests are capped by API key tier: Starter `300`, Growth `1000`, Enterprise `2000`. |
| `offset` | integer | 0 | Number of rows to skip for pagination. |
If `limit` is above your tier cap, PolyNode normalizes it to the maximum allowed value instead of rejecting the request. Every response includes the applied `limit`, plus `rows_returned`, `offset`, and `has_more`.
## Sorting
Use `sort=amount` when you need the largest current holders for a condition. The sort is descending, so the first rows are the wallets with the highest current `amount` across the condition's outcome tokens.
## Example
```bash theme={null}
curl -H "x-api-key: $POLYNODE_API_KEY" \
"https://api.polynode.dev/v3/markets/condition/0x713641f745d71f6ec61f906237ffca3c8583f251e49384429a63ceb0ccdb2d37/positions?sort=amount&limit=1&offset=0"
```
### Example response
```json theme={null}
{
"data": [
{
"amount": "6660546928621",
"avg_price": null,
"realized_pnl": "0",
"token_id": "1770840559776249239623005379825945674336282130390798724203946923853499387834",
"total_bought": "0",
"user": "0xa5ef39c3d3e10d0b270233af41cac69796b12966"
}
],
"elapsed_ms": 3,
"has_more": true,
"limit": 1,
"offset": 0,
"rows_returned": 1
}
```
## Response columns
| Column | Type | Description |
| -------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `user` | string | Wallet address (lowercased) |
| `token_id` | string | Outcome token this row covers (each condition has multiple outcomes; a wallet can appear once per outcome it has touched) |
| `amount` | string | Current shares held in 6-decimal token units (`0` if the wallet has exited this outcome) |
| `avg_price` | string \| null | Average entry price in 6-decimal USDC units (`500000` = \$0.50). May be `null` when cost basis is not available. |
| `realized_pnl` | string | Realized P\&L on the closed portion of the position in 6-decimal USDC units |
| `total_bought` | string | Cumulative purchase cost basis in 6-decimal USDC units |
# Market Positions (Token)
Source: https://docs.polynode.dev/data/markets/positions
GET /v3/markets/{token_id}/positions
Get all wallets holding positions in a specific outcome token, sorted by P&L or size.
Returns all wallets that hold or have held positions in a specific outcome token.
## Request
```
GET /v3/markets/{token_id}/positions
```
### Query parameters
| Parameter | Type | Default | Description |
| --------- | ------- | ------- | ---------------------------------- |
| `sort` | string | `pnl` | Sort by: `pnl`, `amount`, `volume` |
| `limit` | integer | 100 | Max 300 |
| `offset` | integer | 0 | Pagination offset |
## Example
```bash theme={null}
curl https://api.polynode.dev/v3/markets/75783394880030392863380883800697645018418815910449662777195626260206142035810/positions?limit=1
```
```json theme={null}
{
"data": [
{
"amount": "0",
"avg_price": "490000",
"realized_pnl": "9999999",
"total_bought": "19607842",
"user": "0xa435d9eb84bf50f5d72d88023a77e8c7fd1d572d"
}
],
"elapsed_ms": 204,
"has_more": true,
"limit": 1,
"offset": 0,
"rows_returned": 1
}
```
## Response columns
| Column | Type | Description |
| -------------- | ------ | ---------------------------------------- |
| `user` | string | Wallet address |
| `amount` | string | Current shares held (6-decimal) |
| `avg_price` | string | Weighted-average entry price (6-decimal) |
| `realized_pnl` | string | Realized P\&L (6-decimal USDC) |
| `total_bought` | string | Total volume bought (6-decimal USDC) |
# Token Price
Source: https://docs.polynode.dev/data/markets/price
GET /v3/markets/{token_id}/price
Get the current price, resolution status, and price source for any outcome token.
Returns the latest price for an outcome token, including where the price was sourced from and whether the market has resolved.
## Request
```
GET /v3/markets/{token_id}/price
```
### Path parameters
| Parameter | Type | Description |
| ---------- | ------ | ---------------- |
| `token_id` | string | Outcome token ID |
## Example
```bash theme={null}
curl https://api.polynode.dev/v3/markets/75783394880030392863380883800697645018418815910449662777195626260206142035810/price
```
```json theme={null}
{
"token_id": "75783394880030392863380883800697645018418815910449662777195626260206142035810",
"price": 1.0,
"resolved": true,
"source": "settlement",
"updated_at": "2026-05-13 12:27:28.558218",
"elapsed_ms": 1
}
```
## Response fields
| Field | Type | Description |
| ------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `token_id` | string | Outcome token ID |
| `price` | number | Token price in USD. Range `0`–`1` for unresolved markets, exactly `0` or `1` for resolved. |
| `resolved` | boolean | Whether the market has settled |
| `source` | string | Price source: `settlement` (final payout), `clob_mid` (CLOB orderbook midpoint), `v1_condition` (V1 condition payout), `last_fill` (most recent trade price) |
| `updated_at` | string | When the price was last updated |
### Price priority
Prices follow this priority chain (higher sources are never overwritten by lower ones):
1. `settlement` -- final payout after market resolution (`0` or `1`)
2. `clob_mid` -- live CLOB orderbook midpoint (updated every 5 minutes)
3. `v1_condition` -- V1 condition payout data
4. `last_fill` -- price from the most recent trade
# Search Markets
Source: https://docs.polynode.dev/data/markets/search
GET /v3/markets/search
Full-text search across all Polymarket markets. Returns matching markets with metadata.
Search market questions by keyword. Returns matching markets with metadata, volume, and resolution status.
## Request
```
GET /v3/markets/search
```
### Query parameters
| Parameter | Type | Default | Description |
| --------- | ------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `q` | string | required | Search query. Case-insensitive substring match against the `question`, `slug`, and `event_title` fields. Multiple words are AND-matched. |
| `limit` | integer | 100 | Max 300 |
| `offset` | integer | 0 | Pagination offset |
### How matching works
* `?q=bitcoin` matches any market whose question, slug, or event title contains the substring `bitcoin` (any case).
* `?q=trump pardon` matches questions containing both substrings, in any order.
* Punctuation in the query is preserved (`?q=$72,000` matches the literal characters).
* Results are ordered by `volume_all_time DESC` so higher-traded markets surface first.
## Example
```bash theme={null}
curl "https://api.polynode.dev/v3/markets/search?q=bitcoin&limit=1"
```
```json theme={null}
{
"data": [
{
"active": true,
"category": null,
"closed": true,
"condition_id": "0x58c931c00e31fd20d02aade0c1e287e46a15b6da514c3ac3624e956e41bc45d0",
"question": "Will the price of Bitcoin be above $72,000 on February 13?",
"slug": "bitcoin-above-72k-on-february-13",
"tag_slugs": [
"Crypto",
"Bitcoin",
"Crypto Prices",
"Recurring",
"Hide From New",
"Weekly",
"Multi Strikes"
],
"volume_all_time": "999989.65550899994559586048126220703125",
"winning_outcome_index": 1
}
],
"elapsed_ms": 668,
"has_more": true,
"limit": 1,
"offset": 0,
"rows_returned": 1
}
```
## Response fields (per row in `data`)
| Field | Type | Nullable | Description |
| ----------------------- | ---------------- | -------- | -------------------------------------- |
| `condition_id` | string | no | Market condition ID |
| `question` | string | no | Market question text |
| `slug` | string | no | URL slug |
| `category` | string | yes | Optional category label |
| `volume_all_time` | string (decimal) | no | Total volume in USD at full precision |
| `active` | boolean | no | Currently active |
| `closed` | boolean | no | No longer accepting orders |
| `winning_outcome_index` | integer | yes | Which outcome won (null if unresolved) |
| `tag_slugs` | array\ | yes | Tags in canonical (Title Case) form |
# Market Trades
Source: https://docs.polynode.dev/data/markets/trades
GET /v3/markets/{token_id}/trades
Get all trades for a specific outcome token or market slug, with full enrichment.
Returns fills involving a specific outcome token, enriched with market metadata, computed prices, and order hashes. Can look up by token ID or market slug.
## By token ID
```
GET /v3/markets/{token_id}/trades
```
## By market slug
```
GET /v3/markets/slug/{slug}/trades
```
Resolves the slug to all token IDs for that market and returns trades across all outcomes.
### Query parameters
| Parameter | Type | Default | Description |
| --------- | ------- | ------- | ------------------------------------ |
| `after` | integer | -- | Start of time range (Unix timestamp) |
| `before` | integer | -- | End of time range |
| `limit` | integer | 100 | Max 300 |
| `offset` | integer | 0 | Pagination offset |
## Example (by token ID)
```bash theme={null}
curl https://api.polynode.dev/v3/markets/75783394880030392863380883800697645018418815910449662777195626260206142035810/trades?limit=1
```
```json theme={null}
{
"token_id": "75783394880030392863380883800697645018418815910449662777195626260206142035810",
"trades": [
{
"id": "0xc6708a8b57ab936d8bd50be18b0bb55b64d27b5224b7c48ce487fef16fa0de0a_1077",
"maker": "0x3248e74f1dafd9acb332fd6a511fe7e4600baad4",
"taker": "0xf8f3c0269b1bff87dba772666864737b168f12a9",
"maker_asset_id": "0",
"taker_asset_id": "75783394880030392863380883800697645018418815910449662777195626260206142035810",
"maker_amount": 31.130816,
"taker_amount": 61.040815,
"fee": 0.0,
"price": 0.51,
"size": 61.040815,
"timestamp": "1778674995",
"transaction_hash": "0xc6708a8b57ab936d8bd50be18b0bb55b64d27b5224b7c48ce487fef16fa0de0a",
"order_hash": "0xf91a35be0abe447ed8c7d5e4c7511db22e0707136f3850b6708487bf2e6d834c",
"builder": "0x0000000000000000000000000000000000000000000000000000000000000000",
"side": 0,
"role": "maker",
"direction": "BUY",
"market": "Dota 2: Aurora vs Team Liquid (BO3) - DreamLeague Group A",
"slug": "dota2-aur1-liquid-2026-05-13",
"outcome": "Aurora",
"outcome_index": 0,
"image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/dota2-7ffacddb21.jpg",
"condition_id": "0x4f05dbc6273b89aed46bb79a961c1d8771c01925d92d439e9a81fa6241900661"
}
],
"rows_returned": 1,
"has_more": true,
"elapsed_ms": 30
}
```
## Example (by slug)
```bash theme={null}
curl https://api.polynode.dev/v3/markets/slug/dota2-aur1-liquid-2026-05-13/trades?limit=5
```
## Response fields
Each trade contains the same fields as [Wallet Trades](/data/wallets/trades), including all market enrichment fields.
# API Overview
Source: https://docs.polynode.dev/data/overview
polynode API reference. Real-time streaming, historical data, P&L analytics, market data, and more.
The polynode API provides two main surfaces:
1. **V3 Historical Data** — query the complete Polymarket V1 + V2 historical dataset. 1.2 billion fills, 228 million positions, 2.7 million wallets. Dual P\&L (realized + unrealized), position tracking with settlement status, builder analytics, market search, and leaderboards.
2. **Real-time Streaming** — live WebSocket feeds for settlements, trades, price updates, position changes, and more. See the [WebSocket tab](/websocket/overview).
The V3 endpoints are **live** and provide the most comprehensive Polymarket dataset available.
## V3 Data API — Base URL
```
https://api.polynode.dev/v3
```
## Authentication
Every `/v3/*` endpoint requires an API key. Pass it either as a `key` query parameter or as a bearer token in the `Authorization` header:
```bash theme={null}
# Query string
curl "https://api.polynode.dev/v3/stats?key=pn_live_..."
# Bearer header
curl -H "Authorization: Bearer pn_live_..." https://api.polynode.dev/v3/stats
```
Get a key from the [dashboard](https://polynode.dev/dashboard). All examples in this section assume `?key=YOUR_KEY` is appended (omitted only for brevity in the code blocks).
## Dataset Scale
Query `GET /v3/stats` for live counts. Current scale:
| Metric | Count |
| --------------- | ------------ |
| Fills (trades) | 1+ billion |
| Positions | 200+ million |
| Unique wallets | 2.5+ million |
| Redemptions | 160+ million |
| Splits | 30+ million |
| Merges | 14+ million |
| Markets tracked | 1.07 million |
| Token prices | 2 million |
## Key features
* **Dual P\&L** — realized and unrealized profit/loss for every wallet, computed from the full event history using weighted-average cost basis math
* **Position status** — every position is tagged as `open`, `closed`, `redeemable`, or `redeemed` based on live settlement data
* **Redeemable payouts** — see exactly how much USDC a wallet can claim from resolved markets
* **Builder attribution** — 77 million fills attributed to 1,502 unique builders
* **Batch queries** — query up to 100 wallets in a single call
* **Time-range filters** — filter trades, redemptions, and activity by Unix timestamp
* **Market search** — full-text search across 1 million+ market questions
## Amounts and precision
Monetary fields come in two conventions depending on the endpoint:
**Decimal USD (JSON number).** Used by aggregated P\&L responses (wallet summary, batch, pnl, positions) and by token-price responses (`/v3/markets/{token_id}/price`, `/v3/tokens/{token_id}`):
```json theme={null}
{ "net_realized_pnl": 22053845.825455, "total_volume": 43013258.515682 }
{ "price": 0.42 }
```
`net_realized_pnl: 22053845.825455` means **$22,053,845.83 USD**. `price: 0.42` means **$0.42 per share** (Polymarket outcome prices are always in the range 0–1).
**Raw 6-decimal USDC (JSON string).** Used by event-level rows — fees, splits, merges, redemptions, NRC, activity, tag-leaderboard rollups, market-positions rows:
```json theme={null}
{ "amount": "120469388830" }
```
Divide by `1_000_000` to get USD. `"120469388830"` is **120,469.38883 USDC**.
**Full-precision decimal (JSON string).** Used by raw subgraph fields like `volume_all_time`, `liquidity`, `price`:
```json theme={null}
{ "volume_all_time": "99993.937724000003072433173656463623046875" }
```
These are stored at full Postgres `numeric` precision (up to \~50 digits). Round on the client; do not parse as a JS `Number`.
Each endpoint's response-fields table explicitly notes which convention applies.
## Identifiers and large numbers
Polymarket `token_id` and `position_id` values are 78-digit decimal integers (uint256). They are returned as **JSON strings** so they survive standard JSON parsers — IEEE 754 doubles cannot represent them. Treat them as opaque strings; do not parse them as numbers.
## Nullable fields
Many fields can be `null` in real responses even when the example shows a populated value — typically when a row was created before the metadata pipeline backfilled it, or when a market hasn't resolved yet. Each endpoint's response-fields table has a **Nullable** column. Defensive clients should accept `null` for any field marked nullable, plus for any field whose row predates indexing.
## Response envelopes
Endpoints fall into three envelope shapes:
| Shape | Endpoints | Top-level data key |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| Single-object | `GET /v3/wallets/{addr}`, `GET /v3/wallets/{addr}/pnl`, `GET /v3/markets/{token_id}/price`, `GET /v3/conditions/{cid}`, `GET /v3/builders/{code}`, `GET /v3/stats` | Fields at top level (no wrapper) |
| Named list | `/v3/trades`, `/v3/wallets/{addr}/trades`, `/v3/markets/{token_id}/trades`, `/v3/markets/slug/{slug}/trades`, `/v3/builders/{code}/trades` | `"trades": [...]` |
| Named list | `/v3/positions`, `/v3/wallets/{addr}/positions` | `"positions": [...]` |
| Named list | `/v3/leaderboard` | `"leaderboard": [...]` |
| Named list | `/v3/tags`, `/v3/builders` | `"tags": [...]`, `"builders": [...]` |
| Named list | `POST /v3/wallets/batch` | `"wallets": [...]` |
| Generic list | Everything else (fees, resolutions, splits, merges, NRC, activity, redemptions, tag-leaderboard, tag-markets, market-positions, condition-positions, market-by-condition, market-search, tokens/info) | `"data": [...]` |
All list responses also include `rows_returned`, `has_more`, `offset`, `limit`, `elapsed_ms`.
## Timestamps
All Unix timestamps in responses are in **seconds** (e.g., `"1778674056"`). The `resolved_at` field in resolutions uses **milliseconds** (e.g., `"1778674836000"`). Datetime strings like `updated_at` are in UTC.
## Rate limits
V3 REST data endpoints require a paid API key and are rate limited per key.
| Plan | Standard V3 REST | Heavy V3 trade endpoints |
| ---------- | ------------------------------------------ | ------------------------------------------ |
| Starter | 1,000 req/min | 1,000 req/min |
| Growth | 2,000 req/min | 1,500 req/min |
| Enterprise | 4,000 req/min default; custom by agreement | 1,500 req/min default; custom by agreement |
See [Rate Limits](/guides/rate-limits) for the full tier table and heavy-endpoint details.
## Pagination
All list endpoints support:
* `?limit=N` — results per page (default 100, max 300)
* `?offset=N` — skip N results
Responses include the actual `limit` used, plus `has_more: true/false` to indicate if more pages exist. For deep trade-history walks, prefer `after` and `before` time windows instead of very large offsets.
## Response format
Every response includes `elapsed_ms` showing server-side query time in milliseconds.
```json theme={null}
{
"data": [...],
"rows_returned": 100,
"has_more": true,
"offset": 0,
"limit": 100,
"elapsed_ms": 12
}
```
## Endpoint categories
| Category | Endpoints | What you can do |
| --------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------ |
| [Wallet P\&L](/data/wallets/summary) | 4 | Wallet summary, dual P\&L breakdown, batch queries, enriched positions |
| [Wallet Activity](/data/wallets/trades) | 6 | Trades, redemptions, splits, merges, NRC, combined activity |
| [Combos](/data/combos/overview) | 7 | Combo markets, wallet combo positions, combo trades, lifecycle activity, and additive wallet summaries |
| [Markets](/data/markets/search) | 8 | Search, lookup by condition/token/slug, trades, positions, prices, candles |
| [Leaderboard](/data/leaderboard/global) | 3 | Global P\&L rankings, tag discovery, tag leaderboards |
| [Builders](/data/builders/list) | 3 | Builder rankings, stats, attributed trades |
| [Global](/data/global/stats) | 5 | Platform stats, trade feed, fees, resolutions, conditions |
| [Tokens](/data/tokens/info) | 1 | Token price and market metadata |
## Explorer
Browse the data interactively with pre-built SQL recipes at [explorer.polynode.dev](https://explorer.polynode.dev).
# Builders Query Catalog
Source: https://docs.polynode.dev/data/query-catalog/builders
Builder recipes for rankings, profiles, fee sorting, attributed trades, and filtered builder feeds.
Use these recipes when building builder dashboards, attribution analytics, or builder-specific trade feeds.
Rank builders by fills, volume, fees, makers, or takers.
Stats and public profile metadata for one builder.
Trades attributed to one builder code.
Builder-attributed trades inside one market.
Indexed builder tag/category filters paired with market, condition, or token filters.
Buy/sell and amount filters for builder trades.
# Builder Detail
Source: https://docs.polynode.dev/data/query-catalog/builders/detail
Get stats and public profile metadata for one builder.
Canonical reference: [Builder Detail](/data/builders/detail)
```bash theme={null}
curl "https://api.polynode.dev/v3/builders/0x4898df15ec6590495dc6c0fedf951ade3e64001d47f9caf44a64e86fc11959df" \
-H "x-api-key: $POLYNODE_API_KEY"
```
# Builder Leaderboard
Source: https://docs.polynode.dev/data/query-catalog/builders/leaderboard
Rank builders by fills, volume, fees, makers, or takers.
Canonical reference: [Builder Leaderboard](/data/builders/list)
```bash theme={null}
curl "https://api.polynode.dev/v3/builders?sort=fees&limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
Sort options include `fills`, `volume`, `fees`, `makers`, and `takers`.
# Builder Trades
Source: https://docs.polynode.dev/data/query-catalog/builders/trades
Get trades attributed to one builder code.
Canonical reference: [Builder Trades](/data/builders/trades)
```bash theme={null}
curl "https://api.polynode.dev/v3/builders/0x4898df15ec6590495dc6c0fedf951ade3e64001d47f9caf44a64e86fc11959df/trades?limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
# Builder Trades by Market
Source: https://docs.polynode.dev/data/query-catalog/builders/trades-by-market
Filter builder-attributed trades to one market slug.
Canonical reference: [Builder Trades](/data/builders/trades)
```bash theme={null}
curl "https://api.polynode.dev/v3/builders/0x4898df15ec6590495dc6c0fedf951ade3e64001d47f9caf44a64e86fc11959df/trades?market_slug=dota2-tundra-xtreme-2026-05-22-game1&limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
You can also use `condition_id` or `token_id` when you already have those identifiers.
# Builder Trades by Side and Size
Source: https://docs.polynode.dev/data/query-catalog/builders/trades-by-side-size
Filter builder-attributed trades by side and minimum amount.
Canonical reference: [Builder Trades](/data/builders/trades)
```bash theme={null}
curl "https://api.polynode.dev/v3/builders/0x4898df15ec6590495dc6c0fedf951ade3e64001d47f9caf44a64e86fc11959df/trades?side=buy&min_amount=1000000&limit=10" \
-H "x-api-key: $POLYNODE_API_KEY"
```
Use `side=buy` or `side=sell`. `min_amount` is useful for large-trade filters.
# Builder Trades by Tag or Category
Source: https://docs.polynode.dev/data/query-catalog/builders/trades-by-tag-category
Filter builder trades by tag or category on a narrow market, condition, or token scope.
Canonical reference: [Builder Trades](/data/builders/trades)
```bash theme={null}
curl "https://api.polynode.dev/v3/builders/0x4898df15ec6590495dc6c0fedf951ade3e64001d47f9caf44a64e86fc11959df/trades?market_slug=dota2-tundra-xtreme-2026-05-22-game1&tag_slug=sports&limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
```bash theme={null}
curl "https://api.polynode.dev/v3/builders/0x4898df15ec6590495dc6c0fedf951ade3e64001d47f9caf44a64e86fc11959df/trades?market_slug=dota2-tundra-xtreme-2026-05-22-game1&category=Sports&limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
Pair broad tag or category filters with `market_slug`, `condition_id`, or `token_id` for the indexed query path.
# Combos Query Catalog
Source: https://docs.polynode.dev/data/query-catalog/combos
Combo recipes for markets, activity, wallet summaries, combo positions, combo trades, and combined wallet views.
Use these recipes when building combo market explorers, combo wallet dashboards, or lifecycle activity feeds.
List observed combo markets with leg metadata.
Lifecycle activity by combo market, condition, position, or wallet.
Combo-only P\&L and position counts for one wallet.
Combo positions for one wallet.
Combo trades for one wallet.
Combo lifecycle activity for one wallet.
Add combo branches to standard wallet P\&L, positions, and trades.
# Combo Activity
Source: https://docs.polynode.dev/data/query-catalog/combos/activity
Query combo lifecycle activity by market, condition, position, or wallet.
Canonical reference: [Combo Activity](/data/combos/activity)
```bash theme={null}
curl "https://api.polynode.dev/v3/combos/activity?condition_id=0x03d98f6ac4c108c5ca66f79f34908a1d820000000000000000000000000000&limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
# Include Combos in Wallet Views
Source: https://docs.polynode.dev/data/query-catalog/combos/include-combos
Add combo branches to standard wallet P&L, positions, and trades.
Canonical reference: [Combos Overview](/data/combos/overview)
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x63613e3b96f418332d43cd2af8dc321014d15907/pnl?include_combos=true" \
-H "x-api-key: $POLYNODE_API_KEY"
```
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x63613e3b96f418332d43cd2af8dc321014d15907/positions?include_combos=true&limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x63613e3b96f418332d43cd2af8dc321014d15907/trades?include_combos=true&limit=10" \
-H "x-api-key: $POLYNODE_API_KEY"
```
# Combo Markets
Source: https://docs.polynode.dev/data/query-catalog/combos/markets
List observed combo markets with leg metadata.
Canonical reference: [Combo Markets](/data/combos/markets)
```bash theme={null}
curl "https://api.polynode.dev/v3/combos/markets?limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
# Wallet Combo Activity
Source: https://docs.polynode.dev/data/query-catalog/combos/wallet-activity
List combo lifecycle activity for one wallet.
Canonical reference: [Wallet Combo Activity](/data/combos/wallet-activity)
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x63613e3b96f418332d43cd2af8dc321014d15907/combos/activity?limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
# Wallet Combo Positions
Source: https://docs.polynode.dev/data/query-catalog/combos/wallet-positions
List combo positions for one wallet.
Canonical reference: [Wallet Combo Positions](/data/combos/wallet-positions)
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x63613e3b96f418332d43cd2af8dc321014d15907/combos/positions?limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
# Wallet Combo Summary
Source: https://docs.polynode.dev/data/query-catalog/combos/wallet-summary
Get combo-only P&L and position counts for one wallet.
Canonical reference: [Wallet Combo Summary](/data/combos/wallet-summary)
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x63613e3b96f418332d43cd2af8dc321014d15907/combos/summary" \
-H "x-api-key: $POLYNODE_API_KEY"
```
# Wallet Combo Trades
Source: https://docs.polynode.dev/data/query-catalog/combos/wallet-trades
List combo trades for one wallet.
Canonical reference: [Wallet Combo Trades](/data/combos/wallet-trades)
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x63613e3b96f418332d43cd2af8dc321014d15907/combos/trades?limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
# Markets Query Catalog
Source: https://docs.polynode.dev/data/query-catalog/markets
Market recipes for search, condition lookup, slug lookup, token info, prices, tags, and resolutions.
Use these recipes when building market search, market detail pages, token pages, tag pages, or resolution feeds.
Full-text market search.
Market metadata, outcomes, and token IDs from a condition ID.
Resolution and payout data for one condition.
Use a Polymarket slug to retrieve market trades or holders.
Outcome-token metadata, price, category, tags, and resolution status.
Current price and resolution status for one outcome token.
OHLCV candles from V3 trade fills.
List tags and find markets by tag.
Recent market resolution feed.
# Market by Condition
Source: https://docs.polynode.dev/data/query-catalog/markets/by-condition
Get market metadata and outcome tokens from a condition ID.
Canonical reference: [Market by Condition](/data/markets/by-condition)
```bash theme={null}
curl "https://api.polynode.dev/v3/markets/condition/0x97269cbb95bd572b3dde34cdd619be278c8f13ba5e43e2e6c2d8639411128a86" \
-H "x-api-key: $POLYNODE_API_KEY"
```
Use this when your app stores market condition IDs.
# Trades and Positions by Slug
Source: https://docs.polynode.dev/data/query-catalog/markets/by-slug
Use a market slug to fetch trades or positions.
Canonical reference: [Market by Slug](/data/markets/by-slug)
```bash theme={null}
curl "https://api.polynode.dev/v3/markets/slug/dota2-tundra-xtreme-2026-05-22-game1/trades?limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
```bash theme={null}
curl "https://api.polynode.dev/v3/markets/slug/dota2-tundra-xtreme-2026-05-22-game1/positions?sort=amount&limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
# Market Candles
Source: https://docs.polynode.dev/data/query-catalog/markets/candles
Build OHLCV candles for a V3 market token, condition ID, or slug.
Canonical reference: [Market Candles](/data/markets/candles)
```bash theme={null}
curl "https://api.polynode.dev/v3/markets/{token_id}/candles?resolution=5m&limit=120" \
-H "x-api-key: $POLYNODE_API_KEY"
```
Use this for V3 charting. Candles are built from trade fills and support `1m`, `5m`, `15m`, `1h`, `4h`, and `1d` buckets.
# Condition Detail
Source: https://docs.polynode.dev/data/query-catalog/markets/condition-detail
Get resolution and payout data for a condition.
Canonical reference: [Condition Detail](/data/global/conditions)
```bash theme={null}
curl "https://api.polynode.dev/v3/conditions/0x97269cbb95bd572b3dde34cdd619be278c8f13ba5e43e2e6c2d8639411128a86" \
-H "x-api-key: $POLYNODE_API_KEY"
```
Use this for settlement, payout, and resolution status views.
# Market Resolutions
Source: https://docs.polynode.dev/data/query-catalog/markets/resolutions
Browse recent resolved markets with payout data.
Canonical reference: [Market Resolutions](/data/global/resolutions)
```bash theme={null}
curl "https://api.polynode.dev/v3/resolutions?limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
Use this for settlement feeds and post-resolution workflows.
# Search Markets
Source: https://docs.polynode.dev/data/query-catalog/markets/search-markets
Search Polymarket markets by text.
Canonical reference: [Search Markets](/data/markets/search)
```bash theme={null}
curl "https://api.polynode.dev/v3/markets/search?q=dota&limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
Use market search when a user enters free text or when you need to discover slugs and condition IDs.
# Tags and Markets by Tag
Source: https://docs.polynode.dev/data/query-catalog/markets/tags
List tags and discover markets under a tag.
Canonical references: [Tags](/data/leaderboard/tags), [Tag Markets](/data/leaderboard/tag-markets)
```bash theme={null}
curl "https://api.polynode.dev/v3/tags?limit=3" \
-H "x-api-key: $POLYNODE_API_KEY"
```
```bash theme={null}
curl "https://api.polynode.dev/v3/tags/ufc?limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
Tag values are case-insensitive. URL hyphens are treated like spaces.
# Token Info
Source: https://docs.polynode.dev/data/query-catalog/markets/token-info
Get metadata, price, category, tags, and resolution status for one outcome token.
Canonical reference: [Token Info](/data/tokens/info)
```bash theme={null}
curl "https://api.polynode.dev/v3/tokens/21578184864194020862792310253337084708609281967112790860038966988826118995357" \
-H "x-api-key: $POLYNODE_API_KEY"
```
Use this when you have an outcome token ID and need the market context around it.
# Token Price
Source: https://docs.polynode.dev/data/query-catalog/markets/token-price
Get the current price and resolution status for one outcome token.
Canonical reference: [Token Price](/data/markets/price)
```bash theme={null}
curl "https://api.polynode.dev/v3/markets/21578184864194020862792310253337084708609281967112790860038966988826118995357/price" \
-H "x-api-key: $POLYNODE_API_KEY"
```
Use token price for lightweight price checks when you do not need full market metadata.
# Query Catalog
Source: https://docs.polynode.dev/data/query-catalog/overview
Intent-based catalog of the most useful PolyNode V3 query shapes.
The API reference is organized by endpoint. The query catalog is organized by what you are trying to build: trade feeds, wallet dashboards, P\&L screens, market pages, fee accounting, builder analytics, and combo views.
All examples use the public V3 API and an API key. Set `POLYNODE_API_KEY` once, then copy any example.
```bash theme={null}
export POLYNODE_API_KEY="pn_live_..."
```
Global trades, wallet trades, market trades, builder-attributed trades, grouped orders, and combo trades.
Wallet positions, open positions, global holders, token holders, condition holders, slug holders, and combo positions.
Wallet P\&L, time-windowed P\&L, tag/market P\&L, P\&L events, batch reads, and leaderboards.
Search, condition lookup, condition detail, token info, price, tags, tag markets, and resolutions.
Unified activity, splits, merges, redemptions, neg-risk conversions, PolyUSD flows, fees, rebates, and rewards.
Wallet-paid fees, global fee events, receiver fees, builder fee ranking, rebates, and reward configs.
Builder leaderboard, builder profiles, builder trades, market filters, side/size filters, tag filters, and category filters.
Combo markets, combo activity, combo wallet summary, combo positions, combo trades, and additive wallet views.
## Granular Recipes
The left navigation includes both category pages and individual query recipes. Use the category pages for orientation, then jump to the exact query shape you need.
# P&L Query Catalog
Source: https://docs.polynode.dev/data/query-catalog/pnl
P&L recipes for wallet totals, time windows, tag and market filters, events, batches, and leaderboards.
Use these recipes when building wallet performance pages, time-series charts, filtered rankings, and leaderboard views.
Recovered all-time wallet P\&L with realized, unrealized, total, wins, losses, and volume.
Realized P\&L for periods like 1d, 7d, 30d, and 1y.
Wallet P\&L filtered to a tag or category.
Wallet P\&L filtered to one condition or market slug.
Bucketed P\&L events for charts.
Read P\&L for multiple wallets in one request.
Rank wallets by total P\&L, realized P\&L, volume, or win count, with one primary filter dimension when needed.
Rank wallets inside a tag/category; broad filters should use the global leaderboard filter.
# Batch Wallet P&L
Source: https://docs.polynode.dev/data/query-catalog/pnl/batch-wallet-pnl
Query P&L for multiple wallets in one request.
Canonical reference: [Batch Wallet P\&L](/data/wallets/batch)
```bash theme={null}
curl -X POST "https://api.polynode.dev/v3/wallets/batch" \
-H "content-type: application/json" \
-H "x-api-key: $POLYNODE_API_KEY" \
-d '{"wallets":["0x56687bf447db6ffa42ffe2204a05edaa20f55839","0x952d11ebff81d6bd3185e608ed3515b94618ab8a"]}'
```
Use this when a dashboard needs several wallet summaries at once.
# Global P&L Leaderboard
Source: https://docs.polynode.dev/data/query-catalog/pnl/global-leaderboard
Rank wallets by P&L, volume, profit, or win count.
Canonical reference: [Leaderboard](/data/leaderboard/global)
```bash theme={null}
curl "https://api.polynode.dev/v3/leaderboard?sort=total&limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
Use `sort=total`, `realized`, `volume`, `profit`, or `wins` depending on the leaderboard view.
For filtered leaderboards, use one primary market dimension per request:
```bash theme={null}
curl "https://api.polynode.dev/v3/leaderboard?category=crypto&sort=total&limit=20" \
-H "x-api-key: $POLYNODE_API_KEY"
curl "https://api.polynode.dev/v3/leaderboard?tag_slug=us-election&sort=volume&limit=20" \
-H "x-api-key: $POLYNODE_API_KEY"
```
Supported dimensions include `category`, `tags`/`tag_slug`, `market`/`market_slug`, `event_slug`, and `condition_id`.
# Tag P&L Leaderboard
Source: https://docs.polynode.dev/data/query-catalog/pnl/tag-leaderboard
Rank wallets inside a tag or category.
Canonical reference: [Tag Leaderboard](/data/leaderboard/tag-leaderboard)
```bash theme={null}
curl "https://api.polynode.dev/v3/tags/ufc/leaderboard?limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
Use direct tag leaderboards for niche tags, sport-specific pages, or event-family rankings.
For broad tags/categories, prefer the projected global leaderboard filter:
```bash theme={null}
curl "https://api.polynode.dev/v3/leaderboard?tags=crypto&limit=20" \
-H "x-api-key: $POLYNODE_API_KEY"
curl "https://api.polynode.dev/v3/leaderboard?category=crypto&limit=20" \
-H "x-api-key: $POLYNODE_API_KEY"
```
# Wallet All-Time P&L
Source: https://docs.polynode.dev/data/query-catalog/pnl/wallet-all-time
Get recovered all-time wallet P&L.
Canonical reference: [Wallet P\&L](/data/wallets/pnl)
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x56687bf447db6ffa42ffe2204a05edaa20f55839/pnl" \
-H "x-api-key: $POLYNODE_API_KEY"
```
The response includes realized P\&L, unrealized P\&L, total P\&L, gross profit, gross loss, wins, losses, open positions, and volume.
# Wallet P&L by Market
Source: https://docs.polynode.dev/data/query-catalog/pnl/wallet-by-market
Filter wallet P&L to one condition ID or market slug.
Canonical reference: [Wallet P\&L](/data/wallets/pnl)
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x952d11ebff81d6bd3185e608ed3515b94618ab8a/pnl?market=0x97269cbb95bd572b3dde34cdd619be278c8f13ba5e43e2e6c2d8639411128a86" \
-H "x-api-key: $POLYNODE_API_KEY"
```
Use `market` for a condition ID, or `market_slug` for a Polymarket slug.
`event_slug` and `condition_id` are also accepted for focused market/event P\&L views.
# Wallet P&L by Tag
Source: https://docs.polynode.dev/data/query-catalog/pnl/wallet-by-tag
Filter wallet P&L to a tag or category.
Canonical reference: [Wallet P\&L](/data/wallets/pnl)
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x952d11ebff81d6bd3185e608ed3515b94618ab8a/pnl?tags=sports" \
-H "x-api-key: $POLYNODE_API_KEY"
```
Use comma-separated `tags` for multiple tags where supported. `category` filters the first market tag, and `tag_slug` is accepted as an alias for single-tag filters.
# Wallet P&L Events
Source: https://docs.polynode.dev/data/query-catalog/pnl/wallet-events
Get bucketed wallet P&L events for charts.
Canonical reference: [Wallet P\&L Time Series](/data/wallets/pnl-events)
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x952d11ebff81d6bd3185e608ed3515b94618ab8a/pnl/events?period=7d&group=day&limit=10" \
-H "x-api-key: $POLYNODE_API_KEY"
```
Use `group=hour`, `day`, `week`, or `month` depending on the chart resolution.
# Wallet Time-Windowed P&L
Source: https://docs.polynode.dev/data/query-catalog/pnl/wallet-time-window
Get wallet P&L for a period such as 30 days.
Canonical reference: [Wallet P\&L](/data/wallets/pnl)
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x952d11ebff81d6bd3185e608ed3515b94618ab8a/pnl?period=30d&include_unrealized=true" \
-H "x-api-key: $POLYNODE_API_KEY"
```
Supported period shortcuts are `1d`, `7d`, `30d`, and `1y`. You can also use `after` and `before` Unix timestamps.
# Positions Query Catalog
Source: https://docs.polynode.dev/data/query-catalog/positions
Position recipes for wallet positions, open positions, holders, market positions, and combo positions.
Use these recipes when building wallet dashboards, holder tables, market pages, or position discovery views.
All standard positions for one wallet.
Current open positions for one wallet.
Platform-wide open-position discovery.
Holders for one outcome token.
Holders across all outcomes in one market condition.
Holders for a market URL slug.
Combo-only and combined wallet position views.
# Positions by Condition
Source: https://docs.polynode.dev/data/query-catalog/positions/by-condition
Get holders across all outcomes in one market condition.
Canonical reference: [Market Positions by Condition](/data/markets/condition-positions)
```bash theme={null}
curl "https://api.polynode.dev/v3/markets/condition/0x97269cbb95bd572b3dde34cdd619be278c8f13ba5e43e2e6c2d8639411128a86/positions?sort=amount&limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
Use condition-level positions when a market has multiple outcome tokens and you want the full holder table.
# Positions by Market Slug
Source: https://docs.polynode.dev/data/query-catalog/positions/by-market-slug
Get holders for a market URL slug.
Canonical reference: [Market by Slug](/data/markets/by-slug)
```bash theme={null}
curl "https://api.polynode.dev/v3/markets/slug/dota2-tundra-xtreme-2026-05-22-game1/positions?sort=amount&limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
Use this when your app stores or receives Polymarket slugs.
# Positions by Market Token
Source: https://docs.polynode.dev/data/query-catalog/positions/by-market-token
Get holders for one outcome token.
Canonical reference: [Market Positions](/data/markets/positions)
```bash theme={null}
curl "https://api.polynode.dev/v3/markets/21578184864194020862792310253337084708609281967112790860038966988826118995357/positions?limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
Use this when you already know the outcome token ID.
# Positions by Wallet
Source: https://docs.polynode.dev/data/query-catalog/positions/by-wallet
Get all standard positions for one wallet.
Canonical reference: [Wallet Positions](/data/wallets/positions)
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x952d11ebff81d6bd3185e608ed3515b94618ab8a/positions?limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
Use this for wallet position tables with market context, current prices, unrealized P\&L, and settlement status.
# Combo Positions
Source: https://docs.polynode.dev/data/query-catalog/positions/combo-positions
Get combo-only wallet positions or include combos in the standard wallet position view.
Canonical references: [Wallet Combo Positions](/data/combos/wallet-positions), [Wallet Positions](/data/wallets/positions)
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x63613e3b96f418332d43cd2af8dc321014d15907/combos/positions?limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x63613e3b96f418332d43cd2af8dc321014d15907/positions?include_combos=true&limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
# Global Positions
Source: https://docs.polynode.dev/data/query-catalog/positions/global-positions
Browse platform-wide open positions.
Canonical reference: [Global Positions](/data/global/positions)
```bash theme={null}
curl "https://api.polynode.dev/v3/positions?status=open&sort=size&limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
Use this for holder discovery across the platform. Add `condition_id`, `token_id`, `status`, `min_size`, and `sort` to narrow the result.
# Open Wallet Positions
Source: https://docs.polynode.dev/data/query-catalog/positions/open-wallet-positions
Get current open positions for one wallet.
Canonical reference: [Wallet Positions](/data/wallets/positions)
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x952d11ebff81d6bd3185e608ed3515b94618ab8a/positions?status=open&limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
Use `status=open` when you only want active exposure.
# Tags and Categories Query Catalog
Source: https://docs.polynode.dev/data/query-catalog/tags-categories
Tag and category recipes for discovery, market lists, P&L filters, leaderboards, and builder filters.
Tags are market labels. In V3 market and token responses, the first tag is also the category used by category-style query filters.
Discover available tag values.
List markets under a tag.
Rank wallets inside a tag/category; broad tags should use the global leaderboard filter.
Filter wallet P\&L to a tag/category.
Filter builder-attributed trades by tag/category on an indexed market, condition, or token scope.
# Trades Query Catalog
Source: https://docs.polynode.dev/data/query-catalog/trades
Trade feed recipes for global, wallet, market, builder, grouped-order, and combo trade queries.
Use these recipes when building trade feeds, market tapes, wallet histories, builder analytics, or order-level views.
Global trade feed with pagination and time filters.
Trades where one wallet was maker or taker.
Trades for one outcome token.
Trades for all outcomes in one market slug.
One wallet's trades inside one market condition.
Order-level grouping for wallet trades.
Trades attributed to a builder code.
Combo-only and combined wallet trade views.
# All Trades
Source: https://docs.polynode.dev/data/query-catalog/trades/all-trades
Browse the global Polymarket trade feed.
Canonical reference: [Global Trades](/data/global/trades)
```bash theme={null}
curl "https://api.polynode.dev/v3/trades?limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
Use `after`, `before`, `limit`, `offset`, `order`, and `min_amount` to page and filter the feed.
# Builder Trades
Source: https://docs.polynode.dev/data/query-catalog/trades/by-builder
Get trades attributed to a builder code.
Canonical reference: [Builder Trades](/data/builders/trades)
```bash theme={null}
curl "https://api.polynode.dev/v3/builders/0x4898df15ec6590495dc6c0fedf951ade3e64001d47f9caf44a64e86fc11959df/trades?limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
Builder trades can be narrowed by `market_slug`, `condition_id`, `token_id`, `side`, `min_amount`, `tag_slug`, and `category`.
# Trades by Market Slug
Source: https://docs.polynode.dev/data/query-catalog/trades/by-market-slug
Get trades for all outcome tokens in one market slug.
Canonical reference: [Market by Slug](/data/markets/by-slug)
```bash theme={null}
curl "https://api.polynode.dev/v3/markets/slug/dota2-tundra-xtreme-2026-05-22-game1/trades?limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
Use this when your application works from Polymarket URLs or slugs instead of token IDs.
# Trades by Market Token
Source: https://docs.polynode.dev/data/query-catalog/trades/by-market-token
Get trades for one outcome token.
Canonical reference: [Market Trades](/data/markets/trades)
```bash theme={null}
curl "https://api.polynode.dev/v3/markets/21578184864194020862792310253337084708609281967112790860038966988826118995357/trades?limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
Use this when you already know the ERC-1155 outcome token ID.
# Trades by Wallet
Source: https://docs.polynode.dev/data/query-catalog/trades/by-wallet
Get trades where a wallet participated as maker or taker.
Canonical reference: [Wallet Trades](/data/wallets/trades)
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x952d11ebff81d6bd3185e608ed3515b94618ab8a/trades?limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
Useful filters include `after`, `before`, `side`, `condition_id`, `token_id`, `market_slug`, `group_by=order_hash`, and `include_combos=true`.
# Combo Wallet Trades
Source: https://docs.polynode.dev/data/query-catalog/trades/combo-wallet-trades
Get combo-only wallet trades or include combo trades in the standard wallet trade view.
Canonical references: [Wallet Combo Trades](/data/combos/wallet-trades), [Wallet Trades](/data/wallets/trades)
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x63613e3b96f418332d43cd2af8dc321014d15907/combos/trades?limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x63613e3b96f418332d43cd2af8dc321014d15907/trades?include_combos=true&limit=10" \
-H "x-api-key: $POLYNODE_API_KEY"
```
# Wallet Trades by Condition
Source: https://docs.polynode.dev/data/query-catalog/trades/wallet-by-condition
Filter one wallet's trades to a specific market condition.
Canonical reference: [Wallet Trades](/data/wallets/trades)
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x952d11ebff81d6bd3185e608ed3515b94618ab8a/trades?condition_id=0x97269cbb95bd572b3dde34cdd619be278c8f13ba5e43e2e6c2d8639411128a86&limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
Use `condition_id` when you want both outcomes for one market. Use `token_id` when you want one specific outcome.
# Wallet Trades Grouped by Order
Source: https://docs.polynode.dev/data/query-catalog/trades/wallet-grouped-by-order
Group wallet fills by order hash.
Canonical reference: [Wallet Trades](/data/wallets/trades)
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x952d11ebff81d6bd3185e608ed3515b94618ab8a/trades?group_by=order_hash&limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
Use this for order-level analytics where multiple fills share the same order hash.
# Wallet Activity Query Catalog
Source: https://docs.polynode.dev/data/query-catalog/wallet-activity
Wallet activity recipes for lifecycle events, PolyUSD flows, fees, rebates, and rewards.
Use these recipes when building wallet timelines, cash movement views, settlement history, or fee accounting pages.
Splits, merges, redemptions, and neg-risk conversions in one feed.
USDC converted into outcome tokens.
Outcome tokens converted back into USDC.
Claimed payouts from resolved markets.
Neg-risk conversion events for a wallet.
PolyUSD deposits and withdrawals.
Fee-bearing fills for one wallet.
Polymarket-reported maker rebates and reward surfaces.
# Wallet Fees Paid
Source: https://docs.polynode.dev/data/query-catalog/wallets/fees-paid
Get fee-bearing fills for one wallet.
Canonical reference: [Wallet Fees Paid](/data/wallets/fees-paid)
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x63ce342161250d705dc0b16df89036c8e5f9ba9a/fees-paid?limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
This is trader-paid fill fee data. It is separate from maker rebates and reward configuration.
# Wallet Merges
Source: https://docs.polynode.dev/data/query-catalog/wallets/merges
Get merge events where outcome tokens were converted back into USDC.
Canonical reference: [Wallet Merges](/data/wallets/merges)
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x01a23b7408650ba910b11740a814071e57fbfbe3/merges?limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
# Neg-Risk Conversions
Source: https://docs.polynode.dev/data/query-catalog/wallets/neg-risk-conversions
Get neg-risk conversion activity for one wallet.
Canonical reference: [Wallet NRC](/data/wallets/nrc)
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x4ce73141dbfce41e65db3723e31059a730f0abad/nrc?limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
# PolyUSD Flows
Source: https://docs.polynode.dev/data/query-catalog/wallets/polyusd-flows
Get PolyUSD deposits and withdrawals for one wallet.
Canonical reference: [Wallet PolyUSD Flows](/data/wallets/polyusd-flows)
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x2a1f579283C87c4574102bbF6E4B39F7A12fe77E/polyusd-flows?limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
# Wallet Rebates and Rewards
Source: https://docs.polynode.dev/data/query-catalog/wallets/rebates-rewards
Query maker rebates and wallet reward surfaces.
Canonical references: [Wallet Maker Rebates](/data/wallets/rebates), [Reward Markets](/data/rewards/markets)
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x8ecb4b228e07b6ddc58a32997093032a6907b8f6/rebates" \
-H "x-api-key: $POLYNODE_API_KEY"
```
```bash theme={null}
curl "https://api.polynode.dev/v3/rewards/markets" \
-H "x-api-key: $POLYNODE_API_KEY"
```
Rebates, rewards, and fill fees are distinct accounting surfaces.
# Wallet Redemptions
Source: https://docs.polynode.dev/data/query-catalog/wallets/redemptions
Get redemption events where a wallet claimed resolved-market payouts.
Canonical reference: [Wallet Redemptions](/data/wallets/redemptions)
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x56687bf447db6ffa42ffe2204a05edaa20f55839/redemptions?limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
# Wallet Splits
Source: https://docs.polynode.dev/data/query-catalog/wallets/splits
Get split events where USDC was converted into outcome tokens.
Canonical reference: [Wallet Splits](/data/wallets/splits)
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x0f35109cc3ceaf729609c3109310791bd616c84a/splits?limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
# Unified Wallet Activity
Source: https://docs.polynode.dev/data/query-catalog/wallets/unified-activity
Get non-trade wallet activity in one feed.
Canonical reference: [Wallet Activity](/data/wallets/activity)
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x56687bf447db6ffa42ffe2204a05edaa20f55839/activity?limit=1" \
-H "x-api-key: $POLYNODE_API_KEY"
```
This feed includes split, merge, redemption, and neg-risk conversion activity.
# Reward Market Detail
Source: https://docs.polynode.dev/data/rewards/market
GET /v3/rewards/markets/{condition_id}
Get public Polymarket reward configuration for one condition ID.
Returns Polymarket's public reward-market configuration for a single condition ID. This describes market reward settings; it is not per-wallet earned LP rewards.
## Request
```
GET /v3/rewards/markets/{condition_id}
```
### Path parameters
| Parameter | Type | Description |
| -------------- | ------ | -------------------------- |
| `condition_id` | string | `0x` + 64 hex condition ID |
### Query parameters
| Parameter | Type | Default | Description |
| ------------- | ------- | ------- | ---------------------------------- |
| `sponsored` | boolean | `false` | Filter to sponsored reward markets |
| `next_cursor` | string | -- | Pagination cursor |
## Example
```bash theme={null}
curl "https://api.polynode.dev/v3/rewards/markets/0x0001cb8c0b39aeb614ab9a43867595317f06ede9c011661513065c638fbbefda"
```
```json theme={null}
{
"source": "reward_market_config_detail",
"reward_semantics": "public Polymarket reward market configuration; this is not per-wallet earned LP rewards",
"data": {
"count": 1,
"data": [
{
"condition_id": "0x0001cb8c0b39aeb614ab9a43867595317f06ede9c011661513065c638fbbefda",
"native_daily_rate": 1,
"rewards_config": [
{
"asset_address": "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB",
"rate_per_day": 1,
"start_date": "2026-02-17",
"end_date": "2500-12-31"
}
]
}
]
},
"elapsed_ms": 236
}
```
## Response fields
| Field | Type | Description |
| ------------------ | ------- | ---------------------------------------- |
| `source` | string | Dataset label |
| `reward_semantics` | string | Meaning note |
| `data` | object | Reward-market configuration response |
| `elapsed_ms` | integer | Server-side request time in milliseconds |
## Errors
| HTTP | When |
| ----- | --------------------------------------- |
| `400` | Invalid condition ID format |
| `401` | Missing or invalid PolyNode API key |
| `502` | Temporary rewards data provider failure |
# Reward Markets
Source: https://docs.polynode.dev/data/rewards/markets
GET /v3/rewards/markets
List public Polymarket reward market configurations.
Returns Polymarket's public reward-market configuration feed. This describes markets with active reward programs; it is not per-wallet earned LP rewards.
## Request
```
GET /v3/rewards/markets
```
### Query parameters
| Parameter | Type | Default | Description |
| ------------- | ------- | ------- | ---------------------------------- |
| `sponsored` | boolean | `false` | Filter to sponsored reward markets |
| `next_cursor` | string | -- | Pagination cursor |
## Example
```bash theme={null}
curl "https://api.polynode.dev/v3/rewards/markets"
```
```json theme={null}
{
"source": "reward_market_config",
"reward_semantics": "public Polymarket reward market configuration; this is not per-wallet earned LP rewards",
"data": {
"count": 500,
"data": [
{
"condition_id": "0x0001cb8c0b39aeb614ab9a43867595317f06ede9c011661513065c638fbbefda",
"native_daily_rate": 1,
"rewards_config": [
{
"asset_address": "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB",
"end_date": "2500-12-31",
"id": 0,
"rate_per_day": 1,
"start_date": "2026-02-17",
"total_rewards": 0
}
],
"rewards_max_spread": 4.5,
"rewards_min_size": 50,
"total_daily_rate": 1
}
],
"next_cursor": "..."
},
"elapsed_ms": 447
}
```
## Response fields
| Field | Type | Description |
| ------------------ | ------- | ---------------------------------------- |
| `source` | string | Dataset label |
| `reward_semantics` | string | Meaning note |
| `data` | object | Reward-market configuration response |
| `elapsed_ms` | integer | Server-side request time in milliseconds |
## Errors
| HTTP | When |
| ----- | --------------------------------------- |
| `401` | Missing or invalid PolyNode API key |
| `502` | Temporary rewards data provider failure |
# Token Info
Source: https://docs.polynode.dev/data/tokens/info
GET /v3/tokens/{token_id}
Get full details for an outcome token: price, resolution status, market metadata, and tags.
Returns price, resolution status, and market context for a specific outcome token.
## Request
```
GET /v3/tokens/{token_id}
```
### Path parameters
| Parameter | Type | Description |
| ---------- | ------ | ---------------- |
| `token_id` | string | Outcome token ID |
## Example
```bash theme={null}
curl https://api.polynode.dev/v3/tokens/75783394880030392863380883800697645018418815910449662777195626260206142035810
```
```json theme={null}
{
"data": [
{
"category": null,
"condition_id": "0xaa08b43f2e39a28c52278738d456dfce516fcd966d43f157aab3727457cd4b6c",
"outcome": "Down",
"outcome_index": 1,
"price": 1.0,
"question": "Bitcoin Up or Down - March 29, 1:10AM-1:15AM ET",
"resolved": true,
"slug": "btc-updown-5m-1774761000",
"source": "v1_condition",
"tag_slugs": [
"Crypto",
"Bitcoin",
"Crypto Prices",
"Recurring",
"Up or Down",
"Hide From New",
"5M"
],
"token_id": "40671034664065625078050343379339320067070367341089437265715901074008336896448",
"updated_at": "2026-05-16 11:42:10.666489"
}
],
"elapsed_ms": 3,
"has_more": false,
"limit": 1,
"offset": 0,
"rows_returned": 1
}
```
## Response columns
| Column | Type | Description |
| --------------- | ------- | ------------------------------------------------------------------------------ |
| `token_id` | string | Outcome token ID |
| `price` | number | Current price in USD (`0`–`1` for unresolved, exactly `0` or `1` for resolved) |
| `resolved` | boolean | Whether the market has settled |
| `source` | string | Price source: `settlement`, `clob_mid`, `v1_condition`, `last_fill` |
| `updated_at` | string | When the price was last updated |
| `condition_id` | string | Market condition ID |
| `outcome` | string | Outcome label (e.g. "Aurora", "Yes") |
| `outcome_index` | integer | Outcome position (0 or 1) |
| `question` | string | Market question |
| `slug` | string | Market URL slug |
| `category` | string | Market category |
| `tag_slugs` | array | Category tags |
# Wallet Activity
Source: https://docs.polynode.dev/data/wallets/activity
GET /v3/wallets/{address}/activity
Get all non-trade activity for a wallet: splits, merges, neg-risk conversions, and redemptions.
Returns a unified feed of all non-trade events for a wallet, sorted by timestamp. Includes splits, merges, neg-risk conversions, and redemptions.
This endpoint is the conditional-token activity feed for splits, merges, neg-risk conversions, and redemptions. PolyUSD deposits and withdrawals are available separately through [Wallet PolyUSD Flows](/data/wallets/polyusd-flows).
## Request
```
GET /v3/wallets/{address}/activity
```
### Query parameters
| Parameter | Type | Default | Description |
| --------- | ------- | ------- | ------------------------------------ |
| `after` | integer | -- | Start of time range (Unix timestamp) |
| `before` | integer | -- | End of time range |
| `limit` | integer | 100 | Max 300 |
| `offset` | integer | 0 | Pagination offset |
## Example
```bash theme={null}
curl https://api.polynode.dev/v3/wallets/0x56687bf447db6ffa42ffe2204a05edaa20f55839/activity?limit=1
```
```json theme={null}
{
"data": [
{
"amount": "706561610",
"event_type": "redemption",
"extra": "{1,2}",
"id": "0x7a7494699af0d7bf9734db8c803fc9ecad0330df97ebc7893bb05a81bc3c93bf_0x353",
"ref_id": "0xfbf0af69aa98b4662b95ff5ffb6dde560ec2e3da723a1980a6ef3f919cbeddea",
"timestamp": "1777356700"
}
],
"elapsed_ms": 8,
"has_more": true,
"limit": 1,
"offset": 0,
"rows_returned": 1
}
```
## Response fields
| Field | Type | Description |
| ------------ | ------ | ---------------------------------------------------------------------- |
| `event_type` | string | `split`, `merge`, `nrc` (neg-risk conversion), or `redemption` |
| `id` | string | Event ID |
| `timestamp` | string | Unix timestamp |
| `ref_id` | string | Condition ID or neg-risk market ID |
| `amount` | string | Amount (6-decimal USDC) |
| `extra` | string | Additional data (index\_sets for redemptions, question\_count for NRC) |
# Batch Wallet P&L
Source: https://docs.polynode.dev/data/wallets/batch
POST /v3/wallets/batch
Query P&L for up to 100 wallets in a single request. All amounts in USD.
Query multiple wallets at once. Returns the same summary data as the single wallet endpoint, but for up to 100 wallets in one call. All monetary values are in USD.
## Request
```
POST /v3/wallets/batch
```
### Request body
```json theme={null}
{
"wallets": [
"0x56687bf447db6ffa42ffe2204a05edaa20f55839",
"0xa9857c7bcb9bcfafd2c132ab053f34f678610058"
]
}
```
| Field | Type | Description |
| --------- | ----- | -------------------------- |
| `wallets` | array | Up to 100 wallet addresses |
## Example
```bash theme={null}
curl -X POST https://api.polynode.dev/v3/wallets/batch \
-H 'Content-Type: application/json' \
-d '{"wallets":["0x56687bf447db6ffa42ffe2204a05edaa20f55839"]}'
```
```json theme={null}
{
"count": 1,
"elapsed_ms": 1,
"wallets": [
{
"wallet": "0x56687bf447db6ffa42ffe2204a05edaa20f55839",
"net_realized_pnl": 22053845.825455,
"gross_profit": 22057977.181649,
"gross_loss": -4131.356194,
"unrealized_pnl": 0.000688755,
"total_pnl": 22053845.826143753,
"wins": 18,
"losses": 4,
"position_count": 22,
"open_positions": 1,
"total_volume": 43013258.515682
}
]
}
```
## Response fields
Same fields as [Wallet Summary](/data/wallets/summary), with `wallet` instead of `address`. All amounts in USD.
# Wallet Fees Paid
Source: https://docs.polynode.dev/data/wallets/fees-paid
GET /v3/wallets/{address}/fees-paid
Get fee-bearing order fills for a wallet.
Returns fills where the wallet was the order owner and `OrderFilled.fee` was greater than zero. Each row is enriched with the same market context used by wallet trade history.
This is fees paid by the trader on executed order fills. It is not the same as maker rebates, LP rewards, or builder payouts.
`page_total_fees_paid` is the sum for the returned page only. For the exact all-time fee total, use `GET /v3/wallets/{address}?include_accounting_summary=true` and read `accounting_summary.fees_paid.amount`.
## Request
```
GET /v3/wallets/{address}/fees-paid
```
### Query parameters
| Parameter | Type | Default | Description |
| --------- | ------- | ------------ | --------------------------------------------- |
| `after` | integer | `0` | Start of time range (Unix seconds, inclusive) |
| `before` | integer | `9999999999` | End of time range (Unix seconds, inclusive) |
| `order` | string | `desc` | `asc` or `desc` |
| `limit` | integer | `100` | Max 300 |
| `offset` | integer | `0` | Pagination offset |
## Example
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0xbddf61af533ff524d27154e589d2d7a81510c684/fees-paid?limit=1&after=1779700000"
```
```json theme={null}
{
"address": "0xbddf61af533ff524d27154e589d2d7a81510c684",
"fees": [
{
"id": "0x4f8447fda6ed5b200d876ffc677fedd853d524c9a2751d055d7578c1575124dc_847",
"maker": "0xbddf61af533ff524d27154e589d2d7a81510c684",
"taker": "0xe111180000d2663c0091e4f400237545b87b996b",
"fee": 352.774,
"fee_paid": 352.774,
"fee_paid_raw": "352774000",
"price": 0.45,
"size": 47511.65,
"direction": "BUY",
"market": "Spread: Thunder (-2.5)",
"slug": "nba-sas-okc-2026-05-26-spread-home-2pt5",
"condition_id": "0x7bcc428d368834c6dfbb4925f699079f65925fef69ad259274b88c5671aaa29e",
"timestamp": "1779839786",
"source": "wallet_fees_paid"
}
],
"rows_returned": 1,
"has_more": true,
"page_total_fees_paid": 352.774,
"fee_semantics": "Trader-paid fees on executed order fills where this wallet was the order owner. Includes supported Polymarket exchange versions.",
"elapsed_ms": 14
}
```
## Response fields
| Field | Type | Description |
| ------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------ |
| `address` | string | Wallet address, lowercased |
| `fees` | array | Fee-bearing fill rows |
| `fees[].fee` | number | Fee in USD |
| `fees[].fee_paid` | number | Same value as `fee`, included for category-breakdown clarity |
| `fees[].fee_paid_raw` | string | Raw 6-decimal fee amount |
| `fees[].source` | string | Dataset label |
| `rows_returned` | integer | Number of rows returned |
| `has_more` | boolean | Whether another page exists |
| `offset` | integer | Offset used for this page |
| `limit` | integer | Limit used for this page |
| `page_total_fees_paid` | number | Sum of `fee_paid` for this page only. Use wallet summary `include_accounting_summary=true` for the all-time total. |
| `page_total_fees_paid_raw_note` | string | Reminder that the total is page-scoped |
| `fee_semantics` | string | Attribution note |
| `after` | string | Applied start timestamp |
| `before` | string | Applied end timestamp |
| `elapsed_ms` | integer | Server-side query time in milliseconds |
The fill objects also include standard trade fields such as `maker_asset_id`, `taker_asset_id`, `maker_amount`, `taker_amount`, `token_id`, `asset`, `amount`, `amount_usd`, `transaction_hash`, `order_hash`, `builder`, `side`, `role`, `outcome`, `outcome_index`, and `image` when available.
## Errors
| HTTP | When |
| ----- | ----------------------------------- |
| `400` | Invalid wallet address |
| `400` | Invalid `order` value |
| `401` | Missing or invalid PolyNode API key |
# Wallet Merges
Source: https://docs.polynode.dev/data/wallets/merges
GET /v3/wallets/{address}/merges
Get all merge events for a wallet. Merges occur when outcome tokens are converted back into USDC.
Returns merge events where outcome tokens were converted back into USDC for a market condition.
## Request
```
GET /v3/wallets/{address}/merges
```
### Query parameters
| Parameter | Type | Default | Description |
| --------- | ------- | ------- | ------------------------------------ |
| `after` | integer | -- | Start of time range (Unix timestamp) |
| `before` | integer | -- | End of time range |
| `limit` | integer | 100 | Max 300 |
| `offset` | integer | 0 | Pagination offset |
## Example
```bash theme={null}
curl https://api.polynode.dev/v3/wallets/0xada100874d00e3331d00f2007a9c336a65009718/merges?limit=1
```
```json theme={null}
{
"data": [
{
"id": "0x56e84b69492dff41fae8aa932695eec8b90931aa286ce09d9565932d72e4a1f4_0x612",
"timestamp": "1778482999",
"condition": "0x649340b2a3e39f7d0de414d6891a14b6e1969293f8fec3ccb481572fd2375e34",
"amount": "20000000"
}
],
"rows_returned": 1,
"has_more": true,
"elapsed_ms": 2
}
```
## Response fields
| Field | Type | Description |
| ----------- | ------ | -------------------------------- |
| `id` | string | Merge event ID |
| `timestamp` | string | Unix timestamp |
| `condition` | string | Market condition ID |
| `amount` | string | USDC amount received (6-decimal) |
# Wallet NRC
Source: https://docs.polynode.dev/data/wallets/nrc
GET /v3/wallets/{address}/nrc
Get neg-risk conversion events for a wallet. NRC events convert between multi-outcome market positions.
Returns neg-risk conversion events for a wallet. These occur when positions in multi-outcome (neg-risk) markets are converted between outcome tokens.
## Request
```
GET /v3/wallets/{address}/nrc
```
### Query parameters
| Parameter | Type | Default | Description |
| --------- | ------- | ------- | ------------------------------------ |
| `after` | integer | -- | Start of time range (Unix timestamp) |
| `before` | integer | -- | End of time range |
| `limit` | integer | 100 | Max 300 |
| `offset` | integer | 0 | Pagination offset |
## Example
```bash theme={null}
curl https://api.polynode.dev/v3/wallets/0x4ce73141dbfce41e65db3723e31059a730f0abad/nrc?limit=1
```
```json theme={null}
{
"data": [],
"elapsed_ms": 2,
"has_more": false,
"limit": 1,
"offset": 0,
"rows_returned": 0
}
```
## Response fields
| Field | Type | Description |
| -------------------- | ------- | -------------------------------------------------------- |
| `id` | string | NRC event ID |
| `timestamp` | string | Unix timestamp |
| `neg_risk_market_id` | string | Parent neg-risk market ID |
| `amount` | string | Amount converted (6-decimal USDC) |
| `index_set` | string | Bit-packed index indicating which outcomes were involved |
| `question_count` | integer | Total number of outcomes in the neg-risk market |
# Wallet P&L
Source: https://docs.polynode.dev/data/wallets/pnl
GET /v3/wallets/{address}/pnl
Get profit and loss for a wallet in USD. Supports time-windowed P&L with ?period=1d|7d|30d|1y plus tag, category, market, event, and condition filters.
Returns P\&L data for a wallet in USD. Without a `period` parameter, returns all-time P\&L. With `period`, `after`, or `before`, returns realized P\&L for that time window.
The two modes have different counting semantics: all-time `wins` and `losses` are position counts; time-windowed `wins`, `losses`, and `events` are realized P\&L event counts. Do not compare the counts directly.
Wallet P\&L supports focused market filters such as `category`, `tags`/`tag_slug`, `market`/`market_slug`, `event_slug`, and `condition_id`. For leaderboard views, use one primary filter dimension per request.
## Request
```
GET /v3/wallets/{address}/pnl
```
### Query parameters
| Parameter | Type | Default | Description |
| -------------------- | ------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `period` | string | -- | Time window: `1d`, `7d`, `30d`, `1y` |
| `after` | integer | -- | Start timestamp (Unix seconds). Time-windowed queries clamp values below `1` to `1`. |
| `before` | integer | -- | End timestamp (Unix seconds) |
| `category` | string | -- | Filter to one market category, case-insensitive |
| `tags` | string | -- | Comma-separated tag slugs to filter by, case-insensitive (e.g. `politics,crypto`, `nfl`, `Iran`) |
| `tag_slug` | string | -- | Alias for a single `tags` value |
| `market` | string | -- | Filter by condition ID or market slug |
| `market_slug` | string | -- | Filter by market slug |
| `event_slug` | string | -- | Filter by parent event slug |
| `condition_id` | string | -- | Filter by market condition ID |
| `include_unrealized` | string | `false` | Set to `true` to include current unrealized P\&L alongside the realized event-window result. Only applies to time-filtered queries. |
| `include_combos` | boolean | `false` | Add combo position P\&L to all-time wallet P\&L. If the wallet has no combo exposure, the response remains 200 with a zero combo contribution. |
## Examples
### All-time P\&L
```bash theme={null}
curl https://api.polynode.dev/v3/wallets/0x56687bf447db6ffa42ffe2204a05edaa20f55839/pnl
```
### All-time P\&L including combos
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x63613e3b96f418332d43cd2af8dc321014d15907/pnl?include_combos=true"
```
```json theme={null}
{
"address": "0x63613e3b96f418332d43cd2af8dc321014d15907",
"net_realized_pnl": 113.510513,
"realized_pnl": 113.510513,
"gross_profit": 184.901354,
"gross_loss": -71.390841,
"unrealized_pnl": 0.0,
"total_pnl": 113.510513,
"position_count": 21496,
"open_positions": 1495,
"total_volume": 129.837476,
"include_combos": true,
"included_position_types": ["market", "combo"],
"combo_pnl": {
"position_type": "combo",
"included": true,
"realized_pnl": 113.510513,
"unrealized_pnl": 0.0,
"total_pnl": 113.510513,
"position_count": 21492,
"open_positions": 1495
}
}
```
For time-windowed or tag-filtered P\&L, the response stays successful. If combo P\&L cannot be safely added to that filtered view, `combo_pnl.included` is `false` and the standard market P\&L fields remain unchanged.
```json theme={null}
{
"address": "0x56687bf447db6ffa42ffe2204a05edaa20f55839",
"net_realized_pnl": 22053845.825455,
"gross_profit": 22057977.181649,
"gross_loss": -4131.356194,
"unrealized_pnl": 0.000688755,
"total_pnl": 22053845.826143753,
"position_count": 22,
"open_positions": 1,
"total_volume": 43013258.515682,
"wins": 18,
"losses": 4,
"elapsed_ms": 41
}
```
### 30-day P\&L
```bash theme={null}
curl https://api.polynode.dev/v3/wallets/0xbddf61af533ff524d27154e589d2d7a81510c684/pnl?period=30d
```
```json theme={null}
{
"address": "0xbddf61af533ff524d27154e589d2d7a81510c684",
"realized_pnl": 1815261.28,
"gross_profit": 1918402.27,
"gross_loss": -103140.98,
"wins": 33039,
"losses": 58867,
"events": 91906,
"source": "realized_pnl_series",
"period": "30d",
"after": 1776134996,
"before": 1778726996,
"elapsed_ms": 5902
}
```
### Filtered P\&L
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x952d11ebff81d6bd3185e608ed3515b94618ab8a/pnl?category=crypto"
curl "https://api.polynode.dev/v3/wallets/0x952d11ebff81d6bd3185e608ed3515b94618ab8a/pnl?tag_slug=us-election&period=30d"
curl "https://api.polynode.dev/v3/wallets/0x952d11ebff81d6bd3185e608ed3515b94618ab8a/pnl?market_slug=will-donald-trump-win-the-popular-vote-in-the-2024-presidential-election"
curl "https://api.polynode.dev/v3/wallets/0x952d11ebff81d6bd3185e608ed3515b94618ab8a/pnl?event_slug=presidential-election-popular-vote-winner-2024"
```
### 30-day P\&L with unrealized
Pass `include_unrealized=true` to include current unrealized P\&L alongside the realized event-window result.
```bash theme={null}
curl https://api.polynode.dev/v3/wallets/0xbddf61af533ff524d27154e589d2d7a81510c684/pnl?period=30d&include_unrealized=true
```
```json theme={null}
{
"address": "0xbddf61af533ff524d27154e589d2d7a81510c684",
"realized_pnl": 1815261.28,
"gross_profit": 1918402.27,
"gross_loss": -103140.98,
"unrealized_pnl": 14714.71,
"total_pnl": 1829975.99,
"open_positions_at_date": 363,
"wins": 33039,
"losses": 58867,
"events": 91906,
"source": "realized_pnl_series",
"period": "30d",
"after": 1776135002,
"before": 1778727002,
"elapsed_ms": 3803
}
```
## Response fields (all-time)
| Field | Type | Description |
| ------------------------- | ------- | ------------------------------------------------- |
| `net_realized_pnl` | number | Net realized P\&L (USD) |
| `gross_profit` | number | Sum of winning positions (USD) |
| `gross_loss` | number | Sum of losing positions (USD, negative) |
| `unrealized_pnl` | number | Paper P\&L from open positions (USD) |
| `total_pnl` | number | `net_realized_pnl + unrealized_pnl` (USD) |
| `wins` | integer | Winning position count |
| `losses` | integer | Losing position count |
| `position_count` | integer | Total positions |
| `open_positions` | integer | Currently held positions |
| `total_volume` | number | Total volume traded (USD) |
| `include_combos` | boolean | Present when `include_combos=true` was requested |
| `included_position_types` | array | Position families included in aggregate totals |
| `combo_pnl` | object | Combo-only contribution to the aggregate response |
## Response fields (with period)
| Field | Type | Description |
| -------------- | ------- | -------------------------------------------------- |
| `realized_pnl` | number | Realized P\&L in the time window (USD) |
| `gross_profit` | number | Sum of winning events in the window (USD) |
| `gross_loss` | number | Sum of losing events in the window (USD, negative) |
| `wins` | integer | Winning event count |
| `losses` | integer | Losing event count |
| `events` | integer | Total P\&L events in the window |
| `period` | string | The requested period |
| `after` | integer | Start timestamp (Unix seconds) |
| `before` | integer | End timestamp (Unix seconds) |
| `source` | string | Dataset label for time-windowed queries |
### Additional fields (with `include_unrealized=true`)
| Field | Type | Description |
| ------------------------ | ------- | ------------------------------------------------------------------------------------------------------ |
| `unrealized_pnl` | number | Current unrealized P\&L for open positions (USD), returned alongside the realized event-window result. |
| `total_pnl` | number | `realized_pnl + unrealized_pnl` (USD) |
| `open_positions_at_date` | integer | Current open position count returned with the unrealized P\&L estimate |
# Wallet P&L Time Series
Source: https://docs.polynode.dev/data/wallets/pnl-events
GET /v3/wallets/{address}/pnl/events
Realized P&L bucketed by hour, day, week, or month — chart-ready time series for a wallet.
Returns wallet P\&L buckets for charting. With `period`, `after`, or `before`, each row is a fixed time window (hour, day, week, or month). A running `cumulative_pnl` is included so the response can be plotted directly.
Without an explicit time filter, the endpoint returns a single all-time summary bucket that matches [`GET /v3/wallets/{address}/pnl`](/data/wallets/pnl).
In event-series mode, `wins`, `losses`, and `events` count realized P\&L events in the selected window. In default summary mode, counts are position counts and the response includes `count_type: "positions"` and `summary_bucket: true`.
Rows at timestamp `0` or earlier are ignored in event-series mode, so explicit event windows start at Unix second `1`.
## Request
```
GET /v3/wallets/{address}/pnl/events
```
### Query parameters
| Parameter | Type | Default | Description |
| --------- | ------- | ------- | ------------------------------------------------------------------------------- |
| `group` | string | `day` | Bucket size: `hour`, `day`, `week`, `month` |
| `period` | string | -- | Shortcut for `after`: `1d`, `7d`, `30d`, `1y` |
| `after` | integer | `1` | Start timestamp (Unix seconds, inclusive). Values below `1` are treated as `1`. |
| `before` | integer | now | End timestamp (Unix seconds, inclusive) |
| `limit` | integer | `5000` | Max buckets returned, clamped 1-10000 |
If `after` and `period` are both set, `after` wins.
## Examples
### All-time summary bucket
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0xbddf61af533ff524d27154e589d2d7a81510c684/pnl/events"
```
```json theme={null}
{
"address": "0xbddf61af533ff524d27154e589d2d7a81510c684",
"granularity": "day",
"bucket_count": 1,
"buckets": [
{
"bucket": 1779908613,
"realized_pnl": 29035964.931344,
"cumulative_pnl": 29035964.931344,
"gross_profit": 29818047.886063,
"gross_loss": -782082.954719,
"wins": 482,
"losses": 33,
"events": 1016,
"position_count": 1016,
"open_positions": 511,
"unrealized_pnl": -26917534.501466,
"total_pnl": 2118430.429878,
"total_volume": 177203290.120101
}
],
"total_realized_pnl": 29035964.931344,
"total_events": 1016,
"total_wins": 482,
"total_losses": 33,
"total_positions": 1016,
"open_positions": 511,
"unrealized_pnl": -26917534.501466,
"total_pnl": 2118430.429878,
"source": "wallet_summary",
"count_type": "positions",
"summary_bucket": true,
"elapsed_ms": 3
}
```
### Daily P\&L for the last 7 days
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x44c1dfe43260c94ed4f1d00de2e1f80fb113ebc1/pnl/events?period=7d&group=day"
```
```json theme={null}
{
"address": "0x44c1dfe43260c94ed4f1d00de2e1f80fb113ebc1",
"granularity": "day",
"after": 1778478745,
"before": 1779083545,
"bucket_count": 3,
"buckets": [
{
"bucket": 1778889600,
"realized_pnl": 0.000525,
"cumulative_pnl": 0.000525,
"gross_profit": 0.000525,
"gross_loss": 0,
"wins": 1,
"losses": 0,
"events": 1
},
{
"bucket": 1778976000,
"realized_pnl": 611.069702,
"cumulative_pnl": 611.070227,
"gross_profit": 618.41811,
"gross_loss": -7.348408,
"wins": 23,
"losses": 3,
"events": 26
},
{
"bucket": 1779062400,
"realized_pnl": 11.774724,
"cumulative_pnl": 622.844951,
"gross_profit": 11.774724,
"gross_loss": 0,
"wins": 2,
"losses": 0,
"events": 2
}
],
"total_realized_pnl": 622.844951,
"total_events": 29,
"total_wins": 26,
"total_losses": 3,
"source": "realized_pnl_series",
"elapsed_ms": 4
}
```
### Hourly P\&L for an explicit window
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x44c1dfe43260c94ed4f1d00de2e1f80fb113ebc1/pnl/events?after=1778976000&before=1779062400&group=hour"
```
### Weekly P\&L for the last year
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x44c1dfe43260c94ed4f1d00de2e1f80fb113ebc1/pnl/events?period=1y&group=week"
```
## Response fields
| Field | Type | Description |
| -------------------------- | ------- | ------------------------------------------------------------------------------ |
| `address` | string | The wallet address (lowercased) |
| `granularity` | string | Resolved bucket size (`hour`, `day`, `week`, `month`) |
| `after` | integer | Start of the window (Unix seconds, inclusive) |
| `before` | integer | End of the window (Unix seconds, inclusive) |
| `bucket_count` | integer | Number of buckets returned |
| `buckets` | array | Time-series rows, ordered oldest → newest |
| `buckets[].bucket` | integer | Bucket start timestamp (Unix seconds, UTC, truncated to bucket boundary) |
| `buckets[].realized_pnl` | number | Realized P\&L in that bucket (USD) |
| `buckets[].cumulative_pnl` | number | Running sum of `realized_pnl` over the returned series (USD) |
| `buckets[].gross_profit` | number | Sum of winning events in that bucket (USD) |
| `buckets[].gross_loss` | number | Sum of losing events in that bucket (USD, negative) |
| `buckets[].wins` | integer | Count of winning events in that bucket |
| `buckets[].losses` | integer | Count of losing events in that bucket |
| `buckets[].events` | integer | Total P\&L events in event-series mode; position count in default summary mode |
| `buckets[].position_count` | integer | Position count. Present in default summary mode. |
| `buckets[].open_positions` | integer | Open position count. Present in default summary mode. |
| `buckets[].unrealized_pnl` | number | Current unrealized P\&L. Present in default summary mode. |
| `buckets[].total_pnl` | number | `realized_pnl + unrealized_pnl`. Present in default summary mode. |
| `buckets[].total_volume` | number | Wallet volume from the summary source. Present in default summary mode. |
| `total_realized_pnl` | number | Sum of `realized_pnl` across all returned buckets (USD) |
| `total_events` | integer | Sum of `events` across all returned buckets |
| `total_wins` | integer | Sum of `wins` across all returned buckets |
| `total_losses` | integer | Sum of `losses` across all returned buckets |
| `source` | string | Dataset label |
| `count_type` | string | `positions` in default summary mode |
| `summary_bucket` | boolean | `true` in default summary mode |
| `elapsed_ms` | integer | Server-side query time in milliseconds |
## Errors
| HTTP | When |
| ----- | ----------------------------------------------------------------- |
| `400` | Invalid wallet address (must be `0x` + 40 hex) |
| `400` | Invalid `group` value (must be `hour`, `day`, `week`, or `month`) |
| `400` | `after` greater than `before` |
# Wallet PolyUSD Flows
Source: https://docs.polynode.dev/data/wallets/polyusd-flows
GET /v3/wallets/{address}/polyusd-flows
Get PolyUSD deposits and withdrawals for a wallet.
Returns PolyUSD deposit and withdrawal history for one wallet.
Deposits are incoming PolyUSD mints to the wallet. Withdrawals are outgoing PolyUSD movements that redeem back to USDC or USDC.e.
This endpoint is focused on user cash movement. It excludes split, merge, and trading settlement mechanics, which are available through the wallet activity and trade endpoints.
## Request
```
GET /v3/wallets/{address}/polyusd-flows
```
### Query parameters
| Parameter | Type | Default | Description |
| --------- | ------- | --------- | --------------------------------------------- |
| `kind` | string | `all` | `all`, `deposit`, or `withdrawal` |
| `after` | integer | `0` | Start of time range (Unix seconds, inclusive) |
| `before` | integer | unlimited | End of time range (Unix seconds, inclusive) |
| `order` | string | `desc` | `asc` or `desc` |
| `limit` | integer | `100` | Max 300 |
| `offset` | integer | `0` | Pagination offset |
## Example
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x2a1f579283C87c4574102bbF6E4B39F7A12fe77E/polyusd-flows?limit=2"
```
```json theme={null}
{
"address": "0x2a1f579283c87c4574102bbf6e4b39f7a12fe77e",
"flows": [
{
"event_type": "withdrawal",
"direction": "out",
"amount": 2690.200068,
"amount_usdc": 2690.200068,
"amount_raw": "2690200068",
"asset": "0x2791bca1f2de4661ed88a30c99a7a9449aa84174",
"asset_symbol": "USDC.e",
"ramp": "CollateralOfframp",
"source_event": "Transfer+Unwrapped",
"transaction_hash": "0xed54095cbf5c5f434cd3c12d7d226e8922bb40d597d0c61a623412567b5f0e45"
},
{
"event_type": "deposit",
"direction": "in",
"amount": 1500,
"amount_usdc": 1500,
"amount_raw": "1500000000",
"asset": "0x2791bca1f2de4661ed88a30c99a7a9449aa84174",
"asset_symbol": "USDC.e",
"ramp": "CollateralOnramp",
"source_event": "Wrapped",
"transaction_hash": "0x1d516fa44c127232be3f8aad9dc974fca73e2f1e1c8716914a8ef6c8222cbf25"
}
],
"rows_returned": 2,
"total_matching_rows": 21,
"has_more": true,
"total_deposited": 12854.05392,
"total_withdrawn": 5930.200068,
"net_deposited": 6923.853852,
"source": "polyusd_flows"
}
```
## Response fields
| Field | Type | Description |
| -------------------------- | ------- | ---------------------------------------------------- |
| `address` | string | Wallet address, lowercased |
| `flows` | array | Deposit and withdrawal rows for the requested page |
| `flows[].event_type` | string | `deposit` or `withdrawal` |
| `flows[].direction` | string | `in` for deposits, `out` for withdrawals |
| `flows[].amount` | number | 6-decimal normalized USD amount |
| `flows[].amount_raw` | string | Raw PolyUSD amount |
| `flows[].asset` | string | Underlying asset address |
| `flows[].asset_symbol` | string | `USDC.e`, `USDC`, or `unknown` |
| `flows[].ramp` | string | Ramp contract used |
| `flows[].recipient` | string | Recipient of minted PolyUSD or unwrapped USDC/USDC.e |
| `flows[].transaction_hash` | string | Polygon transaction hash |
| `flows[].block_number` | integer | Polygon block number |
| `flows[].block_timestamp` | integer | Polygon block timestamp |
| `rows_returned` | integer | Number of rows returned |
| `total_matching_rows` | integer | Total rows after filters before pagination |
| `has_more` | boolean | Whether another page exists |
| `total_deposited` | number | Sum of matching deposit rows |
| `total_withdrawn` | number | Sum of matching withdrawal rows |
| `net_deposited` | number | Deposits minus withdrawals |
| `source` | string | Dataset label |
## Errors
| HTTP | When |
| ----- | ------------------------------------------ |
| `400` | Invalid wallet address, `kind`, or `order` |
| `401` | Missing or invalid PolyNode API key |
| `502` | Temporary data provider failure |
# Wallet Positions
Source: https://docs.polynode.dev/data/wallets/positions
GET /v3/wallets/{address}/positions
Get all positions for a wallet with market context, current prices, unrealized P&L, and settlement status.
Returns every position a wallet has ever held, enriched with market metadata, live token prices, computed unrealized P\&L, settlement status, and redemption data. All amounts are in USD.
## Request
```
GET /v3/wallets/{address}/positions
```
### Path parameters
| Parameter | Type | Description |
| --------- | ------ | ---------------------------------------------- |
| `address` | string | Wallet address (0x-prefixed, case-insensitive) |
### Query parameters
| Parameter | Type | Default | Description |
| ---------------- | ------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `status` | string | all | Filter: `open`, `closed`, `redeemable`, `redeemed` |
| `sort` | string | `realized_pnl` | Sort by: `realized_pnl` (default), `amount` (current shares), `volume` / `total_bought`, `avg_price`, `recent` (last trade time) |
| `order` | string | `desc` | Sort direction: `asc` or `desc` |
| `condition_id` | string | -- | Filter by market condition ID |
| `market_slug` | string | -- | Filter by market slug |
| `min_size` | number | -- | Minimum position size in USD (e.g. `min_size=10.0`) |
| `include_combos` | boolean | `false` | Append combo positions to the response. Combo rows have `position_type: "combo"` and include `legs` metadata. |
| `limit` | integer | 100 | Results per page (max 300) |
| `offset` | integer | 0 | Pagination offset |
### Position statuses
Wallet and global position endpoints use lifecycle statuses:
| Status | Meaning |
| ------------ | --------------------------------------------------------------- |
| `open` | Wallet holds shares in an active (unresolved) market |
| `closed` | Position fully sold before market resolved |
| `redeemable` | Market resolved, wallet still holds shares that can be redeemed |
| `redeemed` | Market resolved and shares have been redeemed |
Market-scoped holder endpoints may use `open`/`closed` differently when explicitly documented as holder-state filters: `open` means the current balance is nonzero, and `closed` means the current balance is zero.
## Examples
### All positions
```bash theme={null}
curl https://api.polynode.dev/v3/wallets/0xa9857c7bcb9bcfafd2c132ab053f34f678610058/positions?limit=1
```
```json theme={null}
{
"address": "0xa9857c7bcb9bcfafd2c132ab053f34f678610058",
"positions": [
{
"token_id": "56311531524793560667677947517355066819344622894461317832902534277368408457717",
"size": 0.003332,
"avg_price": 0.479999,
"realized_pnl": 0.083202,
"unrealized_pnl": 0.0,
"total_pnl": 0.083202,
"current_price": 0.0,
"total_bought": 2.083332,
"initial_value": 0.001599,
"redeemable_payout": 0.0,
"resolved": true,
"price_source": "settlement",
"status": "redeemable",
"market": "Bitcoin Up or Down - May 13, 6:55AM-7:00AM ET",
"slug": "btc-updown-5m-1778669700",
"outcome": "Down",
"outcome_index": 1,
"opposite_asset": "75458928898922591833639657019011228747535375591777673334858165385124723756977",
"image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/BTC+fullsize.png",
"condition_id": "0xa1118bf183058190b9b47529d94eed53d0210c65531990019c09b893cb64b73b",
"event_slug": "btc-updown-5m-1778669700",
"neg_risk": false,
"market_status": "live",
"tag_slugs": ["Hide From New", "Recurring", "Up or Down", "Crypto Prices", "Crypto", "Bitcoin", "5M"],
"last_trade_at": "1778670064",
"resolved_at": "0",
"winning_outcome_index": null,
"won": null
}
],
"rows_returned": 1,
"has_more": true,
"offset": 0,
"limit": 1,
"elapsed_ms": 45
}
```
### Only open positions
```bash theme={null}
curl https://api.polynode.dev/v3/wallets/0x.../positions?status=open
```
### Only redeemable (resolved but not yet claimed)
```bash theme={null}
curl https://api.polynode.dev/v3/wallets/0x.../positions?status=redeemable
```
The `redeemable_payout` field shows the exact USD amount the wallet can claim.
### Sort by most recent trade
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x.../positions?sort=recent"
```
### Sort by current position size
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x.../positions?sort=amount"
```
### Include combo positions
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x63613e3b96f418332d43cd2af8dc321014d15907/positions?include_combos=true&limit=2"
```
When combo rows are present, they are appended with `position_type: "combo"` and a combo-specific field set:
```json theme={null}
{
"position_type": "combo",
"combo_condition_id": "0x030002fa8781d9f445d838e60524d70bf30000000000000000000000000000",
"combo_position_id": "1356959103499736670017337806334234879289930581423500836189165811753797287936",
"shares_balance": "0.000100",
"entry_avg_price_usdc": "0.2285",
"entry_cost_usdc": "0.00",
"realized_pnl_usdc": "0.000000",
"total_pnl_usdc": "0.000000",
"status": "open",
"legs": [
{
"leg_index": 0,
"leg_position_id": "943244391856361009754982642766252708422485833075867696330382296106234020352",
"leg_outcome_label": "Yes",
"leg_status": "OPEN"
}
]
}
```
## Response fields
### Top-level combo fields
| Field | Type | Description |
| ------------------------- | ------- | ---------------------------------------------------------------- |
| `include_combos` | boolean | Present when `include_combos=true` was requested |
| `included_position_types` | array | Includes `market` and `combo` when combo inclusion was requested |
| `combo_position_count` | integer | Number of combo position rows returned in this page |
### Position data
| Field | Type | Description |
| ------------------- | ------- | ------------------------------------------------------------------- |
| `token_id` | string | Outcome token ID |
| `size` | number | Current shares held in USD (0 if closed/redeemed) |
| `avg_price` | number | Weighted-average entry price in USD |
| `realized_pnl` | number | Realized P\&L from closed trades (USD) |
| `unrealized_pnl` | number | Paper P\&L: `(current_price - avg_price) * size` |
| `total_pnl` | number | `realized_pnl + unrealized_pnl` |
| `current_price` | number | Latest token price (USD, from settlement, CLOB mid, or last fill) |
| `total_bought` | number | Total USD spent buying this position |
| `initial_value` | number | `avg_price * size` at current holdings |
| `redeemable_payout` | number | USD claimable if redeemed now (0 for losing outcomes) |
| `resolved` | boolean | Whether the market has settled |
| `price_source` | string | Price source: `settlement`, `clob_mid`, `v1_condition`, `last_fill` |
| `status` | string | `open`, `closed`, `redeemable`, or `redeemed` |
### Market context
| Field | Type | Description |
| ---------------- | ------- | ---------------------------------------------------- |
| `market` | string | Market question text |
| `slug` | string | Market URL slug |
| `outcome` | string | Outcome label (e.g. "Yes", "No", "Trump") |
| `outcome_index` | integer | Outcome position (0 or 1) |
| `opposite_asset` | string | Token ID of the other outcome in this market |
| `image` | string | Market image URL |
| `condition_id` | string | Market condition ID |
| `event_slug` | string | Parent event slug |
| `neg_risk` | boolean | Whether this is a neg-risk (multi-outcome) market |
| `market_status` | string | `live`, `closed`, `resolved-win`, or `resolved-loss` |
| `tag_slugs` | array | Category tags for this market |
### Timestamps and resolution
| Field | Type | Description |
| ----------------------- | ------- | ------------------------------------------------------- |
| `last_trade_at` | string | Unix timestamp of the most recent fill for this token |
| `resolved_at` | string | Unix timestamp when the market was resolved |
| `winning_outcome_index` | integer | Which outcome won (0 or 1), null if unresolved |
| `won` | boolean | Whether this position's outcome won, null if unresolved |
## Use cases
* **Portfolio dashboard** — show a trader's open positions with live P\&L and market context
* **Redemption alerts** — find wallets with unclaimed payouts using `?status=redeemable`
* **Performance analysis** — sort by P\&L to see best and worst positions
* **Copy trading** — see what a wallet is currently holding with `?status=open&sort=amount`
# Wallet Maker Rebates
Source: https://docs.polynode.dev/data/wallets/rebates
GET /v3/wallets/{address}/rebates
Get Polymarket-reported maker rebates for a wallet and date.
Returns maker rebate records reported by Polymarket for one maker address and one date.
Maker rebates are Polymarket accounting credits. They are not the same value as trader fees paid.
Polymarket rebate lookups are keyed by CLOB maker/signing address. If you pass a Safe or proxy wallet that Polymarket does not accept as a maker address, PolyNode returns an empty `rebates` array with an explanatory `hint`.
## Request
```
GET /v3/wallets/{address}/rebates
```
### Query parameters
| Parameter | Type | Default | Description |
| --------- | ------ | ---------------- | --------------------------- |
| `date` | string | current UTC date | Date in `YYYY-MM-DD` format |
## Example
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x8ecb4b228e07b6ddc58a32997093032a6907b8f6/rebates"
```
```json theme={null}
{
"address": "0x8ecb4b228e07b6ddc58a32997093032a6907b8f6",
"date": "2026-05-27",
"rebates": [],
"rows_returned": 0,
"total_rebated_fees_usdc": 0,
"source": "polymarket_rebates",
"rebate_semantics": "Maker rebates reported for one maker/date. Separate from trader-paid fees.",
"elapsed_ms": 271
}
```
## Response fields
| Field | Type | Description |
| ------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------- |
| `address` | string | Wallet or maker address, lowercased |
| `date` | string | Date used for the rebate lookup |
| `rebates` | array | Polymarket rebate records |
| `rows_returned` | integer | Number of rebate records returned |
| `total_rebated_fees_usdc` | number | Sum of `rebated_fees_usdc` across returned records when present |
| `source` | string | Dataset label |
| `rebate_semantics` | string | Accounting note |
| `provider_status` | integer | Present when the rebate data provider returned an invalid maker-address response that PolyNode converted to an empty result |
| `provider_warning` | object | Present with the provider warning body for invalid maker-address responses |
| `hint` | string | Present when a Safe/proxy address may need to be resolved to a maker EOA |
| `elapsed_ms` | integer | Server-side request time in milliseconds |
The objects inside `rebates` follow Polymarket's rebate record format. When a record includes `rebated_fees_usdc`, PolyNode includes it in `total_rebated_fees_usdc`.
## Errors
| HTTP | When |
| ----- | -------------------------------------- |
| `400` | Invalid wallet address |
| `400` | Invalid `date` format |
| `401` | Missing or invalid PolyNode API key |
| `502` | Temporary rebate data provider failure |
# Wallet Redemptions
Source: https://docs.polynode.dev/data/wallets/redemptions
GET /v3/wallets/{address}/redemptions
Get all redemption events for a wallet. Redemptions occur when a wallet claims payouts from resolved markets.
Returns redemption events where the wallet claimed payouts from resolved market conditions.
Rows are enriched with market and resolution metadata when available so clients can identify
the winning outcome without a second market lookup. The original response fields remain
present and unchanged.
## Request
```
GET /v3/wallets/{address}/redemptions
```
### Query parameters
| Parameter | Type | Default | Description |
| --------- | ------- | ------- | ------------------------------------ |
| `after` | integer | -- | Start of time range (Unix timestamp) |
| `before` | integer | -- | End of time range |
| `limit` | integer | 100 | Max 300 |
| `offset` | integer | 0 | Pagination offset |
## Example
```bash theme={null}
curl https://api.polynode.dev/v3/wallets/0x56687bf447db6ffa42ffe2204a05edaa20f55839/redemptions?limit=1
```
```json theme={null}
{
"data": [
{
"id": "0x6173027f191670852e049df6ded0b353069a531f5ea647220bdcb64ab634348e_cac",
"timestamp": "1776731610",
"condition": "0x19be7c46e28b61455ec766679ee1122fba39eb0d5c85d0878e4b203ab8b18406",
"payout": "4078910900",
"index_sets": ["1", "2"],
"condition_id": "0x19be7c46e28b61455ec766679ee1122fba39eb0d5c85d0878e4b203ab8b18406",
"payout_e6": "4078910900",
"payout_usdc": 4078.9109,
"market_title": "Will John Ternus be the next CEO of Apple?",
"market_slug": "will-john-ternus-be-the-next-ceo-of-apple",
"market_image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/next-ceo-of-apple-leUfWn-_USCO.jpg",
"event_title": "Next CEO of Apple?",
"event_slug": "next-ceo-of-apple",
"neg_risk": true,
"token_ids": [
"92311850691119989700494243314039291208974625045063458908184129720108035114164",
"14954786464025422340381119759227166561985048022429770302259194276382916274663"
],
"outcomes": ["Yes", "No"],
"payouts": [1.0, 0.0],
"outcome_prices": [1.0, 0.0],
"winning_outcome_index": 0,
"winning_outcome": "Yes",
"winning_token_id": "92311850691119989700494243314039291208974625045063458908184129720108035114164",
"resolved_at": "2026-05-28T04:49:07.589252+00:00"
}
],
"rows_returned": 1,
"has_more": true,
"elapsed_ms": 3
}
```
## Response fields
| Field | Type | Description |
| ----------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `id` | string | Redemption event ID |
| `timestamp` | string | Unix timestamp |
| `condition` | string | Market condition ID that was redeemed |
| `payout` | string | Exact raw payout amount in six-decimal integer units. Preserved for backwards compatibility. |
| `index_sets` | array | Which outcome index sets were redeemed |
| `condition_id` | string | Alias of `condition`, matching the WebSocket redemption payload naming. |
| `payout_e6` | string | Alias of `payout`; exact six-decimal payout amount for accounting-safe comparisons. |
| `payout_usdc` | number | Human-readable payout amount in USDC. |
| `market_title` | string \| null | Human-readable market question. |
| `market_slug` | string \| null | Market slug. |
| `market_image` | string \| null | Market image URL. |
| `event_title` | string \| null | Parent event title, when available. |
| `event_slug` | string \| null | Parent event slug, when available. |
| `neg_risk` | boolean \| null | Whether the market uses Polymarket negative-risk settlement. |
| `token_ids` | string\[] \| null | Ordered market token IDs. Same order as `outcomes`, `payouts`, and `outcome_prices`. |
| `outcomes` | string\[] \| null | Ordered outcome labels for the market. |
| `payouts` | number\[] \| null | Resolved payout numerators for each outcome, when resolution metadata is available. |
| `outcome_prices` | number\[] \| null | Payouts normalized by the payout denominator. Winners are usually `1.0`; losers are usually `0.0`. |
| `winning_outcome_index` | number \| null | Index of the winning outcome when exactly one outcome has a non-zero payout, or when metadata already provides the winner. |
| `winning_outcome` | string \| null | Label of the winning outcome. |
| `winning_token_id` | string \| null | Token ID of the winning outcome. |
| `resolved_at` | string \| null | ISO timestamp for when the market resolution metadata indicates the condition became resolved. |
# Wallet Splits
Source: https://docs.polynode.dev/data/wallets/splits
GET /v3/wallets/{address}/splits
Get all split events for a wallet. Splits occur when USDC is converted into outcome tokens.
Returns split events where USDC was converted into outcome tokens for a market condition.
## Request
```
GET /v3/wallets/{address}/splits
```
### Query parameters
| Parameter | Type | Default | Description |
| --------- | ------- | ------- | ------------------------------------ |
| `after` | integer | -- | Start of time range (Unix timestamp) |
| `before` | integer | -- | End of time range |
| `limit` | integer | 100 | Max 300 |
| `offset` | integer | 0 | Pagination offset |
## Example
```bash theme={null}
curl https://api.polynode.dev/v3/wallets/0xada100874d00e3331d00f2007a9c336a65009718/splits?limit=1
```
```json theme={null}
{
"data": [
{
"id": "0x48a4ffe402ea660ccb9dea5f802ca4ea5d22ed395ae07d249fca0ec94b79d325_0x566",
"timestamp": "1778492385",
"condition": "0xd863a481323564b64f431d4c8fa8f0237f56075b2bb4b6f3ec9be8986ec292f0",
"amount": "33927075"
}
],
"rows_returned": 1,
"has_more": true,
"elapsed_ms": 3
}
```
## Response fields
| Field | Type | Description |
| ----------- | ------ | ----------------------------- |
| `id` | string | Split event ID |
| `timestamp` | string | Unix timestamp |
| `condition` | string | Market condition ID |
| `amount` | string | USDC amount split (6-decimal) |
# Wallet Summary
Source: https://docs.polynode.dev/data/wallets/summary
GET /v3/wallets/{address}
Get a complete P&L summary for any Polymarket wallet in USD. Realized and unrealized profit/loss, win rate, position count, and total volume.
Returns a full profile for a single wallet in USD. Includes realized P\&L, unrealized P\&L from open positions, gross profit and loss, win/loss record, and total trading volume.
## Request
```
GET /v3/wallets/{address}
```
### Path parameters
| Parameter | Type | Description |
| --------- | ------ | ---------------------------------------------- |
| `address` | string | Wallet address (0x-prefixed, case-insensitive) |
### Query parameters
| Parameter | Type | Default | Description |
| ---------------------------- | ------- | ------- | -------------------------------------------------------------------------------------------------------- |
| `include_combos` | boolean | `false` | Add combo P\&L and combo position counts to the all-time wallet summary. |
| `include_accounting_summary` | boolean | `false` | Add the all-time wallet accounting summary, including exact trader-paid fees from indexed onchain fills. |
## Example
```bash theme={null}
curl https://api.polynode.dev/v3/wallets/0x56687bf447db6ffa42ffe2204a05edaa20f55839
```
### Summary including combos
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x63613e3b96f418332d43cd2af8dc321014d15907?include_combos=true"
```
```json theme={null}
{
"address": "0x63613e3b96f418332d43cd2af8dc321014d15907",
"net_realized_pnl": 113.510513,
"realized_pnl": 113.510513,
"total_pnl": 113.510513,
"position_count": 21496,
"open_positions": 1495,
"include_combos": true,
"included_position_types": ["market", "combo"],
"combo_pnl": {
"position_type": "combo",
"included": true,
"realized_pnl": 113.510513,
"total_pnl": 113.510513,
"position_count": 21492,
"open_positions": 1495
}
}
```
### Summary including accounting
Use this when you need the exact all-time fee summary without paging through raw fill rows.
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x7553e42ce1b37727b819dfe4bac495b3968f65ca?include_accounting_summary=true"
```
```json theme={null}
{
"address": "0x7553e42ce1b37727b819dfe4bac495b3968f65ca",
"net_realized_pnl": -138570.0369464795,
"gross_profit": 524921.0508558493,
"gross_loss": -671088.850081543,
"unrealized_pnl": -7597.762279214215,
"total_pnl": -146167.79922569369,
"position_count": 915,
"open_positions": 0,
"total_volume": 2718779.615141,
"accounting_summary": {
"available": true,
"loaded": true,
"has_order_filled_rows": true,
"source": "api.wallet_accounting_summary_current",
"fees_paid": {
"available": true,
"loaded": true,
"amount": 99973.115598,
"raw": "99973115598",
"fee_fill_count": 2984,
"order_owner_fill_count": 5122,
"first_fee_block": "84892414",
"last_fee_block": "88904629"
},
"maker_rebates": {
"available": false,
"loaded": false,
"status": "not_loaded",
"amount": null,
"raw": null
},
"rewards": {
"available": false,
"loaded": false,
"status": "not_loaded",
"amount": null,
"raw": null
},
"order_filled_through_block": "88978163",
"projector_status": "ready"
},
"elapsed_ms": 2
}
```
```json theme={null}
{
"address": "0x56687bf447db6ffa42ffe2204a05edaa20f55839",
"net_realized_pnl": 22053845.825455,
"gross_profit": 22057977.181649,
"gross_loss": -4131.356194,
"unrealized_pnl": 0.000688755,
"total_pnl": 22053845.826143753,
"wins": 18,
"losses": 4,
"position_count": 22,
"open_positions": 1,
"total_volume": 43013258.515682,
"elapsed_ms": 1
}
```
## Response fields
| Field | Type | Description |
| ----------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------ |
| `net_realized_pnl` | number | Net realized P\&L in USD |
| `gross_profit` | number | Sum of all winning position P\&L (USD) |
| `gross_loss` | number | Sum of all losing position P\&L (USD, negative) |
| `unrealized_pnl` | number | Unrealized P\&L from open positions based on current prices (USD) |
| `total_pnl` | number | `net_realized_pnl + unrealized_pnl` (USD) |
| `wins` | integer | Number of positions closed with positive P\&L |
| `losses` | integer | Number of positions closed with negative P\&L |
| `position_count` | integer | Total positions (open + closed) |
| `open_positions` | integer | Positions with shares > 0 |
| `total_volume` | number | Total volume traded (USD) |
| `combo_pnl` | object | Present when `include_combos=true`; combo-only contribution to the wallet summary |
| `accounting_summary` | object | Present when `include_accounting_summary=true`; all-time compact wallet accounting summary |
| `accounting_summary.fees_paid.amount` | number | Exact all-time trader-paid fees in USD |
| `accounting_summary.fees_paid.raw` | string | Exact all-time trader-paid fees in 6-decimal raw units |
| `accounting_summary.fees_paid.fee_fill_count` | integer | Number of order-owner fills with a positive fee |
| `accounting_summary.fees_paid.order_owner_fill_count` | integer | Number of indexed order-owner fills for the wallet |
| `accounting_summary.maker_rebates.status` | string | Maker rebate projection status. `not_loaded` means do not treat the amount as zero. |
| `accounting_summary.rewards.status` | string | Reward projection status. `not_loaded` means do not treat the amount as zero. |
| `accounting_summary.order_filled_through_block` | string | Latest block covered by the order-filled accounting projector |
| `accounting_summary.projector_status` | string | Current projector status |
# Wallet Trades
Source: https://docs.polynode.dev/data/wallets/trades
GET /v3/wallets/{address}/trades
Get all trades for a wallet with market context, computed price, direction, and order hash.
Returns fills where the wallet participated as maker, taker, or both. Each trade is enriched with market metadata, computed price/size in USD, buy/sell direction, and the order hash.
## Request
```
GET /v3/wallets/{address}/trades
```
### Query parameters
| Parameter | Type | Default | Description |
| ---------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `side` | string | `both` | `maker`, `taker`, or `both` |
| `token_id` | string | -- | Filter trades involving this token |
| `condition_id` | string | -- | Filter by market condition ID (resolves to token IDs) |
| `market_slug` | string | -- | Filter by market slug (resolves to token IDs) |
| `min_amount` | integer | -- | Minimum `maker_amount_filled` (raw 6-decimal) |
| `sort_by` | string | -- | `order_hash` groups fills by limit order visually (individual fill rows, sorted by most recent order) |
| `group_by` | string | -- | `user_trade` returns Polymarket-style user-visible trade rows; `transaction_hash` is an alias; `order_hash` aggregates wallet-visible fills per order |
| `after` | integer | -- | Start of time range (Unix timestamp) |
| `before` | integer | -- | End of time range (Unix timestamp) |
| `include_combos` | boolean | `false` | Add combo trades in a separate `combo_trades` branch. If the wallet has no combo trades, the response remains 200 with an empty combo branch. |
| `order` | string | `desc` | `asc` or `desc` |
| `limit` | integer | 100 | Max 300 |
| `offset` | integer | 0 | Pagination offset |
## Example
```bash theme={null}
curl https://api.polynode.dev/v3/wallets/0xa9857c7bcb9bcfafd2c132ab053f34f678610058/trades?limit=1
```
```json theme={null}
{
"address": "0xa9857c7bcb9bcfafd2c132ab053f34f678610058",
"trades": [
{
"id": "0x284b61d18c8e0a60333bfe883288c7d9861c9c07a410050f537550940038a713_951",
"maker": "0xa9857c7bcb9bcfafd2c132ab053f34f678610058",
"taker": "0xe111180000d2663c0091e4f400237545b87b996b",
"maker_asset_id": "0",
"taker_asset_id": "75783394880030392863380883800697645018418815910449662777195626260206142035810",
"maker_amount": 0.999999,
"taker_amount": 1.694914,
"fee": 0.01229,
"price": 0.59,
"size": 1.694914,
"timestamp": "1778674056",
"transaction_hash": "0x284b61d18c8e0a60333bfe883288c7d9861c9c07a410050f537550940038a713",
"order_hash": "0x21245a1d81ff19d7effcdb7e3b78d5fe66098708a7dc7cad2267f81d237acc7f",
"builder": "0x0000000000000000000000000000000000000000000000000000000000000000",
"side": 0,
"role": "maker",
"direction": "BUY",
"market": "Dota 2: Aurora vs Team Liquid (BO3) - DreamLeague Group A",
"slug": "dota2-aur1-liquid-2026-05-13",
"outcome": "Aurora",
"outcome_index": 0,
"image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/dota2-7ffacddb21.jpg",
"condition_id": "0x4f05dbc6273b89aed46bb79a961c1d8771c01925d92d439e9a81fa6241900661"
}
],
"rows_returned": 1,
"has_more": true,
"offset": 0,
"limit": 1,
"elapsed_ms": 40
}
```
### Maker-only trades
```bash theme={null}
curl https://api.polynode.dev/v3/wallets/0x.../trades?side=maker&limit=10
```
### Trades for a specific token
```bash theme={null}
curl https://api.polynode.dev/v3/wallets/0x.../trades?token_id=75783394...&limit=10
```
### Trades in a time window
```bash theme={null}
curl https://api.polynode.dev/v3/wallets/0x.../trades?after=1778000000&before=1778604000
```
### Include combo trades
```bash theme={null}
curl "https://api.polynode.dev/v3/wallets/0x63613e3b96f418332d43cd2af8dc321014d15907/trades?include_combos=true&limit=5"
```
Combo trades are returned in `combo_trades` so existing consumers of the standard `trades` array do not need to change parsing logic.
```json theme={null}
{
"address": "0x63613e3b96f418332d43cd2af8dc321014d15907",
"trades": [],
"rows_returned": 0,
"include_combos": true,
"included_position_types": ["market", "combo"],
"combo_trade_count": 5,
"combo_has_more": true,
"combo_source": "v3.wallet_combos.trades",
"combo_trades": [
{
"position_type": "combo",
"wallet_role": "maker",
"wallet_address": "0x63613e3b96f418332d43cd2af8dc321014d15907",
"counterparty_address": "0xe3333700ca9d93003f00f0f71f8515005f6c00aa",
"combo_condition_id": "0x0310c8c1e924b0ddffd987430c42fc26e10000000000000000000000000000",
"position_id": "1741334187009265192213210063949860811096650382021683265628751751539647840256",
"side": "BUY",
"price": "0.0197",
"size": "0.050750",
"fee": "0.000020",
"tx_hash": "0xf3a8985f04bf869483ef4163a185f296c834eb827b5e5ae3db5bd44558121d51",
"block_number": 88276713,
"timestamp": 1781120055,
"source": "v3.wallet_combos.trades"
}
]
}
```
## Response fields
### Top-level combo fields
| Field | Type | Description |
| ------------------------- | ------- | ---------------------------------------------------------------- |
| `include_combos` | boolean | Present when `include_combos=true` was requested |
| `included_position_types` | array | Includes `market` and `combo` when combo inclusion was requested |
| `combo_trades` | array | Combo trade rows for the wallet |
| `combo_trade_count` | integer | Number of combo trade rows returned in this page |
| `combo_has_more` | boolean | Whether more combo trade rows are available |
| `combo_source` | string | Source label for the combo trade branch |
### Trade data
| Field | Type | Description |
| ------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `id` | string | Unique fill ID (transaction\_hash + log index) |
| `maker` | string | Maker wallet address |
| `taker` | string | Taker wallet address |
| `maker_asset_id` | string | Asset the maker provided (`0` = USDC) |
| `taker_asset_id` | string | Asset the taker provided (`0` = USDC) |
| `maker_amount` | number | USD amount the maker provided |
| `taker_amount` | number | USD amount the taker provided |
| `fee` | number | Fee charged (USD) |
| `token_id` | string | Outcome token ID for the visible trade leg |
| `asset` | string | Alias for `token_id`, included for Polymarket compatibility |
| `amount` | number | Alias for `size`, included for Polymarket compatibility |
| `price` | number | Computed price per outcome token (maker\_amount / taker\_amount or inverse) |
| `size` | number | Number of outcome tokens traded (USD) |
| `timestamp` | string | Unix timestamp |
| `transaction_hash` | string | On-chain transaction hash |
| `order_hash` | string | Unique order hash for this fill |
| `builder` | string | Builder attribution code (hex) |
| `side` | integer | Maker's limit order direction: `0` = maker was buying tokens, `1` = maker was selling tokens, `null` for V1 trades. This is the maker's side, not the queried wallet's action. Use `direction` instead for the wallet's perspective. |
| `role` | string | `maker` or `taker` -- which side the queried wallet was on in this fill |
| `direction` | string | `BUY` or `SELL` -- whether the queried wallet bought or sold outcome tokens. This is the field you want for understanding what the wallet did. |
### Market context
| Field | Type | Description |
| --------------- | ------- | ------------------------------------ |
| `market` | string | Market question text |
| `slug` | string | Market URL slug |
| `outcome` | string | Outcome label (e.g. "Yes", "Aurora") |
| `outcome_index` | integer | Outcome position (0 or 1) |
| `image` | string | Market image URL |
| `condition_id` | string | Market condition ID |
### Sort fills by order hash
```bash theme={null}
curl https://api.polynode.dev/v3/wallets/0xa9857c.../trades?sort_by=order_hash&limit=10
```
Groups all fills belonging to the same limit order together visually, sorted by most recent order first. Returns individual fill rows.
### User-visible trades
```bash theme={null}
curl https://api.polynode.dev/v3/wallets/0xa9857c.../trades?market_slug=dota2-aur1-liquid-2026-05-13&group_by=user_trade
```
Returns one row per user-visible trade, matching the Polymarket-style wallet trade feed. This removes complementary order-side rows that can appear in raw CTF fills and merges fills that represent the same user action. Use this grouping for UI trade history and user-facing volume math.
`group_by=transaction_hash` is accepted as an alias for this behavior.
### Aggregate fills by order hash
```bash theme={null}
curl https://api.polynode.dev/v3/wallets/0x61b0ea32aff59e9893e867a2ab196476ab22dd96/trades?group_by=order_hash
```
Returns one row per wallet-visible order with aggregated totals. Complementary order-side rows are collapsed from the queried wallet's perspective before order aggregation, so UI volume and share counts do not double-count both sides of the same user action. Use `group_by=user_trade` when you want one row per user-visible fill without order-level rollup.
```json theme={null}
{
"trades": [
{
"order_hash": "0xea91f0d67086be...",
"fill_count": 5,
"avg_price": 0.57,
"total_amount_usd": 669.11,
"total_usd": 669.11,
"total_shares": 1173.88,
"total_fee": 0.0,
"total_maker_amount": 669.11,
"total_taker_amount": 1173.88,
"first_fill_at": 1778100000,
"last_fill_at": 1778200000,
"tx_hashes": ["0xabc...", "0xdef..."],
"market": "MegaETH market cap (FDV) >$1.5B one day after TGE?",
"slug": "megaeth-fdv-1-5b-one-day-after-tge",
"outcome": "Yes",
"direction": "BUY"
}
],
"rows_returned": 6,
"grouped_by": "order_hash"
}
```
#### Grouped response fields
| Field | Type | Description |
| -------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `order_hash` | string | The limit order hash |
| `fill_count` | integer | Number of fills in this order |
| `avg_price` | number | Volume-weighted average price (`total_amount_usd / total_shares`) |
| `total_amount_usd` | number | Total USD value across all fills. Alias of `total_usd`, included for compatibility with grouped onchain trade responses. |
| `total_usd` | number | Total USD value across all fills |
| `total_shares` | number | Total outcome tokens across all fills |
| `total_fee` | number | Total fees (USD) |
| `total_maker_amount` | number | Raw sum of maker amounts (side-dependent) |
| `total_taker_amount` | number | Raw sum of taker amounts (side-dependent) |
| `first_fill_at` | integer | Earliest fill timestamp (Unix seconds) |
| `last_fill_at` | integer | Latest fill timestamp (Unix seconds) |
| `timestamp` | string | Same as `last_fill_at` but as a string. Present for consistency with ungrouped trade rows. |
| `tx_hashes` | array\ | Unique transaction hashes contributing to this order |
| `market` | string \| null | Market question |
| `slug` | string \| null | Market slug |
| `outcome` | string \| null | Outcome label |
| `outcome_index` | integer \| null | Outcome position (0 or 1) |
| `image` | string \| null | Market image URL |
| `condition_id` | string \| null | Market condition ID |
| `direction` | string | `BUY` or `SELL` (queried wallet's perspective) |
| `role` | string | `maker` or `taker` (which side the queried wallet was on) |
| `side` | integer \| null | Maker's limit-order direction (raw on-chain value). Use `direction` instead. |
| `maker` | string | Maker wallet address |
| `taker` | string | Taker wallet address |
### Filter by market condition
```bash theme={null}
curl https://api.polynode.dev/v3/wallets/0xa9857c.../trades?condition_id=0x4f05dbc6...
```
### Filter by market slug
```bash theme={null}
curl https://api.polynode.dev/v3/wallets/0xa9857c.../trades?market_slug=dota2-aur1-liquid-2026-05-13
```
# Best Practices
Source: https://docs.polynode.dev/guides/best-practices
Patterns for correctly handling the pending-to-confirmed lifecycle, snapshots, and reconnection.
## The pending-to-confirmed lifecycle
PolyNode detects Polymarket settlements **before** they confirm on-chain. This means every settlement goes through two stages, delivered as **two separate WebSocket events**:
1. **`settlement`** — the trade is detected pre-chain (`status: "pending"`)
2. **`status_update`** — the trade confirms in a Polygon block (typically 2–4 seconds later)
Link them together by `tx_hash`:
```javascript theme={null}
const pending = new Map(); // tx_hash → settlement data
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === "settlement" && msg.data.status === "pending") {
pending.set(msg.data.tx_hash, {
...msg.data,
received_at: Date.now(),
});
console.log(`PENDING: ${msg.data.taker_side} $${msg.data.taker_size}`);
}
if (msg.type === "status_update") {
const original = pending.get(msg.data.tx_hash);
if (original) {
const leadMs = msg.data.confirmed_at - original.received_at;
console.log(`CONFIRMED: ${msg.data.latency_ms}ms server lead, ${leadMs}ms client lead`);
pending.delete(msg.data.tx_hash);
}
}
};
```
These are **two separate events**, not an update to the original event. If you only listen for `settlement` events, you'll see pending trades but never know when they confirm.
## Handle the snapshot correctly
When you subscribe, PolyNode sends a **snapshot** of recent events before streaming live data. The snapshot contains both `settlement` and `status_update` events mixed together.
**The gotcha**: Settlement events in the snapshot always have their **original** status (`pending`), even if they were confirmed seconds ago. The corresponding `status_update` is a separate entry in the same snapshot.
If you only process `settlement` events from the snapshot, you'll load stale "pending" settlements that are actually already confirmed — and their confirmation event will never arrive as a live message because it was already sent in the snapshot you ignored.
### Correct snapshot handling
```javascript theme={null}
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === "snapshot") {
// Step 1: Collect all status updates from the snapshot
const confirmations = new Map();
for (const ev of msg.events) {
if (ev.type === "status_update" && ev.data) {
confirmations.set(ev.data.tx_hash, ev.data);
}
}
// Step 2: Process settlements, checking for matching confirmations
for (const ev of msg.events) {
if (ev.type === "settlement" && ev.data) {
const confirmation = confirmations.get(ev.data.tx_hash);
if (confirmation) {
// This settlement is already confirmed — use the confirmation data
handleConfirmedSettlement(ev.data, confirmation);
} else if (ev.data.status === "pending") {
// Genuinely pending — will receive a live status_update soon
handlePendingSettlement(ev.data);
}
}
}
return;
}
// Live events — standard handling
if (msg.type === "settlement") handlePendingSettlement(msg.data);
if (msg.type === "status_update") handleStatusUpdate(msg.data);
};
```
## Add a stuck-pending timeout
In rare cases, a `status_update` might be missed (e.g., network interruption, reconnection timing). Add a timeout so stale pending items don't get stuck forever:
```javascript theme={null}
const PENDING_TIMEOUT_MS = 15_000; // 15 seconds
setInterval(() => {
const now = Date.now();
for (const [hash, data] of pending) {
if (now - data.received_at > PENDING_TIMEOUT_MS) {
console.log(`Timeout: ${hash} — no confirmation after 15s`);
pending.delete(hash);
}
}
}, 5_000);
```
Under normal conditions, confirmations arrive within 2–5 seconds. A 15-second timeout is generous enough to never fire in normal operation but catches edge cases.
## Reconnection with state recovery
When reconnecting, use `snapshot_count` to recover recent state. The snapshot fills in events you missed during the disconnection:
```javascript theme={null}
function connect() {
const ws = new WebSocket("wss://ws.polynode.dev/ws?key=pn_live_YOUR_KEY");
let delay = 1000;
ws.onopen = () => {
delay = 1000;
ws.send(JSON.stringify({
action: "subscribe",
type: "settlements",
filters: { snapshot_count: 100 }, // Catch up on missed events
}));
};
ws.onclose = () => {
setTimeout(connect, Math.min(delay *= 2, 30000));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === "heartbeat") return;
// Handle snapshot + live events as above
};
}
```
The snapshot includes both `settlement` and `status_update` events from the recent buffer. Processing both event types from the snapshot (as shown above) ensures you have accurate state immediately after reconnection.
## Connection health
The server sends a WebSocket Ping frame and a text heartbeat (`{"type": "heartbeat"}`) every 30 seconds. If the server receives no activity from the client within 5 minutes, the connection is closed.
Activity that resets the server's liveness timer:
* **Pong frames** (automatic response to server Ping, handled by most WS libraries)
* **Any text message** you send: subscribe, unsubscribe, or `{"action": "ping"}`
For most clients, this works automatically. All standard WebSocket libraries respond to Ping with Pong:
* **Browser `WebSocket`** — handled by the browser
* **Node.js `ws`** — automatic by default
* **Python `websockets`** — automatic by default
* **Go `gorilla/websocket`** — automatic with `SetPongHandler` (default)
**Cloud-hosted clients (Railway, Render, Heroku, fly.io, AWS ALB):** Many cloud platforms run a reverse proxy in front of your container that intercepts WebSocket Ping/Pong frames at the proxy layer. Your application never sees the server's Ping, so it never sends a Pong, and the server eventually drops the connection for inactivity.
**Symptoms:** Connection works on subscribe, receives a snapshot, then drops after 1-5 minutes with an empty error or no close frame. Reconnects immediately but the cycle repeats.
**Fix:** Send `{"action": "ping"}` every 30 seconds from your application code. This is a regular JSON text frame that passes through any proxy. See the [WebSocket Overview](/websocket/overview#application-level-keepalive) for full code examples.
If your read loop is blocked (e.g. doing heavy synchronous work before reading the next message), the client cannot send Pong frames and the server will eventually drop the connection. Keep your read loop running — offload heavy processing to a separate thread or task.
## Summary
| Pattern | Why |
| -------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| Process **both** `settlement` and `status_update` events | Settlements and confirmations are separate events linked by `tx_hash` |
| Process **both event types from the snapshot** | Snapshot settlements keep their original `pending` status — confirmations are separate entries |
| Add a **stuck-pending timeout** (15s) | Safety net for missed confirmations during reconnection |
| Use **`snapshot_count`** on reconnect | Recovers events missed during disconnection |
| Keep read loop running | Server drops connections with no activity after 5 minutes |
| Send `{"action": "ping"}` every 30s **if cloud-hosted** | Keeps connection alive through reverse proxies that strip Ping/Pong frames |
# Copy Trading Pipeline
Source: https://docs.polynode.dev/guides/copy-trading
Private documentation for copy trading integration partners
# Copy Trading API
Source: https://docs.polynode.dev/guides/copy-trading-api
Build a copy-trading product on top of polynode. Register followers + leaders, we detect fills and webhook your backend an unsigned order ready to sign.
**Beta.** This API is in beta while we onboard the first external customers. The shape of the request/response bodies is stable, but we may add optional fields. Non-breaking changes only. Email `josh@polynode.dev` if you're building something and want direct support.
## What this is
An HTTP + webhook service that does the hard parts of copy trading for you:
* You register your followers' wallets and the leaders they want to copy.
* We watch the Polymarket V2 CLOB for leader fills.
* The instant a leader fills, we size + price a matching order for each follower, build the unsigned V2 CLOB order and EIP-712 typed data, and POST it to your webhook URL.
* Your backend has your user sign the order (via Privy, MetaMask, whatever you use for signing) and submit it to Polymarket.
* Optional: you can turn on a per-trade platform fee that gets pulled into our on-chain FeeEscrow and shared with affiliates.
You never run a polynode node, never subscribe to a WebSocket, never decode a fill on-chain. You just handle signing and UI.
## Who this is for
You're the right fit if:
* You're building a Polymarket-facing trading product (terminal, wallet, copy-trading UI, mobile app).
* Your users already exist and you already hold or sign for their Polymarket wallets — Safe proxies or deposit wallets (typically via Privy embedded wallets).
* You want to ship copy trading in days, not months, and you don't want to maintain the settlement-detection / order-building stack.
If you're a single retail trader looking to copy a whale, this API is not for you — use one of the consumer products built on top of it.
## How it maps to Polymarket
Copy trading on Polymarket V2 is the same order flow as any other trade: your user's wallet (Safe proxy or deposit wallet) is the `maker` of a V2 CLOB Order, signed with EIP-712, submitted to `clob-v2.polymarket.com/order`. The only thing we do is generate the order body and typed data *on the exact same block a leader fills*, so the order's `timestamp` + builder fields are correct and sizing/pricing matches the leader.
For deposit wallet users (`sig_type: 3`), the typed data uses the `TypedDataSign` wrapper automatically. Your signing code should use `signV2Order()` from the polynode SDK, which handles both Safe and deposit wallet signatures transparently.
If you're new to V2, read [V2 Order Details](/guides/v2-details) first — this guide assumes you already know what an `Order` struct looks like.
## Base URL
```
https://api.polynode.dev/copytrade
```
All endpoints are HTTPS. Every request must carry your polynode API key.
## Authentication
Include your polynode API key as a Bearer token on every request:
```http theme={null}
Authorization: Bearer pn_live_...
```
The SHA-256 of your bearer is your **tenant ID** — a 64-char hex string that scopes everything you create. Two different keys = two different tenants. Data is fully isolated at the SQL level: you cannot read or modify anything registered under another tenant's key.
* Free-tier keys are rejected with `402 Payment Required`. You need a paid key (Starter tier or above).
* Rate limits follow your paid tier's limits. The standard `x-ratelimit-{limit,remaining,reset}` response headers are always returned.
## Quick start (5 minutes)
```bash theme={null}
# 1. Create your tenant config — tells us where to webhook events
curl -X POST https://api.polynode.dev/copytrade/config \
-H "Authorization: Bearer $PN_KEY" \
-H "Content-Type: application/json" \
-d '{"webhook_url": "https://your-backend.example/webhooks/polynode-copy"}'
# Response:
# {
# "webhook_url": "https://your-backend.example/webhooks/polynode-copy",
# "webhook_secret": "whsec_abc...", ← save this, shown only once
# "schema_version": 1,
# "supports_fee_escrow": false
# }
# 2. Register one of your users as a follower
curl -X POST https://api.polynode.dev/copytrade/followers \
-H "Authorization: Bearer $PN_KEY" \
-H "Content-Type: application/json" \
-d '{
"wallet": "0xUserWalletAddress...",
"signer": "0xUserEOAAddress...",
"sig_type": 2,
"settings": {
"size_mode": "percentage",
"size_pct": 100,
"min_trade_pusd": 1,
"max_trade_pusd": 1000,
"slippage_bps": 50,
"copy_buys": true,
"copy_sells": true
}
}'
# 3. Point that follower at a leader
curl -X POST "https://api.polynode.dev/copytrade/followers/0xUserSafeAddress.../leaders" \
-H "Authorization: Bearer $PN_KEY" \
-H "Content-Type: application/json" \
-d '{"leaders": ["0xLeaderWalletAddress..."]}'
```
From now on, every time `0xLeaderWalletAddress...` fills an order on V2 CLOB, your webhook endpoint receives a signed POST with an unsigned V2 order ready for your user to sign.
## Concepts
| Term | Meaning |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Tenant** | You. Scoped by `sha256(your API key)`. All data is tenant-partitioned. |
| **Follower** | One of your end users. Identified by their trading `wallet` (Safe or deposit wallet) + their EOA `signer`. `sig_type` is `2` for Safe, `3` for deposit wallet. A follower belongs to exactly one tenant. |
| **Leader** | A Polymarket wallet being copied. Can be attached to many followers across many tenants. |
| **Settings** | Per-follower JSON: how much to copy (%/fixed/match), slippage, whether to copy buys/sells, TP/SL levels, fee config (if enabled). |
| **Leader override** | Per-leader tweak to a follower's settings. `"leader_overrides": {"0xWhale...": {"size_pct": 50}}` means "copy this one leader at 50% size, keep everything else the same". |
| **Delivery** | One webhook POST. Every delivery has a stable `event_id` and a sequence number. Retried up to 5 times; failures land in your DLQ. |
## REST endpoints
All paths below are under `https://api.polynode.dev/copytrade`.
### Config — the tenant-level settings
| Method | Path | Body | Purpose |
| ------- | ----------------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------- |
| `POST` | `/config` | `{webhook_url, builder?}` | Create or upsert your tenant config. Returns a fresh `webhook_secret` (shown **once** — save it). |
| `GET` | `/config` | — | Read your current config. `webhook_secret` is always `null` on reads; rotate if you lost it. |
| `POST` | `/config/rotate-secret` | — | Generate a new `webhook_secret`. Old secret stays valid for 1 hour so in-flight retries don't break. |
| `PATCH` | `/config/fee-escrow` | `{enabled: bool, contract?}` | Turn on/off fee-escrow at the tenant level. See [Fee escrow](#fee-escrow-optional). |
### Followers — your end users
| Method | Path | Body | Purpose |
| -------- | ------------------------------------------------ | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `POST` | `/followers` | `{wallet, signer, sig_type, settings}` | Register a follower. |
| `GET` | `/followers` | — | List all your followers. |
| `GET` | `/followers/{wallet}` | — | Get one. 404 if not yours. |
| `PATCH` | `/followers/{wallet}` | partial `{settings, active}` | Update settings or pause (`active: false`). |
| `DELETE` | `/followers/{wallet}` | — | Remove follower + cascade delete their leaders and positions. |
| `POST` | `/followers/{wallet}/positions/{token_id}/reset` | — | Clear a tracked position. Use when your user manually closed via a different path and you want TP/SL to stop firing. |
### Leaders — who each follower copies
| Method | Path | Body | Purpose |
| -------- | ----------------------------- | ----------------------------------- | ----------------------------- |
| `POST` | `/followers/{wallet}/leaders` | `{leaders: [addr,...], overrides?}` | Attach leaders to a follower. |
| `GET` | `/followers/{wallet}/leaders` | — | List this follower's leaders. |
| `DELETE` | `/followers/{wallet}/leaders` | `{leaders: [addr,...]}` | Detach leaders. |
### Deliveries — observability
| Method | Path | Purpose |
| ------ | ------------------------------- | -------------------------------------------------------------------------------------- |
| `GET` | `/deliveries/recent?limit=50` | Last hour of successful webhook deliveries (tenant-scoped). |
| `GET` | `/deliveries/failed?limit=50` | Your dead-letter queue. Capped at 10 K entries per tenant. |
| `POST` | `/deliveries/{event_id}/replay` | Replay a failed delivery. Same `event_id`, fresh HMAC signed with your current secret. |
### Status
| Method | Path | Returns |
| ------ | --------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `GET` | `/status` | `{ok, followers_count, leaders_count, ws_connected, events_last_hour, dlq_size, schema_version, supports_fee_escrow, uptime_secs}` |
## Follower settings
The `settings` JSON on a follower controls how we size, price, and filter copies.
```jsonc theme={null}
{
"size_mode": "percentage", // "percentage" | "fixed_pusd" | "match_leader"
"size_pct": 100, // for percentage: 100 = 1:1, 50 = half-size
"size_pusd": null, // for fixed_pusd: copy size in pUSD
"min_trade_pusd": 1.0, // skip copies below this
"max_trade_pusd": 10000.0, // clamp copies above this
"slippage_bps": 50, // 50 = 0.5% worse than leader's price
"copy_buys": true, // copy the leader's opens
"copy_sells": true, // copy the leader's closes
"copy_as": ["taker", "maker"], // taker = urgent (IOC), maker = post (GTC)
"token_whitelist": null, // array of token_ids, or null for all
"token_blacklist": null,
"tp_pct": null, // take-profit as a % of entry price (e.g. 20 = +20%)
"tp_price": null, // absolute price threshold
"sl_pct": null, // stop-loss as a % of entry
"sl_price": null,
"tpsl_enabled": true,
"max_trades_per_hour": null, // rate limit per follower (enforcement landing soon)
// Optional fee-escrow config (see Fee Escrow section):
"fee_bps": 0,
"fee_affiliate_wallet": null,
"fee_affiliate_share_bps": 0,
// Per-leader overrides: apply to one specific leader only
"leader_overrides": {
"0xWhale0000000000000000000000000000000000": {
"size_pct": 50,
"copy_sells": false
}
}
}
```
**`size_mode`:**
* `percentage` — size the copy at `size_pct`% of the leader's order size.
* `fixed_pusd` — always copy at `size_pusd` pUSD regardless of leader size.
* `match_leader` — exactly match the leader's size (subject to follower collateral).
**`slippage_bps`:** the copy's price is set at the leader's fill price ± your slippage tolerance. Lower slippage = more protection but more skipped copies when the book moves.
## Webhook contract
Whenever your tenant has a copy to execute, we POST to your `webhook_url`.
### Headers
```http theme={null}
Content-Type: application/json
X-PolyNode-Signature: sha256=
X-PolyNode-Event-Id:
X-PolyNode-Delivery:
X-PolyNode-Sequence:
```
**Always verify `X-PolyNode-Signature`** before processing. During a secret rotation, your code must accept HMACs computed with either your current or your previous secret for up to 1 hour.
Example verification (Node.js):
```js theme={null}
import crypto from "node:crypto";
function verify(rawBody, header, secret) {
const expected = "sha256=" + crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(header), Buffer.from(expected));
}
```
Python:
```python theme={null}
import hmac, hashlib
def verify(raw_body: bytes, header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(header, expected)
```
### Body
```jsonc theme={null}
{
"event_id": "sha256(tx_hash || follower_wallet || fill_index)",
"event_type": "copy_order" | "tp_trigger" | "sl_trigger",
"sequence_number": 1234,
"timestamp": 1777000000,
"leader_wallet": "0x...",
"leader_trade": {
"tx_hash": "0x...",
"token_id": "...",
"side": "BUY" | "SELL",
"price": 0.35,
"size": 100,
"market_title": "...",
"outcome": "Yes" | "No",
"condition_id": "0x...",
"neg_risk": false,
"exchange_address": "0xe111...",
"source": "taker" | "maker",
"exchange_version": "v2"
},
"follower_payload": {
"follower_wallet": "0xSafe...",
"follower_signer": "0xEOA...",
"v2_order": {
"salt": "...",
"maker": "0xSafe...",
"signer": "0xEOA...",
"tokenId": "...",
"makerAmount": "...",
"takerAmount": "...",
"side": 0, // 0 = BUY, 1 = SELL
"signatureType": 2,
"timestamp": 1777000000,
"metadata": "0x0000...",
"builder": "0x0000..." // your builder bytes32 from config.builder
},
"v2_typed_data": {
"domain": {...},
"types": {...},
"message": {...},
"primaryType": "Order"
},
"v2_order_digest": "0x...",
"verifying_contract": "0xe111...",
"clob_host": "clob-v2.polymarket.com",
"copy_size_pusd": 2.5,
"copy_size_tokens": 7.143,
"copy_price": 0.3535
// Present ONLY when fee escrow fires for this event:
// "fee_auth": { ... },
// "submit_target": "cosigner",
// "submit_url": "https://trade.polynode.dev/submit"
}
}
```
### What your backend does with this
1. **Verify the HMAC.** If it doesn't match, drop the event and 200 OK (don't leak that verification failed).
2. **Look up the follower in your system** by `follower_payload.follower_wallet`.
3. **Ask the user to sign `v2_typed_data`.** This is standard EIP-712 via Privy / ethers / viem / your wallet of choice. The user's signer EOA signs.
4. **Submit to Polymarket.** If `fee_auth` is absent, POST to `clob-v2.polymarket.com/order`. If `fee_auth` is present, also have the user sign the `fee_auth.typed_data`, then POST the bundled signed payload to `submit_url`.
5. **Return 2xx within 5 seconds.** We mark the delivery successful. If we don't see 2xx in that window, we retry.
### Delivery semantics
* Up to 5 attempts with backoff `1s / 5s / 15s / 30s / 60s`.
* 2xx within 5s → success, dropped from the retry queue.
* After 5 failures → the event goes to your DLQ (`GET /deliveries/failed`). You can fix your endpoint and `POST /deliveries/{event_id}/replay`.
* The same `event_id` is used on every retry — dedupe on it.
* `sequence_number` is monotonic **per follower** — buffer and process in order. Gaps mean you missed a delivery.
## Fee escrow (optional)
**This is your platform's fee, not Polymarket's.** Polymarket may also charge its own protocol fee or builder rev share — those are separate and always take effect regardless of this setting. Read [Fee Escrow](/guides/fee-escrow) for the full explanation.
If you want to monetize with a per-trade fee — optionally split with affiliates — enable fee escrow:
```bash theme={null}
# 1. Turn on fee escrow at the tenant level
curl -X PATCH https://api.polynode.dev/copytrade/config/fee-escrow \
-H "Authorization: Bearer $PN_KEY" \
-H "Content-Type: application/json" \
-d '{"enabled": true}'
# 2. On a follower, set fee_bps (and optionally affiliate info)
curl -X PATCH https://api.polynode.dev/copytrade/followers/0xSafe... \
-H "Authorization: Bearer $PN_KEY" \
-H "Content-Type: application/json" \
-d '{"settings": {
"fee_bps": 50,
"fee_affiliate_wallet": "0xAffiliate...",
"fee_affiliate_share_bps": 3000
}}'
```
When a fee would be charged for an event, the webhook gets an extra block:
```jsonc theme={null}
{
"follower_payload": {
"...": "...",
"fee_auth": {
"escrow_order_id": "0x...",
"payer": "0xSafe...",
"signer": "0xEOA...",
"fee_amount_raw": "50000", // 6-decimal pUSD
"fee_amount_pusd": 0.05,
"deadline": 1777000600,
"nonce": 12,
"affiliate": "0xAffiliate...",
"affiliate_share_bps": 3000,
"escrow_contract": "0x3A43D88ef8Aae4dF5a50B3abf67122CAAeEF7c9F",
"typed_data": {...}, // EIP-712, V2 domain
"digest": "0x..."
},
"submit_target": "cosigner",
"submit_url": "https://trade.polynode.dev/submit"
}
}
```
**Matrix:**
| Tenant fee-escrow | Follower `fee_bps` | `fee_auth` on webhook | Submit path |
| ----------------- | ------------------ | --------------------- | ---------------------------------------- |
| off | any | not present | direct to `clob-v2.polymarket.com/order` |
| on | `0` | not present | direct to `clob-v2.polymarket.com/order` |
| on | `> 0` | present | bundled to `submit_url` (our cosigner) |
When `fee_auth` is present, your user signs **two** typed-data objects (the V2 order AND the fee auth), you bundle them, and POST to `submit_url`. The cosigner pulls the fee into escrow, forwards the signed order to Polymarket, and on fill/cancel handles the split or refund automatically.
## Errors
Standard HTTP status codes apply. Common ones:
| Code | Meaning |
| ----- | ----------------------------------------------------------------------------------------------- |
| `400` | Bad request (malformed JSON, invalid wallet format, missing required field). |
| `401` | Missing or invalid API key. |
| `402` | Free-tier key. Upgrade to Starter or above. |
| `404` | Resource not under your tenant, or doesn't exist. |
| `409` | Wallet conflict (a follower with this `wallet` is already registered under a different tenant). |
| `429` | Rate limit hit. See `x-ratelimit-*` headers. |
| `5xx` | Transient. Retry with backoff. |
## Non-goals / what we don't do
* **We don't custody anything.** We never hold your users' keys, funds, or signed orders. Every signature happens on your side.
* **We don't submit orders for you** (unless fee escrow is on, in which case we cosign + submit via our cosigner — still, we never see the user's signer key).
* **We don't guarantee fills.** We generate an order with a sensible price + size; whether it fills depends on the V2 CLOB book. If the book moves past your `slippage_bps`, the order won't fill; that's intended.
* **No V1 support.** This API is V2-only, aligned with Polymarket's 2026-04-28 V2 launch.
## Roadmap
* `max_trades_per_hour` enforcement (currently accepted as config, not yet enforced)
* `systemd` + graceful shutdown (currently a dev process, small retry loss possible on restart)
* Additional webhook event types (pending cancellations, resync notifications)
* Self-serve dashboard for inspecting followers / leaders / deliveries
## Questions
Email `josh@polynode.dev` or book a slot at [cal.com/bosh-jerns-vozdcd/15min](https://cal.com/bosh-jerns-vozdcd/15min). We're actively supporting beta integrations.
# Deposit Wallets
Source: https://docs.polynode.dev/guides/deposit-wallets
Everything you need to know about Polymarket's new deposit wallet system and how it affects your integration.
Polymarket is rolling out **deposit wallets** as a new account type alongside the existing Gnosis Safe proxy wallets. This guide explains what changed, whether you need to do anything, and how to handle both wallet types in your integration.
## What are deposit wallets?
Deposit wallets are Polymarket's new smart contract wallet type for new accounts. They replace the Gnosis Safe proxy that existing accounts use.
From your perspective as a builder, the key differences are:
| | Safe proxy (existing) | Deposit wallet (new) |
| ----------------------------- | -------------------------- | ------------------------------------------- |
| **Signature type** | `2` (POLY\_GNOSIS\_SAFE) | `3` (POLY\_1271) |
| **How addresses are derived** | CREATE2 from Safe factory | CREATE2 from deposit wallet factory |
| **Order signing** | Standard EIP-712 | EIP-712 with ERC-7739 TypedDataSign wrapper |
| **Approvals** | Gasless via Safe multisend | Gasless via WALLET batch |
| **V1 support** | Yes | No (V2 only) |
**The good news:** if you use the polynode SDK, all of this is handled automatically. `ensureReady()` detects the wallet type, `order()` signs correctly, and the `/resolve` endpoint resolves both wallet types.
## Do I need to change anything?
**If you use the polynode SDK for trading:** update to the latest version. Everything else is automatic.
```bash theme={null}
# Minimum versions for deposit wallet support
cargo add polynode@0.13.3 # Rust
npm install polynode-sdk@0.10.6 # TypeScript
pip install polynode==0.10.3 # Python
```
**If you only read data (positions, trades, /resolve):** the response shape is unchanged. `/resolve` still returns `safe`, `eoa`, `username`, and `type`, but `safe` now means the preferred Polymarket trading wallet. If an EOA has a deployed deposit wallet, `safe` returns the deposit wallet.
**If you call the Polymarket CLOB directly (without our SDK):** you need to implement ERC-7739 TypedDataSign wrapping for `signatureType: 3` orders. See the [signing details](#how-poly_1271-signing-works) below.
## How to detect wallet type
For trading, the SDK auto-detects wallet type when you call `ensureReady()` or `detectWalletType()`. Detection checks on-chain in this order:
1. Safe deployed? -> `POLY_GNOSIS_SAFE` (type 2)
2. Proxy deployed? -> `POLY_PROXY` (type 1)
3. Deposit wallet deployed? -> `POLY_1271` (type 3)
4. Nothing deployed -> defaults to `POLY_GNOSIS_SAFE` (new Safe will be created)
This controls how the SDK signs orders. The `/resolve` identity endpoint uses a different rule for compatibility with Polymarket's deposit-wallet migration: if a resolved EOA has a deployed deposit wallet, `/resolve` returns that deposit wallet in `safe`.
```typescript theme={null}
import { PolyNodeTrader } from 'polynode-sdk';
const trader = new PolyNodeTrader({ polynodeKey: 'pn_live_...' });
const status = await trader.ensureReady(privateKey);
console.log(status.signatureType);
// 2 = Safe (existing user)
// 3 = Deposit wallet (new user)
// Either way, order() works the same:
await trader.order({ tokenId: '...', side: 'BUY', price: 0.55, size: 100 });
```
## How to handle mixed user bases
If your platform has existing Safe users and new deposit wallet users, no special handling is needed. The SDK stores each user's wallet type in the local database and uses the correct signing method automatically.
```typescript theme={null}
// User A: existing Safe wallet
await trader.ensureReady(userA_privateKey);
// status.signatureType === 2, uses standard EIP-712
await trader.order({ tokenId: '...', side: 'BUY', price: 0.5, size: 50 });
// User B: new deposit wallet
await trader.ensureReady(userB_privateKey);
// status.signatureType === 3, uses ERC-7739 TypedDataSign wrapper
await trader.order({ tokenId: '...', side: 'BUY', price: 0.5, size: 50 });
// Same API, different signing under the hood
```
## Wallet address derivation
You can derive any user's deposit wallet address from their EOA without any network calls:
```typescript TypeScript theme={null}
const { deriveDepositWalletAddress, deriveSafeAddress } = require('polynode-sdk');
const eoa = '0xA60601A4d903af91855C52BFB3814f6bA342f201';
const depositWallet = await deriveDepositWalletAddress(eoa);
const safe = await deriveSafeAddress(eoa);
// Both are deterministic — same EOA always produces the same addresses
```
```python Python theme={null}
from polynode.trading import derive_deposit_wallet_address, derive_safe_address
eoa = "0xA60601A4d903af91855C52BFB3814f6bA342f201"
deposit_wallet = derive_deposit_wallet_address(eoa)
safe = derive_safe_address(eoa)
```
```rust Rust theme={null}
use polynode::trading::onboarding::{derive_deposit_wallet_address, derive_safe_address};
let eoa = "0xA60601A4d903af91855C52BFB3814f6bA342f201".parse().unwrap();
let deposit_wallet = derive_deposit_wallet_address(eoa);
let safe = derive_safe_address(eoa);
```
## The /resolve endpoint
The [Resolve Wallet](/api-reference/wallets/resolve) endpoint keeps the same fields and prioritizes deposit wallets in the existing `safe` field. The field name remains `safe` for backward compatibility.
```bash theme={null}
curl "https://api.polynode.dev/v1/resolve/0x8b60bf0f650bf7a0d93f10d72375b37de18f8c40" \
-H "x-api-key: YOUR_KEY"
```
```json theme={null}
{
"safe": "0x8b60bf0f650bf7a0d93f10d72375b37de18f8c40",
"eoa": "0xa60601a4d903af91855c52bfb3814f6ba342f201",
"username": null,
"type": "deposit_wallet"
}
```
The `safe` field contains the preferred trading wallet address. If the controlling EOA has a deployed deposit wallet, `safe` is the deposit wallet. Otherwise it is the existing Safe/proxy wallet. The `type` field tells you which kind of wallet was returned.
| `type` value | Description |
| ------------------ | ------------------------------------------ |
| `"safe"` | Gnosis Safe proxy (most existing accounts) |
| `"deposit_wallet"` | Deposit wallet (newer accounts) |
| `"proxy"` | Legacy proxy wallet |
Both forward resolution (EOA -> wallet) and reverse resolution (wallet -> EOA) work for all wallet types. When both a Safe and deposit wallet exist, deposit wallet wins.
## How POLY\_1271 signing works
You only need this section if you're implementing signing without the polynode SDK. If you use `signV2Order()` or `trader.order()`, this is handled automatically.
For deposit wallet users, V2 orders use `signatureType: 3` (POLY\_1271). The signing process wraps the standard Order EIP-712 hash in a Solady TypedDataSign struct:
1. Build the standard V2 Order struct (same fields as type 0/1/2)
2. Set `maker` and `signer` to the **deposit wallet address** (not the EOA)
3. Sign using `TypedDataSign` as the EIP-712 primary type under the exchange domain, with the deposit wallet parameters as struct fields
4. Wrap the resulting signature: `innerSig (65 bytes) + appDomainSeparator (32 bytes) + contentsHash (32 bytes) + orderTypeString (186 bytes) + typeStringLength (2 bytes)`
The final wrapped signature is 317 bytes (634 hex chars + "0x" prefix = 636 total).
The `TypedDataSign` struct includes the deposit wallet's identity:
* `name`: `"DepositWallet"`
* `version`: `"1"`
* `verifyingContract`: the deposit wallet address
* `salt`: `bytes32(0)`
## FAQ
No. Safe wallets continue to work exactly as before. The SDK detects your wallet type automatically. All existing integrations are backward compatible.
No field names changed. The endpoint still returns `safe`, `eoa`, `username`, and `type`. The important behavior change is that `safe` now returns the deposit wallet when the resolved EOA has one deployed.
Yes. The SDK defaults to creating Safe wallets for new users when no wallet is detected on-chain. Deposit wallets are created through Polymarket's frontend or relayer, not through the polynode SDK's default flow.
Deposit wallet address derivation and detection: `>= 0.10.0`. POLY\_1271 order signing: `>= 0.10.5`. Full onboarding flow (deploy + approve): `>= 0.10.6`. Latest recommended: Rust `0.13.3`, TypeScript `0.10.6`, Python `0.10.3`.
For `/resolve`, the deposit wallet is returned in `safe` when it is deployed. The field name stays `safe` so existing clients do not need to parse a new field.
No. POLY\_1271 (signature type 3) is V2 only. V1 orders only support types 0 (EOA), 1 (POLY\_PROXY), and 2 (POLY\_GNOSIS\_SAFE). Since Polymarket's V2 exchange is now live, all new activity uses V2.
Register followers with their actual `sig_type` (2 for Safe, 3 for deposit wallet). The copy trading engine automatically generates the correct typed data for each follower's wallet type. Your signing code should use `signV2Order()` from the SDK, which handles both types transparently.
Deposit wallet signatures are larger (317 bytes vs 65 bytes) due to the ERC-7739 wrapping, but this has no meaningful impact on order submission speed or gas costs. The CLOB processes both types identically.
# Errors
Source: https://docs.polynode.dev/guides/errors
Error format, status codes, and troubleshooting.
All errors return a JSON object with an `error` field.
## Error format
```json theme={null}
{
"error": "Human-readable error message."
}
```
## Status codes
| Code | Meaning | When |
| ---- | --------------------- | ------------------------------------------------------------------ |
| 200 | OK | Successful request |
| 201 | Created | API key generated successfully |
| 400 | Bad Request | Invalid parameters (e.g. bad candle resolution, missing `q` param) |
| 401 | Unauthorized | Missing or malformed API key |
| 403 | Forbidden | API key is invalid or inactive |
| 404 | Not Found | Market, wallet, or token not found |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Server-side error |
| 503 | Service Unavailable | Service temporarily unavailable |
## Common errors
### Missing API key
```json 401 theme={null}
{
"error": "Missing or invalid API key. Use x-api-key header or ?key= param."
}
```
**Fix**: Include your API key as a header (`x-api-key: pn_live_...`) or query parameter (`?key=pn_live_...`).
### Invalid API key
```json 403 theme={null}
{
"error": "Invalid or inactive API key."
}
```
**Fix**: Verify your key is correct. Keys cannot be retrieved after creation — generate a new one if lost.
### Rate limited
```json 429 theme={null}
{
"error": "Rate limit exceeded. Retry after 1772500060."
}
```
**Fix**: Wait until the timestamp, or reduce request frequency. Default limit is 120 requests per minute.
### Market not found
```json 404 theme={null}
{
"error": "Market 0x123... not found"
}
```
**Fix**: Verify the token ID. Use `/v1/search` to find the correct token.
### Invalid resolution
```json 400 theme={null}
{
"error": "Invalid resolution. Use: 1m, 5m, 15m, 1h, 4h, 1d"
}
```
### Missing search query
```json 400 theme={null}
{
"error": "Missing required query parameter 'q'"
}
```
### Key generation rate limit
```json 429 theme={null}
{
"error": "Too many key generation requests. Max 3 per hour."
}
```
## WebSocket disconnections
The server may close your WebSocket connection for any of these reasons:
| Reason | Trigger | What to do |
| -------------------- | ------------------------------------------------------ | -------------------------------------------------------------------------------------- |
| **Pong timeout** | No Pong response for 90 seconds (3 missed Ping frames) | Ensure your read loop is not blocked — standard WS libraries handle Pong automatically |
| **Send buffer full** | Client is consuming messages too slowly | Increase processing speed or reduce subscription scope with filters |
| **Invalid API key** | Key rejected during connection upgrade | Verify your `pn_live_` key is correct and active |
| **Server restart** | API binary redeployed or service restarted | Reconnect with exponential backoff (see [best practices](/guides/best-practices)) |
In all cases, the server closes the connection without a close frame. Implement reconnection with exponential backoff as described in the [WebSocket overview](/websocket/overview#reconnection).
# Fee Escrow
Source: https://docs.polynode.dev/guides/fee-escrow
Optional per-order fee collection with on-chain escrow, refund on cancel, and affiliate revenue sharing.
## Overview
The Fee Escrow lets platforms charge a fee on trades placed through the polynode SDK. Fees are optional, per-order, and fully on-chain. The system uses an escrow model: fees are pulled before order placement and either distributed on fill or refunded on cancel. Users never lose fees on unfilled orders.
**Key properties:**
* Fees are opt-in. Set `feeBps: 0` to skip the escrow entirely.
* Each order gets its own escrow entry with an independent affiliate and split.
* If the operator doesn't settle within 72 hours, the user can self-refund on-chain.
* All escrow operations are gasless via Polymarket's relayer.
**This is your platform's fee — separate from anything Polymarket charges.**
A user trading through your SDK may pay three distinct fees, each collected by a different system:
| Fee | Who charges it | Where it's set | How it's collected |
| ------------------------------------------ | -------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| **Polymarket protocol fee** | Polymarket | V1 Order struct `feeRateBps` field. V2 has no per-order fee slot. | Taken by the CLOB at settlement, goes to Polymarket's treasury |
| **Polymarket builder rev share** (V2 only) | Polymarket | V2 Order struct `builder` bytes32 field | Polymarket reads the field from the `OrderFilled` event and pays the builder rev share off-chain |
| **Your platform fee** (this guide) | Your platform | `feeConfig.feeBps` in the polynode SDK | Pulled from the user's collateral into our FeeEscrow contract before the order reaches the CLOB |
All three are independent. `feeBps: 0` turns off **your** platform fee only — it does not waive Polymarket's protocol fee or builder rev share. Setting your `feeBps` does not change anything inside the signed CLOB Order struct.
## Frequently Asked Questions
It doesn't know anything about the Polymarket order. The amount is locked at deposit time. When `pullFee` is called, the contract stores `escrows[orderId].amount = feeAmount`. That number is immutable. `distribute` sends from that stored amount. `refund` sends back whatever's left. The contract just moves money between three fixed addresses: the escrow pool, the affiliate wallet, and the treasury.
Three access controls, each with a single hardcoded destination:
* **`distribute()`** requires `onlyAuthorized` (our operator wallet or contract owner). Sends funds only to the `affiliate` and `treasury` addresses stored at deposit time. Anyone else calling it gets `Unauthorized`.
* **`refund()`** also requires `onlyAuthorized`. Sends funds only back to the original `payer`.
* **`claimRefund()`** can be called by anyone, but the contract checks `msg.sender == escrow.payer` and `block.timestamp > pulledAt + 72 hours`. Only the original payer can claim, and only after 72h. Funds go exclusively to `escrow.payer`.
The affiliate address and share split are signed by the user in the EIP-712 FeeAuth message and stored on-chain at deposit time. Nobody can change them after deposit. The user signed it, the contract stored it, and that's where the money goes.
The contract splits every distribution into two transfers:
* `affiliateShareBps / 10000` of the amount goes to the `affiliate` address (your wallet).
* The remainder goes to the `treasury` address (set by the contract owner).
If `affiliateShareBps` is `10000` (the default), you keep 100% and nothing goes to the treasury. Both addresses are immutable for each escrow entry.
Three independent paths back to the user, any one of which is sufficient:
1. **Inline refund on cancel.** When you call `cancelOrder()` or `cancelAll()`, our cosigner automatically calls `refund()` in the same request. The collateral returns to the user's wallet immediately.
2. **Lifecycle refund.** Our background service checks pending escrow orders every 30 seconds using on-chain verification. If an order has no fills after 72 hours, it calls `refund()` automatically.
3. **User self-refund.** After 72 hours, the user (or anyone on their behalf) can call `claimRefund(orderId)` directly on the contract. No involvement from us needed. Funds go to the original payer, always.
These three paths are completely independent. If our backend is down, path 3 still works. If the user is offline, paths 1 and 2 still work.
No. Every escrowed fee has exactly two possible exits, and at least one is always available:
* **Distribute** sends it to the affiliate and treasury.
* **Refund / claimRefund** sends it back to the payer.
There is no expiration that burns funds. There is no third destination. If all automated paths fail, the contract owner has `emergencyWithdraw` as a last resort. And `claimRefund` is always available to the payer after 72 hours with zero dependencies on our infrastructure.
The only scenario where a refund doesn't happen automatically: our cosigner is down at the moment of cancellation AND the lifecycle service is also down AND the user doesn't call `claimRefund` themselves after 72 hours. In that case the funds sit in the contract, untouched, until someone acts. They don't disappear, they don't move, and no one else can take them. The moment any one of those three systems comes back online, the refund happens.
Our lifecycle service checks on-chain `OrderFilled` events every 30 seconds. Each partial fill emits an event with the exact amount filled. The service sums all fills, computes the fill fraction against the original order size, and distributes only the new portion since the last check. The contract tracks cumulative distributed amounts and caps at the total, so there's no risk of over-distribution even if our math has a rounding error. Undistributed remainder is refunded after the 72-hour timeout.
Pure on-chain verification. The CLOB order ID is the EIP-712 hash of the signed order, and that same hash appears in every matching on-chain `OrderFilled` event. Our lifecycle queries `eth_getLogs` with that hash across all Polymarket V2 exchange contracts. If the event exists on the blockchain, the fill happened. If it doesn't exist, it didn't. No API calls, no authentication, no trust assumptions. Immutable blockchain state is the source of truth.
## V1 and V2 — which one do I use?
Two contracts are live. Both are fully supported; neither is being deprecated.
| You're trading on… | Use | Collateral token |
| -------------------------------------- | ------------------------------------------------------------- | ---------------- |
| **V1 CLOB** (`clob.polymarket.com`) | **FeeEscrow V1** `0xa11D28433B79D0A88F3119b16A090075752258EA` | USDC.e |
| **V2 CLOB** (`clob-v2.polymarket.com`) | **FeeEscrow V2** `0x3A43D88ef8Aae4dF5a50B3abf67122CAAeEF7c9F` | pUSD |
The two contracts have byte-identical calldata, identical function signatures, and identical behavior. The only differences are the collateral token (USDC.e vs pUSD) and the EIP-712 domain name / version. If you're writing a new V2 integration you do not need to support V1 — just point at the V2 contract and sign against the V2 domain.
Existing V1 integrations keep working unchanged — no migration required.
## How It Works
```
1. User signs EIP-712 fee authorization (off-chain, free)
2. Operator calls pullFee → collateral moves from user's Safe to escrow
3. Order goes to Polymarket CLOB (unchanged, builder creds preserved)
4. On fill → operator calls distribute → fee splits to treasury + affiliate
On cancel → operator calls refund → collateral returns to user's Safe
On timeout (72h) → user calls claimRefund → collateral returns automatically
```
The fee escrow is completely independent from the Polymarket order flow. Builder credentials, order signing, and CLOB submission are unchanged. The fee is a separate ERC-20 transfer (USDC.e on V1, pUSD on V2) that happens before the order.
## Contract addresses
Both contracts are on Polygon mainnet (chain ID 137), both use the same 72-hour self-refund window, and both have the same `FEE_AUTH_TYPEHASH`.
### V1 — USDC.e collateral (V1 CLOB)
| Field | Value |
| ------------------ | ---------------------------------------------------------------- |
| **Address** | `0xa11D28433B79D0A88F3119b16A090075752258EA` |
| **Collateral** | USDC.e `0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174` (6 decimals) |
| **EIP-712 domain** | `name="PolyNodeFeeEscrow"`, `version="1"`, chainId 137 |
### V2 — pUSD collateral (V2 CLOB, live 2026-04-24)
| Field | Value |
| ------------------ | -------------------------------------------------------------- |
| **Address** | `0x3A43D88ef8Aae4dF5a50B3abf67122CAAeEF7c9F` |
| **Collateral** | pUSD `0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB` (6 decimals) |
| **EIP-712 domain** | `name="PolyNodeFeeEscrowV2"`, `version="2"`, chainId 137 |
## Setup
Users need one additional ERC-20 approval for the escrow contract — USDC.e for V1, pUSD for V2. This is added to the existing approval batch during wallet setup (7th approval, gasless, batched with the 6 Polymarket approvals).
```typescript theme={null}
// V1: ensureReady() approves USDC.e for the V1 FeeEscrow alongside
// the six Polymarket approvals. No manual setup needed.
const status = await trader.ensureReady(privateKey);
```
For V2, set `exchangeVersion: "v2"` in your `TraderConfig` (TypeScript), `exchange_version=ExchangeVersion.V2` (Python), or `exchange_version: ExchangeVersion::V2` (Rust). `ensureReady()` then approves pUSD for the V2 FeeEscrow contract as part of the same gasless Safe approval batch as V1 does for USDC.e.
Supported from SDK versions: `polynode-sdk@0.9.2` (TypeScript), `polynode==0.9.2` (Python), `polynode = "0.12.2"` (Rust).
## Fee Authorization (EIP-712)
The user signs a typed message authorizing a specific fee amount for a specific order. This prevents anyone from pulling more than the user agreed to. The `FeeAuth` struct is identical across V1 and V2 — only the domain changes.
```typescript theme={null}
// V1 domain (USDC.e collateral)
{
name: "PolyNodeFeeEscrow",
version: "1",
chainId: 137,
verifyingContract: "0xa11D28433B79D0A88F3119b16A090075752258EA"
}
// V2 domain (pUSD collateral)
{
name: "PolyNodeFeeEscrowV2",
version: "2",
chainId: 137,
verifyingContract: "0x3A43D88ef8Aae4dF5a50B3abf67122CAAeEF7c9F"
}
// FeeAuth struct — same for V1 and V2
{
orderId: bytes32, // unique order identifier
payer: address, // user's Safe wallet (where collateral is pulled from)
signer: address, // user's EOA (signs this message)
feeAmount: uint256, // fee amount (6 decimals — USDC.e on V1, pUSD on V2)
deadline: uint256, // unix timestamp — authorization expires after this
nonce: uint256 // signer's current nonce (prevents replay)
}
```
### Selecting V2 at submit time
The polynode cosigner routes every fee request to V1 by default. To use V2, include the contract address in the `fee_auth` body of your `/submit` call:
```jsonc theme={null}
// POST /submit to trade.polynode.dev
{
"method": "POST",
"path": "/order",
"body": "...", // V2 CLOB order body
"headers": { ... }, // CLOB L2 HMAC headers
"fee_auth": {
"escrow_contract": "0x3A43D88ef8Aae4dF5a50B3abf67122CAAeEF7c9F", // ← routes to V2
"escrow_order_id": "0x...",
"payer": "0xSafe...",
"signer": "0xEOA...",
"fee_amount": "27500",
"deadline": 1777000000,
"nonce": 0,
"signature": "0x...", // signed against the V2 domain above
"affiliate": "0xPartnerWallet...",
"affiliate_share_bps": 10000
}
}
```
Omit `escrow_contract` (or set it to the V1 address) to use V1. Any other value falls through to V1, where the signature check will fail because the V2 domain separator won't match — you'll get an `InvalidSignature` revert rather than funds going to the wrong place.
## Order Types
Every order type works with the escrow. The fee is pulled before submission and settled based on the order outcome.
### Limit Buy (GTC, resting)
Order rests on the book. Fills at any point in the future distribute the fee normally. If cancelled before fill, the fee is refunded. Orders resting past 72 hours are covered explicitly in the [72-Hour Safety Net](#72-hour-safety-net) section below.
```
pullFee($0.005) → order BUY 5 @ $0.20 → cancel → refund($0.005)
↳ user gets fee back
```
Verified on Polygon — order `0x39cb2d...` placed at 20c, cancelled, fee refunded. Safe net change: \$0.00.
### Market Buy (fills immediately)
Order fills instantly. Fee is distributed to treasury.
```
pullFee($0.005) → order BUY 5 @ $0.36 → fills → distribute($0.005)
↳ 5 shares for $1.80 ↳ treasury receives fee
```
Verified on Polygon — matched, making=\$1.80 taking=5 shares. Fee distributed to treasury.
### Limit Sell (GTC, resting)
Same flow as limit buy. Rests on book, refunded on cancel.
```
pullFee($0.005) → order SELL 5 @ $0.45 → cancel → refund($0.005)
```
Verified on Polygon — order `0x71f943...` placed at 45c, cancelled, fee refunded.
### Market Sell (fills immediately)
```
pullFee($0.005) → order SELL 5 @ $0.35 → fills → distribute($0.005)
```
Verified on Polygon — matched, fee distributed.
### Batch Cancel
Multiple resting orders can be cancelled at once. Each gets its own refund.
```
pullFee(order_a) → place order A
pullFee(order_b) → place order B
cancelAll()
refund(order_a)
refund(order_b)
```
Verified on Polygon — 2 orders cancelled via `cancelAll()`, both fees refunded.
### No Fee (Opt-Out)
If `feeBps` is 0, the SDK skips the escrow entirely. The order goes straight to the CLOB with no fee interaction. Existing behavior is completely unchanged.
## Affiliate Revenue Sharing
Each order can specify an affiliate address and their share of the fee. The split is set per-order, so different partners can have different rates.
```typescript theme={null}
// Example: partner keeps 90% of a $0.10 fee
pullFee({
orderId: "0x...",
feeAmount: 100000, // $0.10
affiliate: "0xPartnerWallet...",
affShareBps: 9000, // 90% to partner
})
// On distribute:
// Partner receives: $0.090
// Treasury receives: $0.010
```
| Split | Treasury | Affiliate |
| ---------------------------------------------------- | -------- | --------- |
| `affiliateShareBps: 10000` (default — you keep 100%) | \$0.00 | \$0.10 |
| `affiliateShareBps: 7000` (share 30% with polynode) | \$0.03 | \$0.07 |
| `affiliateShareBps: 5000` (50/50 split) | \$0.05 | \$0.05 |
All splits verified on Polygon mainnet with real USDC transfers.
## Partial Fills
If an order partially fills, the fee is proportionally distributed. The remainder stays in escrow until the order fully fills or is cancelled.
```
pullFee($0.20)
→ distribute(30%) → $0.06 to treasury
→ distribute(40%) → $0.08 to treasury
→ refund(rest) → $0.06 back to user
```
Verified on Polygon — partial fills distribute proportionally, refund returns the exact remainder.
## 72-Hour Safety Net
The escrow contract has a 72-hour timeout. After that window, the user can always get their undistributed fee back.
```solidity theme={null}
// After 72 hours, anyone can call this
escrow.claimRefund(orderId);
// → undistributed USDC returns to the payer's Safe, always
```
Three things to know:
1. **The transfer target is hardcoded.** `claimRefund` always sends funds back to the original payer, no matter who calls it. Nobody can steal anyone else's fee. The reason anyone can trigger it (not just the payer) is purely so a bot or a friend can help a user recover funds if the user is offline.
2. **This is a fail-safe, not the expected path.** It exists so that if our backend goes down, gets paused, or disappears entirely, users are guaranteed to get their escrowed money back without needing us. Most users will never know the function exists, and most will never call it.
3. **It does not fire automatically.** The timeout doesn't trigger anything on its own — it just unlocks the *option* for the user (or anyone acting on their behalf) to reclaim the fee. If nobody calls `claimRefund`, the escrow sits untouched and `distribute` / `refund` continue to work normally, forever.
### What this means for limit orders resting on the book
This is the scenario to understand clearly. A limit order can rest on the CLOB for as long as needed. The 72-hour timeout starts when the fee is pulled, not when the order fills. Here is exactly what happens at each boundary:
| Situation | Behavior |
| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Order fills within 72h | Operator calls `distribute` — funds flow to treasury and affiliate normally. Nothing unusual. |
| Order cancelled within 72h | Operator calls `refund` — full fee returns to the user. Nothing unusual. |
| Order still resting at hour 72, user does nothing | Escrow sits untouched. When the order eventually fills — minute 73, day 5, month 2 — operator calls `distribute` and funds flow normally. Everyone is paid. |
| Order still resting at hour 72, user calls `claimRefund` | User gets their fee back. If the order then fills later, the fill is effectively fee-free. The builder gives up the fee on that one order; nothing else breaks. |
In practice, virtually no user will invoke `claimRefund` while their own order is still trying to fill. They'd have no reason to — they placed the order because they wanted the trade. The timeout exists as a safety valve for the case where the operator is *unable* to settle (outage, pause, shutdown), not as a routine workflow step.
### Practical guidance for builders
* If you charge fees on orders expected to fill quickly (aggressive limits near midprice), the 72h window is a non-issue.
* If you charge fees on long-dated limits that commonly rest for days, you can either accept that a fully-informed user has an escape hatch, or skip the fee on those specific orders by passing `feeBps: 0`.
* If you really want to keep fee coverage alive on a long-resting order, the operator can proactively `refund` before hour 72 and re-pull a fresh signature from the user with a new nonce. This is a manual flow and most builders won't need it.
Funds are never permanently locked, never routed to a third party, and never surprised away from either the user or the builder during the normal fill path.
## Security
**V1** has been through three independent security audits and 57 Foundry tests covering unit, fuzz, and adversarial scenarios.
**V2** is a strict two-change diff from V1 (collateral token address + EIP-712 domain strings). Every other line of the contract is byte-identical to the audited V1 source. Verification against the deployed V2 contract on Polygon mainnet: 8/8 fork tests pass (pullFee, distribute 30/70 split, refund, 72h claimRefund, and every revert path — expired deadline, wrong nonce, tampered signature). Tests impersonate a real on-chain pUSD holder to fund the payer.
Both versions enforce the same invariants:
* **Signature malleability** — rejected (secp256k1 s-value upper bound check)
* **Replay attacks** — prevented by per-signer sequential nonces
* **Affiliate share overflow** — capped at 10000 bps (100%)
* **Self-referential payer** — blocked (payer cannot be the escrow contract)
* **ecrecover(0)** — rejected explicitly
* **Operator compromise** — operator can be revoked by Safe owner instantly
* **Stuck funds** — 72-hour user self-refund + owner emergency withdraw
## SDK Integration
Enable fee collection with a single config option. Everything else is automatic.
### Basic Setup
```typescript theme={null}
import { PolyNodeTrader } from 'polynode-sdk';
const trader = new PolyNodeTrader({
polynodeKey: 'pn_live_...',
feeConfig: {
feeBps: 50, // 0.5% fee on every order
affiliate: '0xYourWallet...', // REQUIRED: your wallet that receives the fee
},
});
```
The `affiliate` field is **required** when `feeBps > 0`. This is the wallet where your fees are sent. By default, you keep 100% of the fee. If you want to share a portion with the polynode treasury, set `affiliateShareBps` to less than 10000:
```typescript theme={null}
feeConfig: {
feeBps: 50,
affiliate: '0xYourWallet...',
affiliateShareBps: 7000, // optional: keep 70%, share 30% with polynode
}
```
When `feeConfig` is set with `feeBps > 0`, every order automatically:
1. Calculates the fee from the order's price and size
2. Signs an EIP-712 authorization (free, off-chain)
3. Pulls the fee into escrow before the order reaches the CLOB
4. Refunds the fee automatically if the order is cancelled
### Fee Calculation
The fee is calculated as:
```
feeAmount = floor(price × size × 1e6 × feeBps / 10000)
```
For example, BUY 10 shares at \$0.55 with `feeBps: 50`:
```
floor(0.55 × 10 × 1e6 × 50 / 10000) = 27500 raw USDC = $0.0275
```
### Placing an Order with Fees
```typescript theme={null}
// Initialize wallet (one-time setup — handles Safe deploy + all 7 approvals including escrow)
const status = await trader.ensureReady(privateKey);
console.log('Wallet ready:', status.funderAddress);
// Send USDC.e to status.funderAddress before placing orders
// Find a market
const results = await fetch('https://api.polynode.dev/v1/search?q=hungary', {
headers: { 'x-api-key': 'pn_live_...' },
}).then(r => r.json());
const market = results.results[0];
console.log(market.question, '→ token:', market.token_ids[0]);
// Check the orderbook
const book = await fetch(`https://api.polynode.dev/v1/orderbook/${market.token_ids[0]}`, {
headers: { 'x-api-key': 'pn_live_...' },
}).then(r => r.json());
console.log('Best bid:', book.bids[0]?.price, 'Best ask:', book.asks[0]?.price);
// Place order — fee escrow happens automatically
const result = await trader.order({
tokenId: market.token_ids[0],
side: 'BUY',
price: 0.02, // well below best ask, will rest on book
size: 10,
});
console.log('Order ID:', result.orderId);
console.log('Fee TX:', result.feeEscrowTxHash); // on-chain pullFee transaction
console.log('Fee:', result.feeAmount, 'USDC'); // fee amount charged
```
The response includes `feeEscrowTxHash` (the on-chain transaction that moved USDC into escrow) and `feeAmount` (how much was charged).
### Cancelling (Auto-Refund)
```typescript theme={null}
if (result.orderId) {
const cancel = await trader.cancelOrder(result.orderId);
console.log('Cancelled:', cancel.canceled);
}
// Fee is automatically refunded on-chain — no extra call needed
```
When you cancel an order that had a fee, the refund happens inline during the cancel. The USDC returns to your Safe wallet in the same request. No polling, no background jobs needed.
`cancelAll()` also triggers refunds for every cancelled order that had a fee.
### Per-Order Fee Override
Override the global `feeConfig` on individual orders:
```typescript theme={null}
// This order uses a different fee rate and affiliate
const result = await trader.order({
tokenId: '...',
side: 'BUY',
price: 0.55,
size: 100,
feeConfig: {
feeBps: 100, // 1% for this order
affiliate: '0xSpecialPartner...',
affiliateShareBps: 5000, // 50/50 split
},
});
```
### Opting Out
Set `feeBps: 0` or omit `feeConfig` entirely. The SDK skips the escrow and the order goes straight to the CLOB with zero overhead. Existing behavior is completely unchanged.
```typescript theme={null}
// No fee — identical to pre-escrow behavior
const trader = new PolyNodeTrader({ polynodeKey: 'pn_live_...' });
```
### What the SDK Does Under the Hood
When you call `trader.order()` with `feeBps > 0`:
```
SDK Cosigner FeeEscrow CLOB
│ │ │ │
│ 1. Calculate fee │ │ │
│ 2. Generate escrowOrderId │ │ │
│ 3. Sign EIP-712 FeeAuth │ │ │
│ │ │ │
│── POST /submit (w/ fee) ───>│ │ │
│ │── pullFee(feeAuth) ──────>│ │
│ │<── tx confirmed ──────────│ │
│ │── forward order ─────────────────────────>│
│ │<── orderId ──────────────────────────────│
│<── { orderId, feeTxHash } ──│ │ │
│ │ │ │
│── DELETE /submit (cancel) ─>│ │ │
│ │── forward cancel ────────────────────────>│
│ │<── canceled ─────────────────────────────│
│ │── refund() ──────────────>│ │
│ │<── tx confirmed ──────────│ │
│<── { canceled } ────────────│ │ │
```
### Approvals
The escrow contract requires one USDC approval. This is automatically included in the approval batch during `ensureReady()` — the 7th approval alongside the 6 Polymarket approvals. For Safe wallets, it's batched into the same multicall transaction (no extra gas). For EOA wallets, it's one additional approval TX (\~\$0.001 MATIC).
# Nonce Exploit Research
Source: https://docs.polynode.dev/guides/nonce-exploit
Live research on the Polymarket settlement exploit — methodology, on-chain evidence, attacker identification, and detection.
polynode monitors every Polymarket settlement in real time, including ones that fail. This page documents our ongoing research into settlement failures caused by the `incrementNonce()` exploit on Polymarket's V1 CTF Exchange.
All data on this page is from Polygon mainnet. Every transaction hash is real and verifiable.
**Update — 2026-04-17: Fixed in V2.** The V2 CTF Exchange Order struct removes the `nonce` field entirely (along with `taker`, `expiration`, and `feeRateBps`). With no on-chain nonce to manipulate, `incrementNonce()` no longer exists as an attack surface. V2 cutover is scheduled for April 29, 2026 — at that point, Variant 1 of this exploit becomes structurally impossible. See the [V2 Migration Guide](/guides/v2-migration) for the full Order struct changes.
## Background
Polymarket uses a hybrid architecture: orders are matched off-chain by the CLOB (Central Limit Order Book), then settled on-chain by an operator calling `matchOrders` on the CTF Exchange contract. There is a 2-3 second window between when the operator builds the settlement transaction and when it lands in a block. This window is the attack surface.
## The exploit
The CTF Exchange contract has a public function called `incrementNonce()` (selector `0x627cdcb9`). Anyone can call it on their own address for less than \$0.10 in gas. When called, it increments the caller's on-chain nonce, which instantly invalidates every outstanding order they have on the exchange.
The attack:
1. The attacker places orders on **both sides** of a market (YES and NO)
2. One side gets matched by the CLOB
3. Before the operator's `matchOrders` transaction lands on-chain, the attacker calls `incrementNonce()` on the CTF Exchange
4. The `matchOrders` transaction reverts because the attacker's order nonce no longer matches their on-chain nonce
5. Since `matchOrders` is **atomic** (no try/catch per order), one invalid order kills the entire batch — every other order in that transaction also fails
6. The counterparties who were matched against the attacker get nothing. Their trades evaporate.
7. Polymarket's off-chain system removes all affected orders from the book
The attacker keeps their winning side and loses nothing on their losing side. The cost is \~\$0.10 per cycle.
## Two attack variants
Our research identified two distinct methods that cause settlement failures:
### Variant 1: Nonce flip
The attacker calls `incrementNonce()` directly on the CTF Exchange contract. This is the canonical exploit described in public disclosures. The on-chain call is permanent and auditable — every instance can be traced to a specific wallet and block.
### Variant 2: Balance drain
The attacker transfers their USDC out of their wallet between order matching and on-chain settlement. When the operator's `matchOrders` transaction executes, the token transfer fails with `"ERC20: transfer amount exceeds balance"`. The effect is identical — the entire atomic batch reverts, killing all orders in the transaction.
This variant does not leave an `incrementNonce()` trace. It manifests as a standard ERC-20 balance error. Polymarket's CLOB validates balances at order placement time, so a balance error at settlement time indicates the funds were moved deliberately in the \~2 second window between matching and settlement.
Both variants produce the same outcome: ghost fills. A trade appears to match off-chain but never settles on-chain.
## Live audit: April 9, 2026
### Settlement failure rate
We ran 10 consecutive 30-second monitoring sessions collecting every `matchOrders` transaction from polynode's settlement stream and checking each receipt for revert status.
| Metric | Value |
| ------------------------- | ---------------------- |
| **Timestamp** | 2026-04-09 \~06:15 UTC |
| **Collection** | 10 rounds x 30 seconds |
| **Total matchOrders TXs** | 9,294 |
| **Succeeded** | 9,263 |
| **Reverted** | 31 |
| **Overall failure rate** | 0.334% |
A separate scan of 1,000 consecutive blocks (\~33 minutes, blocks 85296314–85297314) found 66,638 total `matchOrders` transactions with 100 reverts (0.150% failure rate).
The rate fluctuates between 0% and 1% depending on whether short-term crypto markets are actively resolving.
### Per-round breakdown
| Round | TXs | Reverted | Rate |
| ----- | ----- | -------- | ------ |
| 1 | 1,091 | 2 | 0.183% |
| 2 | 792 | 2 | 0.253% |
| 3 | 1,313 | 2 | 0.152% |
| 4 | 1,684 | 7 | 0.416% |
| 5 | 683 | 7 | 1.025% |
| 6 | 557 | 4 | 0.718% |
| 7 | 513 | 2 | 0.390% |
| 8 | 541 | 3 | 0.555% |
| 9 | 1,252 | 2 | 0.160% |
| 10 | 868 | 0 | 0.000% |
### Market category breakdown
| Category | Reverts | Percentage |
| ------------------------------------ | ------- | ---------- |
| Short-term crypto (BTC/ETH 5–15 min) | 29 | 93.5% |
| Other crypto | 1 | 3.2% |
| Other (politics) | 1 | 3.2% |
93.5% of all settlement failures occur in short-term crypto markets. These are the 5-minute and 15-minute BTC/ETH "up or down" markets where the outcome becomes clear in the final seconds, giving the attacker a window to invalidate their losing orders before settlement.
### Revert cause analysis
We traced every reverted transaction using `trace_transaction` to decode the exact revert reason.
| Revert reason | Count | Variant |
| ---------------------------------------- | ----- | ------------- |
| `ERC20: transfer amount exceeds balance` | 20 | Balance drain |
| `SafeMath: subtraction overflow` | 11 | Balance drain |
| Total | 31 | |
All 31 reverts in this sample produced balance-related errors at the ERC-20 transfer level, consistent with both the nonce flip (which cascades into balance errors when the atomic batch unwinds) and the balance drain variant.
### Attacker identification
We scanned 1,800 consecutive blocks (\~1 hour) for all calls to `incrementNonce()` (selector `0x627cdcb9`) on the CTF Exchange contract (`0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E`).
**21 `incrementNonce()` calls found in 1 hour. Two wallets responsible for all of them.**
#### Attacker 1: `0xe5f5f9bf130f4e0947ab9e79b2ee0ab3f33885f5`
* **12 calls** in 1 hour
* Calls `incrementNonce()` **directly** on the CTF Exchange (no proxy)
* Regular \~150 block cadence (\~5 minutes), matching BTC/ETH short-term market resolution cycles
* No Polymarket profile found for this address
* **Directly caused 7 of 31 reverted settlements** in our audit (confirmed via same-block correlation)
Nonce flip blocks: 85296283, 85296433, 85296583, 85296733, 85296883, 85297033, 85297183, 85297333, 85297483, 85297633, 85297783, 85297933
At block **85297633**, this wallet called `incrementNonce()` in the same block as 3 reverted BTC Up/Down settlements, and 4 more reverted in the next 1–3 blocks as the operator's queued transactions hit the invalidated nonce.
#### Attacker 2: `0xfd29d745edaf724bae8ade4f7b3a3465eed3b905`
* **7 calls** in 1 hour
* Calls `incrementNonce()` through a **Safe proxy wallet** at `0xfc64b6660ed1...`
* Similar \~5 minute cadence
* Has a Polymarket account and profile page
* Attribution to specific reverts pending (proxy routing makes block-level correlation less precise)
Nonce flip blocks (via proxy): 85296604, 85296825, 85297016, 85297484, 85297770, 85297921, 85297985
#### Additional callers
Two wallets (`0x2df3626c...`, `0xa2b2a759...`) made single `incrementNonce()` calls during the scan period. These may be legitimate order cancellations rather than exploit activity.
### Attribution: nonce flip to settlement failure
For the 31 reverted settlements in our audit, we cross-referenced each revert block against the `incrementNonce()` call log:
| Attribution | Count | Notes |
| ------------------------ | ----- | --------------------------------------------------------------------------------------------------------- |
| **Confirmed nonce flip** | 7 | `incrementNonce()` call in same block or 1–3 blocks before revert. All from Attacker 1. |
| **Unattributed** | 24 | No nearby `incrementNonce()` call. Likely balance drain variant or nonce flip from undetected proxy call. |
The 7 confirmed nonce-flip reverts all occurred in BTC Up/Down 2:10–2:15AM ET markets within blocks 85297633–85297636. The 24 unattributed reverts are concentrated in ETH Up/Down markets and revert with balance errors, consistent with the balance drain variant.
### Sample reverted transactions
```
BTC Up/Down 2:10-2:15AM — Nonce flip confirmed (Attacker 1, block 85297633):
0x6694bb41b21ec924d29e8126659db6be34bfa057a9a32f7abbc2fc773855a031
0x727488a9527e0b140e2907e51385d3ef3c1df07bdfa54e2de5bda867b8daeb8a
0x5e20b0d3d6bb59dfa981c75b2e0900c1a9474d5127f4859c88b9a0cd5469ddda
ETH Up/Down 2:15-2:30AM — Balance drain (unattributed):
0x1ba2ed39c77e767ae2b000f521e14e618187746520c4385092f083d4871d158d
0x56aec1b48fc1c6e0d7c87e7533d66b4d00537979ab9fb0ce0c87e9a6f6b6f5e4
0xf595c73f55dd2008f5a83855fd44b277ca4cf3468f8e19e155d1cdec584445c6
```
## How polynode detects ghost fills
polynode detects settlements from the mempool 3–5 seconds before on-chain confirmation. When a `matchOrders` transaction is detected, a pending `settlement` event is emitted. When the block confirms:
* If the receipt shows `status: 0x1` (success): a `status_update` event is emitted with `confirmed_fills` containing exact on-chain execution data
* If the receipt shows `status: 0x0` (reverted): no `status_update` is emitted. The pending settlement was a ghost fill.
To detect ghost fills, track pending settlements that never receive a `status_update`:
```javascript theme={null}
const pending = new Map();
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === "settlement" && msg.data.status === "pending") {
pending.set(msg.data.tx_hash, {
...msg.data,
detected_at: Date.now(),
});
}
if (msg.type === "status_update") {
// Confirmed on-chain — not a ghost fill
pending.delete(msg.data.tx_hash);
}
};
// Check for ghost fills every 5 seconds
setInterval(() => {
const now = Date.now();
for (const [txHash, data] of pending) {
if (now - data.detected_at > 15000) {
console.log("Ghost fill:", txHash, data.market_title);
pending.delete(txHash);
}
}
}, 5000);
```
For verified trade data, use the `confirmed_fills` field on `status_update` events. See the [Trade Tracking Guide](/guides/trade-tracking) for details on using pending settlements vs confirmed fills.
## Methodology
All data was collected using polynode's live settlement stream and Polygon RPC.
**Settlement failure rate:** Subscribe to settlements via WebSocket, collect unique `tx_hash` values for a fixed period, wait for block confirmations, batch-query receipts, check `receipt.status`.
**Revert cause analysis:** Use `trace_transaction` on reverted TXs to decode the revert reason string from the deepest failing call.
**Attacker identification:** Scan all transactions in the block range for calls to `incrementNonce()` (selector `0x627cdcb9`) targeting the CTF Exchange contract (`0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E`) or the NegRisk CTF Exchange. Both direct calls and proxy calls (where the selector appears inside `execTransaction` calldata) are detected.
**Attribution:** Cross-reference `incrementNonce()` call blocks against reverted settlement blocks. A revert is "confirmed" if a nonce flip occurred in the same block or 1–3 blocks prior.
No mock data is used. All transaction hashes, block numbers, and wallet addresses are from Polygon mainnet.
# Polymarket Profile Setup
Source: https://docs.polynode.dev/guides/polymarket-profile-setup
Let your users create or change their Polymarket username from an EOA wallet without handling private keys.
The Polymarket Profile API lets your app set a user's Polymarket username from their EOA wallet.
The flow is built for platforms that already have a wallet-connected frontend and a backend that can call PolyNode with an API key. The user signs two payloads in their wallet. Your backend sends the signatures to PolyNode. PolyNode handles the Polymarket profile create or username update.
Never send user private keys to PolyNode. Never expose your PolyNode API key in a browser app. Your backend calls PolyNode. Your frontend only asks the user wallet to sign the returned messages.
## What this does
This API can:
* check whether a username is available
* create a profile for a new EOA-backed Polymarket user
* change the username for an existing EOA-backed Polymarket profile
* return public profile state for an address
This API does not:
* upload profile images
* link X accounts
* enable trading
* bypass Polymarket compliance, eligibility, captcha, geoblock, or terms flows
* accept private keys or long-lived Polymarket session credentials
## Endpoints
| Method | Endpoint | Purpose |
| ------ | -------------------------------------------- | --------------------------------------------------- |
| `GET` | `/v3/polymarket/profiles/username-available` | Check whether Polymarket will accept a username |
| `POST` | `/v3/polymarket/profiles/username/challenge` | Create the wallet-signing challenge |
| `POST` | `/v3/polymarket/profiles/username/complete` | Submit signatures and create or update the username |
| `GET` | `/v3/polymarket/profiles/{address}` | Read public profile state |
All endpoints require a paid PolyNode API key.
```bash theme={null}
curl "https://api.polynode.dev/v3/polymarket/profiles/username-available?username=alice123" \
-H "x-api-key: pn_live_..."
```
## The integration flow
1. Your frontend collects the user's EOA address and desired username.
2. Your backend calls `POST /v3/polymarket/profiles/username/challenge`.
3. Your frontend asks the user wallet to sign `challenge.polymarket.message` with `personal_sign`.
4. Your frontend asks the user wallet to sign `challenge.consent` with `eth_signTypedData_v4`.
5. Your backend calls `POST /v3/polymarket/profiles/username/complete` with both signatures.
6. PolyNode logs in to Polymarket with the user-signed SIWE message, creates the profile if needed, and sets the username.
The two signatures are intentional:
* The Polymarket SIWE signature authorizes sign-in to Polymarket.
* The PolyNode consent signature authorizes this specific profile action: address, username, action, challenge id, chain id, and expiration.
That prevents a generic login signature from being reused later to mutate a username.
## Backend: create a challenge
Your backend owns the PolyNode API key.
With the TypeScript SDK:
```ts theme={null}
import { PolyNode } from "polynode-sdk";
const pn = new PolyNode({ apiKey: process.env.POLYNODE_KEY || "pn_live_YOUR_KEY" });
// POST /api/polymarket-profile/challenge in your own backend
async function createPolymarketProfileChallenge(input) {
return pn.v3.createPolymarketUsernameChallenge({
address: input.address,
username: input.username,
action: "set_username",
});
}
```
With raw HTTP:
```ts theme={null}
// POST /api/polymarket-profile/challenge in your own backend
export async function createPolymarketProfileChallenge(input: {
address: string;
username: string;
}) {
const resp = await fetch(
"https://api.polynode.dev/v3/polymarket/profiles/username/challenge",
{
method: "POST",
headers: {
"content-type": "application/json",
"x-api-key": process.env.POLYNODE_KEY || "pn_live_YOUR_KEY",
},
body: JSON.stringify({
address: input.address,
username: input.username,
action: "set_username",
}),
}
);
const body = await resp.json();
if (!resp.ok) {
throw new Error(`${body.error}: ${body.message}`);
}
return body;
}
```
Challenge response shape:
```json theme={null}
{
"challenge_id": "pmprof_652e85a9839fe3e97348a72ff5018ef5",
"address": "0xC3579b0C908F43d50C63951A30C6f72fCEA4F6A0",
"username": "alice123",
"action": "set_username",
"expires_at": "2026-05-26T13:03:06Z",
"polymarket": {
"signature_type": "personal_sign",
"message": "polymarket.com wants you to sign in with your Ethereum account:\n0xC3579b0C908F43d50C63951A30C6f72fCEA4F6A0\n\nWelcome to Polymarket! Sign to connect.\n\nURI: https://polymarket.com\nVersion: 1\nChain ID: 137\nNonce: 4b7d...\nIssued At: 2026-05-26T12:53:06.000Z\nExpiration Time: 2026-06-02T12:53:06.000Z",
"fields": {
"domain": "polymarket.com",
"address": "0xC3579b0C908F43d50C63951A30C6f72fCEA4F6A0",
"statement": "Welcome to Polymarket! Sign to connect.",
"uri": "https://polymarket.com",
"version": "1",
"chainId": 137,
"nonce": "4b7d...",
"issuedAt": "2026-05-26T12:53:06.000Z",
"expirationTime": "2026-06-02T12:53:06.000Z"
}
},
"consent": {
"signature_type": "typed_data_v4",
"domain": {
"name": "Polynode",
"version": "1",
"chainId": 137
},
"types": {
"EIP712Domain": [
{ "name": "name", "type": "string" },
{ "name": "version", "type": "string" },
{ "name": "chainId", "type": "uint256" }
],
"PolymarketProfileAction": [
{ "name": "action", "type": "string" },
{ "name": "address", "type": "address" },
{ "name": "username", "type": "string" },
{ "name": "challengeId", "type": "string" },
{ "name": "expiresAt", "type": "uint256" }
]
},
"primaryType": "PolymarketProfileAction",
"message": {
"action": "set_username",
"address": "0xc3579b0c908f43d50c63951a30c6f72fcea4f6a0",
"username": "alice123",
"challengeId": "pmprof_652e85a9839fe3e97348a72ff5018ef5",
"expiresAt": 1779800586
}
},
"derived": {
"deposit_wallet": "0xf6d30d58add6c6814d1b086ec543a16ab6cc9a31",
"safe_address": "0xba2ef45d0fe68cee6351420b60fb554bbe91bf02",
"safe_deployed": false
}
}
```
`expires_at` is an ISO string for display. `consent.message.expiresAt` is a Unix timestamp because it is signed as `uint256` in EIP-712.
## Frontend: ask the wallet to sign
With an EIP-1193 wallet provider:
```ts theme={null}
type EthereumProvider = {
request(args: { method: string; params?: unknown[] }): Promise;
};
declare const ethereum: EthereumProvider;
const challenge = await fetch("/api/polymarket-profile/challenge", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ address, username }),
}).then((r) => r.json());
const polymarketSignature = await ethereum.request({
method: "personal_sign",
params: [challenge.polymarket.message, address],
});
const consentSignature = await ethereum.request({
method: "eth_signTypedData_v4",
params: [
address,
JSON.stringify({
domain: challenge.consent.domain,
types: challenge.consent.types,
primaryType: challenge.consent.primaryType,
message: challenge.consent.message,
}),
],
});
```
With viem:
```ts theme={null}
const challenge = await fetch("/api/polymarket-profile/challenge", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ address, username }),
}).then((r) => r.json());
const polymarketSignature = await walletClient.signMessage({
account: address,
message: challenge.polymarket.message,
});
const { EIP712Domain: _EIP712Domain, ...types } = challenge.consent.types;
const consentSignature = await walletClient.signTypedData({
account: address,
domain: challenge.consent.domain,
types,
primaryType: challenge.consent.primaryType,
message: challenge.consent.message,
});
```
With ethers v6:
```ts theme={null}
import { BrowserProvider } from "ethers";
const browserProvider = new BrowserProvider(window.ethereum);
const signer = await browserProvider.getSigner();
const address = await signer.getAddress();
const polymarketSignature = await signer.signMessage(
challenge.polymarket.message
);
const { EIP712Domain, ...types } = challenge.consent.types;
const consentSignature = await signer.signTypedData(
challenge.consent.domain,
types,
challenge.consent.message
);
```
## Backend: complete the action
The complete request must be sent from your backend with the same PolyNode API key that created the challenge.
With the TypeScript SDK:
```ts theme={null}
export async function completePolymarketProfile(input: {
challenge_id: string;
address: string;
username: string;
polymarket_signature: string;
consent_signature: string;
}) {
return pn.v3.completePolymarketUsername(input);
}
```
With raw HTTP:
```ts theme={null}
// POST /api/polymarket-profile/complete in your own backend
export async function completePolymarketProfile(input: {
challenge_id: string;
address: string;
username: string;
polymarket_signature: string;
consent_signature: string;
}) {
const resp = await fetch(
"https://api.polynode.dev/v3/polymarket/profiles/username/complete",
{
method: "POST",
headers: {
"content-type": "application/json",
"x-api-key": process.env.POLYNODE_KEY || "pn_live_YOUR_KEY",
},
body: JSON.stringify(input),
}
);
const body = await resp.json();
if (!resp.ok) {
throw new Error(`${body.error}: ${body.message}`);
}
return body;
}
```
Complete response:
```json theme={null}
{
"status": "success",
"action": "set_username",
"username": "alice123",
"address": "0xc3579b0c908f43d50c63951a30c6f72fcea4f6a0",
"deposit_wallet": "0xf6d30d58add6c6814d1b086ec543a16ab6cc9a31",
"safe_address": "0xba2ef45d0fe68cee6351420b60fb554bbe91bf02",
"gamma_user_id": "8313180",
"gamma_profile_id": "8391726",
"created_profile": true,
"changed_username": true,
"profile_url": "https://polymarket.com/profile/0xf6d30d58add6c6814d1b086ec543a16ab6cc9a31"
}
```
## Username rules
PolyNode validates the username before calling Polymarket:
```text theme={null}
^[A-Za-z0-9_]{3,32}$
```
Polymarket is still the final authority. A username can pass local validation and still return `available: false`.
Recommended UX:
1. Call `username-available` as the user types, with debounce.
2. Disable submit until it returns `available: true`.
3. Re-check availability when creating the challenge.
4. Handle `username_taken` on complete, because another user can claim the username between challenge and completion.
## Security checklist
* Keep `POLYNODE_KEY` only on your backend.
* Never ask the user for a private key.
* Treat `challenge_id` as one-time use.
* Submit complete within 10 minutes.
* Send complete with the same API key that created the challenge.
* Verify on your side that the connected wallet address matches the address in the challenge before asking the wallet to sign.
* Do not cache or log raw signatures, SIWE tokens, cookies, or Authorization headers.
## Error codes
| Code | Meaning |
| ---------------------------------- | ------------------------------------------------------------------------------------ |
| `invalid_address` | Address is not a valid EVM address |
| `invalid_username` | Username failed local validation |
| `username_taken` | Polymarket says the username is unavailable |
| `challenge_not_found` | Challenge expired, was consumed, or never existed |
| `challenge_expired` | Challenge is older than 10 minutes |
| `challenge_mismatch` | API key, address, username, action, or challenge id does not match |
| `signature_invalid` | One of the wallet signatures cannot be recovered to the challenge EOA |
| `safe_already_deployed` | The derived Safe is already deployed but Gamma did not return a user for the session |
| `polymarket_login_failed` | Polymarket login rejected the SIWE token |
| `polymarket_profile_create_failed` | Profile creation failed upstream |
| `polymarket_profile_update_failed` | Username or preference update failed upstream |
| `polymarket_compliance_blocked` | Polymarket returned a compliance, auth, or eligibility block |
| `upstream_unavailable` | Polymarket, Gamma, or Polygon RPC could not be reached |
| `rate_limited` | Profile-specific rate limit exceeded |
Error responses use:
```json theme={null}
{
"error": "username_taken",
"message": "Username is not available"
}
```
## Rate limits
Profile endpoints have stricter limits than normal data endpoints:
| Endpoint | Limit |
| -------------------- | --------------------------------------------- |
| `username-available` | 5 req/sec per API key, 1 req/sec per username |
| `challenge` | 1 req/sec per API key, 5 req/min per EOA |
| `complete` | 1 req/sec per API key, 3 req/min per EOA |
Responses include the standard PolyNode rate-limit headers:
```text theme={null}
x-ratelimit-limit
x-ratelimit-remaining
x-ratelimit-reset
```
# PolyUSD Guide
Source: https://docs.polynode.dev/guides/polyusd
How to wrap and unwrap PolyUSD — the collateral token for Polymarket V2.
PolyUSD (pUSD) is the collateral token for Polymarket's V2 exchange system. It is backed 1:1 by USDC and replaces USDC.e as the exchange collateral. To trade on V2, you need PolyUSD.
**Token info:** Name: Polymarket USD | Symbol: pUSD | Decimals: 6 | [View on Polygonscan](https://polygonscan.com/token/0xc011a7e12a19f7b1f670d46f03b03f3342e82dfb)
## How PolyUSD Works
According to Polymarket's announcement, PolyUSD is backed 1:1 by USDC. Power users and API traders can wrap either **USDC or USDC.e** into PolyUSD. For most frontend users, the transition is seamless — the Polymarket UI handles wrapping automatically with a one-time approval.
There are two wrapping paths:
| Path | Contract | Input Token | Status |
| --------------------- | -------------------------------------------- | -------------------- | --------------------------------------------------- |
| **Collateral Onramp** | `0x93070a847efef7f70739046a929d47a521f5b8ee` | USDC.e (bridged) | **Active** — working today |
| **PermissionedRamp** | `0xebc2459ec962869ca4c0bd1e06368272732bcb08` | Native USDC (Circle) | **Not active yet** — deployed but zero transactions |
Both paths produce the same PolyUSD token. The V2 exchange doesn't care how you obtained your PolyUSD.
The **USDC.e → Onramp** path is what's working right now. We've tested it on mainnet and verified the full flow. The native USDC path via the PermissionedRamp exists on-chain but has not processed any transactions yet. When Polymarket activates it, wrapping native USDC will also be supported.
## Quick Reference
| | Address |
| ---------------------------------------------- | -------------------------------------------- |
| PolyUSD Token | `0xc011a7e12a19f7b1f670d46f03b03f3342e82dfb` |
| Collateral Onramp (USDC.e → PolyUSD) | `0x93070a847efef7f70739046a929d47a521f5b8ee` |
| Collateral Offramp (PolyUSD → USDC.e) | `0x2957922eb93258b93368531d39facca3b4dc5854` |
| PermissionedRamp (native USDC, not yet active) | `0xebc2459ec962869ca4c0bd1e06368272732bcb08` |
| USDC.e (bridged USDC) | `0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174` |
| Backing Vault | `0xC417fD8E9661c0d2120B64a04Bb3278C17E99DB1` |
## Wrapping: USDC.e → PolyUSD (Active Path)
To get PolyUSD today, wrap your USDC.e through the Collateral Onramp contract.
### Steps
1. **Approve** the Onramp to spend your USDC.e
2. **Call** `wrap(underlyingToken, recipient, amount)` on the Onramp
```javascript Node.js theme={null}
import { ethers } from "ethers";
const provider = new ethers.JsonRpcProvider("YOUR_RPC_URL");
const wallet = new ethers.Wallet("YOUR_PRIVATE_KEY", provider);
const USDC_E = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174";
const ONRAMP = "0x93070a847efef7f70739046a929d47a521f5b8ee";
// 1. Approve Onramp for USDC.e
const usdc = new ethers.Contract(USDC_E, [
"function approve(address,uint256) returns (bool)",
"function balanceOf(address) view returns (uint256)"
], wallet);
await (await usdc.approve(ONRAMP, ethers.MaxUint256)).wait();
// 2. Wrap USDC.e → PolyUSD
const amount = await usdc.balanceOf(wallet.address);
const wrapData = new ethers.Interface(["function wrap(address,address,uint256)"])
.encodeFunctionData("wrap", [USDC_E, wallet.address, amount]);
const tx = await wallet.sendTransaction({ to: ONRAMP, data: wrapData, gasLimit: 300000 });
await tx.wait();
console.log("Wrapped", (Number(amount) / 1e6).toFixed(2), "USDC.e → PolyUSD");
```
```python Python theme={null}
from web3 import Web3
w3 = Web3(Web3.HTTPProvider("YOUR_RPC_URL"))
account = w3.eth.account.from_key("YOUR_PRIVATE_KEY")
USDC_E = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"
ONRAMP = "0x93070a847efef7f70739046a929d47a521f5b8ee"
# 1. Approve
usdc = w3.eth.contract(address=USDC_E, abi=[{
"name": "approve", "type": "function",
"inputs": [{"name": "spender", "type": "address"}, {"name": "amount", "type": "uint256"}],
"outputs": [{"name": "", "type": "bool"}]
}])
tx = usdc.functions.approve(ONRAMP, 2**256 - 1).build_transaction({
"from": account.address, "nonce": w3.eth.get_transaction_count(account.address)
})
w3.eth.send_raw_transaction(account.sign_transaction(tx).raw_transaction)
# 2. Wrap USDC.e → PolyUSD
amount = w3.eth.call({"to": USDC_E, "data": usdc.functions.balanceOf(account.address)._encode_transaction_data()})
amount = int.from_bytes(amount, "big")
wrap_data = Web3.keccak(text="wrap(address,address,uint256)")[:4]
wrap_data += bytes.fromhex(USDC_E[2:].zfill(64))
wrap_data += bytes.fromhex(account.address[2:].zfill(64))
wrap_data += amount.to_bytes(32, "big")
tx = {
"from": account.address,
"to": ONRAMP,
"data": "0x" + wrap_data.hex(),
"gas": 300000,
"nonce": w3.eth.get_transaction_count(account.address),
"gasPrice": w3.eth.gas_price,
"chainId": 137,
}
signed = account.sign_transaction(tx)
w3.eth.send_raw_transaction(signed.raw_transaction)
print(f"Wrapped {amount / 1e6:.2f} USDC.e → PolyUSD")
```
### Using the polynode SDK
The SDK handles wrapping automatically:
```rust Rust theme={null}
// Wrap 1 USDC.e → PolyUSD
let tx_hash = trader.wrap_to_polyusd(1_000_000).await?;
// Check balance
let balance = trader.get_polyusd_balance().await?;
```
```typescript TypeScript (SDK) theme={null}
// Wrap 1 USDC.e → PolyUSD
const txHash = await trader.wrapToPolyUsd(1_000_000n);
// Check balance
const balance = await trader.getPolyUsdBalance();
```
```python Python theme={null}
# Wrap 1 USDC.e → PolyUSD (wrap_to_polyusd is async)
tx_hash = await trader.wrap_to_polyusd(1_000_000)
# Balance getter is synchronous (returns raw int)
balance_raw = trader.get_polyusd_balance()
```
### Wrap Function Signature
```
function wrap(address underlyingToken, address recipient, uint256 amount)
```
* `underlyingToken` — the USDC.e contract address (`0x2791Bca1...`)
* `recipient` — who receives the PolyUSD (usually yourself)
* `amount` — raw amount in 6-decimal units (1 USDC = 1,000,000)
## Unwrapping: PolyUSD → USDC.e
Unwrapping is designed to be **permissionless** — any EOA can call `CollateralOfframp.unwrap(asset, to, amount)` and burn their pUSD in exchange for USDC.e. The function signature mirrors wrap:
```
function unwrap(address underlyingToken, address recipient, uint256 amount)
```
**Unwrap is live today.** The Collateral Offramp holds `WRAPPER_ROLE` on the pUSD token and processes user unwraps directly. Verified 2026-04-21 via tx [`0x154a906c5790e272cb209146e6716ff255d92791519f642f5c43a4997a47e030`](https://polygonscan.com/tx/0x154a906c5790e272cb209146e6716ff255d92791519f642f5c43a4997a47e030) (0.1 pUSD → 0.1 USDC.e from a Safe wallet via the Polymarket relayer).
Alternative paths that also still work:
* **Polymarket's withdraw UI** at `bridge.polymarket.com/withdraw` — routes through a Uniswap v3 pool or off-chain witness flow.
* **Settlement through the V2 exchange** — `CTFCollateralAdapter` handles unwrapping during `matchOrders`.
* **Redeeming resolved positions** — ConditionalTokens pays out USDC.e directly.
### Steps
1. **Approve** the Offramp to spend your pUSD
2. **Call** `unwrap(USDC.e, recipient, amount)` on the Offramp
Both steps are EOA-callable. For **Safe wallets**, route both through the Polymarket relayer (gasless) — the polynode SDK does this automatically, see below.
### Via the polynode SDK (recommended for Safe wallets)
The SDK's `wrapToPolyUsd()` / `unwrapFromPolyUsd()` automatically detect your wallet type. If you onboarded with `ensureReady(privateKey)` (Safe wallet, the default), both calls route through the Polymarket relayer and execute as gasless Safe transactions signed by your EOA. Amounts are raw 6-decimal integers (`1_000_000` = \$1).
```typescript TypeScript theme={null}
// polynode-sdk >= 0.9.1
import { PolyNodeTrader } from 'polynode-sdk';
const trader = new PolyNodeTrader({
polynodeKey: 'pn_live_...',
exchangeVersion: 'v2',
builderCredentials: { key: '...', secret: '...', passphrase: '...' },
});
await trader.ensureReady('0xYOUR_EOA_PRIVATE_KEY');
// Wrap 10 USDC.e → pUSD (gasless via Safe+relayer)
const wrapTx = await trader.wrapToPolyUsd(10_000_000n);
// Unwrap 5 pUSD → USDC.e (gasless via Safe+relayer)
const unwrapTx = await trader.unwrapFromPolyUsd(5_000_000n);
```
```python Python theme={null}
# polynode >= 0.9.1
from polynode.trading import PolyNodeTrader, TraderConfig, ExchangeVersion, BuilderCredentials
trader = PolyNodeTrader(TraderConfig(
polynode_key="pn_live_...",
exchange_version=ExchangeVersion.V2,
builder_credentials=BuilderCredentials(key="...", secret="...", passphrase="..."),
))
await trader.ensure_ready("0xYOUR_EOA_PRIVATE_KEY")
# Wrap 10 USDC.e → pUSD
wrap_tx = await trader.wrap_to_polyusd(10_000_000)
# Unwrap 5 pUSD → USDC.e
unwrap_tx = await trader.unwrap_from_polyusd(5_000_000)
```
```rust Rust theme={null}
// polynode >= 0.12.1
use polynode::trading::{
PolyNodeTrader, TraderConfig, ExchangeVersion, BuilderCredentials, PrivateKeySigner,
};
let mut trader = PolyNodeTrader::new(TraderConfig {
polynode_key: "pn_live_...".into(),
exchange_version: ExchangeVersion::V2,
builder_credentials: Some(BuilderCredentials {
key: "...".into(), secret: "...".into(), passphrase: "...".into(),
}),
..Default::default()
})?;
let signer = PrivateKeySigner::from_hex("0xYOUR_EOA_PRIVATE_KEY")?;
trader.ensure_ready(Box::new(signer), None).await?;
// Wrap 10 USDC.e → pUSD
let wrap_tx = trader.wrap_to_polyusd(10_000_000).await?;
// Unwrap 5 pUSD → USDC.e
let unwrap_tx = trader.unwrap_from_polyusd(5_000_000).await?;
```
**Which wallet type am I using?** `ensureReady(privateKey)` defaults to `POLY_GNOSIS_SAFE` — your EOA controls a Safe at the derived funder address, and SDK methods route through the Polymarket relayer (gasless). If you pass `{ type: SignatureType.EOA }` explicitly, the SDK signs directly from the EOA and you pay MATIC for gas. For Safe mode, `builderCredentials` is required because the SDK uses your Polymarket builder HMAC to authenticate relayer requests — mint one at [polymarket.com/settings?tab=builder](https://polymarket.com/settings?tab=builder).
**Placing an order right after a wrap?** Call `await trader.refreshBalanceAllowance()` in between. The V2 CLOB caches its view of your balance + allowances per API key; for a few seconds after wrap, that view is stale. If you submit an order before the cache refreshes, the CLOB can return a cryptic `"error parsing fee rate bps"` instead of the real "balance not yet visible" message. A single `refreshBalanceAllowance()` call fixes it. `ensureReady` already does this for you on first setup — `wrapToPolyUsd` does not, because not every wrap is followed by an immediate order.
### Related contracts
| Contract | Address | Role on pUSD (today) |
| --------------------------------- | -------------------------------------------- | -------------------- |
| Collateral Onramp (wrap) | `0x93070a847efef7f70739046a929d47a521f5b8ee` | WRAPPER\_ROLE ✓ |
| Collateral Offramp (unwrap) | `0x2957922eb93258b93368531d39facca3b4dc5854` | WRAPPER\_ROLE ✓ |
| PermissionedRamp (witness-signed) | `0xebc2459ec962869ca4c0bd1e06368272732bcb08` | WRAPPER\_ROLE ✓ |
| CtfCollateralAdapter | `0xAdA100Db00Ca00073811820692005400218FcE1f` | WRAPPER\_ROLE ✓ |
| NegRiskCtfCollateralAdapter | `0xadA2005600Dec949baf300f4C6120000bDB6eAab` | WRAPPER\_ROLE ✓ |
## How It Works Under the Hood
PolyUSD is backed 1:1 by collateral held in a vault contract (`0xC417fD8E...`). When you wrap:
1. Your USDC.e transfers to the vault
2. PolyUSD is minted to your wallet
When you unwrap:
1. Your PolyUSD is burned
2. USDC.e is released from the vault to your wallet
During V2 trade settlements, the exchange automatically handles PolyUSD ↔ USDC.e conversion through the CTFCollateralAdapter. The ConditionalTokens contract underneath still uses USDC.e for position splitting — PolyUSD is the user-facing layer that sits on top.
The vault currently holds USDC.e as the backing collateral. Polymarket has stated that PolyUSD is backed 1:1 by USDC. A PermissionedRamp contract exists for native USDC wrapping but is not yet active. When it activates, the vault may hold a mix of USDC.e and native USDC, but PolyUSD remains 1:1 redeemable regardless of the backing composition.
## Tracking PolyUSD Events
polynode detects PolyUSD wrapping and unwrapping through the settlement stream. These appear as `deposit` events:
* **Wrap** (deposit): `direction: "deposit"`, `from` is the Onramp contract
* **Unwrap** (withdrawal): `direction: "withdrawal"`, `to` is the user wallet
Subscribe to `deposits` on the WebSocket to receive these events. Internal settlement wraps/unwraps (between exchange and adapter contracts) are filtered out automatically.
## Key Facts
* **Decimals:** 6 (same as USDC)
* **Backing:** 1:1 by USDC (currently held as USDC.e in the vault)
* **Wrapping:** USDC.e via Onramp (active) or native USDC via PermissionedRamp (not yet active)
* **Proxy:** ERC-1967 upgradeable proxy
* **Chain:** Polygon mainnet (chain ID 137)
* **Minimum wrap:** No minimum (tested with amounts as low as \$0.007)
# Position Management
Source: https://docs.polynode.dev/guides/position-management
Split, merge, and convert positions on Polymarket. Gasless via the TypeScript SDK.
## What are split, merge, and convert?
Polymarket positions are ERC-1155 tokens representing outcomes. Three on-chain operations let you manage them directly:
**Split** — Turn USDC into YES + NO tokens for a market. You put in $100 and get 100 YES shares + 100 NO shares. One side will be worth $1 at resolution, the other \$0.
**Merge** — The reverse. Put YES + NO tokens back together and get USDC. 100 YES + 100 NO = \$100 USDC returned.
**Convert** — Rebalance positions across outcomes in a multi-outcome market (neg-risk only). If you hold NO tokens on certain outcomes, convert them into USDC plus YES tokens on the remaining outcomes.
## When to use each
| Operation | Use case |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Split** | Mint new outcome tokens without going through the orderbook. Useful for market making or taking positions on illiquid markets. |
| **Merge** | Exit positions without selling on the orderbook. Redeem paired YES+NO tokens for USDC at any time. |
| **Convert** | Rebalance across outcomes in multi-outcome markets (e.g. "Who will win the World Cup?"). Swap your NO exposure on some outcomes into YES exposure on others. |
## TypeScript (gasless)
The TypeScript SDK executes these operations gaslessly through the Polymarket relayer. No MATIC/POL needed.
```bash theme={null}
npm install polynode-sdk@latest
```
### Split
```typescript theme={null}
import { PolyNodeTrader } from 'polynode-sdk';
const trader = new PolyNodeTrader({ polynodeKey: 'pn_live_...' });
await trader.ensureReady('0xYOUR_PRIVATE_KEY');
// Split $100 into YES + NO tokens
const result = await trader.split({
conditionId: '0x895e01db...', // from market data
amount: 100, // $100 USDC
});
console.log(result.txHash); // on-chain transaction hash
```
### Merge
```typescript theme={null}
// Merge YES + NO tokens back into $50 USDC
const result = await trader.merge({
conditionId: '0x895e01db...',
amount: 50,
});
```
### Convert
Convert is only available on neg-risk multi-outcome markets (e.g. "Republican Presidential Nominee" with 36 outcomes).
```typescript theme={null}
// Convert NO positions on outcomes 0 and 1 ($100 each)
const result = await trader.convert({
marketId: '0xc7d902c4...', // negRiskMarketID from market data
outcomeIndices: [0, 1], // which outcomes to convert
amount: 100, // $100 per outcome
});
```
**What happens:**
* Your NO tokens on outcomes 0 and 1 are burned
* You receive **(number of outcomes - 1) x amount** in USDC (here: \$100)
* You receive YES tokens on all other outcomes (here: outcomes 2 through 35)
The `outcomeIndices` correspond to the position of each outcome in the market. Index 0 is the first outcome, index 1 is the second, etc. You can find these by looking at the `questionID` field in market data — the last byte of each questionID is the outcome index.
## Rust (transaction builder)
The Rust SDK builds transactions that you submit via your own provider or the Polymarket relayer.
```rust theme={null}
use polynode::trading::position_management::*;
// Build a split transaction
let tx = build_split_txn(
"0x895e01db...", // conditionId
100.0, // $100
true, // neg_risk
);
// tx.to = contract address
// tx.data = ABI-encoded calldata
// Submit via your provider or the Polymarket relayer
```
```rust theme={null}
// Build a convert transaction
let tx = build_convert_txn(
"0xc7d902c4...", // marketId
&[0, 1], // outcome indices
100.0, // $100 per outcome
);
```
## Python (transaction builder)
```python theme={null}
from polynode.trading.position_management import build_split_txn, build_convert_txn
# Build a split transaction
tx = build_split_txn("0x895e01db...", 100.0, neg_risk=True)
# tx.to, tx.data, tx.value — submit via your provider
# Build a convert transaction
tx = build_convert_txn("0xc7d902c4...", [0, 1], 100.0)
```
## Finding market IDs
To use these operations, you need the right identifiers from market data:
| Field | Where to find it | Used by |
| ----------------- | ------------------------------------------------- | -------------------------- |
| `conditionId` | Market data from `/v1/events/search` or Gamma API | `split()`, `merge()` |
| `negRiskMarketID` | Market data (only on neg-risk markets) | `convert()` |
| Outcome index | Last byte of each outcome's `questionID` | `convert()` outcomeIndices |
```typescript theme={null}
// Example: finding IDs for a market
import { PolyNode } from 'polynode-sdk';
const pn = new PolyNode({ apiKey: 'pn_live_...' });
const results = await pn.searchEvents('Republican nominee', { limit: 1 });
const market = results.events[0].markets[0];
console.log(market.conditionId); // for split/merge
console.log(market.negRiskMarketID); // for convert
```
## Neg-risk vs standard markets
| | Standard markets | Neg-risk markets |
| ------------------ | --------------------------- | --------------------------- |
| **Outcomes** | 2 (binary YES/NO) | Many (e.g. 36 candidates) |
| **Split/Merge** | Via CTF contract | Via NegRiskAdapter |
| **Convert** | Not available | Available |
| **Auto-detection** | TypeScript SDK handles this | TypeScript SDK handles this |
The TypeScript SDK auto-detects the market type and routes to the correct contract. Rust and Python default to neg-risk (most multi-outcome markets). Pass `neg_risk=False` for standard binary markets.
Convert operations are gasless in the TypeScript SDK. Rust and Python SDKs return pre-built transactions — submit them via the Polymarket relayer or your own provider.
# Rate Limits
Source: https://docs.polynode.dev/guides/rate-limits
Rate limiting details and best practices.
PolyNode uses per-key rate limiting to ensure fair usage. Limits scale with your plan and with the endpoint class.
## Tier limits
**Firehose** = a WebSocket subscription with no filters applied. It receives every event (all settlements, trades, blocks, etc.) at full throughput. Filtered subscriptions — where you specify wallets, tokens, slugs, or size thresholds — use far less bandwidth and don't count toward your firehose limit.
| | Free | Starter (\$50/mo) | Growth (\$200/mo) | Enterprise (\$750/mo) |
| ------------------------------------ | ---------------- | ------------------------ | ------------------------ | ------------------------------------------ |
| **Standard V3 REST data** | Not available | 1,000 req/min (\~16 QPS) | 2,000 req/min (\~33 QPS) | 4,000 req/min default; custom by agreement |
| **Heavy V3 trade-history endpoints** | Not available | 1,000 req/min | 1,500 req/min | 1,500 req/min default; custom by agreement |
| **Event WebSocket connections** | 1 | 10 | 50 | Unlimited |
| **Orderbook WebSocket connections** | 1 | 10 | 50 | Unlimited |
| **Filter items per subscription** | 10,000 | 10,000 | 10,000 | Custom |
| **Orderbook market subs** | Unlimited | Unlimited | Unlimited | Unlimited |
| **WebSocket session** | 1 hour/day | Unlimited | Unlimited | Unlimited |
| **API keys** | 1 | 3 | 10 | 25 |
| **Snapshot size** (`snapshot_count`) | 20 | 100 | 200 | 500 |
| **Snapshot lookback** (`since`) | 30 seconds | 2 minutes | 5 minutes | 5 minutes |
| **Key generation** | 1 per IP per day | Via dashboard | Via dashboard | Via dashboard |
| **Support** | Community | Email | Priority | Dedicated + Slack |
**Snapshot size vs snapshot lookback** — these are two separate snapshot modes controlled by different parameters. `snapshot_count` returns the last N events (capped by your tier). `since` returns all events after a UNIX-ms timestamp within your tier's lookback window. If you pass both, `since` takes priority and `snapshot_count` is ignored. See [subscribing](/websocket/subscribing#param-since) for details.
**Enterprise** includes dedicated server infrastructure. Contact [josh@quantish.live](mailto:josh@quantish.live) before activation.
V3 REST data endpoints require a paid plan. Heavy trade-history endpoints have a separate per-key bucket in addition to your standard V3 REST data limit. See [V3 REST data limits](#v3-rest-data-limits) below.
## How it works
* Rate limits are tracked per API key using a sliding window.
* V3 REST data endpoints require a paid API key. Free-tier keys receive `402 Payment Required`.
* Heavy V3 trade-history endpoints check both your standard V3 REST data limit and the heavy endpoint bucket. The stricter remaining limit applies.
* When you exceed the limit, you'll receive a `429 Too Many Requests` response.
* The error message includes a Unix timestamp for when you can retry.
* Successful REST responses include `x-ratelimit-limit`, `x-ratelimit-remaining`, and `x-ratelimit-reset` headers.
## Best practices
### Use WebSocket for real-time data
Instead of polling REST endpoints, connect via WebSocket for live updates:
```javascript theme={null}
// Instead of polling /v3/trades every 5 seconds (12 req/min)...
// Use WebSocket (1 connection, unlimited events)
const ws = new WebSocket("wss://ws.polynode.dev/ws?key=pn_live_YOUR_KEY");
ws.send(JSON.stringify({
action: "subscribe",
type: "settlements",
}));
```
### Cache metadata locally
Market metadata (question, slug, outcomes) changes infrequently. Cache it and only refresh periodically:
```javascript theme={null}
// Fetch full market list once
const markets = await fetch("/v3/markets/search?q=bitcoin&limit=100", { headers })
.then((r) => r.json());
// Cache by token_id
const cache = new Map(markets.data.map((m) => [m.condition_id, m]));
// Use cache for lookups, refresh every 5 minutes
```
### Batch where possible
Use `POST /v3/wallets/batch` instead of many individual wallet summary requests when you need P\&L for multiple wallets.
### Use filtered subscriptions
WebSocket subscriptions with filters reduce message volume and processing overhead:
```json theme={null}
{
"action": "subscribe",
"type": "settlements",
"filters": {
"tokens": ["specific-token-id"],
"min_size": 100
}
}
```
## Expected WebSocket throughput
WebSocket subscriptions are not rate-limited by requests/min, but message volume varies significantly by subscription type:
| Subscription | Typical messages/sec | Notes |
| --------------------- | -------------------- | ------------------------------- |
| Firehose (no filters) | 50–150 | \~0.5–1.5 Mbps uncompressed |
| Filtered (1 market) | 1–20 | Depends on market activity |
| Filtered (1 wallet) | 0–5 | Most wallets trade infrequently |
| Blocks only | \~0.5 | One per Polygon block (\~2s) |
`status_update` events arrive in bursts (30–80 per block). If you don't need confirmation tracking, exclude them from your `event_types` filter to reduce volume.
## V3 REST data limits
Standard V3 REST reads count against your plan's V3 REST data limit.
| Plan | Standard V3 REST data | Heavy V3 trade-history endpoints |
| ---------- | -----------------------------------------: | -----------------------------------------: |
| Free | Not available | Not available |
| Starter | 1,000 req/min | 1,000 req/min |
| Growth | 2,000 req/min | 1,500 req/min |
| Enterprise | 4,000 req/min default; custom by agreement | 1,500 req/min default; custom by agreement |
Heavy V3 trade endpoints have a separate per-key bucket because trade-history scans are the highest-throughput REST surface:
* `/v3/trades`
* `/v3/wallets/{address}/trades`
* `/v3/wallets/{address}/combos/trades`
* `/v3/markets/{token_id}/trades`
* `/v3/markets/slug/{slug}/trades`
* `/v3/builders/{code}/trades`
When the heavy bucket is exceeded, you'll receive a `429` response with:
```json theme={null}
{
"error": "rate limit exceeded",
"scope": "v3_heavy",
"reset_at": 1774108000
}
```
For real-time trades, use the [WebSocket stream](/websocket/overview) instead of polling trade-history endpoints.
## Pagination limits
Most V3 list endpoints default to `limit=100` and clamp `limit` to a maximum of `300` rows per page. If you request more than the page cap, PolyNode normalizes `limit` to the maximum allowed value. Responses include the actual `limit`, `offset`, `rows_returned`, and `has_more` values.
`/v3/markets/condition/{condition_id}/positions` supports tiered page sizes for holder lists: Starter keys can request up to `300` rows, Growth keys up to `1000`, and Enterprise keys up to `2000`.
`/v3/wallets/{address}/pnl/events` is the exception: it defaults to `limit=5000` and clamps to a maximum of `10000` buckets.
For deep trade-history walks, prefer `after` and `before` time windows instead of very large offsets.
## Higher limits
Upgrade your plan at [polynode.dev/pricing](https://polynode.dev/pricing) for higher rate limits and more WebSocket connections. Contact [josh@quantish.live](mailto:josh@quantish.live) for Enterprise capacity above the default V3 REST limits.
# Serving ~1 Billion On-Chain Events at Sub-10ms
Source: https://docs.polynode.dev/guides/trade-index
How polynode replaced third-party subgraph infrastructure with a purpose-built trade index covering every Polymarket trade, position, and settlement since November 2022.
# Serving \~1 Billion On-Chain Events at Sub-10ms
Every Polymarket trade. Every position split. Every merge. Every redemption. Every conversion. From November 2022 to right now, updated in real-time, queryable in under 10 milliseconds.
We replaced our entire third-party subgraph dependency with our own on-chain trade index. The result: 10-100x faster responses across every query type, zero external data dependencies, and complete coverage of Polymarket's on-chain history.
## Why We Built This
Subgraph-based indexing served us well early on, but as Polymarket's volume grew, the cracks showed. Daily candle queries took over 10 seconds. Position P\&L calculations stalled during peak traffic. Data freshness lagged behind the chain. We had no control over uptime or performance, and our users felt it.
We needed something that could handle the scale of Polymarket today, nearly a billion on-chain events and growing, while delivering the kind of response times that make real-time trading tools actually feel real-time.
## The Results
| Query | Response Time |
| ----------------------------------------- | :-----------: |
| Wallet trade history | 9-30ms |
| Market trades by token | 9ms |
| Portfolio positions with P\&L | 25ms |
| Hourly OHLCV candles | 14ms |
| Daily OHLCV candles | 98ms |
| Market volume stats | 17ms |
| Wallet activity (splits, merges, redeems) | 8ms |
**\~1 billion rows. Real-time chain tip. Every query under 100ms.**
For context, here's what the same queries looked like before:
| Query | Before | After | Improvement |
| ------------- | :------: | :---: | :---------: |
| Wallet trades | 340ms | 30ms | **11x** |
| Market trades | 500ms | 9ms | **55x** |
| Positions | 1,330ms | 25ms | **53x** |
| Daily candles | 10,300ms | 98ms | **105x** |
Daily candles went from 10 seconds to under 100 milliseconds. That's the difference between a chart that feels broken and one that feels instant.
## Subgraph Indexers Can't Keep Up
As of April 2026, the most widely used Polymarket subgraph has its positions index nearly a week behind the chain:
| Data Source | Block | Lag |
| ------------------------ | :--------: | :---------------------------: |
| **polynode trade index** | 85,634,892 | **0 blocks** |
| Subgraph orderbook | 85,634,889 | 2 blocks |
| Subgraph positions | 85,344,742 | **290,149 blocks (6.7 days)** |
If you're querying positions from a subgraph, you're getting portfolio data that's almost a week old. Any trade, any entry, any exit that happened in the last 6.7 days simply doesn't exist in those results.
This isn't a one-time outage. Positions require computing VWAP across every trade a wallet has ever made, and generic subgraph-style systems struggle with that at scale. The heaviest Polymarket wallets have millions of trades, and raw-event position rebuilds fall further behind every day as volume grows.
polynode's trade index pre-computes positions for all 2.5 million wallets and maintains them in real-time as new trades confirm. There is no lag. When a trade confirms on-chain, positions update within seconds.
## Complete On-Chain Coverage
The index covers every event type that matters for trade history and position reconstruction on Polymarket:
* **OrderFilled** events across all exchange contracts (V1 and V2)
* **PositionSplit**, collateral minted into outcome tokens
* **PositionsMerge**, outcome tokens burned back to collateral
* **PayoutRedemption**, winning positions claimed after resolution
* **PositionsConverted**, multi-outcome NegRisk swaps
Everything is decoded directly from on-chain logs. The blockchain is the source of truth. We don't rely on any third-party data pipeline, hosted subgraph, or external API to serve this data.
## Real-Time, Not Cached
New trades and position events stream into the index via WebSocket as they confirm on-chain. There's no batch delay, no hourly refresh, no stale cache sitting between users and the latest data. When a trade confirms, it's queryable.
Position P\&L, average entry prices, and realized gains are computed live from the raw event history. No pre-aggregated snapshots that drift out of sync. Every query reflects the actual current state of the chain.
## What This Means for Developers
If you're building on Polymarket data, this changes what's possible:
* **Portfolio dashboards** that load instantly, even for the heaviest wallets
* **OHLCV candles at any resolution**, 1m, 5m, 15m, 1h, 4h, 1d, computed from real trade data, not sampled price feeds
* **Complete trade history** for any wallet or market in a single API call
* **Live P\&L tracking** that updates as trades confirm, not on a polling interval
* **Settlement and redemption history** for compliance, accounting, or analytics
Every polynode on-chain endpoint is powered by this system.
## Try It
```bash theme={null}
# Wallet trades
curl "https://api.polynode.dev/v2/onchain/trades?wallet=0x...&key=YOUR_KEY"
# Positions with P&L
curl "https://api.polynode.dev/v2/onchain/positions?wallet=0x...&key=YOUR_KEY"
# OHLCV candles at any resolution
curl "https://api.polynode.dev/v2/onchain/candles/{token_id}?resolution=1h&key=YOUR_KEY"
# Market volume
curl "https://api.polynode.dev/v2/onchain/markets/{token_id}/volume?key=YOUR_KEY"
# Wallet redemptions
curl "https://api.polynode.dev/v2/onchain/wallets/{wallet}/redemptions?key=YOUR_KEY"
```
Full endpoint documentation is available in the [API Reference](/api-reference).
# Trade Tracking Guide
Source: https://docs.polynode.dev/guides/trade-tracking
How to track Polymarket trades for copy trading, analytics, and bookkeeping.
polynode gives you two layers of trade data. Which one you use depends on what you're building.
## Two data layers, one subscription
When you subscribe to `settlements`, you get two events per trade:
1. **`settlement`** (pending) — arrives 3–5 seconds before the block. Decoded from the transaction's calldata while it's still in the mempool.
2. **`status_update`** (confirmed) — arrives when the block confirms. Includes `confirmed_fills` with exact execution data from on-chain `OrderFilled` receipt logs.
Both come through the same subscription. No extra configuration needed.
## How they differ
The pending settlement and confirmed fills come from different data sources inside the same transaction:
| | Pending settlement | Confirmed fills |
| ------------------- | ---------------------------------------------------------------------------- | ----------------------------------------------------- |
| **Source** | Transaction calldata (input) | OrderFilled receipt logs (output) |
| **Speed** | 3–5 seconds before the block | At block confirmation |
| **Price accuracy** | Exact for single-maker fills. Off by 0.01–0.04 on \~5% of multi-maker fills. | Exact. Always. This is the on-chain canonical record. |
| **Size** | Gross token amount | Gross token amount (separate `fee` field) |
| **Per-fill detail** | Per-maker breakdown from calldata | Per-maker breakdown from receipt logs |
**Why the difference exists:** When a taker order sweeps multiple makers in one transaction, the calldata contains each maker's order and the total amounts being exchanged. But the total USDC is an aggregate across all makers. polynode estimates each maker's share proportionally. The contract computes and emits the exact per-maker amounts in the `OrderFilled` logs. For most fills, the estimate and the result are identical. For multi-maker sweeps, the contract's integer rounding can produce slightly different per-maker splits.
**Size vs Polymarket:** Polymarket's activity API reports sizes net of fees. polynode's confirmed fills report the gross size from the OrderFilled event plus the fee as a separate field. To get the Polymarket-equivalent size: `net_size = size - (fee / price)`.
## Use case 1: Copy trading
For copy trading, use the **pending settlement**. Speed matters more than the 0.01 price difference on the occasional multi-maker fill.
To find the wallet's actual trade in a settlement event, iterate `data.trades[]` and match by `maker`:
```javascript theme={null}
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type !== "settlement" || msg.data.status !== "pending") return;
for (const fill of msg.data.trades) {
if (fill.maker.toLowerCase() === WALLET_IM_COPYING.toLowerCase()) {
// This is the wallet's actual trade — same side, same token, same price
placeTrade(fill.token_id, fill.side, fill.price);
}
}
};
```
Always match by `fill.maker`, never by `fill.taker`. The `taker` field on each fill is the counterparty, not the wallet you're tracking. Matching by `taker` returns the wrong wallet's perspective with the **opposite token** and the **complement price** — copy trading from that data would mirror the inverse of every trade.
The pending settlement tells you what the wallet is trading, which direction, and at what price. That's all you need to mirror the trade before the block confirms.
## Use case 2: Analytics and bookkeeping
For trade logs, P\&L tracking, portfolio analytics, or any scenario where you need exact prices, use `confirmed_fills` from the `status_update`.
```javascript theme={null}
const tradeLog = [];
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type !== "status_update" || !msg.data.confirmed_fills) return;
for (const fill of msg.data.confirmed_fills) {
if (fill.maker === WALLET_IM_TRACKING) {
tradeLog.push({
tx_hash: msg.data.tx_hash,
block: msg.data.block_number,
timestamp: msg.data.confirmed_at,
market: msg.data.market_title,
token_id: fill.token_id,
side: fill.side,
price: fill.price,
size: fill.size,
fee: fill.fee,
order_hash: fill.order_hash,
});
}
}
};
```
This is the same data Polymarket reads from the blockchain. Prices will match Polymarket's activity data exactly.
## Use case 3: Full lifecycle tracking
Track both layers to get the complete picture — early detection plus exact execution. Match by `maker` in both the pending `trades[]` array and the confirmed `confirmed_fills[]` array:
```javascript theme={null}
const pending = new Map();
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === "settlement" && msg.data.status === "pending") {
// Find the wallet's actual leg in the pending trades array
const myLeg = msg.data.trades.find(
t => t.maker.toLowerCase() === WALLET_IM_TRACKING.toLowerCase()
);
if (myLeg) {
pending.set(msg.data.tx_hash, {
detected_at: Date.now(),
pending_price: myLeg.price,
pending_side: myLeg.side,
});
}
}
if (msg.type === "status_update" && msg.data.confirmed_fills) {
const original = pending.get(msg.data.tx_hash);
const lead_time = original ? msg.data.confirmed_at - original.detected_at : null;
// Match by maker in confirmed_fills too — same rule
for (const fill of msg.data.confirmed_fills) {
if (fill.maker.toLowerCase() === WALLET_IM_TRACKING.toLowerCase()) {
console.log({
market: msg.data.market_title,
side: fill.side,
pending_price: original?.pending_price,
confirmed_price: fill.price,
lead_time_ms: lead_time,
block: msg.data.block_number,
});
}
}
pending.delete(msg.data.tx_hash);
}
};
```
## Confirmed fills field reference
Each object in the `confirmed_fills` array:
| Field | Type | Description |
| -------------- | ------ | ----------------------------------------------- |
| `order_hash` | string | EIP-712 order hash for this fill |
| `maker` | string | Maker wallet address |
| `taker` | string | Taker wallet or exchange contract |
| `token_id` | string | Conditional token ID for this fill |
| `side` | string | `"BUY"` or `"SELL"` (maker's perspective) |
| `price` | number | Exact execution price |
| `size` | number | Gross token amount (before fees) |
| `maker_amount` | string | Raw maker amount (integer, 6 decimals for USDC) |
| `taker_amount` | string | Raw taker amount (integer) |
| `fee` | number | Fee in USDC, or `null` |
## Ghost fills and the nonce exploit
A small percentage of Polymarket settlements (\~0.15–0.35%) fail on-chain. These are "ghost fills" — trades that appear to match off-chain but revert during on-chain settlement. The most common cause is the `incrementNonce()` exploit on Polymarket's V1 CTF Exchange, which allows users to invalidate their own orders after matching but before settlement.
This is another reason to use `confirmed_fills` for any application where trade accuracy matters. A pending `settlement` event tells you a trade was matched. Only a `status_update` with `confirmed_fills` tells you it actually settled on-chain.
To detect ghost fills, track pending settlements that never receive a `status_update`:
```javascript theme={null}
const pending = new Map();
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === "settlement" && msg.data.status === "pending") {
pending.set(msg.data.tx_hash, {
...msg.data,
detected_at: Date.now(),
});
}
if (msg.type === "status_update") {
// Trade confirmed on-chain — not a ghost fill
pending.delete(msg.data.tx_hash);
}
};
// Pending settlements older than 15 seconds without a status_update are ghost fills
setInterval(() => {
const now = Date.now();
for (const [txHash, data] of pending) {
if (now - data.detected_at > 15000) {
console.log("Ghost fill detected:", txHash);
pending.delete(txHash);
}
}
}, 5000);
```
For detailed research on the exploit, affected markets, and attacker identification, see the [Nonce Exploit Research](/guides/nonce-exploit) page.
# V2 Technical Details
Source: https://docs.polynode.dev/guides/v2-details
Detailed V2 contract information for polynode subscribers.
**Last updated: April 7, 2026.** This page contains detailed V2 technical information for polynode subscribers. Enter your API key below to unlock.
# Polymarket V2 Migration
Source: https://docs.polynode.dev/guides/v2-migration
polynode is ready for the Polymarket V2 exchange upgrade. Here's what you need to know.
**V2 cutover: April 28, 2026 at \~11am UTC.** Polymarket announced the cutover date on April 18, 2026 (pushed from April 22 for extra builder testing time). The V2 test environment is open to all now at `clob-v2.polymarket.com`. V2 mainnet contracts have been processing real trades since April 3, 2026. polynode is V2-ready today — set `exchange_version: V2` in your SDK config to start placing V2 orders now, or keep V1 until cutover. After April 28, V1 orders will be rejected with `order_version_mismatch`.
Polymarket is deploying a V2 exchange system on Polygon mainnet. polynode has identified, decoded, and tested against the V2 contracts deployed today. Our settlement stream and trading SDK are built to handle V2 alongside V1.
**No action is required from existing settlement stream users.** When V2 settlements begin on-chain, they will flow through the same WebSocket feed automatically.
## Before Cutover: Your Checklist
If you place orders on Polymarket, here's exactly what to do before April 28:
1. **Upgrade your SDK to the V2-ready version.** Older versions won't generate the correct V2 payload.
* Rust: `polynode >= 0.12.0`. Install with `cargo add polynode@0.12.0`.
* TypeScript: `polynode-sdk >= 0.9.0`. Install with `npm install polynode-sdk@^0.9.0`.
* Python: `polynode >= 0.9.0`. Install with `pip install "polynode>=0.9.0"`.
2. **Flip the exchange version flag** in your `TraderConfig`. See [The Switch](#the-switch) below. This is the only code change required.
3. **Mint a V2 builder code** at [polymarket.com/settings?tab=builder](https://polymarket.com/settings?tab=builder) if you want attribution on your orders. Optional, orders still work without one.
4. **Cancel V1 resting orders** you don't want cleared at cutover. Open V1 orders get wiped when Polymarket flips the switch.
5. **Fund PolyUSD.** V2 orders settle in PolyUSD (pUSD), not USDC.e. If your Safe holds USDC.e, call `trader.wrap_to_polyusd(amount)` (Python) / `trader.wrapToPolyUsd(amount)` (TS) / `trader.wrap_to_polyusd(amount)` (Rust) to convert it via the Collateral Onramp. **`amount` is in raw 6-decimal units** (`1_000_000` = \$1; TS takes a `bigint` — use `1_000_000n`). Existing V1 pUSD balance carries over.
6. **Test against V2 now.** Point your SDK at V2 (step 2) and place a small order against `clob-v2.polymarket.com`. `ensure_ready` / `ensureReady` sets the V2 approvals and refreshes the CLOB's cached balance/allowance view for you.
7. **Verify V2 confirmation.** Your settlement stream tags V2 trades with `"exchange_version": "v2"` on every event.
## The Switch
V2 activation is **one line in your SDK config.** Flip it before April 28 and orders route through the V2 exchange automatically. Leave it as V1 and you keep working against V1 until cutover. After that, V1 orders will be rejected.
```rust Rust theme={null}
let config = TraderConfig {
exchange_version: ExchangeVersion::V2, // ← this line
..Default::default()
};
```
```typescript TypeScript theme={null}
const trader = new PolyNodeTrader({
exchangeVersion: "v2", // ← this line
});
```
```python Python theme={null}
config = TraderConfig(
exchange_version=ExchangeVersion.V2, # ← this line
)
```
That's it. No other code changes required. The SDK handles:
* New contract addresses (V2 CTF Exchange + NegRisk A/B)
* New EIP-712 domain (version `"2"`, new `verifyingContract`)
* New Order struct (adds `builder`, `metadata`, `timestamp`; drops `nonce`, `feeRateBps` from the signed hash)
* CLOB host swap to `clob-v2.polymarket.com`
* PolyUSD collateral wrapping via the Onramp contract
## What We've Verified
Tested against live V2 contracts on Polygon mainnet:
* V2 settlement decoding verified against real on-chain transactions
* V2 trade events (OrderFilled, OrdersMatched) decoded from block receipts
* PolyUSD wrapping and unwrapping tested and detected in real time
* V2 order placement tested live on the V2 CLOB
* Full order lifecycle verified: place, check status, cancel, re-place
* All existing market data, token IDs, and enrichment confirmed identical between V1 and V2
* **Order hash verification:** our EIP-712 order hash computation produces byte-for-byte identical results to the live V2 exchange contract's `hashOrder()` function on Polygon mainnet — cryptographic proof that our implementation is correct
## V2 Contract Addresses
| Contract | Address |
| ----------------------------- | -------------------------------------------- |
| PolyUSD | `0xc011a7e12a19f7b1f670d46f03b03f3342e82dfb` |
| Collateral Onramp | `0x93070a847efef7f70739046a929d47a521f5b8ee` |
| Collateral Offramp | `0x2957922eb93258b93368531d39facca3b4dc5854` |
| CTFCollateralAdapter | `0xAdA100Db00Ca00073811820692005400218FcE1f` |
| NegRiskCTFCollateralAdapter | `0xadA2005600Dec949baf300f4C6120000bDB6eAab` |
| V2 CTF Exchange | `0xe111180000d2663c0091e4f400237545b87b996b` |
| V2 NegRisk Exchange A | `0xe2222d279d744050d28e00520010520000310f59` |
| V2 NegRisk Exchange B | `0xe2222d002000ba0053cef3375333610f64600036` |
| ConditionalTokens (unchanged) | `0x4D97DCd97eC945f40cF65F87097ACe5EA0476045` |
All V2 contracts listed above are deployed and live on Polygon mainnet. On April 30 2026, Polymarket updated the collateral adapter contracts. The new adapters are effective May 1 2026 at 15:00 UTC — the relayer stops accepting transactions through the old adapters after that cutoff.
## How V2 Fits Together
The key architectural insight: V2 only changes the **exchange layer** (how orders are matched and settled). The **token layer** (ConditionalTokens) is completely unchanged and cannot be replaced.
### Why Token IDs Don't Change
All prediction market positions live inside the [ConditionalTokens](https://polygonscan.com/address/0x4D97DCd97eC945f40cF65F87097ACe5EA0476045) contract. This is a Gnosis protocol contract that has been on Polygon since Polymarket launched. Every token ID is derived from a condition ID and outcome index inside this contract.
V2 does not replace ConditionalTokens. It can't. Doing so would invalidate every open position on the platform. Instead, V2 introduces an **adapter layer** between the new exchange contracts and the existing ConditionalTokens contract:
```
V1 (current):
USDC.e → V1 Exchange → ConditionalTokens (mints/burns tokens directly)
V2 (new):
PolyUSD → V2 Exchange → CollateralAdapter → ConditionalTokens
|
Unwraps PolyUSD → USDC.e
Deposits USDC.e into ConditionalTokens
Same tokens minted as V1
```
The CollateralAdapter translates between PolyUSD (V2's collateral) and USDC.e (what ConditionalTokens expects). ConditionalTokens never sees PolyUSD. From its perspective, nothing changed.
This means:
* Same token IDs across V1 and V2
* Same condition IDs
* Same market metadata (Gamma API, enrichment)
* Positions opened on V1 are fully compatible with V2 and vice versa
### New V2 Contracts
Five new contracts support the V2 architecture. These are all live on Polygon mainnet:
| Contract | Role | Status |
| ------------------------------- | ------------------------------------------------------------- | -------- |
| **PolyUSD** | Wrapped USDC.e (1:1, 6 decimals) | Deployed |
| **Collateral Onramp** | Wraps USDC.e → PolyUSD | Deployed |
| **Collateral Offramp** | Unwraps PolyUSD → USDC.e | Deployed |
| **CTFCollateralAdapter** | Bridges PolyUSD ↔ USDC.e for standard market settlements | Deployed |
| **NegRiskCTFCollateralAdapter** | Bridges PolyUSD ↔ USDC.e for multi-outcome market settlements | Deployed |
| **V2 CTF Exchange** | New order matching for standard markets | Deployed |
| **V2 NegRisk Exchange A** | New order matching for multi-outcome markets | Deployed |
| **V2 NegRisk Exchange B** | Additional multi-outcome capacity | Deployed |
All V2 contracts are deployed on Polygon mainnet. V2 trading volume is currently minimal as Polymarket has not yet migrated users to V2.
### What's New in V2
**PolyUSD Collateral** — V2 uses PolyUSD instead of USDC.e as the exchange collateral. PolyUSD is a 1:1 wrapper around USDC.e with 6 decimals. See the [PolyUSD Guide](/guides/polyusd) for details on wrapping and unwrapping.
**Updated Order Struct** — The EIP-712 signed hash drops two fields (`nonce`, `feeRateBps`) and adds three (`timestamp`, `metadata`, `builder`). The HTTP POST body still carries `expiration` (defaulted to `"0"` for no expiration). The polynode SDK handles all of this automatically.
The dropped `feeRateBps` field was Polymarket's protocol fee slot inside the V1 order. V2 doesn't have a per-order protocol fee slot — Polymarket's fee model moves to a CLOB-level config plus the `builder` bytes32 rev share. **The polynode Fee Escrow is unaffected** by this change: it was never connected to the `feeRateBps` field. Our fee is pulled via a separate signed authorization (`FeeAuth` under our own EIP-712 domain) and only moves collateral. See the [Fee Escrow Guide](/guides/fee-escrow) for the V2-native FeeEscrow contract address and the three-fee breakdown.
**EIP-712 Domain** — The signing domain version changed from `"1"` to `"2"`. The SDK handles this based on your exchange version config.
**No More Settlement Routers** — V1 used FeeModule router contracts between operators and the exchange. V2 eliminates routers. Operators call the exchange contracts directly.
**exchange\_version Field** — V2 trades include `"exchange_version": "v2"` on each trade object in settlement and trade events. V1 trades omit this field. Use it to distinguish which exchange system processed a settlement.
### What Didn't Change
* **ConditionalTokens contract** — same address, same position system, same token IDs
* **Market data** — same condition IDs, same Gamma metadata, same enrichment pipeline
* **Authentication** — same API key derivation, same HMAC headers
* **WebSocket subscriptions** — same event types, same format
* **Position splits/merges/conversions** — same ConditionalTokens events
* **Oracle system** — unchanged UMA adapters and resolution flow
* **Orderbook streaming** — same WebSocket protocol and message format
## Impact on polynode Users
### Settlement Stream
No changes required. V2 settlements produce the same event types you already receive:
* `settlement` — early detection (3-5 seconds pre-confirmation)
* `status_update` — block confirmation with latency measurement
* `trade` — confirmed trade from OrderFilled event
* `deposit` — now includes PolyUSD wrapping/unwrapping alongside USDC.e
polynode detects V2 transactions automatically alongside V1. Both versions are supported simultaneously.
### Trading SDK
The polynode SDK supports V2 with a single config change:
```rust Rust theme={null}
use polynode::trading::{PolyNodeTrader, TraderConfig, OrderParams, OrderSide, ExchangeVersion};
let mut config = TraderConfig {
polynode_key: "pn_live_...".into(),
exchange_version: ExchangeVersion::V2, // ← only change needed
..Default::default()
};
let mut trader = PolyNodeTrader::new(config)?;
let signer = PrivateKeySigner::from_hex("0x...")?;
let status = trader.ensure_ready(Box::new(signer), None).await?;
// Order placement is identical to V1
let result = trader.order(OrderParams {
token_id: "10293...".into(),
side: OrderSide::Buy,
price: 0.05,
size: 10.0,
..Default::default()
}).await?;
```
```typescript TypeScript (SDK) theme={null}
// npm install polynode-sdk
import { PolyNodeTrader } from "polynode-sdk";
const trader = new PolyNodeTrader({
polynodeKey: "pn_live_...",
exchangeVersion: "v2", // ← only change needed
});
await trader.ensureReady("0xprivatekey...");
// Order placement is identical to V1
const result = await trader.order({
tokenId: "10293...",
side: "BUY",
price: 0.05,
size: 10,
});
```
```python Python theme={null}
# pip install polynode[trading]
from polynode.trading import PolyNodeTrader, TraderConfig, ExchangeVersion, OrderParams
config = TraderConfig(
polynode_key="pn_live_...",
exchange_version=ExchangeVersion.V2, # ← only change needed
)
trader = PolyNodeTrader(config)
await trader.ensure_ready("0xprivatekey...")
# Order placement is identical to V1 — pass OrderParams, add builder for V2 attribution
result = await trader.order(OrderParams(
token_id="10293...",
side="BUY",
price=0.05,
size=10,
builder="0x5472fdd7...", # optional; your V2 builder code
))
```
The SDK handles V2 order structs, EIP-712 signing, CLOB endpoint routing, and PolyUSD collateral automatically. You don't need to change how you construct orders or manage positions.
## Builder Attribution in V2
V2 moves builder attribution from HTTP headers into the signed order struct itself as a `bytes32` field. To attribute trades to your own builder profile, mint a builder code in Polymarket's settings and pass it to the SDK on each order.
**Step 1: Mint your V2 builder code.** Visit `polymarket.com/settings?tab=builder` with your connected wallet, create a builder profile, and copy the generated bytes32 code.
**Step 2: Pass it on each order.** All three polynode SDKs accept a `builder` field on the order params. If you don't pass one, orders sign with `0x0000...0000` (no attribution).
```rust Rust theme={null}
let result = trader.order(OrderParams {
token_id: "10293...".into(),
side: OrderSide::Buy,
price: 0.05,
size: 10.0,
builder: Some("0x0000000000000000000000000000000000000000000000000000000000000001".into()),
..Default::default()
}).await?;
```
```typescript TypeScript theme={null}
const result = await trader.order({
tokenId: "10293...",
side: "BUY",
price: 0.05,
size: 10,
builder: "0x0000000000000000000000000000000000000000000000000000000000000001",
});
```
```python Python theme={null}
result = await trader.order(OrderParams(
token_id="10293...",
side="BUY",
price=0.05,
size=10,
builder="0x0000000000000000000000000000000000000000000000000000000000000001",
))
```
Your existing V1 `builder_credentials` (HMAC key/secret/passphrase) are unrelated to the V2 `builder` bytes32. The HMAC credentials are still used to query builder trade history at `/builder/trades`. The V2 bytes32 is what attributes newly-placed orders on-chain.
## Pre-Migration V1 Orders
Open V1 orders resting at cutover get cleared from V1 orderbooks. Polymarket exposes `GET /data/pre-migration-orders` on the V2 CLOB for fetching your pre-cutover V1 order history. Cancel V1 resting orders before April 28 if you need the capital free for V2 trading.
## Common V2 Errors
If something goes wrong during migration, these are the ones you'll hit.
**`order_version_mismatch`**
Your SDK sent a V1-formatted order but the V2 CLOB expected V2. Flip `exchange_version: V2` in your config.
**`not enough balance / allowance`**
Most common causes, in order:
1. **Your wallet has USDC.e but not PolyUSD.** V2 uses PolyUSD as the collateral token. Call `trader.wrap_to_polyusd(amount_raw)` (Python/Rust) or `trader.wrapToPolyUsd(amount_raw)` (TS) to convert. Amounts are raw 6-decimal integers.
2. **Approvals not yet visible to the CLOB.** The V2 CLOB caches per-API-key balance + allowance state. `ensureReady` / `ensure_ready` refreshes this cache after setting approvals; if you changed approvals outside that flow, call `trader.refreshBalanceAllowance()` / `trader.refresh_balance_allowance()` manually.
3. **Order notional + fee exceeds balance.** V2 markets charge a fee (\~2–5% of notional depending on `base_fee`). For a BUY, `balance >= makerAmount + platformFee` must hold or the CLOB rejects.
**`error parsing fee rate bps () to int64`**
Your SDK is too old and isn't sending the V2 payload correctly. Upgrade to the V2-ready version listed in [the checklist above](#before-cutover-your-checklist).
**Rust-specific: `not enough balance / allowance` even though you have PolyUSD**
If you call `link_wallet(signer, None)` with an EOA wallet, the SDK silently uses the config's default signature type (POLY\_GNOSIS\_SAFE) and derives a Safe address as the maker. That Safe has zero balance, so the CLOB rejects the order. For EOA wallets, pass explicit opts:
```rust theme={null}
use polynode::trading::{LinkOpts, SignatureType};
trader.link_wallet(Box::new(signer), Some(LinkOpts {
signature_type: Some(SignatureType::Eoa),
})).await?;
```
## Timeline
* **Now:** V2 test environment live at `clob-v2.polymarket.com`. V2 contracts live on Polygon mainnet processing real trades. polynode settlement stream tags V2 trades with `exchange_version: "v2"`.
* **Before April 28:** Flip your SDK config to `exchange_version: V2` (or keep V1 until cutover). Mint a builder code if you want attribution. Cancel V1 resting orders you don't want cleared.
* **April 28, \~11am UTC:** Polymarket cutover. V2 becomes the primary exchange. V1 orderbooks cleared.
* **After cutover:** Any integration still on V1 will fail with `order_version_mismatch`. Flip your config.
polynode supports both V1 and V2 simultaneously throughout the transition — no downtime, no disruption.
# Introduction
Source: https://docs.polynode.dev/introduction
Real-time Polymarket data API — settlements detected up to 5 seconds before on-chain confirmation.
**Polymarket V2 cutover: April 28, 2026 at \~11am UTC.** If you place orders via our trading SDK, upgrade to the V2-ready version and flip one config line before cutover. See the [V2 Migration Guide](/guides/v2-migration) for the full checklist.
# PolyNode API
PolyNode delivers Polymarket settlement data **2–5 seconds before on-chain confirmation** (1–2 Polygon blocks). Connect via WebSocket for real-time streaming, or query the REST API for markets, candles, and orderbook data.
Real-time settlements with filtered subscriptions. The core product.
Enriched market metadata, OHLCV candles, and volume analytics.
## Quick start
### 1. Get an API key
```bash theme={null}
curl -s -X POST https://api.polynode.dev/v1/keys \
-H "Content-Type: application/json" \
-d '{"name": "my-app"}'
```
### 2. Query the REST API
```bash cURL theme={null}
curl -s -H "x-api-key: pn_live_YOUR_KEY" \
"https://api.polynode.dev/v1/markets?count=5"
```
```typescript TypeScript theme={null}
const resp = await fetch("https://api.polynode.dev/v1/markets?count=5", {
headers: { "x-api-key": "pn_live_YOUR_KEY" },
});
const data = await resp.json();
console.log(data.markets[0].question, data.markets[0].last_price);
```
```python Python theme={null}
import requests
resp = requests.get(
"https://api.polynode.dev/v1/markets",
params={"count": 5},
headers={"x-api-key": "pn_live_YOUR_KEY"},
)
for m in resp.json()["markets"]:
print(f"{m['question']} — {m['last_price']}")
```
### 3. Stream live settlements via WebSocket
```javascript Node.js theme={null}
const ws = new WebSocket("wss://ws.polynode.dev/ws?key=pn_live_YOUR_KEY");
ws.onopen = () => {
ws.send(JSON.stringify({ action: "subscribe", type: "settlements" }));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === "settlement") {
const d = msg.data;
console.log(`${d.taker_side} ${d.taker_size} @ ${d.taker_price} | ${d.market_title}`);
}
};
```
```python Python theme={null}
import asyncio, json, websockets
async def stream():
async with websockets.connect("wss://ws.polynode.dev/ws?key=pn_live_YOUR_KEY") as ws:
await ws.send(json.dumps({"action": "subscribe", "type": "settlements"}))
async for message in ws:
msg = json.loads(message)
if msg["type"] == "settlement":
d = msg["data"]
print(f"{d['taker_side']} {d['taker_size']} @ {d['taker_price']} | {d.get('market_title')}")
asyncio.run(stream())
```
```bash CLI theme={null}
npx wscat -c "wss://ws.polynode.dev/ws?key=pn_live_YOUR_KEY"
# then send:
{"action":"subscribe","type":"settlements"}
```
### 4. Stream a live orderbook
```javascript Node.js theme={null}
const ws = new WebSocket("wss://ob.polynode.dev/ws?key=pn_live_YOUR_KEY");
ws.onopen = () => {
// Subscribe by condition ID (from the REST API) or token ID
ws.send(JSON.stringify({
action: "subscribe",
markets: ["0x7cb525e831729325d651017f81cbcb6f1adde5011c7b2283babea00b4ae93ae7"]
}));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === "snapshot_batch") {
for (const snap of msg.snapshots) {
console.log(`${snap.question} [${snap.outcome}]: ${snap.bids.length} bids, ${snap.asks.length} asks`);
}
} else if (msg.type === "batch") {
for (const update of msg.updates) {
if (update.type === "price_change") {
for (const asset of update.assets) {
console.log(`${update.slug} [${asset.outcome}]: ${asset.price}`);
}
}
}
}
};
```
```python Python theme={null}
import asyncio, json, websockets
async def stream_orderbook():
async with websockets.connect("wss://ob.polynode.dev/ws?key=pn_live_YOUR_KEY") as ws:
await ws.send(json.dumps({
"action": "subscribe",
"markets": ["0x7cb525e831729325d651017f81cbcb6f1adde5011c7b2283babea00b4ae93ae7"]
}))
async for message in ws:
data = json.loads(message)
if data["type"] == "snapshot_batch":
for snap in data["snapshots"]:
print(f"{snap['question']} [{snap['outcome']}]: {len(snap['bids'])} bids, {len(snap['asks'])} asks")
elif data["type"] == "batch":
for update in data["updates"]:
if update["type"] == "price_change":
for asset in update["assets"]:
print(f"{update['slug']} [{asset['outcome']}]: {asset['price']}")
asyncio.run(stream_orderbook())
```
```bash CLI theme={null}
npx wscat -c "wss://ob.polynode.dev/ws?key=pn_live_YOUR_KEY"
# then send:
{"action":"subscribe","markets":["0x7cb525e831729325d651017f81cbcb6f1adde5011c7b2283babea00b4ae93ae7"]}
```
## What makes PolyNode different
| Feature | PolyNode | Other providers |
| ------------------------------- | -------------------------------------------- | ------------------- |
| Pending settlements (pre-chain) | 2–5 seconds early (1–2 blocks) | Not available |
| Decoded Polymarket data | Enriched with market titles, outcomes, slugs | Raw logs only |
| WebSocket filtering | By wallet, token, slug, side, size | Generic log filters |
| Orderbook, candles, stats | CLOB proxying + enriched analytics | Separate service |
## Base URL
```
REST: https://api.polynode.dev
WebSocket: wss://ws.polynode.dev/ws?key=YOUR_KEY
```
All endpoints require an API key via `x-api-key` header (REST) or `?key=` query param (WebSocket).
# Examples
Source: https://docs.polynode.dev/orderbook/examples
Code examples for connecting to the Orderbook Stream in JavaScript, Python, and from the command line.
## Command line (wscat)
```bash theme={null}
# Subscribe to the full firehose (all active markets)
wscat -c "wss://ob.polynode.dev/ws?key=pn_live_YOUR_KEY" \
-x '{"action":"subscribe","markets":["*"]}'
# Subscribe by slug (easiest for specific markets)
wscat -c "wss://ob.polynode.dev/ws?key=pn_live_YOUR_KEY" \
-x '{"action":"subscribe","markets":["what-price-will-bitcoin-hit-in-march-2026"]}'
# Subscribe by condition ID
wscat -c "wss://ob.polynode.dev/ws?key=pn_live_YOUR_KEY" \
-x '{"action":"subscribe","markets":["0x561ffbf7de21ef3781c441f30536b026d2b301d7a4a0145a8f526f98db049ba2"]}'
# Subscribe by token ID
wscat -c "wss://ob.polynode.dev/ws?key=pn_live_YOUR_KEY" \
-x '{"action":"subscribe","markets":["73624432805780182150964443951045800666977811185963019133914618974858599458273"]}'
```
## Finding markets
Use the PolyNode REST API to search for markets:
```bash theme={null}
# Search by keyword
curl -s "https://api.polynode.dev/v1/search?q=bitcoin" | python3 -m json.tool
# Look up by slug (from any Polymarket URL: polymarket.com/event/{slug})
curl -s "https://api.polynode.dev/v1/markets/slug/what-price-will-bitcoin-hit-in-march-2026"
# Look up by condition ID
curl -s "https://api.polynode.dev/v1/markets/condition/0x561ffbf7de21ef3781c441f30536b026d2b301d7a4a0145a8f526f98db049ba2"
```
The easiest approach: copy the slug from a Polymarket event URL and pass it directly in your subscribe message.
## JavaScript (firehose)
```javascript theme={null}
const ws = new WebSocket("wss://ob.polynode.dev/ws?key=pn_live_YOUR_KEY");
ws.onopen = () => {
ws.send(JSON.stringify({ action: "subscribe", markets: ["*"] }));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === "subscribed") {
console.log(`Firehose active: ${msg.markets} tokens`);
} else if (msg.type === "batch") {
console.log(`${msg.count} updates at ${msg.ts}`);
for (const update of msg.updates) {
if (update.type === "price_change") {
for (const asset of update.assets) {
// size="0" removes the level; otherwise it's the new absolute size at that level.
console.log(` ${update.slug} [${asset.outcome}] ${asset.side} ${asset.price} -> ${asset.size} (bbo ${asset.best_bid}/${asset.best_ask})`);
}
}
}
}
};
```
## JavaScript (specific markets)
```javascript theme={null}
const ws = new WebSocket("wss://ob.polynode.dev/ws?key=pn_live_YOUR_KEY");
ws.onopen = () => {
// Subscribe by slug — both sides (Yes + No) are included automatically
ws.send(JSON.stringify({
action: "subscribe",
markets: [
"what-price-will-bitcoin-hit-in-march-2026",
"netanyahu-out-before-2027"
]
}));
};
// Maintain local orderbook state
const books = {};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
switch (msg.type) {
case "subscribed":
console.log(`Subscribed to ${msg.markets} tokens`);
console.log(`Resolved from: ${JSON.stringify(msg.resolved_from)}`);
break;
case "snapshot_batch":
for (const snap of msg.snapshots) {
books[snap.asset_id] = { bids: snap.bids, asks: snap.asks };
console.log(`Snapshot: ${snap.event_title} [${snap.outcome}] - ${snap.bids.length}b/${snap.asks.length}a`);
}
break;
case "batch":
for (const update of msg.updates) {
if (update.type === "price_change") {
for (const asset of update.assets) {
// Apply directly to local books — price_change carries size + side so you
// can maintain a tick-accurate book without polling REST.
const book = books[asset.asset_id];
if (book) {
const levels = asset.side === "BUY" ? book.bids : book.asks;
applyDelta(levels, [{ price: asset.price, size: asset.size }]);
}
}
} else if (update.type === "book_update") {
const book = books[update.asset_id];
if (book) {
applyDelta(book.bids, update.bids);
applyDelta(book.asks, update.asks);
}
}
}
break;
}
};
// Apply incremental orderbook updates
function applyDelta(existing, deltas) {
for (const delta of deltas) {
const idx = existing.findIndex(l => l.price === delta.price);
if (delta.size === "0") {
if (idx >= 0) existing.splice(idx, 1);
} else if (idx >= 0) {
existing[idx].size = delta.size;
} else {
existing.push(delta);
}
}
}
ws.onclose = () => setTimeout(() => location.reload(), 3000);
```
## Python (firehose)
```python theme={null}
import asyncio
import json
import websockets
async def stream_firehose():
url = "wss://ob.polynode.dev/ws?key=pn_live_YOUR_KEY"
async with websockets.connect(url, max_size=10_000_000) as ws:
await ws.send(json.dumps({
"action": "subscribe",
"markets": ["*"]
}))
async for message in ws:
data = json.loads(message)
if data["type"] == "subscribed":
print(f"Firehose active: {data['markets']} tokens")
elif data["type"] == "batch":
for update in data["updates"]:
if update["type"] == "price_change":
for asset in update["assets"]:
# size="0" removes the level; otherwise it's the new absolute size.
print(f" {update['slug']} [{asset['outcome']}] {asset['side']} {asset['price']} -> {asset['size']} (bbo {asset['best_bid']}/{asset['best_ask']})")
asyncio.run(stream_firehose())
```
Set `max_size` to at least 10 MB when connecting to the firehose (full data stream). Batch messages can be large when many markets update in the same 100ms window.
## Python (specific markets)
```python theme={null}
import asyncio
import json
import websockets
async def stream_orderbook():
url = "wss://ob.polynode.dev/ws?key=pn_live_YOUR_KEY"
async with websockets.connect(url) as ws:
# Subscribe by slug — resolves to all token IDs for those events
await ws.send(json.dumps({
"action": "subscribe",
"markets": [
"what-price-will-bitcoin-hit-in-march-2026",
"netanyahu-out-before-2027",
]
}))
async for message in ws:
data = json.loads(message)
if data["type"] == "subscribed":
print(f"Subscribed to {data['markets']} tokens")
print(f"Resolved from: {data['resolved_from']}")
elif data["type"] == "snapshot_batch":
for snap in data["snapshots"]:
print(f"Snapshot: {snap['event_title']} [{snap['outcome']}]")
print(f" Bids: {len(snap['bids'])} levels")
print(f" Asks: {len(snap['asks'])} levels")
elif data["type"] == "batch":
for update in data["updates"]:
if update["type"] == "price_change":
for asset in update["assets"]:
print(f" {update['slug']} [{asset['outcome']}] {asset['side']} {asset['price']} -> {asset['size']} (bbo {asset['best_bid']}/{asset['best_ask']})")
elif update["type"] == "book_update":
bids = len(update.get("bids", []))
asks = len(update.get("asks", []))
print(f" Book delta: {update['slug']} [{update['outcome']}] +{bids}b/+{asks}a")
asyncio.run(stream_orderbook())
```
## Python with compression
```python theme={null}
import asyncio
import json
import zlib
import websockets
async def stream_compressed():
url = "wss://ob.polynode.dev/ws?key=pn_live_YOUR_KEY&compress=zlib"
async with websockets.connect(url) as ws:
await ws.send(json.dumps({
"action": "subscribe",
"markets": ["what-price-will-bitcoin-hit-in-march-2026"]
}))
async for message in ws:
if isinstance(message, bytes):
text = zlib.decompress(message, -zlib.MAX_WBITS).decode("utf-8")
data = json.loads(text)
else:
data = json.loads(message)
print(f"[{data.get('type')}] {json.dumps(data)[:200]}")
asyncio.run(stream_compressed())
```
## Mixed identifier subscribe
You can mix slugs, condition IDs, and token IDs in a single subscribe:
```python theme={null}
await ws.send(json.dumps({
"action": "subscribe",
"markets": [
"what-price-will-bitcoin-hit-in-march-2026", # slug
"0xd1796c09d0d6f876f8580086ae9808ec991784e3a74b25a1830a25de71a78c96", # condition ID
"73624432805780182150964443951045800666977811185963019133914618974858599458273" # token ID
]
}))
```
# Message Reference
Source: https://docs.polynode.dev/orderbook/messages
Complete reference for all message types sent and received on the Orderbook Stream.
## Client messages
### Subscribe
Subscribe to orderbook updates. You can subscribe to specific markets by **slug**, **condition ID**, or **token ID**, or subscribe to the full firehose (full data stream) with `"*"`.
```json theme={null}
{
"action": "subscribe",
"markets": ["*"]
}
```
Subscribes to every active Polymarket market (100,000+ tokens). Live updates start flowing immediately while snapshots stream in the background.
```json theme={null}
{
"action": "subscribe",
"markets": ["what-price-will-bitcoin-hit-in-march-2026"]
}
```
```json theme={null}
{
"action": "subscribe",
"markets": ["0x561ffbf7de21ef3781c441f30536b026d2b301d7a4a0145a8f526f98db049ba2"]
}
```
```json theme={null}
{
"action": "subscribe",
"markets": [
"73624432805780182150964443951045800666977811185963019133914618974858599458273",
"1666184515532238710431784265702709312060757077236443477960106115591255115343"
]
}
```
| Field | Type | Description |
| --------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `action` | string | Must be `"subscribe"` |
| `markets` | string\[] | Array of market identifiers. Use `["*"]` for the full firehose, or pass specific identifiers: slugs (e.g. `"bitcoin-100k"`), condition IDs (e.g. `"0x561ff..."`), or CLOB token IDs (numeric string). Slugs and condition IDs are resolved to their corresponding token IDs automatically. |
**How identifiers are detected:**
* `"*"` → firehose (all active markets)
* Starts with `0x` → condition ID
* All digits and longer than 10 characters → CLOB token ID
* Everything else → slug
Subscribing again replaces your current subscription. To add markets, send a new subscribe with the full list.
### Unsubscribe
Remove all subscriptions on this connection.
```json theme={null}
{
"action": "unsubscribe"
}
```
### Ping
Client-initiated keepalive.
```json theme={null}
{
"action": "ping"
}
```
Response: `{"type": "pong"}`
***
## Server messages
### `subscribed`
Acknowledgment after a successful subscribe. The response format differs slightly depending on whether you subscribed to specific markets or the firehose.
```json theme={null}
{
"type": "subscribed",
"firehose": true,
"markets": 104972
}
```
| Field | Type | Description |
| ---------- | ------- | -------------------------------------------------------------- |
| `firehose` | boolean | `true` when subscribed to the full firehose |
| `markets` | number | Total number of active tokens you're now receiving updates for |
```json theme={null}
{
"type": "subscribed",
"markets": 4,
"resolved_from": {
"token_ids": 0,
"slugs": 1,
"condition_ids": 0
}
}
```
| Field | Type | Description |
| ----------------------------- | ------ | -------------------------------------------------------------- |
| `markets` | number | Total number of token IDs now subscribed (after resolution) |
| `resolved_from` | object | Breakdown of how many input entries were detected as each type |
| `resolved_from.token_ids` | number | Entries passed through as raw token IDs |
| `resolved_from.slugs` | number | Entries resolved from slugs |
| `resolved_from.condition_ids` | number | Entries resolved from condition IDs |
If you subscribe with a slug and `markets` comes back as 0, the slug didn't match any known event. Use the [search API](https://api.polynode.dev/v1/search?q=bitcoin) to find the correct slug.
### `snapshot_batch`
Orderbook snapshots delivered in batches after subscribing. For small subscriptions (under 100 tokens), snapshots arrive in a single batch almost instantly. For large subscriptions (firehose), snapshots are streamed in batches of 50 every 200ms to avoid flooding your connection.
```json theme={null}
{
"type": "snapshot_batch",
"count": 2,
"total_sent": 2,
"snapshots": [
{
"type": "book_snapshot",
"asset_id": "114694726451307654528948558967898493662917070661203465131156925998487819889437",
"market": "0xd1796c09d0d6f876f8580086ae9808ec991784e3a74b25a1830a25de71a78c96",
"condition_id": "0xd1796c09d0d6f876f8580086ae9808ec991784e3a74b25a1830a25de71a78c96",
"event_title": "Netanyahu out by...?",
"question": "Netanyahu out by end of 2026?",
"outcome": "Yes",
"slug": "netanyahu-out-before-2027",
"bids": [
{ "price": "0.01", "size": "1130070" },
{ "price": "0.02", "size": "71015.52" }
],
"asks": [
{ "price": "0.99", "size": "17615.05" },
{ "price": "0.98", "size": "5000" }
]
}
]
}
```
| Field | Type | Description |
| ------------ | --------- | -------------------------------------------- |
| `count` | number | Number of snapshots in this batch |
| `total_sent` | number | Running total of snapshots sent so far |
| `snapshots` | object\[] | Array of `book_snapshot` objects (see below) |
Once all snapshots have been delivered, you'll receive a completion marker:
```json theme={null}
{
"type": "snapshots_done",
"total": 12239
}
```
Only tokens with active orderbook data are included in snapshots. Empty books are skipped. You'll still receive live `price_change` and `book_update` events for all subscribed tokens via batched updates.
### `book_snapshot`
Full orderbook state for a token. Sent inside `snapshot_batch` messages after subscribing, and occasionally in `batch` updates when the upstream source sends a full refresh.
```json theme={null}
{
"type": "book_snapshot",
"asset_id": "73624432805780182150964443951045800666977811185963019133914618974858599458273",
"market": "0x561ffbf7de21ef3781c441f30536b026d2b301d7a4a0145a8f526f98db049ba2",
"condition_id": "0x561ffbf7de21ef3781c441f30536b026d2b301d7a4a0145a8f526f98db049ba2",
"event_title": "What price will Bitcoin hit in March?",
"question": "Will Bitcoin reach $150,000 in March?",
"outcome": "Yes",
"slug": "what-price-will-bitcoin-hit-in-march-2026",
"bids": [
{ "price": "0.001", "size": "8604930.58" },
{ "price": "0.002", "size": "4851192.42" }
],
"asks": [
{ "price": "0.999", "size": "6152045.08" },
{ "price": "0.998", "size": "55007.54" },
{ "price": "0.997", "size": "5000" }
]
}
```
| Field | Type | Description |
| -------------- | ------------- | ------------------------------------------------------------------------ |
| `asset_id` | string | Polymarket CLOB token ID |
| `market` | string | Condition ID (hex) |
| `condition_id` | string | Condition ID (hex), same as `market` |
| `event_title` | string | Parent event title (e.g. "What price will Bitcoin hit in March?") |
| `question` | string | Specific market question (e.g. "Will Bitcoin reach \$150,000 in March?") |
| `outcome` | string | Which outcome this token represents ("Yes" or "No") |
| `slug` | string | URL-friendly slug for the event page on Polymarket |
| `bids` | PriceLevel\[] | Buy orders, sorted by price descending |
| `asks` | PriceLevel\[] | Sell orders, sorted by price ascending |
Each `PriceLevel` is `{"price": "0.55", "size": "1000.00"}` where price is the probability (0 to 1) and size is the number of shares.
If `bids` and `asks` are both empty, the market exists but has no active orders. You'll still receive `price_change` updates in batches when the price moves.
### `batch`
Batched updates delivered every 250ms. Contains all changes for your subscribed tokens since the last batch. Multiple updates to the same price level within a batch window are coalesced into a single net change, reducing bandwidth without losing accuracy.
```json theme={null}
{
"type": "batch",
"ts": 1773794605604,
"count": 4,
"updates": [
{
"type": "price_change",
"market": "0xd1796c09d0d6f876f8580086ae9808ec991784e3a74b25a1830a25de71a78c96",
"slug": "netanyahu-out-before-2027",
"assets": [
{
"asset_id": "66255671088804707681511323064315150986307471908131081808279119719218775249892",
"outcome": "No",
"price": "0.55",
"size": "2500",
"side": "BUY",
"best_bid": "0.55",
"best_ask": "0.56"
},
{
"asset_id": "114694726451307654528948558967898493662917070661203465131156925998487819889437",
"outcome": "Yes",
"price": "0.45",
"size": "0",
"side": "SELL",
"best_bid": "0.44",
"best_ask": "0.46"
}
]
},
{
"type": "book_update",
"asset_id": "73624432805780182150964443951045800666977811185963019133914618974858599458273",
"market": "0x561ffbf7de21ef3781c441f30536b026d2b301d7a4a0145a8f526f98db049ba2",
"outcome": "Yes",
"slug": "what-price-will-bitcoin-hit-in-march-2026",
"bids": [
{ "price": "0.45", "size": "12000" }
],
"asks": []
}
]
}
```
| Field | Type | Description |
| --------- | --------- | ----------------------------------- |
| `ts` | number | Batch timestamp (Unix milliseconds) |
| `count` | number | Number of updates in this batch |
| `updates` | object\[] | Array of update objects (see below) |
Each update in the `updates` array is one of:
#### `price_change`
A market's price level changed due to an order placement, cancellation, or fill. Each entry in `assets` describes a single level change. `size` is the absolute new size at that level — `"0"` means the level was removed. You can apply `price_change` events directly to your local book to maintain tick-accurate state without polling.
| Field | Type | Description |
| -------- | --------- | -------------------------------------- |
| `type` | string | `"price_change"` |
| `market` | string | Condition ID |
| `slug` | string | Event slug |
| `assets` | object\[] | Array of per-level changes (see below) |
Each entry in `assets`:
| Field | Type | Description |
| ---------- | ------ | ------------------------------------------------------------------ |
| `asset_id` | string | Token ID |
| `outcome` | string | Outcome label (`"Yes"`, `"No"`, etc.) |
| `price` | string | The level that changed |
| `size` | string | Absolute size at that level after the change. `"0"` means removed. |
| `side` | string | `"BUY"` (affects bids) or `"SELL"` (affects asks) |
| `best_bid` | string | Best bid on this asset after the change |
| `best_ask` | string | Best ask on this asset after the change |
Multiple levels on the same `asset_id` may appear in one batch — each represents a distinct level change. The 250ms coalescer merges repeat hits on the same `(asset_id, price, side)` to last-write-wins, so you always get the most recent size for each level.
#### `book_update`
Incremental change to the orderbook. Apply these to the last snapshot.
| Field | Type | Description |
| ---------- | ------------- | -------------------------------------------- |
| `type` | string | `"book_update"` |
| `asset_id` | string | Token ID |
| `market` | string | Condition ID |
| `outcome` | string | Outcome label |
| `slug` | string | Event slug |
| `bids` | PriceLevel\[] | Changed bid levels (size `"0"` means remove) |
| `asks` | PriceLevel\[] | Changed ask levels (size `"0"` means remove) |
`book_update` messages are **incremental deltas**, not full replacements. To maintain an accurate orderbook:
* If a level's `size` is `"0"`, remove that price level
* Otherwise, upsert the level (add or update)
#### `book_snapshot`
Occasional full orderbook replacement (sent when the upstream source resets).
Same schema as the initial `book_snapshot` message above.
#### `last_trade_price`
A trade executed on Polymarket. Delivered for every fill, not coalesced — each trade in a batch window is its own event.
```json theme={null}
{
"type": "last_trade_price",
"asset_id": "196889930626949471000189353840...",
"market": "0x8f3a...",
"price": "0.76",
"size": "5",
"side": "BUY",
"fee_rate_bps": "0",
"timestamp": "1773991152458",
"outcome": "Up",
"slug": "btc-updown-15m-1773990900"
}
```
| Field | Type | Description |
| -------------- | ------ | --------------------------------- |
| `type` | string | `"last_trade_price"` |
| `asset_id` | string | Token ID that was traded |
| `market` | string | Condition ID |
| `price` | string | Execution price (0-1 probability) |
| `size` | string | Number of shares traded |
| `side` | string | `"BUY"` or `"SELL"` |
| `fee_rate_bps` | string | Fee rate in basis points |
| `timestamp` | string | Unix milliseconds of the trade |
| `outcome` | string | Outcome label (e.g. "Yes", "Up") |
| `slug` | string | Event slug |
Use `last_trade_price` events to build trade tapes, calculate VWAP, detect large fills, or trigger alerts on execution activity. Unlike `price_change` events which fire on order placement/cancellation, these fire only when an order is actually filled.
**Need sub-millisecond granularity?** Enterprise plans can be configured with a dedicated stream that delivers every individual orderbook event without batching, including per-event timestamps. [Contact us](mailto:josh@quantish.live) if your strategy requires tick-by-tick data.
### `unsubscribed`
Acknowledgment after unsubscribe.
```json theme={null}
{
"type": "unsubscribed"
}
```
### `pong`
Response to a client ping.
```json theme={null}
{
"type": "pong"
}
```
### Error messages
```json theme={null}
{
"error": "input_too_large",
"message": "Too many markets in one request. Use markets: [\"*\"] for the full firehose, or split very large explicit lists across requests."
}
```
```json theme={null}
{
"error": "session_limit",
"message": "Free tier 1-hour daily limit reached."
}
```
| Error | Cause |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `token_limit` | Explicit subscription resolved above 15,000 token IDs on one connection. Use `markets: ["*"]` for the full firehose, or split large explicit lists across connections. |
| `input_too_large` | Request contains an unusually large explicit identifier list. Use `markets: ["*"]` for the full firehose, or split very large explicit lists across connections. |
| `service_warming_up` | The Orderbook Stream accepted the connection but is still loading market metadata after a restart. Retry the subscribe shortly. |
| `session_limit` | Free tier 1-hour daily session expired (resets at midnight UTC) |
# Orderbook Stream
Source: https://docs.polynode.dev/orderbook/overview
Real-time orderbook data for every Polymarket market, delivered through a single WebSocket connection.
PolyNode's Orderbook Stream delivers live orderbook snapshots, depth updates, and price changes for every active Polymarket market. Subscribe to the markets you care about and get enriched, human-readable data pushed to your connection in real time.
Every active Polymarket market is tracked. Subscribe to specific markets or get everything at once.
Snapshots include the event title, market question, outcome label, slug, and condition ID. Streaming updates include the slug and outcome for lightweight routing.
Updates are batched into 250ms windows for efficient delivery. One message per tick with all changes for your subscribed markets.
Add `&compress=zlib` to your connection URL for \~50% bandwidth savings. Recommended for production.
## Quick start
```bash theme={null}
curl -s -X POST https://api.polynode.dev/v1/keys \
-H "Content-Type: application/json" \
-d '{"name": "my-app"}'
```
Save the `pn_live_...` key from the response.
Subscribe using a **condition ID** or **token ID** from the REST API. Both sides of the market (Yes + No) are included automatically.
```javascript Node.js theme={null}
const ws = new WebSocket("wss://ob.polynode.dev/ws?key=pn_live_YOUR_KEY");
ws.onopen = () => {
// Subscribe by condition ID — resolves to both Yes + No tokens
ws.send(JSON.stringify({
action: "subscribe",
markets: ["0x7cb525e831729325d651017f81cbcb6f1adde5011c7b2283babea00b4ae93ae7"]
}));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === "snapshot_batch") {
for (const snap of msg.snapshots) {
console.log(`${snap.question} [${snap.outcome}]: ${snap.bids.length} bids, ${snap.asks.length} asks`);
}
} else if (msg.type === "batch") {
for (const update of msg.updates) {
if (update.type === "price_change") {
for (const asset of update.assets) {
// size="0" means the level was removed; otherwise it's the new absolute size at that level.
console.log(`${update.slug} [${asset.outcome}] ${asset.side} @ ${asset.price} -> size ${asset.size} (bbo: ${asset.best_bid}/${asset.best_ask})`);
}
}
}
}
};
```
```python Python theme={null}
import asyncio, json, websockets
async def stream_orderbook():
async with websockets.connect("wss://ob.polynode.dev/ws?key=pn_live_YOUR_KEY") as ws:
await ws.send(json.dumps({
"action": "subscribe",
"markets": ["0x7cb525e831729325d651017f81cbcb6f1adde5011c7b2283babea00b4ae93ae7"]
}))
async for message in ws:
data = json.loads(message)
if data["type"] == "snapshot_batch":
for snap in data["snapshots"]:
print(f"{snap['question']} [{snap['outcome']}]: {len(snap['bids'])} bids, {len(snap['asks'])} asks")
elif data["type"] == "batch":
for update in data["updates"]:
if update["type"] == "price_change":
for asset in update["assets"]:
# size="0" removes the level; otherwise it's the new absolute size.
print(f"{update['slug']} [{asset['outcome']}] {asset['side']} @ {asset['price']} -> size {asset['size']} (bbo: {asset['best_bid']}/{asset['best_ask']})")
asyncio.run(stream_orderbook())
```
```bash CLI theme={null}
npx wscat -c "wss://ob.polynode.dev/ws?key=pn_live_YOUR_KEY"
# once connected, send:
{"action":"subscribe","markets":["0x7cb525e831729325d651017f81cbcb6f1adde5011c7b2283babea00b4ae93ae7"]}
```
Use the REST API to find condition IDs: `curl -s -H "x-api-key: pn_live_YOUR_KEY" "https://api.polynode.dev/v1/search?q=bitcoin"` — each result includes the `condition_id` you can pass directly to the orderbook subscribe.
You'll immediately receive an orderbook snapshot for each subscribed token, then live batched updates:
```json theme={null}
{
"type": "book_snapshot",
"asset_id": "73624432805780182150964443951045800666977811185963019133914618974858599458273",
"market": "0x561ffbf7de21ef3781c441f30536b026d2b301d7a4a0145a8f526f98db049ba2",
"condition_id": "0x561ffbf7de21ef3781c441f30536b026d2b301d7a4a0145a8f526f98db049ba2",
"event_title": "What price will Bitcoin hit in March?",
"question": "Will Bitcoin reach $150,000 in March?",
"outcome": "Yes",
"slug": "what-price-will-bitcoin-hit-in-march-2026",
"bids": [
{ "price": "0.001", "size": "8604930.58" },
{ "price": "0.002", "size": "4851192.42" }
],
"asks": [
{ "price": "0.999", "size": "6152045.08" },
{ "price": "0.998", "size": "55007.54" },
{ "price": "0.997", "size": "5000" }
]
}
```
## Ways to subscribe
You can identify markets using any of these formats:
Subscribe to every active market with a single message. Pass `"*"` as the market identifier to get the full firehose (full data stream) of all 100,000+ tokens.
```json theme={null}
{
"action": "subscribe",
"markets": ["*"]
}
```
The server responds with `"firehose": true` and the total number of active tokens. Live updates start flowing immediately while snapshots stream in the background.
The simplest way to subscribe to specific markets. Use the slug from the Polymarket event URL. The server resolves it to all token IDs for that event (typically 2 per market, one for each outcome).
```json theme={null}
{
"action": "subscribe",
"markets": ["what-price-will-bitcoin-hit-in-march-2026"]
}
```
Use the hex condition ID for a specific market within an event. Resolves to the Yes + No token IDs for that market.
```json theme={null}
{
"action": "subscribe",
"markets": ["0x561ffbf7de21ef3781c441f30536b026d2b301d7a4a0145a8f526f98db049ba2"]
}
```
Use the raw CLOB token ID if you already have it. This matches Polymarket's market identifier.
```json theme={null}
{
"action": "subscribe",
"markets": [
"73624432805780182150964443951045800666977811185963019133914618974858599458273",
"1666184515532238710431784265702709312060757077236443477960106115591255115343"
]
}
```
You can mix formats in a single subscribe:
```json theme={null}
{
"action": "subscribe",
"markets": [
"what-price-will-bitcoin-hit-in-march-2026",
"0xd1796c09d0d6f876f8580086ae9808ec991784e3a74b25a1830a25de71a78c96",
"73624432805780182150964443951045800666977811185963019133914618974858599458273"
]
}
```
## Finding markets
Use the PolyNode REST API to search for markets and find their slugs, condition IDs, or token IDs:
```bash theme={null}
# Search by keyword
curl -s "https://api.polynode.dev/v1/search?q=bitcoin" | python3 -m json.tool
# Look up by slug
curl -s "https://api.polynode.dev/v1/markets/slug/what-price-will-bitcoin-hit-in-march-2026"
# Look up by condition ID
curl -s "https://api.polynode.dev/v1/markets/condition/0x561ffbf7de21ef3781c441f30536b026d2b301d7a4a0145a8f526f98db049ba2"
```
Or just grab the slug from any Polymarket event URL: `polymarket.com/event/{slug}`.
## Connection URL
```
wss://ob.polynode.dev/ws?key=pn_live_YOUR_KEY
```
With compression (\~50% bandwidth savings):
```
wss://ob.polynode.dev/ws?key=pn_live_YOUR_KEY&compress=zlib
```
## Subscription limits
Use `markets: ["*"]` for the full all-market firehose. Explicit market lists are capped at **15,000 resolved token IDs per connection** to protect low-latency fan-out; split large explicit watchlists across connections or use the firehose path when you truly need every market.
| Tier | Connections | Explicit list cap | Firehose | Session limit |
| ------------------------- | ----------- | -------------------------- | -------- | ------------- |
| **Free** | 1 | 15,000 tokens / connection | Included | 1 hour/day |
| **Starter** (\$50/mo) | 10 | 15,000 tokens / connection | Included | Unlimited |
| **Growth** (\$200/mo) | 50 | 15,000 tokens / connection | Included | Unlimited |
| **Enterprise** (\$750/mo) | Unlimited | 15,000 tokens / connection | Included | Unlimited |
Each Polymarket market has two tokens (one per outcome). Subscribing to both tokens of a market gives you the full two-sided orderbook.
Plan upgrades increase the number of concurrent Orderbook Stream connections. The explicit-list token cap is a per-connection safety limit and is not removed by upgrading.
**Free tier includes the full firehose.** Subscribe to every market and see the full data flow. The only limit is a 1-hour daily session so you can properly evaluate the product before upgrading.
**Orderbook connections are separate from event WebSocket connections.** Your plan's Event WebSocket limit applies to `ws.polynode.dev`; the limits above apply to the Orderbook Stream at `ob.polynode.dev`. Session time resets at midnight UTC.
## Connection lifecycle
```
Connect -> wss://ob.polynode.dev/ws?key=pn_live_...
<- (connection established)
Send -> {"action": "subscribe", "markets": ["*"]}
<- {"type": "subscribed", "firehose": true, "markets": 104972}
<- {"type": "batch", "ts": ..., "updates": [...]} (live data starts immediately)
<- {"type": "snapshot_batch", "count": 50, "snapshots": [...]} (snapshots stream in parallel)
<- {"type": "batch", ...} (every 100ms)
<- {"type": "snapshot_batch", ...} (every 200ms)
<- {"type": "snapshots_done", "total": 67}
<- Ping frame (every 30s)
-> Pong frame (automatic)
```
Live updates start flowing immediately. You don't have to wait for snapshots to finish. Snapshots are delivered in batches of 50 every 200ms for tokens that have active book data.
```
Connect -> wss://ob.polynode.dev/ws?key=pn_live_...
<- (connection established)
Send -> {"action": "subscribe", "markets": ["bitcoin-slug"]}
<- {"type": "subscribed", "markets": 4, "resolved_from": {"token_ids": 0, "slugs": 1, "condition_ids": 0}}
<- {"type": "snapshot_batch", "count": 2, "snapshots": [...]}
<- {"type": "snapshots_done", "total": 2}
<- {"type": "batch", "ts": ..., "updates": [...]}
<- {"type": "batch", ...} (every 250ms when there are changes)
<- Ping frame (every 30s)
-> Pong frame (automatic)
Send -> {"action": "unsubscribe"}
<- {"type": "unsubscribed"}
```
## Separate from the main WebSocket
The Orderbook Stream runs on a **separate endpoint** (`ob.polynode.dev`) from the main PolyNode WebSocket (`ws.polynode.dev`). They are independent services:
| | Main WebSocket | Orderbook Stream |
| ------------ | --------------------------------------------- | --------------------------------------------- |
| **Endpoint** | `wss://ws.polynode.dev/ws` | `wss://ob.polynode.dev/ws` |
| **Data** | On-chain events (settlements, trades, blocks) | Off-chain orderbook (bids, asks, prices) |
| **Timing** | Pre-confirmation (pending + confirmed) | Real-time (live order placement/cancellation) |
| **Use case** | Trade detection, copy trading, alerts | Market making, pricing, depth analysis |
You can connect to both simultaneously with the same API key.
# Quickstart
Source: https://docs.polynode.dev/quickstart
Generate an API key and make your first request in 30 seconds.
**Polymarket V2 cutover: April 28, 2026 at \~11am UTC.** If you place orders via our trading SDK, upgrade to the V2-ready version and flip one config line before cutover. See the [V2 Migration Guide](/guides/v2-migration) for the full checklist.
## 1. Generate an API key
```bash cURL theme={null}
curl -s -X POST https://api.polynode.dev/v1/keys \
-H "Content-Type: application/json" \
-d '{"name": "my-bot"}'
```
```python Python theme={null}
import requests
resp = requests.post("https://api.polynode.dev/v1/keys", json={"name": "my-bot"})
key = resp.json()["api_key"]
print(key) # pn_live_a1b2c3d4e5f6...
```
```typescript TypeScript theme={null}
const resp = await fetch("https://api.polynode.dev/v1/keys", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "my-bot" }),
});
const { api_key } = await resp.json();
```
```json Response theme={null}
{
"api_key": "pn_live_a1b2c3d4e5f6...",
"name": "my-bot",
"rate_limit_per_minute": 120,
"message": "Store this key securely — it cannot be retrieved again."
}
```
Save the `api_key` immediately. It cannot be retrieved after creation.
## 2. Make your first request
Fetch the top 5 markets by 24h volume:
```bash cURL theme={null}
curl -s "https://api.polynode.dev/v1/markets?count=5" \
-H "x-api-key: pn_live_YOUR_KEY" | jq
```
```python Python theme={null}
import requests
resp = requests.get(
"https://api.polynode.dev/v1/markets",
params={"count": 5},
headers={"x-api-key": "pn_live_YOUR_KEY"},
)
print(resp.json())
```
```typescript TypeScript theme={null}
const resp = await fetch("https://api.polynode.dev/v1/markets?count=5", {
headers: { "x-api-key": "pn_live_YOUR_KEY" },
});
const data = await resp.json();
```
```json Response theme={null}
{
"count": 5,
"total": 69482,
"markets": [
{
"token_id": "247024838963513327646981976118812247380",
"question": "Will Bitcoin reach $100k?",
"last_price": 0.72,
"volume_24h": 48293.50,
"trade_count_24h": 677,
"last_trade_at": 1772512467000,
"slug": "bitcoin-100k",
"outcomes": ["Yes", "No"]
}
]
}
```
## 3. Stream live settlements
Connect via WebSocket to receive real-time settlements:
```javascript theme={null}
const ws = new WebSocket("wss://ws.polynode.dev/ws?key=pn_live_YOUR_KEY");
ws.onopen = () => {
ws.send(JSON.stringify({
action: "subscribe",
type: "settlements",
}));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === "settlement") {
console.log(`${msg.data.taker_side} $${msg.data.taker_size} @ ${msg.data.taker_price}`);
}
};
```
You'll receive a snapshot of recent events, then live updates as new settlements are detected.
## Next steps
API key management and auth methods
Real-time streaming with filtered subscriptions
Full endpoint reference with playground
Usage limits and best practices
# Gas Oracle
Source: https://docs.polynode.dev/rpc/gas-oracle
Real-time gas price intelligence for optimal transaction positioning
## Overview
PolyNode's Gas Oracle scans every block on Polygon to determine the exact gas price needed to achieve top-of-block positioning. Unlike standard `eth_gasPrice` which returns a network average, our oracle tells you what the **competitive floor** is — what the top-of-block bots are actually paying.
## Endpoint
```
GET https://rpc.polynode.dev/v1/gas
```
## Response
```json theme={null}
{
"recommendation": {
"recommended_gas_wei": 1986123386877,
"recommended_gas_gwei": 1986.1,
"avg_top_gas_gwei": 1547.4,
"max_top_gas_gwei": 1805.6,
"blocks_scanned": 20,
"safety_multiplier": 1.1,
"base_gas_gwei": 111.4,
"effective_multiplier": 17.8
},
"recent_blocks": [
{
"block_number": 84210810,
"top_gas_gwei": 1804.6,
"second_gas_gwei": 1804.6,
"median_gas_gwei": 168.5,
"tx_count": 252
}
]
}
```
## Fields
| Field | Description |
| ---------------------- | --------------------------------------------------------- |
| `recommended_gas_wei` | Gas price in wei to beat current top-of-block competition |
| `recommended_gas_gwei` | Same value in gwei for readability |
| `avg_top_gas_gwei` | Average highest gas price across scanned blocks |
| `max_top_gas_gwei` | Maximum top-of-block gas price observed |
| `blocks_scanned` | Number of recent blocks analyzed |
| `safety_multiplier` | Multiplier applied above max observed (default 1.1x) |
| `base_gas_gwei` | Current network base gas price |
| `effective_multiplier` | How many times above base gas the recommendation is |
## Per-Block Data
The `recent_blocks` array provides gas data for each scanned block:
| Field | Description |
| ----------------- | ---------------------------------------- |
| `top_gas_gwei` | Highest gas price in the block (TX #1) |
| `second_gas_gwei` | Second highest gas price |
| `median_gas_gwei` | Median gas price across all transactions |
| `tx_count` | Total transactions in the block |
## Usage
Use the gas oracle to set your transaction's gas price for optimal block positioning:
```python theme={null}
import requests
# Get current recommendation
resp = requests.get('https://rpc.polynode.dev/v1/gas')
data = resp.json()
# Use recommended gas price for TX #1
gas_price = data['recommendation']['recommended_gas_wei']
# Build your transaction with this gas price
tx = {
'gasPrice': gas_price,
# ... rest of your transaction
}
```
## Refresh Rate
The gas oracle updates every **30 seconds**, scanning the most recent 20 blocks. Data reflects real-time market conditions on Polygon.
# Integration Guide
Source: https://docs.polynode.dev/rpc/integration
Connect to polynode RPC from any Ethereum library or wallet
## Endpoint
```
https://rpc.polynode.dev
```
All requests require a valid API key via `x-api-key` header or `Authorization: Bearer` header.
## Integration Examples
```typescript theme={null}
import { createPublicClient, createWalletClient, http } from 'viem'
import { polygon } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'
const transport = http('https://rpc.polynode.dev', {
fetchOptions: {
headers: { 'x-api-key': 'YOUR_API_KEY' }
}
})
// Read contract state
const publicClient = createPublicClient({
chain: polygon,
transport
})
const blockNumber = await publicClient.getBlockNumber()
const balance = await publicClient.getBalance({
address: '0xYOUR_ADDRESS'
})
// Send transactions with priority routing
const account = privateKeyToAccount('0xYOUR_PRIVATE_KEY')
const walletClient = createWalletClient({
account,
chain: polygon,
transport
})
const hash = await walletClient.sendTransaction({
to: '0xRECIPIENT',
value: parseEther('1.0')
})
```
```javascript theme={null}
import { ethers } from 'ethers'
const provider = new ethers.JsonRpcProvider(
'https://rpc.polynode.dev',
137,
{ staticNetwork: true }
)
// Add auth to every request
const originalSend = provider.send.bind(provider)
provider.send = async (method, params) => {
const resp = await fetch('https://rpc.polynode.dev', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'YOUR_API_KEY'
},
body: JSON.stringify({ jsonrpc: '2.0', method, params, id: 1 })
})
const { result, error } = await resp.json()
if (error) throw new Error(error.message)
return result
}
const blockNumber = await provider.getBlockNumber()
const balance = await provider.getBalance('0xYOUR_ADDRESS')
// Send a transaction
const wallet = new ethers.Wallet('YOUR_PRIVATE_KEY', provider)
const tx = await wallet.sendTransaction({
to: '0xRECIPIENT',
value: ethers.parseEther('1.0')
})
```
```python theme={null}
from web3 import Web3
w3 = Web3(Web3.HTTPProvider(
'https://rpc.polynode.dev',
request_kwargs={
'headers': {'x-api-key': 'YOUR_API_KEY'}
}
))
# Read state
block = w3.eth.block_number
balance = w3.eth.get_balance('0xYOUR_ADDRESS')
# Check token balance (USDC)
usdc = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'
data = '0x70a08231' + '0xYOUR_ADDRESS'[2:].lower().zfill(64)
result = w3.eth.call({'to': usdc, 'data': data})
usdc_balance = int(result.hex(), 16) / 1e6
# Send a transaction
account = w3.eth.account.from_key('YOUR_PRIVATE_KEY')
tx = {
'nonce': w3.eth.get_transaction_count(account.address),
'to': '0xRECIPIENT',
'value': w3.to_wei(1, 'ether'),
'gas': 21000,
'gasPrice': w3.eth.gas_price,
'chainId': 137,
}
signed = account.sign_transaction(tx)
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
```
```bash theme={null}
# Check block number
curl -X POST https://rpc.polynode.dev \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'
# Check MATIC balance
curl -X POST https://rpc.polynode.dev \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"jsonrpc":"2.0",
"method":"eth_getBalance",
"params":["0xYOUR_ADDRESS","latest"],
"id":1
}'
# Send a signed transaction
curl -X POST https://rpc.polynode.dev \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"jsonrpc":"2.0",
"method":"eth_sendRawTransaction",
"params":["0xSIGNED_TX_HEX"],
"id":1
}'
```
## Hardhat / Foundry
```javascript theme={null}
// hardhat.config.js
module.exports = {
networks: {
polygon: {
url: 'https://rpc.polynode.dev',
accounts: [process.env.PRIVATE_KEY],
chainId: 137,
httpHeaders: {
'x-api-key': process.env.POLYNODE_API_KEY
}
}
}
}
```
```bash theme={null}
# Set auth via header
export ETH_RPC_URL=https://rpc.polynode.dev
# Deploy
forge script script/Deploy.s.sol \
--rpc-url $ETH_RPC_URL \
--broadcast \
--private-key $PRIVATE_KEY
# Read state
cast call 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359 \
"totalSupply()" \
--rpc-url $ETH_RPC_URL
```
## Response Format
Standard Ethereum JSON-RPC responses:
```json theme={null}
// Success
{"jsonrpc": "2.0", "id": 1, "result": "0x..."}
// Error
{"jsonrpc": "2.0", "id": 1, "error": {"code": -32000, "message": "..."}}
```
# Polygon RPC
Source: https://docs.polynode.dev/rpc/overview
Authenticated Polygon RPC with priority transaction routing
## Endpoint
```
https://rpc.polynode.dev
```
Standard Ethereum JSON-RPC over HTTPS. Drop-in replacement for any Polygon RPC — just change the URL and add your API key.
## Authentication
Every request requires a valid polynode API key. Pass it as a header:
```
x-api-key: YOUR_API_KEY
```
Or as a Bearer token:
```
Authorization: Bearer YOUR_API_KEY
```
Requests without a valid key receive a `-32000` error.
## Rate Limit
All keys are rate-limited to **2 requests per second** per IP. Exceeding this returns a `-32005` error. Batch requests count as a single request.
## Supported Methods
| Method | Source | Description |
| --------------------------- | -------------------- | ----------------------------------------- |
| `eth_sendRawTransaction` | **Priority routing** | P2P delivery to block-producing validator |
| `eth_chainId` | Local | Returns `0x89` (137) |
| `eth_blockNumber` | Local | Latest block from P2P network |
| `eth_gasPrice` | Local | Calibrated for optimal block positioning |
| `net_version` | Local | Returns `137` |
| `net_peerCount` | Local | Connected P2P peer count |
| `web3_clientVersion` | Local | Returns `PolyNode/1.0.0` |
| `eth_call` | P2P node | Contract read calls |
| `eth_getBalance` | P2P node | Account balance queries |
| `eth_getTransactionCount` | P2P node | Nonce queries |
| `eth_estimateGas` | P2P node | Gas estimation |
| `eth_getBlockByNumber` | P2P node | Block data |
| `eth_getTransactionReceipt` | P2P node | Transaction receipts |
| `eth_getLogs` | P2P node | Log queries (requires address filter) |
**Current-state only.** `eth_getBalance`, `eth_getStorageAt`, and `eth_call` only support the `latest` block tag. Historical state at old block numbers is not available. Block and transaction data works for all blocks.
## Common Queries
### Check a wallet's MATIC balance
```bash theme={null}
curl -X POST https://rpc.polynode.dev \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"jsonrpc": "2.0",
"method": "eth_getBalance",
"params": ["0xYOUR_WALLET_ADDRESS", "latest"],
"id": 1
}'
```
The result is the balance in wei (hex). Divide by 10^18 for MATIC.
### Check a wallet's USDC balance
Use `eth_call` with the ERC-20 `balanceOf(address)` function:
```bash theme={null}
curl -X POST https://rpc.polynode.dev \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"jsonrpc": "2.0",
"method": "eth_call",
"params": [{
"to": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
"data": "0x70a08231000000000000000000000000YOUR_WALLET_ADDRESS_WITHOUT_0x"
}, "latest"],
"id": 1
}'
```
Replace `YOUR_WALLET_ADDRESS_WITHOUT_0x` with the 40-character address (no `0x` prefix), zero-padded to 64 characters. The result is the balance in the token's smallest unit (hex). USDC has 6 decimals.
**Common Polygon token contracts:**
| Token | Address |
| ---------------- | -------------------------------------------- |
| USDC | `0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359` |
| USDC.e (bridged) | `0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174` |
| USDT | `0xc2132D05D31c914a87C6611C10748AEb04B58e8F` |
| WMATIC | `0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270` |
| WETH | `0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619` |
### Read any contract function
The `data` field for `eth_call` is the function selector (first 4 bytes of the keccak256 hash) followed by ABI-encoded arguments.
Common selectors:
| Function | Selector | Arguments |
| ---------------------------- | ------------ | ------------------------------------- |
| `balanceOf(address)` | `0x70a08231` | address (32 bytes, left-padded) |
| `totalSupply()` | `0x18160ddd` | none |
| `decimals()` | `0x313ce567` | none |
| `symbol()` | `0x95d89b41` | none |
| `name()` | `0x06fdde03` | none |
| `allowance(address,address)` | `0xdd62ed3e` | owner (32 bytes) + spender (32 bytes) |
### Get the current block number
```bash theme={null}
curl -X POST https://rpc.polynode.dev \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'
```
### Get a transaction receipt
```bash theme={null}
curl -X POST https://rpc.polynode.dev \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"jsonrpc": "2.0",
"method": "eth_getTransactionReceipt",
"params": ["0xYOUR_TX_HASH"],
"id": 1
}'
```
## Batch Requests
Send multiple calls in a single HTTP request:
```bash theme={null}
curl -X POST https://rpc.polynode.dev \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '[
{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1},
{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":2},
{"jsonrpc":"2.0","method":"eth_gasPrice","params":[],"id":3}
]'
```
Batch requests count as a single request against the rate limit.
## Error Codes
| Code | Meaning |
| -------- | --------------------------------- |
| `-32000` | Missing or invalid API key |
| `-32005` | Rate limit exceeded (max 2 req/s) |
| `-32601` | Method not available |
| `-32602` | Invalid parameters |
| `-32603` | Upstream error |
# Transaction Tracking
Source: https://docs.polynode.dev/rpc/tx-tracking
Track your transaction's block position and confirmation status
## Overview
Every transaction submitted through PolyNode RPC is tracked from submission to block inclusion. You can query the exact block position and confirmation latency.
## Submit a Transaction
Hex-encoded signed transaction (with or without `0x` prefix)
```bash theme={null}
POST https://rpc.polynode.dev/v1/tx/submit
Content-Type: application/json
{
"raw_tx": "0x02f87083..."
}
```
### Response
```json theme={null}
{
"tx_hash": "0xabc123...",
"submitted_at": 1710524000,
"status": "pending"
}
```
| Field | Description |
| --------- | --------------------------------------- |
| `tx_hash` | Transaction hash |
| `status` | `pending` — waiting for block inclusion |
## Check Transaction Status
```bash theme={null}
GET https://rpc.polynode.dev/v1/tx/{tx_hash}
```
### Pending
```json theme={null}
{
"status": "pending",
"submitted_at": 1710524000,
"elapsed_ms": 1500
}
```
### Confirmed
```json theme={null}
{
"status": "confirmed",
"submitted_at": 1710524000,
"confirmed_at": 1710524002,
"block_number": 84210500,
"block_hash": "0xdef456...",
"tx_index": 0,
"total_txs_in_block": 186,
"is_first": true,
"latency_ms": 2450
}
```
| Field | Description |
| -------------------- | --------------------------------------------------- |
| `block_number` | Block the transaction was included in |
| `tx_index` | Position within the block (0 = first transaction) |
| `total_txs_in_block` | Total transactions in the block |
| `is_first` | Whether this was the first transaction in the block |
| `latency_ms` | Time from submission to block confirmation |
### Expired
If a transaction isn't included within \~128 seconds (64 blocks), it's marked as expired:
```json theme={null}
{
"status": "expired",
"submitted_at": 1710524000,
"reason": "not included within 64 blocks"
}
```
## Aggregate Stats
```bash theme={null}
GET https://rpc.polynode.dev/v1/stats
```
```json theme={null}
{
"total_submitted": 1000,
"total_confirmed": 998,
"total_first_position": 895,
"first_position_rate": 0.897,
"total_expired": 2,
"pending": 0,
"avg_latency_ms": 2450
}
```
# Local Cache
Source: https://docs.polynode.dev/sdks/local-cache
SQLite-backed local cache for instant offline queries. Backfill wallet history in seconds, stream live updates, query positions and trades locally.
Track wallets, markets, and tokens locally. The SDK streams live events into a SQLite database and backfills recent history on startup. All queries run instantly against the local DB with zero API calls.
## Why Use the Cache
Without the cache, every page view that shows trader positions requires upstream API calls. For apps tracking dozens or hundreds of wallets, this hits rate limits fast.
With the cache:
* **Three API calls per wallet** to backfill: open positions (metadata), onchain positions (complete P\&L), and recent trades
* **Live WebSocket stream** keeps everything up to date after that
* **All queries are local** — positions, trades, P\&L, stats are instant
* **Persists across restarts** — SQLite file stays on disk
## Quick Start
```bash theme={null}
npm install polynode-sdk better-sqlite3
```
`better-sqlite3` is an optional peer dependency. Only needed if you use the cache.
```toml theme={null}
# Cargo.toml
polynode = { version = "0.12", features = ["cache"] }
tokio = { version = "1", features = ["full"] }
```
The `cache` feature includes `rusqlite` with bundled SQLite. No system dependency needed.
Create `polynode.watch.json` in your project root:
```json theme={null}
{
"version": 1,
"wallets": [
{ "address": "0xabc...", "label": "trader-1", "backfill": true },
{ "address": "0xdef...", "label": "trader-2", "backfill": true }
],
"settings": {
"ttl_days": 30
}
}
```
```typescript theme={null}
import { PolyNode, PolyNodeCache } from 'polynode-sdk';
const pn = new PolyNode({ apiKey: 'pn_live_...' });
const cache = new PolyNodeCache(pn, {
dbPath: './my-cache.db',
watchlistPath: './polynode.watch.json',
});
await cache.start();
```
```rust theme={null}
use polynode::{PolyNodeClient, cache::PolyNodeCache};
use std::sync::Arc;
let client = Arc::new(PolyNodeClient::new("pn_live_...")?);
let mut cache = PolyNodeCache::builder(client)
.db_path("./my-cache.db")
.watchlist_path("./polynode.watch.json")
.build()?;
cache.start().await?;
```
```typescript theme={null}
const trades = cache.walletTrades('0xabc...', { limit: 50 });
const positions = cache.walletPositions('0xabc...');
const stats = cache.stats();
```
```rust theme={null}
let trades = cache.wallet_trades("0xabc...", &QueryOptions { limit: Some(50), ..Default::default() })?;
let positions = cache.wallet_positions("0xabc...")?;
let stats = cache.stats()?;
```
## Backfill Timing
Backfill makes three requests per wallet, each spaced by `1 / backfillRatePerSecond` seconds:
1. **Open positions** — metadata (title, slug, outcome) from the standard positions API
2. **Onchain positions** — complete position history with accurate realized P\&L (single call, no client-side pagination)
3. **Recent trades** — trade history for cost basis and trade analytics
| Wallets | Requests | Time at 1 req/s | Time at 2 req/s |
| ------- | -------- | --------------- | --------------- |
| 1 | 3 | \~3 seconds | \~1.5 seconds |
| 10 | 30 | \~30 seconds | \~15 seconds |
| 50 | 150 | \~2.5 minutes | \~1.3 minutes |
| 100 | 300 | \~5 minutes | \~2.5 minutes |
P\&L data comes from the onchain positions call (step 2). This returns every position the wallet has ever held in a single request, no client-side pagination needed. P\&L is accurate and complete regardless of how many trade pages you fetch.
Trade history (step 3) is separate. It's useful if you want individual buy/sell records with prices, timestamps, and maker/taker details. Set `backfillPages` higher for more trade history:
| Pages | Trades per wallet | Extra time per wallet |
| ----------- | ----------------- | --------------------- |
| 1 (default) | up to 500 | +1 second |
| 2 | up to 1,000 | +2 seconds |
| 6 | up to 3,000 | +6 seconds |
Trade history has a 3,000 trade cap from the upstream data source. This does NOT affect P\&L accuracy. P\&L comes from onchain position data, which is complete and has no cap. The live WebSocket stream captures all new trades going forward with no limit.
## Configuration
```typescript theme={null}
const cache = new PolyNodeCache(pn, {
// File paths
dbPath: './polynode-cache.db', // SQLite database location
watchlistPath: './polynode.watch.json', // Watchlist file
// Backfill
backfillRatePerSecond: 1, // Requests per second (default: 1)
backfillPages: 1, // Pages per wallet (default: 1, max: 6)
backfillPageSize: 500, // Trades per page (default: 500, max: 1000)
// Storage
ttlSeconds: 30 * 86400, // Auto-prune after 30 days
purgeOnRemove: false, // Delete data when wallet removed from watchlist
// Progress callback
onBackfillProgress: (p) => {
console.log(`${p.label}: ${p.status} (${p.fetched} trades)`);
},
});
```
## Query Methods
All queries run against the local SQLite database. No API calls. Every example below shows real output from a live backfill.
### Wallet Trades
```typescript theme={null}
const trades = cache.walletTrades('0xad53...', { limit: 3 });
```
```json Example output theme={null}
[
{
"side": "BUY",
"price": 0.821,
"size": 5.92,
"market_title": "Will Iran conduct a military action against Israel on March 20, 2026?",
"outcome": "Yes",
"timestamp": "2026-03-21T18:00:28.223Z"
},
{
"side": "SELL",
"price": 0.18,
"size": 40.51,
"market_title": "Will Iran conduct a military action against Israel on March 20, 2026?",
"outcome": "No"
},
{
"side": "BUY",
"price": 0.181,
"size": 19.1,
"market_title": "Will Iran conduct a military action against Israel on March 20, 2026?",
"outcome": "No"
}
]
```
**Filters:** `side`, `since`, `until`, `orderBy`, `limit`, `offset`:
```typescript theme={null}
// Only BUY trades
const buys = cache.walletTrades('0xad53...', { limit: 3, side: 'BUY' });
// Pagination
const page1 = cache.walletTrades('0xad53...', { limit: 5, offset: 0 });
const page2 = cache.walletTrades('0xad53...', { limit: 5, offset: 5 });
// Time range
const recent = cache.walletTrades('0xad53...', { since: 1774000000 });
// Ascending order
const oldest = cache.walletTrades('0xad53...', { orderBy: 'timestamp_asc', limit: 3 });
```
### Wallet Positions
Positions are backfilled from two sources: the standard positions API (metadata like title, outcome, current price) and the onchain positions endpoint (accurate realized P\&L, average entry price, total bought). Trade timestamps are enriched from the local trades table.
```typescript theme={null}
const positions = cache.walletPositions('0xad53...');
// 200 positions from 500 cached trades
```
```json Example output (first 2 of 500) theme={null}
[
{
"wallet": "0xad53...",
"token_id": "75929940...",
"market_title": "Will \"How to Make a Killing\" score at least 59 on the Rotten Tomatoes Tomatometer?",
"outcome": "Yes",
"size": 10000,
"avg_price": 0.001,
"cur_price": 0.0005,
"current_value": 5.0,
"cash_pnl": -5.0,
"percent_pnl": -50.0,
"redeemable": false,
"trade_count": 3,
"first_trade_at": 1710000000,
"last_trade_at": 1774100000
},
{
"wallet": "0xad53...",
"market_title": "Will Resni.ca (Res) be part of the next Government of Slovenia?",
"outcome": "Yes",
"size": 7.50,
"avg_price": 0.41,
"cash_pnl": 2.5,
"trade_count": 1
}
]
```
### Multi-Wallet Positions
Query positions for multiple wallets in one call:
```typescript theme={null}
const all = cache.multiWalletPositions(['0xad53...', '0x2afd...', '0xe4ca...']);
```
Returns an object keyed by wallet address, where each value is an array of positions:
```json Example output theme={null}
{
"0xad53...": [
{ "wallet": "0xad53...", "market_title": "...", "outcome": "Yes", "size": 10000, "avg_price": 0.04, "cash_pnl": -50.0, "trade_count": 3 },
{ "wallet": "0xad53...", "market_title": "...", "outcome": "No", "size": 500, "avg_price": 0.41, "cash_pnl": 12.5, "trade_count": 1 }
],
"0x2afd...": [
{ "wallet": "0x2afd...", "market_title": "...", "outcome": "Yes", "size": 250, "avg_price": 0.55, "cash_pnl": 30.0, "trade_count": 2 }
],
"0xe4ca...": [...]
}
```
### Market Trades
```typescript theme={null}
const trades = cache.marketTrades('0xe1cc...', { limit: 3 });
```
```json Example output theme={null}
[
{ "taker": "0xad53...", "side": "BUY", "price": 0.821, "size": 5.92 },
{ "taker": "0xad53...", "side": "SELL", "price": 0.18, "size": 40.51 },
{ "taker": "0xad53...", "side": "BUY", "price": 0.181, "size": 19.1 }
]
```
### Market Positions
All positions across all cached wallets for a market:
```typescript theme={null}
const positions = cache.marketPositions('0xe1cc...');
// 9 positions across multiple wallets
```
```json Example output theme={null}
[
{ "outcome": "Yes", "size": -172.12, "avg_price": 0.8399 },
{ "outcome": "No", "size": -81.39, "avg_price": 0.1808 },
{ "outcome": "No", "size": 28.66, "avg_price": 0.18 }
]
```
### Token Trades
```typescript theme={null}
const trades = cache.tokenTrades('11382339...', { limit: 3 });
// All returned trades match the requested token_id
```
### Trade by Transaction Hash
Look up all trades within a single transaction:
```typescript theme={null}
const trades = cache.tradeByTxHash('0x6815497d...');
```
```json Example output theme={null}
[
{ "side": "BUY", "price": 0.821, "size": 5.92 },
{ "side": "SELL", "price": 0.18, "size": 40.51 },
{ "side": "BUY", "price": 0.181, "size": 19.1 }
]
```
### Wallet Settlements
```typescript theme={null}
const settlements = cache.walletSettlements('0xad53...', { limit: 20 });
```
### Cache Stats
```typescript theme={null}
const stats = cache.stats();
```
```json Example output theme={null}
{
"trade_count": 1509,
"settlement_count": 3,
"db_size_kb": 10567.3,
"oldest_trade": "2026-03-14T19:19:25.000Z",
"newest_trade": "2026-03-21T18:00:28.223Z",
"backfill_complete": 3,
"backfill_total": 3,
"backfill_failed": 0
}
```
## Watchlist
### File Format
```json theme={null}
{
"version": 1,
"wallets": [
{ "address": "0xabc...", "label": "whale", "backfill": true }
],
"markets": [
{ "condition_id": "0x789...", "label": "BTC 100k", "backfill": true }
],
"tokens": [
{ "token_id": "12345...", "label": "BTC Yes", "backfill": true }
],
"settings": {
"ttl_days": 30,
"backfill_rate": 1,
"purge_on_remove": false
}
}
```
### Hot Reload
Edit the watchlist file while the cache is running. Changes are detected automatically within 500ms:
* **New entries** trigger backfill and update the WebSocket subscription
* **Removed entries** optionally purge data (if `purgeOnRemove` is enabled)
### Runtime API
Add or remove wallets programmatically. Backfill starts immediately for new entries.
```typescript theme={null}
// Add a wallet — backfill starts within 1 second
cache.addToWatchlist([
{ type: 'wallet', id: '0x99ba...', label: 'UnholyScissors' }
]);
// After ~2 seconds:
cache.stats();
// { trade_count: 1857, backfill_complete: 4 }
// (was 1509 trades / 3 complete before adding)
// Remove a wallet
cache.removeFromWatchlist([
{ type: 'wallet', id: '0x99ba...' }
]);
```
## View Methods
Pre-built queries that return data shaped for dashboards. No SQL, no aggregation — just call the method.
### Watchlist Summary
All watched wallets with summary stats in one call:
```typescript theme={null}
const summary = cache.watchlistSummary();
```
```json Example output theme={null}
[
{ "wallet": "0xad53...", "label": "whale-1", "position_count": 42, "total_pnl": 1250.50, "total_value": 8400.00, "last_active": 1774200000 },
{ "wallet": "0x2afd...", "label": "degen", "position_count": 15, "total_pnl": -320.00, "total_value": 1200.00, "last_active": 1774180000 }
]
```
### Wallet Dashboard
Single wallet view with positions grouped, P\&L totals, win/loss counts, and recent trades:
```typescript theme={null}
const dash = cache.walletDashboard('0xad53...');
```
The dashboard includes realized P\&L computed from onchain position data:
| Field | Type | Description |
| ----------------- | ------ | ------------------------------------------------------------ |
| `total_positions` | number | All positions in cache |
| `total_pnl` | number | Sum of unrealized P\&L on open positions |
| `realized_pnl` | number | Total realized P\&L from onchain data |
| `pnl_confidence` | string | `"full"` when onchain data is present, `"partial"` otherwise |
| `win_count` | number | Positions with positive P\&L |
| `loss_count` | number | Positions with negative P\&L |
| `recent_trades` | array | Last 20 trades |
| `token_pnl` | array | Per-token P\&L breakdown |
### Realized P\&L
Compute accurate realized P\&L for any wallet in the cache. During backfill, the SDK automatically fetches onchain position data from the [onchain positions endpoint](/api-reference/wallets/onchain-positions), which provides precomputed `realized_pnl` per token that matches Polymarket's numbers exactly.
```typescript theme={null}
const pnl = cache.computeRealizedPnl('0xbddf61af533ff524d27154e589d2d7a81510c684');
```
```rust theme={null}
let pnl = cache.compute_realized_pnl("0xbddf61af533ff524d27154e589d2d7a81510c684")?;
```
```json Example output theme={null}
{
"wallet": "0xbddf61af533ff524d27154e589d2d7a81510c684",
"total_realized_pnl": 17183579.48,
"total_unrealized_pnl": 3086.69,
"total_pnl": 17186666.17,
"confidence": "full",
"trades_analyzed": 403,
"tokens": [
{
"token_id": "34158857...",
"condition_id": "0x5346...",
"market_title": "Nuggets vs. Warriors",
"outcome": "Nuggets",
"realized_pnl": 447182.95,
"unrealized_pnl": 0,
"remaining_size": 0,
"avg_cost": 0.3165,
"cur_price": null,
"trades_analyzed": 0,
"buys": 0,
"sells": 0
}
]
}
```
| Field | Type | Description |
| ---------------------- | ------ | ---------------------------------------------------------------------------------------------------- |
| `total_realized_pnl` | number | Sum of realized P\&L across all positions |
| `total_unrealized_pnl` | number | Unrealized P\&L on open positions |
| `confidence` | string | `"full"` when onchain data is available. `"partial"` when only trade-based computation was possible. |
| `tokens` | array | Per-token breakdown with individual P\&L |
The onchain `realized_pnl` is the source of truth. When available, it takes priority over any trade-based P\&L computation. This ensures accuracy even when the trade history is incomplete (Polymarket's trades API silently drops trades for high-volume wallets).
### Leaderboard
Rank watched wallets by any metric:
```typescript theme={null}
const leaders = cache.leaderboard('total_pnl');
// Also: 'total_value', 'trade_count', 'win_rate'
```
```json Example output theme={null}
[
{ "wallet": "0xad53...", "label": "whale-1", "value": 1250.50, "rank": 1 },
{ "wallet": "0x2afd...", "label": "degen", "value": -320.00, "rank": 2 }
]
```
### Leaderboard Builder
New in SDK v0.4.8. The builder extends the basic `leaderboard()` method with multi-metric support, market/slug filtering, wallet scoping, time windows, and 11 available metrics.
Call `cache.leaderboard()` with no arguments to get a `LeaderboardBuilder`. Chain filters and call `.build()` to execute.
#### Single metric
```typescript theme={null}
const rows = cache.leaderboard()
.metric('total_pnl')
.build();
```
```json Output theme={null}
[
{ "wallet": "0xcarol", "label": "Carol", "rank": 1, "metrics": { "total_pnl": 53.5 } },
{ "wallet": "0xalice", "label": "Alice", "rank": 2, "metrics": { "total_pnl": 15 } },
{ "wallet": "0xbob", "label": "Bob", "rank": 3, "metrics": { "total_pnl": -30 } }
]
```
#### Multi-metric
Request multiple metrics per row. Each row includes all metrics, sorted by the first one (or use `.sortBy()` to override):
```typescript theme={null}
const rows = cache.leaderboard()
.metrics(['total_pnl', 'volume', 'win_rate'])
.build();
```
```json Output theme={null}
[
{
"wallet": "0xcarol", "label": "Carol", "rank": 1,
"metrics": { "total_pnl": 53.5, "volume": 138.1, "win_rate": 1.0 }
},
{
"wallet": "0xalice", "label": "Alice", "rank": 2,
"metrics": { "total_pnl": 15, "volume": 102.5, "win_rate": 0.5 }
},
{
"wallet": "0xbob", "label": "Bob", "rank": 3,
"metrics": { "total_pnl": -30, "volume": 70, "win_rate": 0 }
}
]
```
#### Available metrics
| Metric | Description | Source |
| ---------------- | -------------------------------------------------- | --------- |
| `total_pnl` | Sum of unrealized P\&L across positions | Positions |
| `total_value` | Sum of current position values | Positions |
| `realized_pnl` | Sum of realized P\&L | Positions |
| `roi` | Return on investment (total\_pnl / initial\_value) | Positions |
| `win_rate` | Fraction of positions with positive P\&L | Positions |
| `largest_win` | Highest single-position P\&L | Positions |
| `largest_loss` | Lowest single-position P\&L | Positions |
| `market_count` | Number of distinct markets traded | Positions |
| `trade_count` | Total trades | Trades |
| `volume` | Total volume (price \* size) | Trades |
| `avg_trade_size` | Average trade volume | Trades |
#### Sort by a different metric
By default, rows are sorted by the first metric in the array. Override with `.sortBy()`:
```typescript theme={null}
const rows = cache.leaderboard()
.metrics(['total_pnl', 'volume'])
.sortBy('volume')
.build();
// Ranked by volume: Carol (138.1), Alice (102.5), Bob (70)
```
#### Sort direction
Default is `DESC` (highest first). Flip with `.sort('ASC')`:
```typescript theme={null}
const rows = cache.leaderboard()
.metric('total_pnl')
.sort('ASC')
.build();
// Bob (-30), Alice (15), Carol (53.5)
```
#### Filter by market
Scope to specific markets by condition ID:
```typescript theme={null}
const rows = cache.leaderboard()
.metrics(['total_pnl', 'volume'])
.markets(['0xcondition_btc'])
.build();
// Only counts positions and trades in the BTC market
```
```json Output theme={null}
[
{ "wallet": "0xcarol", "rank": 1, "metrics": { "total_pnl": 16, "volume": 40 } },
{ "wallet": "0xbob", "rank": 2, "metrics": { "total_pnl": 0, "volume": 0 } },
{ "wallet": "0xalice", "rank": 3, "metrics": { "total_pnl": -5, "volume": 20 } }
]
```
#### Filter by slug pattern
Use glob patterns on market slugs. `*` matches any characters:
```typescript theme={null}
const rows = cache.leaderboard()
.metrics(['total_pnl', 'trade_count'])
.slugs(['*trump*'])
.build();
// Only counts positions/trades in markets with "trump" in the slug
```
```json Output theme={null}
[
{ "wallet": "0xcarol", "rank": 1, "metrics": { "total_pnl": 37.5, "trade_count": 2 } },
{ "wallet": "0xalice", "rank": 2, "metrics": { "total_pnl": 20, "trade_count": 2 } },
{ "wallet": "0xbob", "rank": 3, "metrics": { "total_pnl": -30, "trade_count": 1 } }
]
```
Multiple patterns are OR'd together:
```typescript theme={null}
cache.leaderboard()
.slugs(['*trump*', 'btc-*', '*election*'])
.metric('total_pnl')
.build();
```
#### Categories
Define reusable named groups of slug patterns:
```typescript theme={null}
const ELECTIONS = { name: 'elections', slugs: ['*election*', '*trump*', '*biden*'] };
const CRYPTO = { name: 'crypto', slugs: ['btc-*', 'eth-*', '*bitcoin*'] };
const SPORTS = { name: 'sports', slugs: ['*nba*', '*nfl*', '*ncaa*'] };
const rows = cache.leaderboard()
.category(ELECTIONS)
.metrics(['total_pnl', 'roi', 'win_rate'])
.limit(10)
.build();
```
#### Wallet scoping
Rank a subset of wallets instead of the full watchlist:
```typescript theme={null}
const rows = cache.leaderboard()
.metric('total_pnl')
.wallets(['0xalice', '0xcarol'])
.build();
// Only Alice and Carol, Bob excluded
```
```json Output theme={null}
[
{ "wallet": "0xcarol", "label": "Carol", "rank": 1, "metrics": { "total_pnl": 53.5 } },
{ "wallet": "0xalice", "label": "Alice", "rank": 2, "metrics": { "total_pnl": 15 } }
]
```
#### Time windows
Filter trade-derived metrics to a time range. Pass UNIX timestamps in seconds:
```typescript theme={null}
const weekAgo = Date.now() / 1000 - 7 * 86400;
const rows = cache.leaderboard()
.metrics(['trade_count', 'volume'])
.since(weekAgo)
.sortBy('volume')
.build();
```
Time windows apply to trade-derived metrics only (`trade_count`, `volume`, `avg_trade_size`). Position-derived metrics (`total_pnl`, `roi`, `win_rate`, etc.) always reflect current state, since positions are a snapshot rather than a time series.
#### Limit
Return only the top N:
```typescript theme={null}
const top5 = cache.leaderboard()
.metrics(['total_pnl', 'volume', 'win_rate'])
.limit(5)
.build();
```
#### Full combination
All filters compose together:
```typescript theme={null}
const rows = cache.leaderboard()
.metrics(['total_pnl', 'volume', 'trade_count'])
.slugs(['*trump*'])
.since(weekAgo)
.wallets(['0xalice', '0xcarol'])
.sortBy('volume')
.limit(10)
.build();
```
#### Type reference
```typescript theme={null}
interface LeaderboardRow {
wallet: string;
label: string;
rank: number;
metrics: Record;
}
interface LeaderboardCategory {
name: string;
slugs: string[];
}
type LeaderboardMetric =
| 'total_pnl' | 'total_value' | 'trade_count' | 'win_rate'
| 'roi' | 'realized_pnl' | 'volume' | 'avg_trade_size'
| 'largest_win' | 'largest_loss' | 'market_count';
```
### Market Overview
All cached positions for a market across watched wallets:
```typescript theme={null}
const overview = cache.marketOverview('0xcondition...');
// overview.positions, overview.total_volume, overview.unique_wallets
```
## Reactive Subscriptions
Fire callbacks when new data lands in the cache from the live WebSocket stream.
```typescript theme={null}
// Subscribe to all changes
const unsub = cache.onChange((event) => {
// event.type: 'trade' | 'settlement'
// event.wallet: string
// event.data: TradeRow | SettlementRow
console.log(`New ${event.type} for ${event.wallet}`);
});
// Wallet-specific — only fires for this wallet
const unsub2 = cache.onWalletChange('0xad53...', (event) => {
updateUI(event.data);
});
// Cleanup
unsub();
unsub2();
```
## Export Helpers
Dump filtered data for charting libraries, spreadsheets, or custom analysis.
```typescript theme={null}
import * as fs from 'fs';
// CSV export
const csv = cache.exportCSV('trades', { wallet: '0xabc...', limit: 1000 });
fs.writeFileSync('trades.csv', csv);
// JSON array export
const json = cache.exportJSON('positions', { wallet: '0xabc...' });
// Raw rows for data libraries
const rows = cache.exportRows('trades', { wallet: '0xabc...', since: 1774000000 });
```
Filter options: `wallet`, `conditionId`, `tokenId`, `side`, `since`, `until`, `limit`, `orderBy`.
## Query Builder
Chainable fluent API for complex queries without writing SQL.
```typescript theme={null}
// Filter trades by wallet, side, time, and market
const results = cache.query('trades')
.wallet('0xabc...')
.side('BUY')
.since(1774000000)
.market('0xcondition...')
.limit(50)
.orderBy('timestamp_desc')
.run();
// Filter positions by size and profitability
const winners = cache.query('positions')
.wallet('0xabc...')
.minSize(100)
.minPnl(0) // only profitable
.run();
```
Available filters: `.wallet()`, `.market()`, `.token()`, `.side()`, `.since()`, `.until()`, `.limit()`, `.skip()`, `.orderBy()`, `.minSize()`, `.minPnl()` (positions only).
## How It Works
```
┌──────────────────┐
│ PolyNodeCache │
│ │
┌──────────────┤ backfill (1x) │
│ │ live stream │
│ │ prune timer │
▼ │ file watcher │
┌───────────┐ └────────┬─────────┘
│ REST API │ (backfill) │ (live events)
│ 3 calls │ ┌───────▼──────────┐
│ per wallet│ │ WebSocket stream │
└───────────┘ │ trades + settle. │
│ └───────┬──────────┘
│ │
▼ ▼
┌──────────────────────────────────────┐
│ SQLite (WAL mode) │
│ positions — open + closed with P&L │
│ trades — full inverted index │
│ settlements — pending + confirmed │
│ backfill_state — crash recovery │
└──────────────────────────────────────┘
```
1. **On start**: opens SQLite, resets any interrupted backfills from a previous crash, loads watchlist, connects WebSocket, begins backfill
2. **Backfill** (per wallet, 3 requests — skipped entirely if already complete):
* Fetches open positions from the standard API (metadata: title, outcome, slug)
* Fetches onchain positions (complete P\&L for all positions including closed, single call, no pagination needed)
* Fetches recent trades (individual buy/sell records)
3. **Live stream**: WebSocket delivers new trades and settlements in real-time
4. **Dedup**: `INSERT OR IGNORE` with unique constraint prevents duplicates between backfill and live data
5. **Prune**: hourly timer removes data older than the configured TTL
6. **On stop**: waits for any in-flight backfill to finish, then closes WebSocket and DB cleanly
## Persistence & Crash Recovery
The SQLite database persists across restarts, deploys, and crashes. When you call `cache.start()` again:
* All trades, positions, and settlements are preserved in the SQLite file
* Completed backfills are skipped entirely (no network calls)
* Interrupted backfills resume automatically. Any entity that was mid-backfill when the process was killed gets retried on the next start
* WebSocket stream reconnects and picks up live events
* The console logs exactly what's happening: how many entities need backfilling vs how many are already done
```
# First run — backfills everything
[PolyNodeCache] Backfilling 4 entities (1 page of 500 each) — ETA: ~4s
# Process killed mid-backfill, then restarted — only resumes incomplete ones
[PolyNodeCache] Reset 1 interrupted backfill(s) from previous session.
[PolyNodeCache] Backfilling 2 entities (1 page of 500 each) — ETA: ~1s
# Clean restart after everything is done — no network calls
[PolyNodeCache] All 4 entities already backfilled, skipping.
```
Backfill state is tracked in the `backfill_state` table with per-entity status (`pending`, `in_progress`, `complete`, `failed`). On startup, any `in_progress` entries left over from a crash are automatically reset to `pending` so they get retried.
## Stop and Cleanup
```typescript theme={null}
await cache.stop(); // closes WebSocket, waits for in-flight backfill to finish, closes DB
// Manual prune
const deleted = cache.prune(); // removes data older than TTL
```
`stop()` is safe to call at any time. It waits for any in-flight backfill operation to complete before closing the database, so you won't get partial writes or corrupted state.
## Testing Utilities
The SDK includes helpers that return known-active Polymarket wallets. Useful for examples, integration tests, and getting started without needing to find wallet addresses yourself.
```typescript theme={null}
import { getActiveTestWallet, getActiveTestWallets } from 'polynode-sdk';
// Get a single active wallet (instant, uses cached fallback)
const wallet = await getActiveTestWallet();
// Get multiple active wallets
const wallets = await getActiveTestWallets(5);
// Fetch a fresh wallet from live leaderboard data
const fresh = await getActiveTestWallet({ fresh: true });
```
Combine with the cache for a zero-config quickstart:
```typescript theme={null}
import { PolyNode, PolyNodeCache, getActiveTestWallet } from 'polynode-sdk';
const pn = new PolyNode({ apiKey: 'pn_live_...' });
const wallet = await getActiveTestWallet();
const cache = new PolyNodeCache(pn, {
dbPath: './cache.db',
watchlistPath: './polynode.watch.json',
});
await cache.start();
cache.addToWatchlist([{ type: 'wallet', id: wallet, label: 'test-trader' }]);
// Wait for backfill, then query
setTimeout(() => {
const trades = cache.walletTrades(wallet, { limit: 10 });
console.log(`${trades.length} trades for ${wallet}`);
}, 3000);
```
`getActiveTestWallet()` returns instantly by default using a cached list of known-active wallets. Pass `{ fresh: true }` to fetch the current top trader from live data (adds \~1-2s network latency).
## Full Example
```typescript theme={null}
import { PolyNode, PolyNodeCache } from 'polynode-sdk';
const pn = new PolyNode({ apiKey: 'pn_live_...' });
const cache = new PolyNodeCache(pn, {
dbPath: './cache.db',
watchlistPath: './polynode.watch.json',
backfillPages: 1,
onBackfillProgress: (p) => {
const icon = p.status === 'complete' ? '✓' : '⟳';
console.log(`${icon} ${p.label}: ${p.fetched} trades`);
},
});
await cache.start();
// First run:
// [PolyNodeCache] Backfilling 10 entities (1 page of 500 each) — ETA: ~10s
// ⟳ trader-1: 500 trades
// ✓ trader-1: 500 trades
// ...
//
// Subsequent runs (data persisted):
// [PolyNodeCache] All 10 entities already backfilled, skipping.
// Query locally — instant
const positions = cache.walletPositions('0xabc...');
for (const p of positions) {
console.log(`${p.outcome}: ${p.size} shares @ ${p.avg_price.toFixed(4)}`);
}
// Add a wallet at runtime
cache.addToWatchlist([
{ type: 'wallet', id: '0xnew...', label: 'new-whale' }
]);
// Stats
const stats = cache.stats();
console.log(`${stats.trade_count} trades, ${(stats.db_size_bytes / 1024 / 1024).toFixed(1)} MB`);
// Cleanup
await cache.stop();
```
# Orderbook Streaming
Source: https://docs.polynode.dev/sdks/orderbook
Real-time orderbook data with local state management and filtered views
The SDK includes a dedicated orderbook client for real-time book data from `ob.polynode.dev`. This is a separate WebSocket connection from the event stream, with its own protocol optimized for orderbook updates across 108k+ Polymarket markets.
TypeScript SDK v0.10.17 hardens orderbook subscriptions: `subscribe()` resolves only after the server acknowledges the token set, reconnects replay the active subscription without replacing handlers, and protocol acknowledgments such as `unsubscribed`/`pong` are handled internally.
## Subscribe
```typescript theme={null}
import { PolyNode, LocalOrderbook } from 'polynode-sdk';
const pn = new PolyNode({ apiKey: 'pn_live_...' });
await pn.orderbook.subscribe(['token_id_1', 'token_id_2']);
```
```rust theme={null}
use polynode::{PolyNodeClient, ObStreamOptions};
let client = PolyNodeClient::new("pn_live_...")?;
let mut stream = client.orderbook_stream(ObStreamOptions::default()).await?;
stream.subscribe(vec!["token_id_1".into()]).await?;
```
## Events
```typescript theme={null}
pn.orderbook.on('snapshot', (snap) => {
console.log(snap.asset_id, snap.bids.length, 'bids');
});
pn.orderbook.on('update', (delta) => {
// Incremental delta. size "0" = removal.
console.log(delta.asset_id, delta.bids.length, 'changes');
});
pn.orderbook.on('price', (change) => {
for (const asset of change.assets) {
console.log(asset.outcome, asset.price);
}
});
```
```rust theme={null}
while let Some(msg) = stream.next().await {
match msg? {
ObMessage::Update(OrderbookUpdate::Snapshot(snap)) => {
println!("{}: {} bids", snap.asset_id, snap.bids.len());
}
ObMessage::Update(OrderbookUpdate::Update(delta)) => {
println!("{}: {} changes", delta.asset_id, delta.bids.len());
}
ObMessage::Update(OrderbookUpdate::PriceChange(change)) => {
for asset in &change.assets {
println!("{}: {}", asset.outcome, asset.price);
}
}
_ => {}
}
}
```
## Update a Subscription
Calling `subscribe()` with a new token list replaces the active orderbook subscription for that connection. To remove only a subset of tokens, pass those token IDs to `unsubscribe()`. Calling `unsubscribe()` with no arguments still removes every token and keeps the previous behavior.
```typescript theme={null}
await pn.orderbook.subscribe([tokenA, tokenB, tokenC]);
pn.orderbook.unsubscribe([tokenB]); // remove one token
pn.orderbook.unsubscribe(); // remove all tokens
```
For high-volume orderbook consumers, register `snapshot`, `update`, `price`, and `error` handlers before calling `subscribe()` so your process is ready as soon as the initial snapshot batches start flowing.
## LocalOrderbook
Maintain a sorted local copy of the book:
```typescript theme={null}
import { LocalOrderbook } from 'polynode-sdk';
const book = new LocalOrderbook();
pn.orderbook.on('snapshot', (snap) => book.applySnapshot(snap));
pn.orderbook.on('update', (delta) => book.applyUpdate(delta));
const fullBook = book.getBook(tokenId); // { bids, asks }
const bestBid = book.getBestBid(tokenId); // { price, size }
const spread = book.getSpread(tokenId); // number
```
```rust theme={null}
use polynode::LocalOrderbook;
let mut book = LocalOrderbook::new();
book.apply_snapshot(&snap);
book.apply_update(&delta);
let best_bid = book.best_bid("token_id");
let spread = book.spread("token_id");
```
## OrderbookEngine
Higher-level wrapper that manages one connection, maintains local state, and supports filtered views for different UI components.
```typescript theme={null}
import { OrderbookEngine } from 'polynode-sdk';
const engine = new OrderbookEngine({ apiKey: 'pn_live_...', compress: true });
await engine.subscribe([tokenId1, tokenId2]);
engine.on('ready', () => {
console.log(`${engine.size} books loaded`);
});
// Query state
engine.midpoint(tokenId); // (bestBid + bestAsk) / 2
engine.spread(tokenId); // bestAsk - bestBid
engine.bestBid(tokenId); // { price, size }
// Filtered views — no extra connections
const view = engine.view([tokenA, tokenB]);
view.on('update', (u) => console.log(u.asset_id));
view.midpoint(tokenA);
// Swap tokens or destroy
view.setTokens([newTokenX]);
view.destroy();
engine.close();
```
```rust theme={null}
use polynode::orderbook::engine::{OrderbookEngine, EngineOptions};
let engine = OrderbookEngine::connect("pn_live_...", EngineOptions::default()).await?;
engine.subscribe(vec![token_id.into()]).await?;
engine.midpoint(token_id).await; // Option
engine.spread(token_id).await; // Option
let mut view = engine.view(vec![token_a.into()]);
while let Some(update) = view.next().await { ... }
engine.close().await?;
```
# SDKs
Source: https://docs.polynode.dev/sdks/overview
Official client libraries for the PolyNode API
PolyNode provides official SDKs for TypeScript, Rust, and Python. All wrap the full REST API and WebSocket streaming interface with typed responses, auto-reconnection, and zlib compression support.
`npm install polynode-sdk`
Zero runtime dependencies. ESM + CJS. Node 18+.
`polynode = "0.12"`
Async with tokio. Typed events via serde.
`pip install polynode`
Sync + async. Pydantic v2 types. Python 3.10+.
`npm install polynode-charts`
Zero-dependency Canvas 2D charting for prediction markets and live crypto prices. Browser only.
**`polynode-sdk`** is the data SDK (REST, WebSocket, trading, cache). It runs in Node.js.
**`polynode-charts`** is the visualization SDK (candlestick charts, orderbooks, live streaming UI). It runs in the browser.
Building a frontend? Install both: `npm install polynode-sdk polynode-charts`
## Features
SQLite-backed local storage. Backfill wallet history in seconds, query trades and positions instantly with zero API calls.
Auto-rotating streams for 5m, 15m, and 1h crypto prediction markets. Includes price-to-beat, odds, and liquidity.
Builder-pattern subscriptions with 10 presets, 11 filter dimensions, and transparent compression.
Real-time orderbook data with local state management, filtered views, and 108k+ markets.
Typed methods for all 25 REST endpoints. Markets, pricing, settlements, wallets, enriched data.
JSON-RPC through PolyNode's optimized Polygon endpoint with validator-targeted TX submission.
Push alerts when tracked wallets' positions become redeemable. Combines REST positions with real-time oracle events.
Place orders on Polymarket with one-call gasless onboarding. Auto-detects wallet type, local credential custody, builder attribution.
## What the SDKs Cover
| Feature | TypeScript | Rust | Python |
| ----------------------------------------------- | ------------------- | ------------------------ | ------------------- |
| Local cache (SQLite) | Yes | Yes | Planned |
| REST API (25 endpoints) | Yes | Yes | Yes |
| WebSocket streaming | Yes | Yes | Yes (async) |
| Short-form markets (auto-rotation) | Yes | Yes | Yes |
| All 9 event types + price feeds | Typed interfaces | Typed structs + enum | Pydantic models |
| Subscription filters | Builder pattern | Builder pattern | Builder pattern |
| Orderbook streaming | Yes | Yes | Yes |
| Local orderbook state | `LocalOrderbook` | `LocalOrderbook` | `LocalOrderbook` |
| OrderbookEngine (views) | Yes | Yes | Yes |
| Zlib compression | Transparent | Transparent | Transparent |
| Auto-reconnect | Exponential backoff | Exponential backoff | Exponential backoff |
| Enriched data (leaderboard, trending, profiles) | Yes | Yes | Yes |
| RPC proxy | Yes | Yes | Yes |
| Redemption watcher | Yes | Yes | Yes |
| Trading (order placement) | Yes | Yes (feature: `trading`) | Yes |
## Quick Comparison
```typescript theme={null}
import { PolyNodeWS } from 'polynode-sdk';
const ws = new PolyNodeWS('pn_live_...', 'wss://ws.polynode.dev/ws');
// Short-form: auto-rotating 15m crypto markets
const stream = ws.shortForm('15m', { coins: ['btc', 'eth'] });
stream.on('rotation', (r) => {
for (const m of r.markets) {
console.log(`${m.coin}: beat $${m.priceToBeat} | ${(m.upOdds * 100).toFixed(0)}% up`);
}
});
stream.on('settlement', (e) => console.log(e.outcome, e.taker_size));
// Manual subscription
const sub = await ws.subscribe('settlements')
.minSize(1000)
.send();
sub.on('settlement', (e) => console.log(e.taker_side, e.taker_size));
```
```rust theme={null}
use polynode::{PolyNodeClient, ShortFormInterval, ShortFormMessage, Coin};
let client = PolyNodeClient::new("pn_live_...")?;
// Short-form: auto-rotating 15m crypto markets
let mut stream = client
.short_form(ShortFormInterval::FifteenMin)
.coins(&[Coin::Btc, Coin::Eth])
.start().await?;
while let Some(msg) = stream.next().await {
match msg {
ShortFormMessage::Rotation(r) => {
for m in &r.markets {
println!("{}: beat ${:?} | {:.0}% up",
m.coin.id(), m.price_to_beat, m.up_odds * 100.0);
}
}
ShortFormMessage::Event(e) => println!("{:?}", e),
ShortFormMessage::Error(e) => eprintln!("{}", e),
}
}
```
```python theme={null}
import asyncio
from polynode import AsyncPolyNode
async def main():
async with AsyncPolyNode(api_key="pn_live_...") as pn:
# Subscribe to settlements with filters
sub = await pn.ws.subscribe("settlements") \
.min_size(1000) \
.status("all") \
.send()
async for event in sub:
print(f"{event.taker_side} ${event.taker_size:.0f} on {event.market_title}")
print(f" status: {event.status}, price: {event.taker_price}")
asyncio.run(main())
```
## Don't Need an SDK?
The PolyNode API uses standard HTTP and WebSocket protocols. You can use any HTTP client or WebSocket library directly. See the [Quickstart](/quickstart) for raw examples in cURL, Python, and JavaScript.
# Python — Configuration
Source: https://docs.polynode.dev/sdks/python/config
## Configuration
```python theme={null}
pn = PolyNode(
api_key="pn_live_...", # required
base_url="https://api.polynode.dev", # default
ws_url="wss://ws.polynode.dev/ws", # default
ob_url="wss://ob.polynode.dev/ws", # default
rpc_url="https://rpc.polynode.dev", # default
timeout=10.0, # seconds, default 10
)
```
# Python — Error Handling
Source: https://docs.polynode.dev/sdks/python/errors
## Error Handling
```python theme={null}
from polynode import PolyNode, ApiError, WsError, PolyNodeError
try:
pn.market("invalid-id")
except ApiError as e:
print(e.status) # 404
print(e.message) # "Market not found"
except PolyNodeError as e:
print(e.message) # base error class
```
# Python — Models
Source: https://docs.polynode.dev/sdks/python/models
## Pydantic Models
All event types are Pydantic v2 models with full IDE support:
```python theme={null}
from polynode.types.events import (
SettlementEvent,
TradeEvent,
StatusUpdateEvent,
BlockEvent,
PositionChangeEvent,
DepositEvent,
PositionSplitEvent,
PositionMergeEvent,
OracleEvent,
PriceFeedEvent,
PolyNodeEvent, # discriminated union of all events
)
from polynode.types.orderbook import (
OrderbookLevel,
BookSnapshot,
BookUpdate,
PriceChange,
)
from polynode.types.rest import (
StatusResponse,
MarketsResponse,
MarketSummary,
CandlesResponse,
SettlementsResponse,
WalletResponse,
OrderbookResponse,
LeaderboardResponse,
TrendingResponse,
TraderProfile,
)
```
The `PolyNodeEvent` union uses Pydantic's discriminated union on `event_type`:
```python theme={null}
from pydantic import TypeAdapter
adapter = TypeAdapter(PolyNodeEvent)
event = adapter.validate_python({"event_type": "settlement", ...})
# Returns a SettlementEvent instance
```
# Python — Orderbook
Source: https://docs.polynode.dev/sdks/python/orderbook
## Orderbook Streaming
The SDK includes a dedicated orderbook client for real-time book data from `ob.polynode.dev`. This is a separate WebSocket connection from the event stream.
### Subscribe
```python theme={null}
# Lazy-initialized, connects on first subscribe
await pn.orderbook.subscribe(["token_id_1", "token_id_2"])
```
### Event Handlers
```python theme={null}
pn.orderbook.on("snapshot", lambda snap: print(f"{snap.asset_id}: {len(snap.bids)} bids"))
pn.orderbook.on("update", lambda delta: print(f"{delta.asset_id} updated"))
pn.orderbook.on("price", lambda c: print(f"price: {c.assets[0].price}"))
pn.orderbook.on("snapshots_done", lambda msg: print(f"All {msg.total} snapshots received"))
pn.orderbook.on("*", lambda u: print(u.type)) # catch-all
```
### LocalOrderbook
Maintain a sorted local copy of the book:
```python theme={null}
from polynode import LocalOrderbook
book = LocalOrderbook()
# Wire to orderbook WS events
pn.orderbook.on("snapshot", lambda snap: book.apply_snapshot(snap))
pn.orderbook.on("update", lambda delta: book.apply_update(delta))
# Query state
full_book = book.get_book(token_id) # (bids, asks) or None
best_bid = book.get_best_bid(token_id) # OrderbookLevel or None
best_ask = book.get_best_ask(token_id) # OrderbookLevel or None
spread = book.get_spread(token_id) # float or None
```
### Cleanup
```python theme={null}
pn.orderbook.unsubscribe() # unsubscribe from all markets
pn.orderbook.disconnect() # close connection
```
# Python — Orderbook Engine
Source: https://docs.polynode.dev/sdks/python/orderbook-engine
## OrderbookEngine
Higher-level wrapper that manages one connection, maintains local state, and routes updates to filtered views.
### Create and Subscribe
```python theme={null}
from polynode import OrderbookEngine
engine = OrderbookEngine(api_key="pn_live_...")
await engine.subscribe([token_a, token_b, token_c])
engine.on("ready", lambda: print(f"{engine.size} books loaded"))
```
### Query State
```python theme={null}
engine.midpoint(token_id) # float | None
engine.spread(token_id) # float | None
engine.best_bid(token_id) # OrderbookLevel | None
engine.best_ask(token_id) # OrderbookLevel | None
engine.book(token_id) # (bids, asks) | None
```
### Filtered Views
Create lightweight views that only receive updates for specific tokens:
```python theme={null}
trade_view = engine.view([token_a, token_b])
portfolio_view = engine.view(my_position_tokens)
trade_view.on("update", lambda u: print(f"{u.asset_id} changed"))
trade_view.on("price", lambda c: print(f"price: {c.assets}"))
trade_view.midpoint(token_a)
trade_view.spread(token_a)
# Swap to different tokens
trade_view.set_tokens([new_token_x, new_token_y])
# Or destroy entirely
trade_view.destroy()
```
### Cleanup
```python theme={null}
engine.close() # disconnects WS, destroys all views, clears state
```
# Python — Overview
Source: https://docs.polynode.dev/sdks/python/overview
## Install
```bash theme={null}
pip install polynode
```
Requires Python 3.10+. For trading support (order placement on Polymarket):
```bash theme={null}
pip install polynode[trading]
```
## Quick Start
```python theme={null}
from polynode import PolyNode
pn = PolyNode(api_key="pn_live_...")
# Fetch top markets
markets = pn.markets(count=10)
print(f"{markets.count} markets, {markets.total} total")
# Search
results = pn.search("bitcoin")
print(results.results[0].question)
pn.close()
```
### Context Manager
```python theme={null}
with PolyNode(api_key="pn_live_...") as pn:
status = pn.status()
print(f"Tracking {status.state.market_count} markets")
```
### Sports and Online Context
Available in `polynode>=0.10.5`.
```python theme={null}
from polynode import PolyNode
with PolyNode(api_key="pn_live_...") as pn:
state = pn.sports_game_state(
"nba-cle-nyk-2026-05-31",
price_limit_tokens=20,
)
context = pn.sports_game_context(
"nba-cle-nyk-2026-05-31",
sources=["online"],
query_set="injuries",
max_queries=2,
max_per_query=5,
include_state=True,
)
web = pn.search_online(
"Cavaliers Knicks injury news",
max_results=5,
)
```
### Async Client
Every method is available in both sync and async variants:
```python theme={null}
import asyncio
from polynode import AsyncPolyNode
async def main():
async with AsyncPolyNode(api_key="pn_live_...") as pn:
status = await pn.status()
markets = await pn.markets(count=3)
print(f"{status.state.market_count} markets, {status.ws_subscribers} ws subs")
asyncio.run(main())
```
# Python — REST API
Source: https://docs.polynode.dev/sdks/python/rest-api
## REST Methods
Every REST endpoint has a typed method on the `PolyNode` client. All return Pydantic models with full IDE autocomplete.
```python theme={null}
# System
pn.healthz() # "ok"
pn.status() # StatusResponse
pn.create_key("my-bot") # ApiKeyResponse
# Markets
pn.markets(count=10) # MarketsResponse
pn.market(token_id) # dict
pn.market_by_slug("bitcoin-100k") # dict
pn.market_by_condition(condition_id) # dict
pn.markets_list(count=20, sort="volume") # MarketsListResponse
pn.search("ethereum", limit=5) # SearchResponse
# Pricing
pn.candles(token_id, resolution="1h", limit=100) # CandlesResponse
pn.stats(token_id) # dict
# Settlements
pn.recent_settlements(count=20) # SettlementsResponse
pn.token_settlements(token_id, count=10)
pn.wallet_settlements(address, count=10)
# Wallets
pn.wallet(address) # WalletResponse
pn.wallet_trades(address, limit=50) # dict
pn.wallet_positions(address, limit=50) # dict
pn.wallet_onchain_positions(address) # dict
# Orderbook (REST)
pn.orderbook_rest(token_id) # OrderbookResponse
pn.midpoint(token_id) # MidpointResponse
pn.spread(token_id) # SpreadResponse
# Enriched Data (1 req/sec rate limit)
pn.leaderboard(period="weekly", sort="profit") # LeaderboardResponse
pn.trending() # TrendingResponse
pn.activity() # ActivityResponse
pn.movers() # MoversResponse
pn.trader_profile("0xabc...") # TraderProfile
pn.trader_pnl("0xabc...", period="1W") # TraderPnlResponse
pn.event("how-many-fed-rate-cuts-2026") # EventDetailResponse
pn.search_events("recession", limit=5) # EventSearchResponse
pn.markets_by_category("crypto") # MarketsListResponse
# RPC (rpc.polynode.dev)
pn.rpc("eth_blockNumber")
pn.rpc("eth_getBlockByNumber", ["latest", False])
```
### Example: Market Data
```python theme={null}
with PolyNode(api_key="pn_live_...") as pn:
# Top 3 markets by volume
markets = pn.markets(count=3)
for m in markets.markets:
print(f"{m.question} — ${m.volume_24h:,.0f} vol")
# OHLCV candles
candles = pn.candles(markets.markets[0].token_id, resolution="1h", limit=3)
for c in candles.candles:
print(f" O={c.open} H={c.high} L={c.low} C={c.close} V={c.volume:.0f}")
```
### Example: Wallet Activity
```python theme={null}
with PolyNode(api_key="pn_live_...") as pn:
wallet = pn.wallet("0xB27BC932bf8110D8F78e55da7d5f0497A18B5b82")
a = wallet.activity
print(f"Trades: {a.trade_count}, Volume: ${a.trade_volume_usd:,.0f}")
profile = pn.trader_profile(wallet.wallet)
print(f"{profile.pseudonym}: PnL ${profile.totalPnl:,.0f}")
```
# Python — Source
Source: https://docs.polynode.dev/sdks/python/source
## Source
[PyPI](https://pypi.org/project/polynode/)
# Python — Trading
Source: https://docs.polynode.dev/sdks/python/trading
## Trading
**Polymarket V2 cutover: April 28, 2026 at \~11am UTC.** Set `exchange_version=ExchangeVersion.V2` in your `TraderConfig` before cutover. V1 orders submitted after April 28 will be rejected with `order_version_mismatch`. Pass your V2 builder code (mint at [polymarket.com/settings?tab=builder](https://polymarket.com/settings?tab=builder)) via `OrderParams.builder`. See the [V2 Migration Guide](/guides/v2-migration).
**Deposit wallets supported.** The SDK auto-detects Safe proxy and deposit wallet users. No code changes needed for existing integrations. See the [Deposit Wallets guide](/guides/deposit-wallets).
Place orders on Polymarket with local credential custody and builder attribution. Supports both the current exchange and the [Polymarket V2 exchange](/guides/v2-migration). See also: [PolyUSD Guide](/guides/polyusd) for V2 collateral wrapping. Requires the trading extras:
```bash theme={null}
pip install polynode[trading]
```
### Generate a Wallet
```python theme={null}
import asyncio
from polynode.trading import PolyNodeTrader
async def main():
wallet = await PolyNodeTrader.generate_wallet()
print(f"Address: {wallet.address}")
print(f"Private key: {wallet.private_key}")
# BACK UP THE PRIVATE KEY — it cannot be recovered
asyncio.run(main())
```
### One-Call Onboarding
```python theme={null}
from polynode.trading import PolyNodeTrader, TraderConfig
trader = PolyNodeTrader(TraderConfig(polynode_key="pn_live_..."))
# Auto-detects wallet type, deploys Safe if needed, sets approvals, creates CLOB credentials
status = await trader.ensure_ready("0xYourPrivateKey...")
print(f"Wallet: {status.wallet}")
print(f"Funder: {status.funder_address}")
print(f"Type: {status.signature_type.name}") # POLY_GNOSIS_SAFE
print(f"Actions: {status.actions}")
```
### Place Orders
```python theme={null}
from polynode.trading import OrderParams
result = await trader.order(OrderParams(
token_id="51037625779056581606819614184446816710505006861008496087735536016411882582167",
side="BUY",
price=0.55,
size=100,
))
print(f"Success: {result.success}, Order ID: {result.order_id}")
```
### Cancel Orders
```python theme={null}
# Cancel one
cancel = await trader.cancel_order("order-id-here")
# Cancel all
cancel = await trader.cancel_all()
# Cancel all in a market
cancel = await trader.cancel_all(market="condition-id")
```
### Open Orders
```python theme={null}
orders = await trader.get_open_orders()
for o in orders:
print(f"{o.side} {o.original_size} @ {o.price} — {o.status}")
```
### Pre-Trade Checks
```python theme={null}
# Check token approvals
approvals = await trader.check_approvals()
print(f"All approved: {approvals.all_approved}")
# Check balances
balance = await trader.check_balance()
print(f"USDC: {balance.usdc}, MATIC: {balance.matic}")
```
### Wallet Management
```python theme={null}
# Link existing credentials (no signing needed)
trader.link_credentials(
wallet="0x...",
api_key="...",
api_secret="...",
api_passphrase="...",
)
# Export for backup
exported = trader.export_wallet()
# Import on another machine
trader.import_wallet(exported)
# List all linked wallets
wallets = trader.get_linked_wallets()
```
### Address Derivation
```python theme={null}
from polynode.trading import derive_safe_address, derive_proxy_address, derive_deposit_wallet_address
eoa = "0xacd89cFCB82Ae1f843467D56b58796bb928C9E1A"
safe = derive_safe_address(eoa) # Gnosis Safe proxy address
proxy = derive_proxy_address(eoa) # Legacy proxy address
deposit = derive_deposit_wallet_address(eoa) # Deposit wallet address (newer accounts)
```
### Polymarket V2 Exchange
To trade on the Polymarket V2 exchange, set `exchange_version` on your `TraderConfig`:
```python theme={null}
from polynode.trading import TraderConfig, ExchangeVersion
config = TraderConfig(
polynode_key="pn_live_...",
exchange_version=ExchangeVersion.V2,
)
trader = PolyNodeTrader(config)
```
V2 uses PolyUSD (a 1:1 USDC.e wrapper) as collateral. You must wrap USDC.e into PolyUSD before placing orders. Amounts are in raw 6-decimal units (`1_000_000` = \$1):
```python theme={null}
# Wrap 100 USDC.e → PolyUSD (100 * 1e6 = 100_000_000 raw units)
tx_hash = await trader.wrap_to_polyusd(100_000_000)
# Unwrap 50 PolyUSD → USDC.e
tx_hash = await trader.unwrap_from_polyusd(50_000_000)
# Balance getters are synchronous and return raw 6-decimal integers
polyusd_raw = trader.get_polyusd_balance()
usdce_raw = trader.get_usdce_balance()
print(f"PolyUSD: ${polyusd_raw / 1e6:.6f}, USDC.e: ${usdce_raw / 1e6:.6f}")
```
See the [V2 Migration Guide](/guides/v2-migration) for details on the Polymarket V2 exchange upgrade.
### Fee Escrow
Charge per-order fees with on-chain escrow. Fees are pulled before the order, distributed on fill, and refunded on cancel. See the [Fee Escrow Guide](/guides/fee-escrow) for the full architecture and security model.
```python theme={null}
from polynode.trading import PolyNodeTrader, TraderConfig, FeeConfig
trader = PolyNodeTrader(TraderConfig(
polynode_key="pn_live_...",
fee_config=FeeConfig(fee_bps=50), # 0.5% platform fee on every order (yours; separate from Polymarket's protocol fee and V2 builder rev share)
))
result = await trader.order(OrderParams(
token_id="...",
side="BUY",
price=0.55,
size=100,
))
print(f"Fee TX: {result.fee_escrow_tx_hash}") # on-chain pullFee TX
print(f"Fee: {result.fee_amount} USDC") # fee amount charged
# Cancel → fee is automatically refunded
await trader.cancel_order(result.order_id)
```
Set `fee_bps=0` or omit `fee_config` to skip your platform fee entirely. This only turns off **your** fee — Polymarket's protocol fee and (on V2) the builder rev share are charged independently by the CLOB. Per-order overrides:
```python theme={null}
result = await trader.order(OrderParams(
token_id="...",
side="BUY",
price=0.55,
size=100,
fee_config=FeeConfig(
fee_bps=100, # 1% for this order
affiliate="0xPartnerWallet...", # partner address
affiliate_share_bps=5000, # 50/50 split
),
))
```
### Configuration
```python theme={null}
from polynode.trading import TraderConfig, SignatureType, ExchangeVersion, FeeConfig
config = TraderConfig(
polynode_key="pn_live_...", # for builder attribution
db_path="./polynode-trading.db", # local SQLite storage
cosigner_url="https://trade.polynode.dev", # co-signer proxy
fallback_direct=True, # direct CLOB if co-signer down
default_signature_type=SignatureType.POLY_GNOSIS_SAFE, # Safe (2), Proxy (1), or EOA (0)
rpc_url="https://polygon-bor-rpc.publicnode.com", # for on-chain reads
exchange_version=ExchangeVersion.V1, # default; set to V2 for the Polymarket V2 exchange
fee_config=FeeConfig(fee_bps=50), # optional: 0.5% platform fee on every order (yours, not Polymarket's)
)
trader = PolyNodeTrader(config)
```
### Signature Types
| Type | Value | Description |
| -------------------------------- | ----- | --------------------------------------------------- |
| `SignatureType.EOA` | 0 | Direct EOA signing (user pays gas for approvals) |
| `SignatureType.POLY_PROXY` | 1 | Legacy Polymarket proxy wallet |
| `SignatureType.POLY_GNOSIS_SAFE` | 2 | Gnosis Safe (default, gasless onboarding) |
| `SignatureType.POLY_1271` | 3 | Deposit wallet (newer Polymarket accounts, V2 only) |
### Privy Signer (Server-Side Wallets)
Use Privy-managed wallets for headless server-side trading. No private key needed — signing is done through Privy's wallet API:
```python theme={null}
from polynode.trading import PolyNodeTrader, TraderConfig
from polynode.trading.privy import PrivySigner, PrivyConfig
signer = PrivySigner(
PrivyConfig(
app_id="your-privy-app-id",
app_secret="your-privy-app-secret",
authorization_key="wallet-auth:your-authorization-key",
),
wallet_id="your-privy-wallet-id",
wallet_address="0xYourWalletAddress",
)
trader = PolyNodeTrader(TraderConfig(polynode_key="pn_live_..."))
status = await trader.ensure_ready(signer)
result = await trader.order(OrderParams(token_id="...", side="BUY", price=0.50, size=100))
```
The Privy signer implements the same `RouterSigner` interface and works with all trading methods (`ensure_ready`, `order`, `cancel_all`, etc.). Gnosis Safe wallets (type 2) are fully gasless.
### Cleanup
```python theme={null}
trader.close() # closes SQLite, clears active signer
```
# Python — WebSocket
Source: https://docs.polynode.dev/sdks/python/websocket
## WebSocket Streaming
Subscribe to real-time events with a builder pattern. WebSocket is async-only (Python convention):
```python theme={null}
import asyncio
from polynode import AsyncPolyNode
async def main():
async with AsyncPolyNode(api_key="pn_live_...") as pn:
sub = await pn.ws.subscribe("settlements") \
.min_size(100) \
.status("pending") \
.snapshot_count(20) \
.send()
async for event in sub:
print(f"{event.taker_side} ${event.taker_size:.0f} on {event.market_title}")
print(f" status: {event.status}, tx: {event.tx_hash[:20]}...")
asyncio.run(main())
```
### Event Callbacks
```python theme={null}
sub.on("settlement", lambda e: print(f"{e.taker_side} ${e.taker_size} on {e.market_title}"))
sub.on("status_update", lambda e: print(f"Confirmed in {e.latency_ms}ms"))
sub.on("*", lambda e: print(e.event_type)) # catch-all
```
### Subscription Filters
All filters from the [Subscriptions & Filters](/websocket/subscribing) page are supported:
```python theme={null}
(
pn.ws.subscribe("settlements")
.wallets(["0xabc..."]) # by wallet
.tokens(["21742633..."]) # by token ID
.slugs(["bitcoin-100k"]) # by market slug
.condition_ids(["0xabc..."]) # by condition ID
.side("BUY") # BUY or SELL
.status("pending") # pending, confirmed, or all
.min_size(100) # min USD size
.max_size(10000) # max USD size
.event_types(["settlement"]) # override event types
.snapshot_count(50) # initial snapshot (max 200)
.feeds(["BTC/USD"]) # chainlink feeds
.send()
)
```
### Subscription Types
```python theme={null}
pn.ws.subscribe("settlements") # pending + confirmed settlements
pn.ws.subscribe("trades") # all trade activity
pn.ws.subscribe("prices") # price-moving events
pn.ws.subscribe("blocks") # new Polygon blocks
pn.ws.subscribe("wallets") # all wallet activity
pn.ws.subscribe("markets") # all market activity
pn.ws.subscribe("large_trades") # $1K+ trades
pn.ws.subscribe("oracle") # UMA resolution events
pn.ws.subscribe("chainlink") # real-time price feeds
```
### Multiple Subscriptions
Subscriptions stack on the same connection:
```python theme={null}
whales = await pn.ws.subscribe("large_trades").min_size(5000).send()
my_wallet = await pn.ws.subscribe("wallets").wallets(["0xabc..."]).send()
# Both active simultaneously, events deduplicated
```
### Context Manager
```python theme={null}
async with await pn.ws.subscribe("settlements").send() as sub:
async for event in sub:
print(event.market_title, event.taker_price)
break
# Auto-unsubscribes on exit
```
### Compression
Zlib compression is enabled by default for all WebSocket connections (\~50% bandwidth savings). No configuration needed.
### Auto-Reconnect
Enabled by default. The SDK reconnects with exponential backoff and replays all active subscriptions:
```python theme={null}
from polynode.ws import PolyNodeWS
from polynode.types.ws import WsOptions
ws = PolyNodeWS("pn_live_...", "wss://ws.polynode.dev/ws", WsOptions(
compress=True,
auto_reconnect=True,
max_reconnect_attempts=0, # 0 = unlimited
reconnect_base_delay=1.0, # seconds
reconnect_max_delay=30.0, # seconds
))
ws.on_connect(lambda: print("connected"))
ws.on_disconnect(lambda reason: print(f"disconnected: {reason}"))
ws.on_reconnect(lambda attempt: print(f"reconnected, attempt {attempt}"))
ws.on_error(lambda err: print(f"error: {err}"))
```
### Cleanup
```python theme={null}
sub.unsubscribe() # remove one subscription
pn.ws.unsubscribe_all() # remove all
pn.ws.disconnect() # close connection
```
# Redemption Watcher
Source: https://docs.polynode.dev/sdks/redemption-watcher
Push notifications when Polymarket positions become redeemable
The `RedemptionWatcher` monitors wallets for redeemable positions. It combines wallet position data from REST with real-time `condition_resolution` events from the oracle stream. When a market resolves, any tracked wallet holding that condition gets an instant alert with full payout details.
One class, one WebSocket connection, zero polling.
## Install
```bash theme={null}
npm install polynode-sdk
```
## Quick Start
```typescript theme={null}
import { RedemptionWatcher } from 'polynode-sdk';
const watcher = new RedemptionWatcher({ apiKey: 'pn_live_...' });
watcher.on('alert', (alert) => {
if (alert.isWinner) {
console.log(`${alert.wallet} can redeem ${alert.outcome} on "${alert.marketTitle}"`);
console.log(`Payout: $${alert.estimatedPayoutUsd}`);
}
});
watcher.on('ready', () => {
console.log(`Tracking ${watcher.size} positions across ${watcher.wallets.length} wallets`);
});
await watcher.start([
'0xabc...', // user wallet 1
'0xdef...', // user wallet 2
]);
```
## How It Works
1. **`start(wallets)`** fetches positions for each wallet via REST (parallel), builds a local watch set keyed by `condition_id`, then subscribes to the oracle WebSocket stream
2. When a `condition_resolution` event arrives, the watcher cross-references `event.condition_id` against the position index
3. For each matched position, a `RedeemableAlert` is emitted with wallet address, win/loss status, payout estimate, and full market metadata
4. After alerts fire, the resolved condition is evicted from memory. Positions that drop to zero size (full sell) are also evicted. This keeps memory bounded to only active, non-zero positions regardless of how long the watcher runs.
5. If `trackPositionChanges` is enabled (default), position sizes stay accurate in real-time via the wallets WebSocket stream. If a wallet enters a new market after `start()`, the watcher automatically picks it up from the enriched transfer event and adds it to the index.
6. A periodic REST refresh (default: every 5 minutes) re-fetches all wallet positions as a safety net, catching any positions the stream may have missed during brief disconnections.
`condition_resolution` is the moment positions become redeemable on the Conditional Tokens contract. For neg-risk markets (the majority on Polymarket), this fires in a separate transaction after the UMA `resolution` event. See [Oracle Events](/websocket/events/oracle) for the full resolution lifecycle.
## Lifecycle
### 1. Construct
```typescript theme={null}
const watcher = new RedemptionWatcher({
apiKey: 'pn_live_...',
trackPositionChanges: true, // live position + new market tracking (default)
});
```
### 2. Register Handlers
Register handlers **before** calling `start()` so you don't miss the `ready` event.
```typescript theme={null}
watcher.on('alert', (alert) => {
pushNotification(alert.wallet, {
title: alert.isWinner ? 'Position Redeemable!' : 'Market Resolved',
body: `${alert.marketTitle} — ${alert.winningOutcome} wins`,
payout: alert.estimatedPayoutUsd,
});
});
watcher.on('ready', () => {
console.log(`Tracking ${watcher.size} positions`);
});
watcher.on('error', (err) => {
console.error('Watcher error:', err.message);
});
```
### 3. Start
```typescript theme={null}
await watcher.start(['0x02227b...', '0xc2e780...']);
```
`start()` fetches positions for all wallets in parallel, indexes them by `condition_id`, subscribes to the oracle stream, and emits `ready`. Typical startup takes 100-300ms depending on wallet count.
Real output from `start()` with one wallet:
```
Tracking 16 positions across 1 wallet(s)
Conditions: 16
```
### 4. Add/Remove Wallets at Runtime
```typescript theme={null}
// New user signs up — fetch their positions and start tracking
await watcher.addWallets(['0xefbc5f...']);
// wallets: 2, size: 55
// User leaves — purge state and update subscriptions
watcher.removeWallets(['0xefbc5f...']);
// wallets: 1, size: 16
```
### 5. Query State
```typescript theme={null}
watcher.wallets; // all tracked addresses
watcher.conditions; // all tracked condition IDs
watcher.size; // total position count
watcher.positionsFor('0x02227b...'); // positions for one wallet
```
### 6. Close
```typescript theme={null}
watcher.close();
```
Unsubscribes from oracle and wallet streams, stops the refresh timer, disconnects the WebSocket, and resolves any pending async iterators.
## Async Iterator
Consume alerts sequentially with `for await`:
```typescript theme={null}
for await (const alert of watcher) {
await processRedemption(alert);
// backpressure: next alert waits until this one is processed
}
```
The iterator terminates when `watcher.close()` is called.
## Alert Object
When a `condition_resolution` event matches a tracked position, the watcher emits a `RedeemableAlert`:
```typescript theme={null}
interface RedeemableAlert {
wallet: string; // which tracked wallet holds this position
conditionId: string; // Polymarket condition ID (hex)
tokenId: string; // conditional token ID held by the wallet
outcome: string; // which outcome the wallet holds ("Over", "Yes", etc.)
winningOutcome: string; // which outcome won ("Under", "No", etc.)
isWinner: boolean; // whether this position pays out
size: number; // position size in tokens
estimatedPayoutUsd: number; // $1 per token if winner, $0 if loser
marketTitle: string; // human-readable market question
marketSlug: string; // URL slug on polymarket.com
marketImage?: string; // market image URL
resolvedPrice: number; // UMA resolved price (1 = first outcome, 0 = second)
payouts: number[]; // payout array from the contract ([1, 0] or [0, 1])
blockNumber: number; // Polygon block number
timestamp: number; // block timestamp in ms
}
```
**Example alert** (if a wallet held 500 "Over" tokens on the Dota 2 kills market below):
```json theme={null}
{
"wallet": "0x02227b8f5a9636e895607edd3185ed6ee5598ff7",
"conditionId": "0xf5fbfb50f2ad1f61569fa475b8883d2f1ee10fdceab24f069528b717a40cc101",
"tokenId": "57488988409414421964789033691537502121286921656702379301876764465477206797606",
"outcome": "Over",
"winningOutcome": "Over",
"isWinner": true,
"size": 500,
"estimatedPayoutUsd": 500,
"marketTitle": "Total Kills Over/Under 45.5 in Game 1?",
"marketSlug": "dota2-ty-mouz-2026-03-25-game1-kill-over-45pt5",
"marketImage": "https://polymarket-upload.s3.us-east-2.amazonaws.com/dota2-7ffacddb21.jpg",
"resolvedPrice": 1,
"payouts": [1, 0],
"blockNumber": 84667307,
"timestamp": 1774454649000
}
```
## Tracked Positions
Each position loaded from the REST API is stored as a `TrackedPosition`:
```typescript theme={null}
interface TrackedPosition {
wallet: string;
tokenId: string;
conditionId: string;
outcome: string; // "Yes", "No", "Over", "Under", team names, etc.
size: number; // position size in tokens
marketTitle: string;
marketSlug: string;
marketImage?: string;
outcomes: string[]; // all outcomes for this market
tokenIds: string[]; // all token IDs for this market
negRisk?: boolean; // neg-risk framework (multi-outcome markets)
}
```
Real output from `positionsFor()`:
```json theme={null}
[
{
"wallet": "0x02227b8f...",
"tokenId": "80628887006942228330415332521239838709...",
"conditionId": "0xf88a2140ac54353c2f51b2ee20de43c72b0d...",
"outcome": "No",
"size": 3440212.886182,
"marketTitle": "Will Manchester City FC win on 2026-02-21?",
"marketSlug": "epl-mac-new-2026-02-21-mac",
"negRisk": true
},
{
"wallet": "0x02227b8f...",
"tokenId": "11424825366156360215190655536940393847...",
"conditionId": "0x18f34febea506bed46b77c3126369eb9cc00...",
"outcome": "Yes",
"size": 1625004.089452,
"marketTitle": "Will Toulouse FC win on 2026-02-15?",
"marketSlug": "fl1-hac-tou-2026-02-15-tou",
"negRisk": true
}
]
```
## The Event That Triggers Alerts
The watcher listens for `condition_resolution` oracle events. Here's a real one from a Dota 2 esports market:
```json theme={null}
{
"event_type": "oracle",
"oracle_type": "condition_resolution",
"block_number": 84667307,
"timestamp": 1774454649000,
"tx_hash": "0x863f3e30d4d3e84c80682565d40502257ec1580e34f212838e6954d32b738f7b",
"condition_id": "0xf5fbfb50f2ad1f61569fa475b8883d2f1ee10fdceab24f069528b717a40cc101",
"question_id": "0x22d5713c949303f38b5add45b0dd694cb6914fcdd33d35e908ef47f918fa2147",
"adapter_address": "0x65070be91477460d8a7aeeb94ef92fe056c2f2a7",
"resolved_price": 1,
"resolved_outcome": "Over",
"payouts": [1, 0],
"market_title": "Total Kills Over/Under 45.5 in Game 1?",
"market_slug": "dota2-ty-mouz-2026-03-25-game1-kill-over-45pt5",
"event_title": "Dota 2: Team Yandex vs MOUZ (BO2) - ESL One Birmingham Group A",
"market_image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/dota2-7ffacddb21.jpg",
"outcomes": ["Over", "Under"],
"token_ids": [
"57488988409414421964789033691537502121286921656702379301876764465477206797606",
"44113030389329339383080791624849973102915570622427276234797737004302338372036"
],
"neg_risk": false
}
```
**Winner detection:** The watcher finds the wallet's `tokenId` in `event.token_ids`, checks the corresponding index in `event.payouts`. If `payouts[index] > 0`, the position is a winner and pays out `$1 × size`.
## Configuration
```typescript theme={null}
interface RedemptionWatcherConfig {
apiKey: string; // required — PolyNode API key
baseUrl?: string; // REST API base (default: https://api.polynode.dev)
wsUrl?: string; // WebSocket URL (default: wss://ws.polynode.dev/ws)
compress?: boolean; // zlib compression (default: true via PolyNodeWS)
autoReconnect?: boolean; // auto-reconnect on disconnect (default: true)
trackPositionChanges?: boolean; // live position delta tracking (default: true)
refreshInterval?: number; // periodic REST refresh in ms (default: 300000 = 5 min)
}
```
### `trackPositionChanges`
When enabled (default), the watcher subscribes to the wallets WebSocket stream. This does two things:
1. **Size tracking** — updates position sizes in real-time as ERC-1155 transfers occur. If a user buys or sells tokens after `start()`, the `size` field stays accurate for payout calculations.
2. **New market discovery** — when a wallet enters a market the watcher hasn't seen before, the enriched transfer event carries full market metadata (`condition_id`, `market_title`, `outcomes`, `token_ids`). The watcher creates a new tracked position automatically. No gaps.
### `refreshInterval`
Periodic REST re-fetch of all wallet positions. Acts as a safety net in case a transfer event is missed due to a brief WebSocket disconnection, ensuring the watcher eventually picks up any positions the stream didn't catch.
Default: `300_000` (5 minutes). Set to `0` to disable, or lower (e.g. `60_000`) if you need faster recovery.
## Full Example: Notification Service
```typescript theme={null}
import { RedemptionWatcher } from 'polynode-sdk';
const watcher = new RedemptionWatcher({
apiKey: process.env.POLYNODE_API_KEY!,
});
watcher.on('error', (err) => console.error('[watcher]', err.message));
watcher.on('ready', () => {
console.log(`Monitoring ${watcher.size} positions across ${watcher.wallets.length} wallets`);
});
// Load wallets from your database
const userWallets = await db.query('SELECT wallet FROM users WHERE active = true');
await watcher.start(userWallets.map(u => u.wallet));
// Process alerts
for await (const alert of watcher) {
await db.insert('redemption_alerts', {
wallet: alert.wallet,
market: alert.marketTitle,
outcome: alert.outcome,
is_winner: alert.isWinner,
payout_usd: alert.estimatedPayoutUsd,
condition_id: alert.conditionId,
block_number: alert.blockNumber,
});
if (alert.isWinner) {
await sendPushNotification(alert.wallet, {
title: 'Position Redeemable!',
body: `"${alert.marketTitle}" resolved ${alert.winningOutcome}. Redeem ~$${alert.estimatedPayoutUsd.toLocaleString()}.`,
});
}
}
```
## Memory Management
The watcher is designed to run indefinitely with bounded memory, even when tracking thousands of wallets.
* **Resolved conditions are evicted** after alerts fire. A condition only resolves once, so there's nothing left to watch.
* **Zero-size positions are evicted** when a wallet fully sells out of a market. If they buy back in, the position is re-created from the next `position_change` event or the periodic REST refresh.
* **The periodic REST refresh** (default: every 5 minutes) acts as a safety net. Even if a stream event is missed, the refresh catches it within one cycle.
Memory usage is proportional to the number of active, non-zero positions across all tracked wallets at any given moment. Historical positions, resolved markets, and fully-sold positions do not accumulate.
## API Reference
### Constructor
```typescript theme={null}
new RedemptionWatcher(config: RedemptionWatcherConfig)
```
### Methods
| Method | Returns | Description |
| ------------------------ | ------------------- | ----------------------------------------------------- |
| `start(wallets)` | `Promise` | Fetch positions, subscribe to streams, begin watching |
| `addWallets(wallets)` | `Promise` | Add wallets at runtime, fetch their positions |
| `removeWallets(wallets)` | `void` | Remove wallets, clean up state |
| `positionsFor(wallet)` | `TrackedPosition[]` | Get tracked positions for one wallet |
| `close()` | `void` | Unsubscribe, disconnect, clean up |
### Properties
| Property | Type | Description |
| ------------ | ---------- | ---------------------------- |
| `wallets` | `string[]` | All tracked wallet addresses |
| `conditions` | `string[]` | All tracked condition IDs |
| `size` | `number` | Total tracked position count |
### Events
| Event | Payload | Description |
| ------- | ----------------- | --------------------------------------- |
| `alert` | `RedeemableAlert` | A tracked position's condition resolved |
| `ready` | (none) | Positions loaded and streams connected |
| `error` | `Error` | REST fetch failed or stream error |
# REST API
Source: https://docs.polynode.dev/sdks/rest-api
Typed methods for all 25 REST endpoints
Every REST endpoint has a typed method with full request/response types.
## Markets
```typescript theme={null}
const markets = await pn.markets({ count: 10 });
const detail = await pn.market(tokenId);
const bySlug = await pn.marketBySlug('bitcoin-100k');
const byCondition = await pn.marketByCondition(conditionId);
const list = await pn.marketsList({ count: 20, sort: 'volume' });
const results = await pn.search('ethereum', { limit: 5 });
```
```rust theme={null}
let markets = client.markets(Some(10)).await?;
let detail = client.market("token_id").await?;
let by_slug = client.market_by_slug("bitcoin-100k").await?;
let by_cond = client.market_by_condition("0xabc...").await?;
let list = client.list_markets(&ListMarketsParams {
count: Some(20), sort: Some("volume".into()), ..Default::default()
}).await?;
let results = client.search("ethereum", Some(5), None).await?;
```
## Pricing
```typescript theme={null}
const candles = await pn.candles(tokenId, { resolution: '1h', limit: 100 });
const stats = await pn.stats(tokenId);
```
```rust theme={null}
let candles = client.candles("token_id", Some(CandleResolution::OneHour), Some(100)).await?;
let stats = client.stats("token_id").await?;
```
## Settlements
```typescript theme={null}
const recent = await pn.recentSettlements({ count: 20 });
const token = await pn.tokenSettlements(tokenId, { count: 10 });
const wallet = await pn.walletSettlements(address, { count: 10 });
```
```rust theme={null}
let recent = client.recent_settlements(Some(20)).await?;
let token = client.token_settlements("token_id", Some(10)).await?;
let wallet = client.wallet_settlements("0xabc...", Some(10)).await?;
```
## Wallets & System
```typescript theme={null}
const wallet = await pn.wallet(address);
const status = await pn.status();
const key = await pn.createKey('my-bot');
```
```rust theme={null}
let wallet = client.wallet("0xabc...").await?;
let status = client.status().await?;
let key = client.create_key(Some("my-bot")).await?;
```
## Enriched Data
```typescript theme={null}
// Leaderboard — top 20 traders by profit or volume
const lb = await pn.leaderboard({ period: 'monthly', sort: 'profit' });
// Trending — carousel, breaking, hot topics, movers
const trending = await pn.trending();
// Activity feed — 50 most recent trades platform-wide
const feed = await pn.activity();
// Biggest movers — largest 24h price changes
const movers = await pn.movers();
// Trader profile — full stats for any wallet
const profile = await pn.traderProfile('0xc2e7...');
// Trader PnL series — cumulative PnL over time
const pnl = await pn.traderPnl('0xc2e7...', { period: '1W' });
// Event detail — all sub-markets for an event
const event = await pn.event('fed-decision-in-april');
// Markets by category
const crypto = await pn.marketsByCategory('crypto');
```
```rust theme={null}
let lb = client.leaderboard(Some("monthly"), Some("profit")).await?;
let trending = client.trending().await?;
let feed = client.activity().await?;
let movers = client.movers().await?;
let profile = client.trader_profile("0xc2e7...").await?;
let pnl = client.trader_pnl("0xc2e7...", Some("1W")).await?;
let event = client.event("fed-decision-in-april").await?;
let crypto = client.markets_by_category("crypto").await?;
```
Enriched endpoints are rate limited to 1 request per second per API key with 1-3 minute caching.
## Error Handling
```typescript theme={null}
import { ApiError } from 'polynode-sdk';
try {
await pn.market('invalid-id');
} catch (err) {
if (err instanceof ApiError) {
console.log(err.status, err.message); // 404, "Market not found"
}
}
```
```rust theme={null}
match client.market("invalid-id").await {
Err(Error::NotFound(msg)) => println!("Not found: {}", msg),
Err(Error::Auth(msg)) => println!("Auth failed: {}", msg),
Err(Error::RateLimited(msg)) => println!("Rate limited: {}", msg),
Err(e) => println!("Other: {}", e),
Ok(detail) => println!("{:?}", detail),
}
```
# RPC Proxy
Source: https://docs.polynode.dev/sdks/rpc
JSON-RPC calls through PolyNode's optimized Polygon RPC endpoint
Both SDKs include a method to send JSON-RPC requests through `rpc.polynode.dev`. Transaction submission goes directly to the current block-producing validator for optimal inclusion. Read calls are served from our P2P infrastructure.
## Usage
```typescript theme={null}
// Block number (served locally, no external call)
const blockNum = await pn.rpc('eth_blockNumber');
// Gas price (from PolyNode's gas oracle)
const gasPrice = await pn.rpc('eth_gasPrice');
// Read calls (proxied to public RPC)
const balance = await pn.rpc('eth_getBalance', ['0xabc...', 'latest']);
const block = await pn.rpc('eth_getBlockByNumber', ['latest', false]);
```
```rust theme={null}
let block_num = client.rpc_call("eth_blockNumber", serde_json::json!([])).await?;
let gas_price = client.rpc_call("eth_gasPrice", serde_json::json!([])).await?;
let balance = client.rpc_call("eth_getBalance",
serde_json::json!(["0xabc...", "latest"])).await?;
```
See the [RPC documentation](/rpc/overview) for the full list of supported methods and limitations.
# Rust — Local Cache
Source: https://docs.polynode.dev/sdks/rust/cache
## Local Cache
Requires the `cache` feature flag: `polynode = { version = "0.12.0", features = ["cache"] }`
Local SQLite cache that backfills trade history on startup, streams live updates via WebSocket, and serves all queries locally with zero additional API calls after initialization. Driven by a JSON watchlist file that specifies which wallets, markets, and tokens to track.
### Setup
Create a `polynode.watch.json` file:
```json theme={null}
{
"version": 1,
"wallets": [
{ "address": "0x1a1A27de044faFFCCf68E28F03dCfCf5eB3d3cE6", "label": "whale-1", "backfill": true },
{ "address": "0xBB39C16C3fc54d3C9B1f9f9E8dF4a09Ee25AB7df", "label": "whale-2", "backfill": true }
],
"markets": [
{ "condition_id": "0xabc...", "label": "bitcoin-100k", "backfill": true }
],
"tokens": [],
"settings": {
"ttl_days": 30,
"backfill_rate": 2.0,
"purge_on_remove": false
}
}
```
### Start the Cache
```rust,no_run theme={null}
use polynode::{PolyNodeClient, cache::PolyNodeCache};
use std::sync::Arc;
#[tokio::main]
async fn main() -> polynode::Result<()> {
let client = Arc::new(PolyNodeClient::new("pn_live_YOUR_KEY")?);
let mut cache = PolyNodeCache::builder(client)
.db_path("./polynode-cache.db")
.watchlist_path("./polynode.watch.json")
.ttl_seconds(30 * 86400) // 30 days
.backfill_rate(2.0) // 2 requests/sec
.backfill_pages(3) // 3 pages per entity
.backfill_page_size(500) // 500 trades per page
.purge_on_remove(false) // keep data when removing from watchlist
.on_backfill_progress(|p| {
println!("[{}] {}: {} fetched ({})", p.entity_type, p.label, p.fetched, p.status);
})
.build()?;
cache.start().await?;
// All queries are local, instant, no API calls
let positions = cache.wallet_positions("0x1a1A27de044faFFCCf68E28F03dCfCf5eB3d3cE6")?;
for p in &positions {
println!("{}: {} {} @ {:.4} (pnl: {:?})",
p.market_title, p.outcome, p.size, p.avg_price, p.cash_pnl);
}
cache.stop().await?;
Ok(())
}
```
### Query Methods
All queries are synchronous and read from local SQLite:
```rust,no_run theme={null}
use polynode::cache::{QueryOptions, OrderBy};
# fn example(cache: &polynode::cache::PolyNodeCache) -> polynode::Result<()> {
// Wallet positions
let positions = cache.wallet_positions("0xabc...")?;
// Multi-wallet positions
let multi = cache.multi_wallet_positions(&[
"0xabc...".into(),
"0xdef...".into(),
])?;
// Wallet trades with filters
let trades = cache.wallet_trades("0xabc...", &QueryOptions {
limit: Some(100),
offset: None,
since: Some(1711843200.0),
until: None,
side: Some("BUY".into()),
order_by: Some(OrderBy::TimestampDesc),
})?;
// Market trades
let market_trades = cache.market_trades("condition_id", &QueryOptions::default())?;
// Market positions
let market_pos = cache.market_positions("condition_id")?;
// Token trades
let token_trades = cache.token_trades("token_id", &QueryOptions::default())?;
// Settlements
let settlements = cache.wallet_settlements("0xabc...", &QueryOptions::default())?;
// Lookup by tx hash
let tx = cache.trade_by_tx_hash("0xdeadbeef...")?;
// Realized P&L (weighted average cost basis)
let pnl = cache.wallet_realized_pnl("0xabc...")?;
println!("realized: ${:.2}, unrealized: ${:.2}, confidence: {}",
pnl.total_realized_pnl, pnl.total_unrealized_pnl, pnl.confidence);
// Cache stats
let stats = cache.stats()?;
println!("{} trades, {} settlements, {:.1}MB",
stats.trade_count, stats.settlement_count, stats.db_size_bytes as f64 / 1_048_576.0);
# Ok(())
# }
```
### Runtime Watchlist Management
Add or remove entities without restarting:
```rust,no_run theme={null}
use polynode::cache::EntityType;
# fn example(cache: &polynode::cache::PolyNodeCache) -> polynode::Result<()> {
// Add a wallet at runtime (will backfill automatically)
cache.add_to_watchlist(&[
(EntityType::Wallet, "0xnew...".into(), "new-whale".into(), true),
])?;
// Remove a wallet
cache.remove_from_watchlist(&[
(EntityType::Wallet, "0xold...".into()),
])?;
# Ok(())
# }
```
The cache also watches the `polynode.watch.json` file for changes. Edit the file and the cache picks up additions and removals automatically.
### Manual Pruning
```rust,no_run theme={null}
# fn example(cache: &polynode::cache::PolyNodeCache) -> polynode::Result<()> {
let pruned = cache.prune()?;
println!("pruned {} expired records", pruned);
# Ok(())
# }
```
# Rust — Error Handling
Source: https://docs.polynode.dev/sdks/rust/errors
## Error Handling
All SDK methods return `polynode::Result`, which wraps `polynode::Error`:
```rust,no_run theme={null}
use polynode::{PolyNodeClient, Error};
# async fn example() -> polynode::Result<()> {
let client = PolyNodeClient::new("pn_live_YOUR_KEY")?;
match client.market("invalid-token-id").await {
Ok(market) => println!("{:?}", market),
Err(Error::NotFound(msg)) => println!("not found: {}", msg),
Err(Error::Auth(msg)) => println!("auth failed: {}", msg),
Err(Error::RateLimited(msg)) => println!("rate limited: {}", msg),
Err(Error::Api { status, message }) => println!("API error {}: {}", status, message),
Err(Error::Http(e)) => println!("network error: {}", e),
Err(Error::Disconnected) => println!("WebSocket disconnected"),
Err(e) => println!("other: {}", e),
}
# Ok(())
# }
```
### Error Variants
| Variant | When it occurs |
| ------------------------- | ---------------------------------------------------- |
| `Http` | Network-level request failure |
| `WebSocket` | WebSocket connection or protocol error |
| `Json` | Response deserialization failure |
| `Api { status, message }` | Server returned a non-success status |
| `Auth` | 401 or 403 from the API |
| `RateLimited` | 429 from the API |
| `NotFound` | 404 from the API |
| `Disconnected` | WebSocket connection lost |
| `Decompression` | zlib decompression failure |
| `ConnectionClosed` | Server closed the WebSocket |
| `Url` | URL parse error |
| `Trading` | Trading-specific error (feature: `trading`) |
| `Signing` | EIP-712 signing failure (feature: `trading`) |
| `Sqlite` | Local database error (feature: `cache` or `trading`) |
| `Io` | File I/O error (feature: `cache` or `trading`) |
| `Cache` | Cache-specific error (feature: `cache`) |
# Rust — Orderbook
Source: https://docs.polynode.dev/sdks/rust/orderbook
## Orderbook Streaming
Real-time orderbook data from `ob.polynode.dev`. Three levels of abstraction: raw stream, local state manager, or the fully managed engine.
### Raw Stream
For full control over message processing:
```rust,no_run theme={null}
use polynode::{PolyNodeClient, ObStreamOptions};
use polynode::types::orderbook::ObMessage;
#[tokio::main]
async fn main() -> polynode::Result<()> {
let client = PolyNodeClient::new("pn_live_YOUR_KEY")?;
let mut stream = client.orderbook_stream(ObStreamOptions::default()).await?;
// Subscribe to specific tokens
stream.subscribe(vec![
"51037625779056581606819614184446816710505006861008496087735536016411882582167".into(),
]).await?;
while let Some(msg) = stream.next().await {
match msg {
Ok(ObMessage::Update(update)) => {
println!("{:?}", update);
}
Ok(ObMessage::Subscribed { markets }) => {
println!("tracking {} markets", markets);
}
Ok(ObMessage::SnapshotsDone { total }) => {
println!("all {} snapshots loaded", total);
}
Err(e) => eprintln!("error: {}", e),
_ => {}
}
}
Ok(())
}
```
### Local Orderbook State
Apply snapshots, deltas, and price changes to maintain a sorted, tick-accurate local copy of the book:
```rust,no_run theme={null}
use polynode::LocalOrderbook;
use polynode::types::orderbook::{ObMessage, OrderbookUpdate};
# async fn example(stream: &mut polynode::ObStream) -> polynode::Result<()> {
let mut book = LocalOrderbook::new();
while let Some(msg) = stream.next().await {
if let Ok(ObMessage::Update(update)) = msg {
match &update {
OrderbookUpdate::Snapshot(snap) => book.apply_snapshot(snap),
OrderbookUpdate::Update(delta) => book.apply_update(delta),
OrderbookUpdate::PriceChange(pc) => book.apply_price_change(pc),
OrderbookUpdate::LastTradePrice(_) => {}
}
}
let token = "21742633...";
if let Some(bid) = book.best_bid(token) {
println!("best bid: {} x {}", bid.price, bid.size);
}
if let Some(ask) = book.best_ask(token) {
println!("best ask: {} x {}", ask.price, ask.size);
}
if let Some(spread) = book.spread(token) {
println!("spread: {:.4}", spread);
}
}
# Ok(())
# }
```
Requires polynode **v0.11.0+**. Earlier versions dropped `price_change` events before they reached your handler — upgrade if your local book drifts from the server.
#### Types
`best_bid` and `best_ask` return `Option<&OrderbookLevel>`. `book.get_book(token)` returns `Option<(&[OrderbookLevel], &[OrderbookLevel])>` — bids first (sorted descending), asks second (sorted ascending).
```rust,no_run theme={null}
pub struct OrderbookLevel {
pub price: String, // string-typed — parse to f64 for math
pub size: String,
}
```
Prices and sizes are strings to preserve full Polymarket precision. Parse with `level.price.parse::()?` when you need numeric values.
### Orderbook Engine
The highest-level abstraction. One shared WebSocket, automatic state management, and filtered views for different consumers:
```rust,no_run theme={null}
use polynode::{OrderbookEngine, EngineOptions};
#[tokio::main]
async fn main() -> polynode::Result<()> {
let engine = OrderbookEngine::connect(
"pn_live_YOUR_KEY",
EngineOptions::default(),
).await?;
// Subscribe to tokens
engine.subscribe(vec![
"token_a".into(),
"token_b".into(),
]).await?;
// Query the shared state directly
if let Some(mid) = engine.midpoint("token_a").await {
println!("midpoint: {:.4}", mid);
}
if let Some(spread) = engine.spread("token_a").await {
println!("spread: {:.4}", spread);
}
if let Some((bids, asks)) = engine.book("token_a").await {
println!("bids: {}, asks: {}", bids.len(), asks.len());
}
// Create a filtered view for a subset of tokens
let mut view = engine.view(vec!["token_a".into()]);
while let Some(update) = view.next().await {
if let Some(mid) = view.midpoint("token_a").await {
println!("token_a midpoint: {:.4}", mid);
}
}
engine.close().await?;
Ok(())
}
```
#### Method signatures
```rust,no_run theme={null}
# use polynode::OrderbookEngine;
# use polynode::types::orderbook::OrderbookLevel;
# async fn sigs(engine: &OrderbookEngine) {
let _: Option = engine.midpoint("token").await;
let _: Option = engine.spread("token").await;
let _: Option = engine.best_bid("token").await;
let _: Option = engine.best_ask("token").await;
let _: Option<(Vec, Vec)> = engine.book("token").await;
# }
```
`midpoint` and `spread` return `None` when either the bid OR ask side is empty — a one-sided book has no defined mid or spread. `best_bid` / `best_ask` only require their own side to be present. `book` returns whatever's there even if one side is empty.
#### EngineOptions
```rust,no_run theme={null}
pub struct EngineOptions {
pub compress: bool, // default: true — gzip on the wire
pub auto_reconnect: bool, // default: true
pub max_reconnect_attempts: Option, // default: None — reconnect forever
}
```
`EngineOptions::default()` is the right call for almost every use case. Override only if you need to disable compression for a CPU-constrained client or cap reconnect attempts in a one-shot script.
#### `subscribe()` returns immediately — snapshots are async
`engine.subscribe(...)` sends the subscribe command to the background WS task and returns as soon as the command is queued. **Snapshots arrive asynchronously** over the WS and populate local state some time later (typically 1–5 seconds, longer for many tokens). Calling `engine.midpoint(id)` the moment `subscribe()` returns will return `None` because the snapshot hasn't landed yet.
Two patterns work:
```rust,no_run theme={null}
# use std::time::Duration;
# use polynode::OrderbookEngine;
# async fn example(engine: &OrderbookEngine) -> polynode::Result<()> {
// (1) coarse wait — simplest, fine for scripts and examples
engine.subscribe(vec!["token_a".into()]).await?;
tokio::time::sleep(Duration::from_secs(3)).await;
let mid = engine.midpoint("token_a").await; // now returns Some(..) once snapshot applied
// (2) event-driven — read updates from a view (production pattern)
let mut view = engine.view(vec!["token_a".into()]);
engine.subscribe(vec!["token_a".into()]).await?;
while let Some(_update) = view.next().await {
if let Some(mid) = view.midpoint("token_a").await {
println!("ready: midpoint={mid:.4}");
break;
}
}
# Ok(())
# }
```
#### Errors `connect()` can return
`OrderbookEngine::connect` returns `polynode::Result` which unwraps to one of:
* `Error::Auth(...)` — API key is invalid, revoked, or not recognized
* `Error::RateLimited(...)` — too many concurrent connections for your tier
* `Error::WebSocket(...)` — TLS/handshake failure (network, DNS, TLS cert)
* `Error::Url(...)` — internal URL parse error (shouldn't happen; file a bug)
In long-running programs, the engine's internal task uses `EngineOptions::auto_reconnect` (default true) to recover from mid-session drops, so you generally only need to handle errors at initial `connect()` time.
### Batch Queries
Query many tokens in a single call. Each batch method takes a slice of token IDs and returns a `HashMap` keyed by `asset_id`. Tokens that aren't in local state are silently omitted, so callers can pass mixed lists without pre-filtering.
```rust,no_run theme={null}
use polynode::{OrderbookEngine, EngineOptions};
#[tokio::main]
async fn main() -> polynode::Result<()> {
let engine = OrderbookEngine::connect("pn_live_...", EngineOptions::default()).await?;
engine.subscribe(vec!["token_a".into(), "token_b".into(), "token_c".into()]).await?;
let ids = vec!["token_a".to_string(), "token_b".to_string(), "token_c".to_string()];
// One read lock, one round-trip — every value at once.
let mids = engine.midpoints(&ids).await;
let spreads = engine.spreads(&ids).await;
let bids = engine.best_bids(&ids).await;
let asks = engine.best_asks(&ids).await;
let books = engine.books(&ids).await;
for (id, m) in &mids {
println!("{} mid={:.4}", id, m);
}
Ok(())
}
```
Sample response shape (actual values from a live run):
```text theme={null}
midpoints(&ids) -> { "77893140…19989216": 0.2350,
"22139576…97751695": 0.0085,
"10905153…10914081": 0.3050 }
spreads(&ids) -> { "58343456…67891911": 0.0010,
"22139576…97751695": 0.0010 }
best_bids(&ids) -> { "72100993…76306968": OrderbookLevel { price: "0.999", size: "154844.11" },
"10905153…10914081": OrderbookLevel { price: "0.3", size: "473238.67" } }
books(&ids) -> { "84387820…19854548": (
vec![], // bids (empty — one-sided)
vec![OrderbookLevel { price: "0.001", size: "687883.2" }, /* +113 more */]
) }
```
Only two of the three tokens have midpoints because the third is a one-sided binary book (see `EngineOptions` → `None` semantics above).
### All Tracked Tokens
Skip the ID list and get every token currently in local state. Useful when you want a global snapshot of everything you're subscribed to.
```rust,no_run theme={null}
# use polynode::OrderbookEngine;
# async fn example(engine: &OrderbookEngine) -> polynode::Result<()> {
let tokens = engine.tracked_tokens().await; // Vec
let mids = engine.midpoints_all().await; // every midpoint
let spreads = engine.spreads_all().await;
let bids = engine.best_bids_all().await;
let asks = engine.best_asks_all().await;
let books = engine.books_all().await; // full L2 for everything
# Ok(())
# }
```
The same `_all` methods exist on `EngineView`.
### EngineView — filtered handle over shared state
`engine.view(token_ids)` returns an `EngineView` — a filtered receiver that only sees updates for the token IDs you pass in. All views share the engine's single WebSocket and single local state; they're cheap to create and you can have many in one process (e.g. one per consumer task).
```rust,no_run theme={null}
# use polynode::OrderbookEngine;
# async fn example(engine: &OrderbookEngine) {
let mut view = engine.view(vec!["token_a".into(), "token_b".into()]);
// Stream updates scoped to this view's tokens. next() returns the next
// OrderbookUpdate for token_a or token_b, and None only if the engine closes.
while let Some(update) = view.next().await {
// update is an OrderbookUpdate::Snapshot | ::Update | ::PriceChange | ::LastTradePrice
// query freshest state (same read lock as the engine):
if let Some(mid) = view.midpoint("token_a").await {
println!("token_a mid: {mid:.4}");
}
}
// Change the filter at runtime — takes effect on the next incoming message:
view.set_tokens(vec!["token_c".into()]).await;
# }
```
Every batch + `_all()` method that exists on `OrderbookEngine` also exists on `EngineView`. `engine.state()` is engine-only (views are filtered; use the engine for the full state handle).
### Detecting Inactive Markets
Each token tracks the moment its local copy was last touched (snapshot or delta). Use this to detect markets that have stopped moving.
```rust,no_run theme={null}
use std::time::{Duration, Instant};
# use polynode::OrderbookEngine;
# async fn example(engine: &OrderbookEngine) -> polynode::Result<()> {
// Return type: Option. Call .elapsed() for Duration since.
let ts: Option = engine.last_change("token_a").await;
if let Some(ts) = ts {
println!("last update was {:?} ago", ts.elapsed());
}
// Return type: Vec — every tracked token whose last update is older than threshold.
let stale: Vec = engine.inactive_since(Duration::from_secs(60)).await;
for token in stale {
println!("inactive: {}", token);
}
# Ok(())
# }
```
Sample response (a tracked token and a stale filter):
```text theme={null}
last_change("77893140…") -> Some(Instant { .. }) // .elapsed() => 3.910s
inactive_since(60s) -> [] // nothing stale right after subscribe
inactive_since(1ms) -> ["77893140…", "22139576…", "10905153…"]
```
The timestamp is stamped client-side at the moment your SDK applies the update, which is typically tens of milliseconds behind the actual exchange event — accurate enough for inactive-market detection, not for sub-second cross-market correlation.
### Direct State Access
For callers who want to hold the lock and walk the full state themselves, `engine.state()` returns the underlying `Arc>`. Useful for custom batch logic, snapshotting all tokens at a single consistent moment, or building your own view.
```rust,no_run theme={null}
# use polynode::OrderbookEngine;
# async fn example(engine: &OrderbookEngine) -> polynode::Result<()> {
let state = engine.state();
let guard = state.read().await;
// Everything in one consistent view
let total = guard.len();
let mids = guard.midpoints_all();
let books = guard.books_all();
for token in guard.tracked_tokens() {
if let Some(ts) = guard.last_change(&token) {
println!("{}: last change {:?} ago", token, ts.elapsed());
}
}
# Ok(())
# }
```
The same methods are available on `LocalOrderbook` directly when used outside the engine.
# Rust — Overview
Source: https://docs.polynode.dev/sdks/rust/overview
## Install
```bash theme={null}
cargo add polynode
```
The core crate has zero required feature flags. Optional features unlock additional capabilities:
| Feature | What it adds |
| --------- | ----------------------------------------------------------------- |
| `cache` | Local SQLite cache with watchlist, backfill, and P\&L computation |
| `trading` | Order placement, wallet management, credential custody |
| `privy` | Privy-based signer (requires `trading`) |
Enable features in your `Cargo.toml`:
```toml theme={null}
[dependencies]
polynode = { version = "0.12.0", features = ["trading", "cache"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
```
## Quick Start
```rust,no_run theme={null}
use polynode::PolyNodeClient;
#[tokio::main]
async fn main() -> polynode::Result<()> {
let client = PolyNodeClient::new("pn_live_YOUR_KEY")?;
// Fetch top 5 markets by volume
let markets = client.markets(Some(5)).await?;
println!("{} markets returned", markets.count);
// Search for a market
let results = client.search("bitcoin", Some(3), None).await?;
for r in &results.results {
println!("{}", r.question.as_deref().unwrap_or("untitled"));
}
Ok(())
}
```
## Client Configuration
Use the builder for full control over endpoints and timeouts:
```rust,no_run theme={null}
use polynode::PolyNodeClient;
use std::time::Duration;
# fn example() -> polynode::Result<()> {
let client = PolyNodeClient::builder("pn_live_YOUR_KEY")
.base_url("https://api.polynode.dev")
.ws_url("wss://ws.polynode.dev/ws")
.ob_url("wss://ob.polynode.dev/ws")
.rpc_url("https://rpc.polynode.dev")
.timeout(Duration::from_secs(15))
.build()?;
# Ok(())
# }
```
All URLs default to the production endpoints shown above.
# Rust — Redemption Watcher
Source: https://docs.polynode.dev/sdks/rust/redemption
## Redemption Watcher
Monitor wallets for redeemable positions after oracle resolution. The watcher fetches current positions via REST, then listens for real-time oracle events on the WebSocket and emits alerts when a watched wallet holds a position in a resolved market.
```rust,no_run theme={null}
use polynode::{PolyNodeClient, RedemptionWatcher, RedemptionWatcherConfig};
use std::sync::Arc;
#[tokio::main]
async fn main() -> polynode::Result<()> {
let client = Arc::new(PolyNodeClient::new("pn_live_YOUR_KEY")?);
let mut watcher = RedemptionWatcher::new(client, RedemptionWatcherConfig {
track_position_changes: true,
refresh_interval_secs: 300,
compress: true,
});
// Start watching specific wallets
watcher.start(&[
"0x1a1A27de044faFFCCf68E28F03dCfCf5eB3d3cE6",
"0xBB39C16C3fc54d3C9B1f9f9E8dF4a09Ee25AB7df",
]).await?;
println!("tracking {} positions", watcher.size());
// Add more wallets at runtime
watcher.add_wallets(&["0x7a25dA10f8cA3b67D5fF55e87E2B0C076D3Dd0bD"]).await?;
// Listen for alerts
while let Some(alert) = watcher.next_alert().await {
if alert.is_winner {
println!("REDEEMABLE: {} holds {} on '{}' — payout: ${:.2}",
alert.wallet, alert.outcome, alert.market_title, alert.estimated_payout_usd);
} else {
println!("RESOLVED (loss): {} on '{}'", alert.wallet, alert.market_title);
}
}
watcher.close();
Ok(())
}
```
# Rust — REST API
Source: https://docs.polynode.dev/sdks/rust/rest-api
## REST API
Every REST endpoint has a typed async method on `PolyNodeClient`.
### System
```rust,no_run theme={null}
# async fn example(client: &polynode::PolyNodeClient) -> polynode::Result<()> {
// Liveness probe (no auth required)
let health = client.healthz().await?;
// Readiness check (no auth required)
let ready = client.readyz().await?;
// System status with metrics
let status = client.status().await?;
println!("uptime: {}s, ws_subscribers: {}", status.uptime_seconds, status.ws_subscribers);
// Generate a new API key (no auth required)
let key = client.create_key(Some("my-bot")).await?;
println!("key: {}", key.api_key);
# Ok(())
# }
```
### Markets
```rust,no_run theme={null}
# async fn example(client: &polynode::PolyNodeClient) -> polynode::Result<()> {
use polynode::rest::ListMarketsParams;
// Top markets by 24h volume
let markets = client.markets(Some(10)).await?;
println!("{} markets, {} total", markets.count, markets.total);
// Single market by token ID
let market = client.market("51037625779056581606819614184446816710505006861008496087735536016411882582167").await?;
// Single market by URL slug
let market = client.market_by_slug("bitcoin-100k").await?;
// Single market by condition ID
let market = client.market_by_condition("0xabc...").await?;
// Filtered, paginated listing
let list = client.list_markets(&ListMarketsParams {
count: Some(20),
sort: Some("volume".into()),
category: Some("crypto".into()),
min_volume: Some(10000.0),
active_only: Some(true),
cursor: None,
}).await?;
// Full-text search
let results = client.search("ethereum", Some(5), Some(false)).await?;
// Search events
let events = client.search_events("election", Some(5)).await?;
// Event detail (includes all markets within the event)
let event = client.event("presidential-election-2028").await?;
// Markets by category
let crypto = client.markets_by_category("crypto").await?;
# Ok(())
# }
```
Token IDs change as markets open and close. Use `client.markets(Some(1)).await?` to get a currently active token ID for testing.
### Pricing
```rust,no_run theme={null}
# async fn example(client: &polynode::PolyNodeClient) -> polynode::Result<()> {
use polynode::common::CandleResolution;
let token_id = "51037625779056581606819614184446816710505006861008496087735536016411882582167";
// OHLCV candles
let candles = client.candles(token_id, Some(CandleResolution::OneHour), Some(100)).await?;
for c in &candles.candles {
println!("{}: o={} h={} l={} c={} v={}", c.timestamp, c.open, c.high, c.low, c.close, c.volume);
}
// Market statistics
let stats = client.stats(token_id).await?;
# Ok(())
# }
```
### Settlements
```rust,no_run theme={null}
# async fn example(client: &polynode::PolyNodeClient) -> polynode::Result<()> {
// Most recent settlements across all markets
let recent = client.recent_settlements(Some(20)).await?;
for s in &recent.settlements {
println!("{}: {} ${} on {}", s.status, s.taker_side, s.taker_size, s.market_title);
}
// Settlements for a specific token
let token_settlements = client.token_settlements("21742633...", Some(10)).await?;
// Settlements for a specific wallet
let wallet_settlements = client.wallet_settlements("0xabc...", Some(10)).await?;
# Ok(())
# }
```
### Wallets
```rust,no_run theme={null}
# async fn example(client: &polynode::PolyNodeClient) -> polynode::Result<()> {
let address = "0x1a1A27de044faFFCCf68E28F03dCfCf5eB3d3cE6";
// Wallet activity summary
let wallet = client.wallet(address).await?;
// Wallet positions with P&L
let positions = client.wallet_positions_data(address, Some(50), None).await?;
for p in &positions.positions {
println!("{:?}", p);
}
// Onchain positions (all open + closed, accurate realized P&L)
let onchain = client.wallet_onchain_positions(address).await?;
// Wallet trade history
let trades = client.wallet_trades(address, Some(100), None).await?;
// Market trade history (by condition ID or slug)
let market_trades = client.market_trades("0xabc...", Some(50), None, Some("BUY"), None).await?;
# Ok(())
# }
```
### Orderbook (REST)
```rust,no_run theme={null}
# async fn example(client: &polynode::PolyNodeClient) -> polynode::Result<()> {
let token_id = "51037625779056581606819614184446816710505006861008496087735536016411882582167";
// Full orderbook snapshot
let book = client.orderbook_rest(token_id).await?;
println!("bids: {}, asks: {}", book.bids.len(), book.asks.len());
// Midpoint price
let mid = client.midpoint(token_id).await?;
println!("midpoint: {}", mid.mid);
// Bid-ask spread
let spread = client.spread(token_id).await?;
println!("spread: {}", spread.spread);
# Ok(())
# }
```
### Enriched Data
```rust,no_run theme={null}
# async fn example(client: &polynode::PolyNodeClient) -> polynode::Result<()> {
// Top traders leaderboard
let leaders = client.leaderboard(Some("daily"), Some("profit")).await?;
// Trending markets (carousel, breaking, hot topics, featured, movers)
let trending = client.trending().await?;
// Recent global activity
let activity = client.activity().await?;
// Biggest 24h price movers
let movers = client.movers().await?;
// Trader profile
let profile = client.trader_profile("0x1a1A27de044faFFCCf68E28F03dCfCf5eB3d3cE6").await?;
println!("trades: {}, pnl: {}", profile.trades, profile.total_pnl);
// Trader P&L time series
let pnl = client.trader_pnl("0x1a1A27de044faFFCCf68E28F03dCfCf5eB3d3cE6", Some("30d")).await?;
# Ok(())
# }
```
### RPC
Send JSON-RPC requests through `rpc.polynode.dev`. Transaction submission is optimized for speed. Standard read methods are supported for `latest` state.
```rust,no_run theme={null}
# use polynode::serde_json;
# async fn example(client: &polynode::PolyNodeClient) -> polynode::Result<()> {
// Get current block number
let block = client.rpc_call("eth_blockNumber", serde_json::json!([])).await?;
println!("block: {}", block);
// Get a block
let block_data = client.rpc_call(
"eth_getBlockByNumber",
serde_json::json!(["latest", false]),
).await?;
// Send a raw transaction (optimized delivery)
let tx_hash = client.rpc_call(
"eth_sendRawTransaction",
serde_json::json!(["0xf86c..."]),
).await?;
// Gas price (recommended for fast inclusion)
let gas = client.rpc_call("eth_gasPrice", serde_json::json!([])).await?;
// Standard reads (latest state only)
let balance = client.rpc_call(
"eth_getBalance",
serde_json::json!(["0xabc...", "latest"]),
).await?;
# Ok(())
# }
```
# Rust — Short-Form
Source: https://docs.polynode.dev/sdks/rust/short-form
## Short-Form Markets
Auto-rotating streams for Polymarket's short-form crypto markets (5m, 15m, 1h windows). The SDK discovers the current market window, subscribes to live events, and automatically rotates to the next window at expiry.
```rust,no_run theme={null}
use polynode::{PolyNodeClient, ShortFormInterval, Coin, ShortFormMessage};
#[tokio::main]
async fn main() -> polynode::Result<()> {
let client = PolyNodeClient::new("pn_live_YOUR_KEY")?;
let mut stream = client
.short_form(ShortFormInterval::FifteenMin)
.coins(&[Coin::Btc, Coin::Eth, Coin::Sol])
.rotation_buffer(3)
.start()
.await?;
while let Some(msg) = stream.next().await {
match msg {
ShortFormMessage::Event(event) => {
println!("event: {:?}", event);
}
ShortFormMessage::Rotation(info) => {
println!("--- window rotated ({}) ---", info.interval);
for m in &info.markets {
println!(" {}: beat ${:?} | {:.0}% up | {}s left",
m.coin.id(), m.price_to_beat,
m.up_odds * 100.0, info.time_remaining);
}
}
ShortFormMessage::Error(e) => {
eprintln!("non-fatal: {}", e);
}
}
}
Ok(())
}
```
### Intervals and Coins
```rust,no_run theme={null}
use polynode::{ShortFormInterval, Coin};
# fn example() {
// Intervals
let _ = ShortFormInterval::FiveMin; // 5-minute windows
let _ = ShortFormInterval::FifteenMin; // 15-minute windows
let _ = ShortFormInterval::Hourly; // 1-hour windows
// Supported coins
let all = Coin::all(); // BTC, ETH, SOL, XRP, DOGE, HYPE, BNB
# }
```
# Rust — Source
Source: https://docs.polynode.dev/sdks/rust/source
## Source
[GitHub](https://github.com/joinQuantish/polynode-rs) | [crates.io](https://crates.io/crates/polynode)
# Rust — Testing
Source: https://docs.polynode.dev/sdks/rust/testing
## Testing Utilities
Helpers for integration tests that need active wallet addresses:
```rust,no_run theme={null}
use polynode::testing;
#[tokio::main]
async fn main() {
// Get a single active wallet (from leaderboard or fallback list)
let wallet = testing::get_active_test_wallet(true).await;
println!("test wallet: {}", wallet);
// Get multiple
let wallets = testing::get_active_test_wallets(3, true).await;
for w in &wallets {
println!(" {}", w);
}
}
```
Set `fresh` to `true` to attempt fetching recently active wallets from the Polymarket leaderboard. Falls back to a hardcoded list of known-active addresses.
# Rust — Trading
Source: https://docs.polynode.dev/sdks/rust/trading
## Trading Module
**Polymarket V2 is live.** Set `exchange_version: ExchangeVersion::V2` in your `TraderConfig`. V1 orders submitted after April 28, 2026 are rejected with `order_version_mismatch`. The Rust SDK defaults V2 order attribution to PolyNode's builder code; pass your own code via `TraderConfig.builder_code` or `OrderParams.builder`.
Requires the `trading` feature flag: `polynode = { version = "0.13.11", features = ["trading"] }`
**Deposit wallets supported.** The SDK auto-detects Safe proxy and deposit wallet users. No code changes needed for existing integrations. See the [Deposit Wallets guide](/guides/deposit-wallets).
Place orders on Polymarket with local credential custody and builder attribution. All signing happens locally. Private keys never leave your machine. Supports both the current exchange and the [Polymarket V2 exchange](/guides/v2-migration). See also: [PolyUSD Guide](/guides/polyusd) for V2 collateral wrapping.
### Credential Model
The SDK separates three credentials so users can choose the easiest path without mixing ownership boundaries:
| Credential | Used for | Default |
| ----------------- | ------------------------------------------------------------------- | ------------------------------------------------------------ |
| User CLOB API key | Authenticates `/order`, cancel, open-order, balance-allowance calls | Created or loaded by `ensure_ready()` |
| V2 builder code | Public bytes32 attribution signed into V2 orders | PolyNode builder code via `TraderConfig.builder_code` |
| Relayer auth | Gasless Safe/proxy/deposit-wallet `/submit` calls | PolyNode managed relayer via `polynode_key` + `cosigner_url` |
Orders are always submitted with the user's CLOB API credentials. PolyNode's builder code and relayer path do not make PolyNode the owner of the order.
### Generate a Wallet
```rust,no_run theme={null}
use polynode::trading::PolyNodeTrader;
# fn example() {
let (private_key, address) = PolyNodeTrader::generate_wallet();
println!("address: {}", address);
println!("key: {}", private_key);
// Fund this address with USDC and POL on Polygon before trading
# }
```
### Onboarding
`ensure_ready()` handles the full onboarding flow in one call: derives addresses, checks Safe deployment and approvals, creates or loads CLOB credentials, and stores everything in a local SQLite database.
```rust,no_run theme={null}
use polynode::trading::{ExchangeVersion, PolyNodeTrader, TraderConfig, PrivateKeySigner};
#[tokio::main]
async fn main() -> polynode::Result<()> {
let mut trader = PolyNodeTrader::new(TraderConfig {
polynode_key: "pn_live_YOUR_KEY".into(),
db_path: "./my-trading.db".into(),
exchange_version: ExchangeVersion::V2,
..Default::default()
})?;
let signer = PrivateKeySigner::from_hex("0xdeadbeef...")?;
let status = trader.ensure_ready(Box::new(signer), None).await?;
println!("wallet: {}", status.wallet);
println!("funder: {}", status.funder_address);
println!("safe deployed: {}", status.safe_deployed);
println!("approvals set: {}", status.approvals_set);
println!("actions taken: {:?}", status.actions);
trader.close();
Ok(())
}
```
`ReadyStatus.actions` is the easiest way to inspect what happened. Common values:
| Action | Meaning |
| -------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `credentials_created` / `credentials_loaded` | User CLOB credentials were created or reused locally |
| `relayer_key_provisioned` | The SDK signed a SIWE message and cached a per-user relayer key through PolyNode's cosigner |
| `relayer_key_skipped: ...` | Non-fatal; future relayer calls fall back to builder auth |
| `safe_approvals_submitted: ` | Safe/proxy approvals were submitted through the configured relayer path |
| `deposit_wallet_create_submitted: ` | Deposit wallet creation was submitted through the configured relayer path |
| `deposit_wallet_approval_submitted: ` | Deposit wallet approvals were submitted through the configured relayer path |
### Place an Order
```rust,no_run theme={null}
use polynode::trading::{PolyNodeTrader, TraderConfig, PrivateKeySigner, OrderParams, OrderSide, OrderType};
# async fn example() -> polynode::Result<()> {
let mut trader = PolyNodeTrader::new(TraderConfig {
polynode_key: "pn_live_...".into(),
..Default::default()
})?;
let signer = PrivateKeySigner::from_hex("0x...")?;
trader.ensure_ready(Box::new(signer), None).await?;
let result = trader.order(OrderParams {
token_id: "51037625779056581606819614184446816710505006861008496087735536016411882582167".into(),
side: OrderSide::Buy,
price: 0.55,
size: 100.0,
order_type: OrderType::GTC,
// Optional per-order override. If omitted, TraderConfig.builder_code is used.
// builder: Some("0xYourBuilderCodeBytes32".into()),
..Default::default()
}).await?;
if result.success {
println!("order placed: {:?}", result.order_id);
} else {
println!("order failed: {:?}", result.error);
}
trader.close();
# Ok(())
# }
```
### Order Types
| Type | Behavior |
| ----- | -------------------------------------------------------- |
| `GTC` | Good til canceled (default) |
| `GTD` | Good til date (set `expiration` to a Unix timestamp) |
| `FOK` | Fill or kill, entire order fills or nothing |
| `FAK` | Fill and kill, partial fills allowed, remainder canceled |
### Cancel Orders
```rust,no_run theme={null}
# async fn example(trader: &mut polynode::trading::PolyNodeTrader) -> polynode::Result<()> {
// Cancel a specific order
let result = trader.cancel_order("order_id_here").await?;
println!("canceled: {:?}", result.canceled);
// Cancel all orders
let result = trader.cancel_all(None).await?;
// Cancel all orders for a specific market
let result = trader.cancel_all(Some("condition_id")).await?;
# Ok(())
# }
```
### Query Open Orders
```rust,no_run theme={null}
# async fn example(trader: &mut polynode::trading::PolyNodeTrader) -> polynode::Result<()> {
// All open orders
let orders = trader.get_open_orders(None).await?;
for o in &orders {
println!("{}: {} {} @ {} (matched: {}/{})",
o.id, o.side, o.asset_id, o.price, o.size_matched, o.original_size);
}
// Open orders for a specific market
let orders = trader.get_open_orders(Some("condition_id")).await?;
# Ok(())
# }
```
### Pre-Trade Checks
```rust,no_run theme={null}
# async fn example(trader: &mut polynode::trading::PolyNodeTrader) -> polynode::Result<()> {
// Check token approvals
let approvals = trader.check_approvals(None).await?;
println!("all approved: {}", approvals.all_approved);
// Check USDC and POL balances
let balance = trader.check_balance(None).await?;
println!("USDC: {}, POL: {}", balance.usdc, balance.matic);
# Ok(())
# }
```
### Local Order History
All orders are logged locally in SQLite:
```rust,no_run theme={null}
use polynode::trading::HistoryParams;
# fn example(trader: &mut polynode::trading::PolyNodeTrader) -> polynode::Result<()> {
let history = trader.get_order_history(Some(HistoryParams {
limit: Some(50),
offset: None,
token_id: None,
side: None,
}))?;
for row in &history {
println!("{}: {} {} @ {} — {}", row.token_id, row.side, row.size, row.price, row.status);
}
# Ok(())
# }
```
### Wallet Export and Import
Back up and restore wallet credentials:
```rust,no_run theme={null}
# fn example(trader: &mut polynode::trading::PolyNodeTrader) -> polynode::Result<()> {
// Export
let exported = trader.export_wallet(None)?;
if let Some(ref data) = exported {
let json = serde_json::to_string_pretty(data).unwrap();
std::fs::write("wallet-backup.json", json).unwrap();
}
// Import
let json = std::fs::read_to_string("wallet-backup.json").unwrap();
let data: polynode::trading::WalletExport = serde_json::from_str(&json).unwrap();
trader.import_wallet(data)?;
# Ok(())
# }
```
### Polymarket V2 Exchange
The SDK supports the Polymarket V2 exchange system. Set `exchange_version` in your config to target V2. Defaults to V1 — no existing code is affected.
```rust,no_run theme={null}
use polynode::trading::{PolyNodeTrader, TraderConfig, ExchangeVersion, RelayerMode};
# async fn example() -> polynode::Result<()> {
let mut trader = PolyNodeTrader::new(TraderConfig {
polynode_key: "pn_live_...".into(),
exchange_version: ExchangeVersion::V2,
relayer_mode: RelayerMode::Auto, // default
..Default::default()
})?;
# Ok(())
# }
```
### Builder Attribution and Relayer Modes
Default managed mode:
```rust,no_run theme={null}
use polynode::trading::{ExchangeVersion, PolyNodeTrader, TraderConfig};
# fn example() -> polynode::Result<()> {
let trader = PolyNodeTrader::new(TraderConfig {
polynode_key: "pn_live_...".into(),
exchange_version: ExchangeVersion::V2,
// builder_code defaults to PolyNode's public V2 builder code.
// relayer_mode defaults to Auto: managed relayer when polynode_key is set.
..Default::default()
})?;
# Ok(())
# }
```
Bring your own builder code but still use PolyNode's managed relayer:
```rust,no_run theme={null}
use polynode::trading::{ExchangeVersion, PolyNodeTrader, TraderConfig};
# fn example() -> polynode::Result<()> {
let trader = PolyNodeTrader::new(TraderConfig {
polynode_key: "pn_live_...".into(),
exchange_version: ExchangeVersion::V2,
builder_code: Some("0xYourBuilderCodeBytes32".into()),
..Default::default()
})?;
# Ok(())
# }
```
Bring your own builder credentials for direct Polymarket relayer auth:
```rust,no_run theme={null}
use polynode::trading::{
BuilderCredentials, ExchangeVersion, PolyNodeTrader, RelayerMode, TraderConfig,
};
# fn example() -> polynode::Result<()> {
let trader = PolyNodeTrader::new(TraderConfig {
exchange_version: ExchangeVersion::V2,
relayer_mode: RelayerMode::BuilderCredentials,
builder_code: Some("0xYourBuilderCodeBytes32".into()),
builder_credentials: Some(BuilderCredentials {
key: "builder-api-key".into(),
secret: "builder-secret-base64".into(),
passphrase: "builder-passphrase".into(),
}),
..Default::default()
})?;
# Ok(())
# }
```
`RelayerMode::Auto` chooses the least-friction path:
| Config | Relayer submit behavior |
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `polynode_key` + `cosigner_url` | Submit through PolyNode `/relay`; per-user relayer key is preferred |
| No `polynode_key`, but `builder_credentials` present | Submit directly to Polymarket relayer with those credentials |
| `relayer_mode: RelayerMode::DirectRpc` | Safe/proxy calls sign `execTransaction` and broadcast through `rpc_url`; deposit-wallet factory calls still need a relayer mode |
| Neither configured | Smart-wallet deploy/approve/wrap calls return a clear configuration error |
Direct RPC mode for Safe/proxy calls:
```rust,no_run theme={null}
use polynode::trading::{ExchangeVersion, PolyNodeTrader, RelayerMode, TraderConfig};
# fn example() -> polynode::Result<()> {
let trader = PolyNodeTrader::new(TraderConfig {
exchange_version: ExchangeVersion::V2,
relayer_mode: RelayerMode::DirectRpc,
rpc_url: "https://polygon-bor-rpc.publicnode.com".into(),
..Default::default()
})?;
# Ok(())
# }
```
`DirectRpc` pays gas from the signer EOA and requires a signer with `TradingSigner::sign_hash` support, such as `PrivateKeySigner`; the built-in `PrivySigner` does not currently sign raw transactions. It does not apply to deposit-wallet create/approval envelopes, which route through Polymarket's deposit-wallet factory relayer API.
V2 uses PolyUSD as collateral instead of USDC.e. The SDK provides helper methods for wrapping and balance checking:
```rust,no_run theme={null}
# async fn example(trader: &mut polynode::trading::PolyNodeTrader) -> polynode::Result<()> {
// Wrap USDC.e → PolyUSD (amount in raw units, 6 decimals)
let tx_hash = trader.wrap_to_polyusd(1_000_000).await?; // 1 USDC
// Unwrap PolyUSD → USDC.e
let tx_hash = trader.unwrap_from_polyusd(1_000_000).await?;
// Check balances
let polyusd = trader.get_polyusd_balance().await?;
let usdce = trader.get_usdce_balance().await?;
# Ok(())
# }
```
Order placement, cancellation, and all other trading methods work identically on V2. See the [V2 Migration Guide](/guides/v2-migration) and [PolyUSD Guide](/guides/polyusd) for full details.
### Custom Signers
Implement the `TradingSigner` trait for HSM, KMS, or other signing backends:
```rust,no_run theme={null}
use polynode::trading::{TradingSigner, Address, async_trait};
struct MyHsmSigner { /* ... */ }
#[async_trait]
impl TradingSigner for MyHsmSigner {
fn address(&self) -> Address {
// Return the EOA address
todo!()
}
async fn sign_typed_data(
&self,
payload: &polynode::trading::Eip712Payload,
) -> polynode::Result> {
// Sign EIP-712 typed data, return 65-byte signature
todo!()
}
async fn sign_message(&self, message: &[u8]) -> polynode::Result> {
// Sign raw message (personal_sign), return 65-byte signature
todo!()
}
}
```
### Fee Escrow
Charge per-order fees with on-chain escrow. Fees are pulled before the order, distributed on fill, and refunded on cancel. See the [Fee Escrow Guide](/guides/fee-escrow) for the full architecture and security model.
```rust theme={null}
use polynode::trading::{PolyNodeTrader, TraderConfig, FeeConfig, OrderParams, OrderSide, OrderType};
let mut trader = PolyNodeTrader::new(TraderConfig {
polynode_key: "pn_live_...".into(),
fee_config: Some(FeeConfig {
fee_bps: 50, // 0.5% fee on every order
affiliate: Some("0xYourWallet...".into()), // REQUIRED: your wallet receives the fee
affiliate_share_bps: None, // default: you keep 100%
}),
..Default::default()
})?;
let result = trader.order(OrderParams {
token_id: "...".into(),
side: OrderSide::Buy,
price: 0.55,
size: 100.0,
order_type: OrderType::GTC,
// fee_config: None uses the trader's global fee_config (via Default)
..Default::default()
}).await?;
println!("Fee TX: {:?}", result.fee_escrow_tx_hash);
println!("Fee: {:?} USDC", result.fee_amount);
// Cancel → fee is automatically refunded
trader.cancel_order("order-id").await?;
```
Set `fee_bps: 0` or omit `fee_config` to skip the escrow entirely. Per-order overrides via `OrderParams.fee_config`.
### Privy Signer
Requires the `privy` feature flag: `polynode = { version = "0.13.11", features = ["trading", "privy"] }`
Sign orders with a Privy server-side wallet. No private key needed on your machine. All signing happens via Privy's HTTP API.
```rust,no_run theme={null}
use polynode::trading::{PolyNodeTrader, TraderConfig};
use polynode::trading::privy::{PrivyConfig, PrivySigner};
#[tokio::main]
async fn main() -> polynode::Result<()> {
let config = PrivyConfig {
app_id: "your-privy-app-id".into(),
app_secret: "your-privy-app-secret".into(),
authorization_key: "wallet-auth:your-authorization-key".into(),
};
// Or load from PRIVY_APP_ID, PRIVY_APP_SECRET, PRIVY_AUTHORIZATION_KEY env vars:
// let config = PrivyConfig::from_env()?;
let signer = PrivySigner::new(
config,
"privy-wallet-id".into(),
"0xYourWalletAddress".parse().unwrap(),
);
let mut trader = PolyNodeTrader::new(TraderConfig {
polynode_key: "pn_live_...".into(),
..Default::default()
})?;
let status = trader.ensure_ready(Box::new(signer), None).await?;
println!("ready: {}", status.credentials_stored);
trader.close();
Ok(())
}
```
The `PrivySigner` implements `TradingSigner`, so it works with `ensure_ready()`, `order()`, and all other trading methods. Get your Privy credentials from the [Privy Dashboard](https://dashboard.privy.io).
### TraderConfig
```rust,no_run theme={null}
use polynode::trading::{TraderConfig, SignatureType, ExchangeVersion, RelayerMode};
# fn example() {
let config = TraderConfig {
polynode_key: "pn_live_...".into(),
db_path: "./my-trading.db".into(), // local SQLite for credentials + history
cosigner_url: "https://trade.polynode.dev".into(), // default
fallback_direct: true, // submit directly if cosigner is down
default_signature_type: SignatureType::PolyGnosisSafe, // default
rpc_url: "https://polygon-bor-rpc.publicnode.com".into(), // default; for on-chain reads
builder_code: Some("0xYourBuilderCodeBytes32".into()), // V2 default order attribution
relayer_mode: RelayerMode::Auto, // managed when polynode_key is set
exchange_version: ExchangeVersion::V1, // default; set to V2 for the Polymarket V2 exchange
builder_credentials: None, // optional direct relayer fallback/override
fee_config: None, // optional fee escrow config
};
# }
```
# Rust — WebSocket
Source: https://docs.polynode.dev/sdks/rust/websocket
## WebSocket Streaming
Subscribe to real-time events with 3-5 second pre-confirmation lead time on settlements.
### Connect and Subscribe
```rust,no_run theme={null}
use polynode::{PolyNodeClient, ws::{StreamOptions, Subscription, SubscriptionType}};
use polynode::ws_messages::WsMessage;
#[tokio::main]
async fn main() -> polynode::Result<()> {
let client = PolyNodeClient::new("pn_live_YOUR_KEY")?;
let mut stream = client.stream(StreamOptions {
compress: true,
auto_reconnect: true,
..Default::default()
}).await?;
// Subscribe to settlements with a minimum size filter
let sub = Subscription::new(SubscriptionType::Settlements)
.min_size(100.0)
.status("pending")
.snapshot_count(20);
stream.subscribe(sub).await?;
// Read events
while let Some(msg) = stream.next().await {
match msg {
Ok(WsMessage::Event(event)) => {
println!("{:?}", event);
}
Ok(WsMessage::Snapshot(events)) => {
println!("snapshot: {} events", events.len());
}
Ok(WsMessage::Subscribed { subscription_id, .. }) => {
println!("subscribed: {}", subscription_id);
}
Ok(WsMessage::PriceFeed(feed)) => {
// Chainlink price feed update
println!("{:?}", feed);
}
Ok(WsMessage::Heartbeat { ts }) => {
// Connection alive
}
Err(e) => eprintln!("error: {}", e),
_ => {}
}
}
Ok(())
}
```
### Subscription Types
```rust,no_run theme={null}
use polynode::ws::{Subscription, SubscriptionType};
# fn example() {
Subscription::new(SubscriptionType::Settlements); // pending + confirmed settlements
Subscription::new(SubscriptionType::Trades); // all trade activity
Subscription::new(SubscriptionType::Prices); // price-moving events
Subscription::new(SubscriptionType::Blocks); // new Polygon blocks
Subscription::new(SubscriptionType::Wallets); // all wallet activity
Subscription::new(SubscriptionType::Markets); // all market activity
Subscription::new(SubscriptionType::LargeTrades); // $1K+ trades
Subscription::new(SubscriptionType::Oracle); // UMA resolution events
Subscription::new(SubscriptionType::Chainlink); // real-time price feeds
Subscription::new(SubscriptionType::Global); // everything
# }
```
### Subscription Filters
All filters from the [Subscriptions & Filters](/websocket/subscribing) page are supported via builder methods:
```rust,no_run theme={null}
use polynode::ws::{Subscription, SubscriptionType};
# fn example() {
let sub = Subscription::new(SubscriptionType::Settlements)
.wallets(vec!["0xabc...".into()]) // filter by wallet
.tokens(vec!["21742633...".into()]) // filter by token ID
.slugs(vec!["bitcoin-100k".into()]) // filter by market slug
.condition_ids(vec!["0xabc...".into()]) // filter by condition ID
.side("BUY") // BUY or SELL
.status("pending") // pending, confirmed, or all
.min_size(100.0) // minimum USD size
.max_size(10000.0) // maximum USD size
.event_types(vec!["settlement".into()]) // override event types
.snapshot_count(50) // initial snapshot (max 200)
.feeds(vec!["BTC/USD".into()]); // chainlink feeds
# }
```
### Multiple Subscriptions
Subscriptions stack on the same connection. Events are deduplicated server-side:
```rust,no_run theme={null}
# async fn example(stream: &polynode::ws::WsStream) -> polynode::Result<()> {
use polynode::ws::{Subscription, SubscriptionType};
// Whale trades
stream.subscribe(
Subscription::new(SubscriptionType::LargeTrades).min_size(5000.0)
).await?;
// Specific wallet activity
stream.subscribe(
Subscription::new(SubscriptionType::Wallets)
.wallets(vec!["0xabc...".into()])
).await?;
# Ok(())
# }
```
### Auto-Reconnect
Enabled by default. The SDK reconnects with exponential backoff and replays all active subscriptions automatically:
```rust,no_run theme={null}
use polynode::ws::StreamOptions;
use std::time::Duration;
# fn example() {
let options = StreamOptions {
compress: true,
auto_reconnect: true,
max_reconnect_attempts: None, // unlimited by default
initial_backoff: Duration::from_secs(1),
max_backoff: Duration::from_secs(30),
};
# }
```
### Cleanup
```rust,no_run theme={null}
# async fn example(stream: polynode::ws::WsStream) -> polynode::Result<()> {
// Unsubscribe from a specific subscription
stream.unsubscribe(Some("sub_id_here".into())).await?;
// Unsubscribe from all
stream.unsubscribe(None).await?;
// Close the connection
stream.close().await?;
# Ok(())
# }
```
# Short-Form Markets
Source: https://docs.polynode.dev/sdks/short-form
Auto-rotating streams for 5-minute, 15-minute, and hourly crypto prediction markets
Short-form markets are Polymarket's Chainlink-based crypto prediction markets that rotate on fixed intervals. Bitcoin, Ethereum, Solana, XRP, Dogecoin, Hyperliquid, and BNB each have active 5-minute, 15-minute, and 1-hour "Up or Down" markets running continuously.
The SDK handles everything: discovering the current window's markets, subscribing for real-time events, and automatically rotating to the next window when the current one expires. You get enriched market data including the **price-to-beat** (Chainlink opening price), live odds, liquidity, and volume with zero additional latency.
New market every 300 seconds. 7 coins.
New market every 900 seconds. 7 coins.
New market every 3600 seconds. 7 coins.
## Quick Start
```typescript theme={null}
import { PolyNodeWS } from 'polynode-sdk';
const ws = new PolyNodeWS('pn_live_...', 'wss://ws.polynode.dev/ws');
const stream = ws.shortForm('15m', { coins: ['btc', 'eth', 'sol'] });
stream.on('rotation', (r) => {
console.log(`New window: ${r.timeRemaining}s remaining`);
for (const m of r.markets) {
console.log(`${m.coin}: beat $${m.priceToBeat} | ${(m.upOdds * 100).toFixed(0)}% up`);
}
});
stream.on('settlement', (event) => {
console.log(`${event.outcome} $${event.taker_size} on ${event.market_title}`);
});
```
```rust theme={null}
use polynode::{PolyNodeClient, ShortFormInterval, ShortFormMessage, Coin};
let client = PolyNodeClient::new("pn_live_...")?;
let mut stream = client
.short_form(ShortFormInterval::FifteenMin)
.coins(&[Coin::Btc, Coin::Eth, Coin::Sol])
.start()
.await?;
while let Some(msg) = stream.next().await {
match msg {
ShortFormMessage::Rotation(r) => {
println!("New window: {}s remaining", r.time_remaining);
for m in &r.markets {
println!("{}: beat ${:?} | {:.0}% up",
m.coin.id(), m.price_to_beat, m.up_odds * 100.0);
}
}
ShortFormMessage::Event(e) => println!("{:?}", e),
ShortFormMessage::Error(e) => eprintln!("{}", e),
}
}
```
## Live Output
Here's actual output from a live 15-minute stream with BTC, ETH, and SOL:
```json Rotation Event theme={null}
{
"interval": "15m",
"timeRemaining": 51,
"windowStart": 1774072800,
"windowEnd": 1774073700,
"marketCount": 3
}
```
Each market in the rotation includes enrichment data at zero cost:
```json BTC Market theme={null}
{
"coin": "btc",
"slug": "btc-updown-15m-1774072800",
"title": "Bitcoin Up or Down - March 21, 2:00AM-2:15AM ET",
"conditionId": "0x037ad8cf0b1d7dc7febd2a46c400398838163dc3...",
"clobTokenIds": ["11496172...", "66255671..."],
"upOdds": 0.845,
"downOdds": 0.155,
"priceToBeat": 70694.18,
"liquidity": 9833.25,
"volume24h": 51679.05
}
```
```json ETH Market theme={null}
{
"coin": "eth",
"slug": "eth-updown-15m-1774072800",
"title": "Ethereum Up or Down - March 21, 2:00AM-2:15AM ET",
"upOdds": 0.365,
"downOdds": 0.635,
"priceToBeat": 2152.93,
"liquidity": 2289.0,
"volume24h": 14602.61
}
```
```json SOL Market theme={null}
{
"coin": "sol",
"slug": "sol-updown-15m-1774072800",
"title": "Solana Up or Down - March 21, 2:00AM-2:15AM ET",
"upOdds": 0.955,
"downOdds": 0.045,
"priceToBeat": 89.97,
"liquidity": 3004.32,
"volume24h": 4953.08
}
```
Settlement events flow in real-time as trades happen. Here's a real settlement captured from the live stream:
```json Settlement Event theme={null}
{
"event_type": "settlement",
"status": "pending",
"event_slug": "btc-updown-15m-1774073700",
"event_title": "Bitcoin Up or Down - March 21, 2:15AM-2:30AM ET",
"market_slug": "btc-updown-15m-1774073700",
"market_title": "Bitcoin Up or Down - March 21, 2:15AM-2:30AM ET",
"outcome": "Down",
"taker_side": "SELL",
"taker_size": 7.58,
"taker_price": 0.33,
"taker_wallet": "0xe49b2c1ca08c4aef70c061ef9cfcabbcea638973",
"taker_base_fee": 1000,
"tick_size": 0.01,
"neg_risk": false,
"condition_id": "0x037ad8cf0b1d7dc7febd2a46c400398838163dc3...",
"detected_at": 1774074055797,
"tx_hash": "0x726b5ba5d7c48d277f7923a9482a3f7f1fcfac36...",
"outcomes": ["Up", "Down"],
"trades": [
{
"maker": "0xd44e29936409019f93993de8bd603ef6cb1bb15e",
"taker": "0xe49b2c1ca08c4aef70c061ef9cfcabbcea638973",
"outcome": "Down",
"side": "BUY",
"price": 0.37,
"size": 5,
"maker_amount": "1850000",
"taker_amount": "5000000"
}
]
}
```
In a 25-second test, the 15-minute stream received **157 settlement events** across 3 coins. The 5-minute stream received **323 events** across 2 coins in 20 seconds. These markets are very active.
## Intervals
| Interval | Code | Window | Slug Pattern |
| ---------- | ---------------------- | ------ | ----------------------------------------- |
| 5 minutes | `'5m'` / `FiveMin` | 300s | `btc-updown-5m-{timestamp}` |
| 15 minutes | `'15m'` / `FifteenMin` | 900s | `btc-updown-15m-{timestamp}` |
| 1 hour | `'1h'` / `Hourly` | 3600s | `bitcoin-up-or-down-march-21-2026-2am-et` |
### Slug Calculator
Pick a coin and interval to see the current market slug. Updates live with a countdown to the next window.
The slug pattern is deterministic: `{coin}-updown-{interval}-{windowTimestamp}`. Compute it yourself:
```javascript theme={null}
const now = Math.floor(Date.now() / 1000);
const window5m = Math.floor(now / 300) * 300;
const window15m = Math.floor(now / 900) * 900;
console.log(`btc-updown-5m-${window5m}`); // current BTC 5m slug
console.log(`eth-updown-15m-${window15m}`); // current ETH 15m slug
```
You don't need to compute slugs manually. The SDK's `shortForm()` handles discovery automatically and gives you the `slug`, `conditionId`, and `clobTokenIds` on every rotation event.
## Supported Coins
All 7 Chainlink-tracked coins are supported:
| Coin | ID | Symbol |
| ----------- | --------------------- | ------ |
| Bitcoin | `btc` / `Coin::Btc` | BTC |
| Ethereum | `eth` / `Coin::Eth` | ETH |
| Solana | `sol` / `Coin::Sol` | SOL |
| XRP | `xrp` / `Coin::Xrp` | XRP |
| Dogecoin | `doge` / `Coin::Doge` | DOGE |
| Hyperliquid | `hype` / `Coin::Hype` | HYPE |
| BNB | `bnb` / `Coin::Bnb` | BNB |
If no coins are specified, all 7 are subscribed.
## Market Enrichments
Every `ShortFormMarket` includes these fields, all populated during discovery with zero hot-path cost:
Coin identifier (`'btc'`, `'eth'`, `'sol'`, etc.).
Market URL slug (e.g., `'btc-updown-15m-1774072800'`).
Human-readable market title (e.g., "Bitcoin Up or Down - March 21, 2:00AM-2:15AM ET").
The Polymarket condition ID. Use this to subscribe via the main WebSocket with `condition_ids` filters.
The two CLOB token IDs (Up and Down outcomes). Pass these directly to `OrderbookEngine.subscribe()` to get real-time orderbook data for this market. See [Orderbook + Short-Form](#orderbook--short-form) below.
Outcome labels, always `["Up", "Down"]`.
Raw outcome prices from the CLOB, matching the `outcomes` array order.
Unix timestamp (seconds) when this market window opened.
Unix timestamp (seconds) when this market window closes.
The Chainlink opening price for this window. For "Up" to win, the closing price must exceed this value. Fetched once per rotation via the Polymarket crypto-price API.
Probability (0-1) that "Up" wins, derived from the market's outcome prices. For example, `0.845` means the market prices an 84.5% chance of Up.
Probability (0-1) that "Down" wins. Always `1 - upOdds`.
Market liquidity in USD, from the Gamma API response. No extra call.
24-hour trading volume in USD across all windows for this coin's series. No extra call.
Seconds remaining in the current window. Computed client-side (zero cost). Available on the `rotation` event and as a live getter on the stream.
None of these enrichments touch the event hot path. `priceToBeat` is one HTTP call per coin per rotation (e.g., 7 calls every 15 minutes). Everything else is parsed from data already fetched during discovery or computed locally.
## Events
### rotation
Emitted once per window when new markets are discovered and subscribed.
```typescript theme={null}
stream.on('rotation', (r) => {
console.log(r.interval); // '15m'
console.log(r.timeRemaining); // 847
console.log(r.markets.length); // 7
for (const m of r.markets) {
console.log(m.coin, m.priceToBeat, m.upOdds);
}
});
```
```rust theme={null}
ShortFormMessage::Rotation(r) => {
println!("{}s left, {} markets", r.time_remaining, r.markets.len());
for m in &r.markets {
println!("{}: ${:?}, {:.0}% up",
m.coin.id(), m.price_to_beat, m.up_odds * 100.0);
}
}
```
### settlement
The primary event. Fired when a trade is detected on any subscribed short-form market. Includes the full settlement payload with market metadata, wallet addresses, trade size, and outcome.
```typescript theme={null}
stream.on('settlement', (event) => {
console.log(event.market_slug); // 'btc-updown-15m-1774072800'
console.log(event.outcome); // 'Up' or 'Down'
console.log(event.taker_size); // 12.5
console.log(event.status); // 'pending' or 'confirmed'
});
```
```rust theme={null}
ShortFormMessage::Event(PolyNodeEvent::Settlement(s)) => {
println!("{} {} ${:.2}",
s.outcome.as_deref().unwrap_or("?"),
s.taker_side,
s.taker_size);
}
```
### error
Non-fatal errors (e.g., a coin's market not found). The stream continues operating.
```typescript theme={null}
stream.on('error', (err) => {
console.error(err.message);
});
```
```rust theme={null}
ShortFormMessage::Error(msg) => {
eprintln!("Warning: {}", msg);
}
```
## Time Remaining
In TypeScript, `stream.timeRemaining` is a live getter that always returns the current seconds remaining:
```typescript theme={null}
// Check anytime — no network call, pure local math
setInterval(() => {
console.log(`${stream.timeRemaining}s until next rotation`);
}, 1000);
```
In Rust, `time_remaining` is provided on each `RotationInfo`. Compute it manually between rotations:
```rust theme={null}
let remaining = rotation.window_end - (SystemTime::now()
.duration_since(UNIX_EPOCH).unwrap().as_secs() as i64);
```
## Options
```typescript theme={null}
const stream = ws.shortForm('15m', {
coins: ['btc', 'eth'], // default: all 7
apiBaseUrl: 'https://api.polynode.dev', // default
rotationBuffer: 3, // seconds after window end before discovering next
});
```
```rust theme={null}
let stream = client
.short_form(ShortFormInterval::FifteenMin)
.coins(&[Coin::Btc, Coin::Eth]) // default: all 7
.rotation_buffer(3) // seconds, default: 3
.start()
.await?;
```
## How It Works
On start and at each rotation, the SDK fetches market data from the Gamma API proxy. For 5m and 15m markets, it constructs deterministic slugs like `btc-updown-15m-{timestamp}`. For hourly markets, it queries by series. All 7 coins are fetched in parallel.
In parallel with discovery, the SDK fetches the Chainlink opening price (price-to-beat) for each coin. Odds, liquidity, and volume are parsed from the existing Gamma response. No extra network calls for these.
The SDK subscribes to settlements with slug filters for all discovered markets on the existing WebSocket connection. Events start flowing immediately.
A timer fires at `windowEnd + 3 seconds`. The SDK unsubscribes, re-discovers the next window's markets, and resubscribes. A `rotation` event is emitted with the new market data.
## Cleanup
```typescript theme={null}
stream.stop(); // cancel timer, unsubscribe
ws.disconnect(); // close WebSocket
```
```rust theme={null}
stream.stop().await; // cancel task, close WS
```
## Full Example: Price Tracker
A complete example that tracks all 7 coins on 5-minute windows and logs the price-to-beat vs current odds:
```typescript theme={null}
import { PolyNodeWS } from 'polynode-sdk';
const ws = new PolyNodeWS('pn_live_...', 'wss://ws.polynode.dev/ws');
const stream = ws.shortForm('5m');
stream.on('rotation', (r) => {
console.log(`\n--- New 5m window | ${r.timeRemaining}s remaining ---`);
for (const m of r.markets) {
const dir = m.upOdds > 0.5 ? 'UP' : 'DOWN';
const pct = Math.max(m.upOdds, m.downOdds) * 100;
console.log(` ${m.coin.toUpperCase().padEnd(5)} $${m.priceToBeat?.toFixed(2) ?? '?'} → ${dir} ${pct.toFixed(0)}% | $${m.liquidity.toFixed(0)} liq`);
}
});
stream.on('settlement', (e) => {
console.log(` Trade: ${e.outcome} $${e.taker_size?.toFixed(2)} on ${e.event_slug}`);
});
// Countdown
setInterval(() => {
process.stdout.write(`\r ${stream.timeRemaining}s remaining `);
}, 1000);
```
```rust theme={null}
use polynode::{PolyNodeClient, ShortFormInterval, ShortFormMessage, Coin};
#[tokio::main]
async fn main() -> polynode::Result<()> {
let client = PolyNodeClient::new("pn_live_...")?;
let mut stream = client
.short_form(ShortFormInterval::FiveMin)
.start()
.await?;
while let Some(msg) = stream.next().await {
match msg {
ShortFormMessage::Rotation(r) => {
println!("\n--- New 5m window | {}s remaining ---", r.time_remaining);
for m in &r.markets {
println!(" {} ${:.2} → {:.0}% up | ${:.0} liq",
m.coin.id(),
m.price_to_beat.unwrap_or(0.0),
m.up_odds * 100.0,
m.liquidity);
}
}
ShortFormMessage::Event(e) => {
// process settlement events
}
ShortFormMessage::Error(e) => eprintln!(" warn: {}", e),
}
}
Ok(())
}
```
### Live Output (all 7 coins, 5m)
```
--- New 5m window | 193s remaining ---
BTC $70697.94 → DOWN 53% | $23248 liq
ETH $2153.39 → DOWN 51% | $30702 liq
BNB $642.23 → DOWN 55% | $9798 liq
HYPE $39.34 → DOWN 50% | $419 liq
XRP $1.45 → DOWN 51% | $10924 liq
DOGE $0.09 → DOWN 59% | $9800 liq
SOL $89.97 → UP 51% | $10977 liq
Trade: Down $30.00 on btc-updown-5m-1774074300
Trade: Down $30.00 on btc-updown-5m-1774074300
Trade: Down $15.00 on btc-updown-5m-1774074300
--- 397 trades in 15s | 179s remaining ---
```
### Live Output (Rust, all 3 intervals)
```
=== Testing 5m ===
ROTATION: 5m | 249s remaining | 2 markets
btc | btc-updown-5m-1774074300 | beat $70697.94 | 48% up | $23248 liq
eth | eth-updown-5m-1774074300 | beat $2153.39 | 50% up | $30702 liq
Events in 10s: 388
=== Testing 15m ===
ROTATION: 15m | 239s remaining | 2 markets
btc | btc-updown-15m-1774073700 | beat $70714.31 | 68% up | $9883 liq
eth | eth-updown-15m-1774073700 | beat $2152.36 | 88% up | $3028 liq
Events in 10s: 73
=== Testing 1h ===
ROTATION: 1h | 2029s remaining | 2 markets
btc | bitcoin-up-or-down-march-21-2026-2am-et | beat $70697.94 | 70% up | $10652 liq
eth | ethereum-up-or-down-march-21-2026-2am-et | beat $2153.39 | 52% up | $7162 liq
Events in 10s: 20
```
## Connect the Orderbook to Crypto Markets
**Want depth data for crypto markets?** Every `ShortFormMarket` in the rotation event includes `clobTokenIds` — the token IDs for both Up and Down outcomes. Pass them straight to `OrderbookEngine` and you get live orderbook depth, bid/ask spreads, and price moves alongside your settlement stream. One setup, three real-time feeds.
Here's the full pattern: short-form discovers markets, hands the token IDs to the orderbook engine, and both streams run in parallel.
```typescript theme={null}
import { PolyNodeWS, OrderbookEngine } from 'polynode-sdk';
const KEY = 'pn_live_...';
const ws = new PolyNodeWS(KEY, 'wss://ws.polynode.dev/ws');
const engine = new OrderbookEngine({ apiKey: KEY, compress: true });
// 1. Start the short-form stream
const stream = ws.shortForm('5m', { coins: ['btc', 'eth'] });
// 2. On each rotation, subscribe the orderbook to the new markets
stream.on('rotation', async (r) => {
const tokenIds = r.markets.flatMap(m => m.clobTokenIds);
await engine.subscribe(tokenIds);
for (const m of r.markets) {
console.log(`${m.coin}: beat $${m.priceToBeat} | ${(m.upOdds * 100).toFixed(0)}% up`);
}
});
// 3. Orderbook events
engine.on('ready', () => console.log(`${engine.size} books loaded`));
engine.on('price', (p) => {
for (const a of p.assets) {
console.log(`Book: ${a.outcome} = ${a.price}`);
}
});
// 4. Settlement events (trades happening on these markets)
stream.on('settlement', (e) => {
console.log(`Trade: ${e.outcome} $${e.taker_size} on ${e.event_slug}`);
});
```
```rust theme={null}
use polynode::{PolyNodeClient, ShortFormInterval, ShortFormMessage, OrderbookEngine, Coin};
let client = PolyNodeClient::new("pn_live_...")?;
let mut engine = OrderbookEngine::new("pn_live_...", true)?;
let mut stream = client
.short_form(ShortFormInterval::FiveMin)
.coins(&[Coin::Btc, Coin::Eth])
.start()
.await?;
while let Some(msg) = stream.next().await {
match msg {
ShortFormMessage::Rotation(r) => {
// Pass token IDs from the rotation to the orderbook engine
let token_ids: Vec<&str> = r.markets.iter()
.flat_map(|m| m.clob_token_ids.iter().map(|s| s.as_str()))
.collect();
engine.subscribe(&token_ids).await?;
for m in &r.markets {
println!("{}: beat ${:?} | {:.0}% up",
m.coin.id(), m.price_to_beat, m.up_odds * 100.0);
}
}
ShortFormMessage::Event(e) => { /* handle settlements */ }
ShortFormMessage::Error(e) => eprintln!("{}", e),
}
}
```
This gives you three real-time feeds from one setup: **settlements** (trade flow), **orderbook** (depth and price moves), and **rotation enrichments** (odds, liquidity, price-to-beat). Add a `chainlink` subscription on the same WebSocket connection for the underlying asset price too.
### What you get
| Stream | Source | Data |
| ------------ | ------------------------- | ----------------------------------------------------------- |
| Settlements | `stream.on('settlement')` | Every trade on the crypto market: size, price, side, wallet |
| Orderbook | `engine.on('price')` | Bid/ask depth, price changes, spread |
| Market info | `stream.on('rotation')` | Price-to-beat, odds, liquidity, volume, time remaining |
| Crypto price | Chainlink subscription | Underlying BTC/ETH/SOL price at \~1/sec |
# Trading
Source: https://docs.polynode.dev/sdks/trading
Place orders on Polymarket through the polynode SDK with local credential custody and gasless onboarding.
**Polymarket V2 cutover: April 28, 2026 at \~11am UTC.** Set `exchangeVersion: "v2"` (TypeScript) / `ExchangeVersion::V2` (Rust) / `ExchangeVersion.V2` (Python) in your `TraderConfig` before cutover. V1 orders submitted after April 28 will be rejected with `order_version_mismatch`. Pass your V2 builder code (mint at [polymarket.com/settings?tab=builder](https://polymarket.com/settings?tab=builder)) via the `builder` field on each order. See the [V2 Migration Guide](/guides/v2-migration) for the full rundown.
**Deposit wallets supported.** Polymarket is rolling out deposit wallets as a new account type for new users. The SDK handles both Safe and deposit wallet users automatically — no code changes needed. See the [Deposit Wallets guide](/guides/deposit-wallets) for details.
## Overview
The trading module lets you place and manage orders on Polymarket. One function call handles wallet setup, credential creation, approvals, and order placement. You don't need to understand Polymarket's wallet types, contract addresses, or signing protocols. The SDK auto-detects whether a user has a Safe proxy, deposit wallet, or no wallet at all, and handles each case transparently.
```bash theme={null}
npm install polynode-sdk viem better-sqlite3 @polymarket/clob-client @polymarket/builder-relayer-client @polymarket/builder-signing-sdk
```
V2 order placement requires `polynode-sdk >= 0.9.0`. Deposit wallet support (address derivation, wallet detection) requires `>= 0.10.0`. Deposit wallet order signing (POLY\_1271) requires `>= 0.10.5`.
Your project must use ESM imports (`import` syntax). Add `"type": "module"` to your `package.json`, or use `.mjs` file extensions.
## Quick Start
Pick the path that matches your situation.
### I'm building a backend service (private keys on your server)
Generate wallets, manage them yourself. This is the simplest path.
```typescript theme={null}
import { PolyNodeTrader } from 'polynode-sdk';
const trader = new PolyNodeTrader({ polynodeKey: 'pn_live_...' });
// 1. Generate a wallet (or use an existing private key)
const { privateKey, address } = await PolyNodeTrader.generateWallet();
// SAVE THIS KEY. It is not stored anywhere. If you lose it, you lose access.
// 2. One call sets up everything: deploys a Safe, sets approvals, creates credentials.
// Completely gasless. No MATIC needed. Takes ~10 seconds for a new wallet.
const status = await trader.ensureReady(privateKey);
// 3. Send USDC.e (Polygon) to the funder address. This is your trading wallet.
console.log('Send USDC.e here:', status.funderAddress);
// 4. Place an order
const result = await trader.order({
tokenId: '...', // Get token IDs from /v1/events/search or /v1/crypto/active
side: 'BUY',
price: 0.55,
size: 100,
});
// 5. Cancel it
if (result.orderId) {
await trader.cancelOrder(result.orderId);
}
```
`ensureReady()` is idempotent. Call it every time your service starts. If the wallet is already set up, it returns instantly from the local database.
### I'm building a platform with Privy (server wallets for each user)
Use `createPrivySigner` to trade with Privy-managed wallets. Each user gets their own wallet without touching private keys.
```typescript theme={null}
import { PolyNodeTrader, createPrivySigner, createPrivyClient } from 'polynode-sdk';
// Set up Privy (one client for your whole app)
// createPrivyClient handles the wallet API config for you.
const privy = createPrivyClient({
appId: process.env.PRIVY_APP_ID!,
appSecret: process.env.PRIVY_APP_SECRET!,
authorizationKey: process.env.PRIVY_AUTHORIZATION_KEY!,
});
// Create a signer for a specific user's wallet
const signer = createPrivySigner(privy, user.privyWalletId, user.walletAddress);
const trader = new PolyNodeTrader({ polynodeKey: 'pn_live_...' });
// Set up the wallet — gasless, ~10 seconds for new users
const status = await trader.ensureReady(signer);
// Send USDC.e to status.funderAddress, then trade
const result = await trader.order({ tokenId: '...', side: 'BUY', price: 0.55, size: 100 });
```
**Privy setup requirements:**
1. Create a Privy app at [dashboard.privy.io](https://dashboard.privy.io)
2. Enable **Server wallets** under Wallet infrastructure
3. Generate an **Authorization keypair** (same section) — this is `PRIVY_AUTHORIZATION_KEY`
4. Install: `npm install @privy-io/server-auth` (the SDK imports it automatically via `createPrivyClient`)
If you enable Privy gas sponsorship in your dashboard, all on-chain operations (approvals, deploys) are free for your users.
### I already have a Polymarket account (exported key or existing credentials)
If you export your private key from Polymarket, the SDK auto-detects your wallet type.
```typescript theme={null}
const trader = new PolyNodeTrader({ polynodeKey: 'pn_live_...' });
const status = await trader.ensureReady('0xYOUR_EXPORTED_KEY');
// Your USDC.e is already in the right place. Start trading.
const balance = await trader.checkBalance();
console.log(`USDC.e: $${balance.usdc}`);
```
If you already have CLOB credentials (from Polymarket or Dome), import them directly:
```typescript theme={null}
trader.linkCredentials({
wallet: '0xYOUR_EOA',
apiKey: 'your-clob-api-key',
apiSecret: 'your-clob-api-secret',
apiPassphrase: 'your-clob-passphrase',
signatureType: 2,
funderAddress: '0xYOUR_SAFE',
});
await trader.linkWallet('0xYOUR_PRIVATE_KEY');
```
## Where to Send USDC.e
Polymarket uses **USDC.e** (bridged USDC) on Polygon, NOT native USDC. These are different tokens.
* **USDC.e (correct):** `0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174`
* **Native USDC (wrong):** `0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359`
If you accidentally send native USDC, the tokens are still in your wallet (recoverable), but they won't work as a trading balance on Polymarket.
After `ensureReady()`, send USDC.e to `status.funderAddress`. This is the address that holds your trading balance.
| Wallet type | `funderAddress` is... |
| -------------- | --------------------------------------------------------------------- |
| Safe (default) | The Safe contract address (different from your private key's address) |
| EOA | The wallet address itself |
Do not send USDC.e to the EOA address if you're using a Safe wallet. It will sit there unused.
## Finding Markets
You need a `tokenId` to place orders. Here's how to find one:
```typescript theme={null}
import { PolyNode } from 'polynode-sdk';
const pn = new PolyNode({ apiKey: 'pn_live_...' });
// Search for markets
const results = await pn.searchEvents('Bitcoin', { limit: 5 });
for (const event of results.events) {
for (const market of event.markets) {
console.log(market.question, '→', market.tokenId);
}
}
```
Or use the REST API directly: `GET /v1/events/search?q=bitcoin&limit=5` or `GET /v1/crypto/active` for live crypto markets.
## Configuration
```typescript theme={null}
const trader = new PolyNodeTrader({
polynodeKey: 'pn_live_...', // Your polynode API key
dbPath: './my-trading.db', // Local SQLite path (default: ./polynode-trading.db)
cosignerUrl: 'https://trade.polynode.dev', // Relay URL (default)
fallbackDirect: true, // Fall back to direct CLOB if relay is down (default: true)
rpcUrl: 'https://polygon-bor-rpc.publicnode.com', // RPC for on-chain reads (default)
builderCredentials: { // Optional: your own Polymarket builder credentials
key: process.env.POLY_BUILDER_API_KEY!,
secret: process.env.POLY_BUILDER_SECRET!,
passphrase: process.env.POLY_BUILDER_PASSPHRASE!,
},
feeConfig: { // Optional: charge fees on orders
feeBps: 50, // 0.5% fee (0 = no fee, default)
affiliate: '0xPartner...', // Partner wallet for revenue share
affiliateShareBps: 3000, // 30% to partner, 70% to treasury
},
});
```
### Builder Attribution (Your Own Credentials)
By default, orders placed through polynode are attributed to polynode's builder profile on Polymarket. If you're running a platform and want volume credited to **your own** builder account, pass your credentials in `builderCredentials`.
1. Go to [polymarket.com/settings?tab=builder](https://polymarket.com/settings?tab=builder) and create a builder profile
2. Generate API credentials (key, secret, passphrase)
3. Pass them in `builderCredentials` when constructing the trader
```typescript theme={null}
const trader = new PolyNodeTrader({
polynodeKey: 'pn_live_...',
builderCredentials: {
key: process.env.POLY_BUILDER_API_KEY!,
secret: process.env.POLY_BUILDER_SECRET!,
passphrase: process.env.POLY_BUILDER_PASSPHRASE!,
},
});
```
When `builderCredentials` is set:
* All orders are attributed to your builder profile on the [Builder Leaderboard](https://builders.polymarket.com)
* Gasless Safe deployments and approvals use your builder account
* polynode never stores your builder credentials. They're sent per-request and used only for HMAC signing.
When `builderCredentials` is omitted, polynode's default builder credentials are used. Your orders still go through, you just don't get the builder attribution on your own profile.
## Polymarket V2 Exchange
The Polymarket V2 exchange uses PolyUSD as collateral instead of USDC.e directly. One config change enables V2 support. Always call `ensureReady` / `ensure_ready` before any V2 method — it deploys the Safe (if needed), sets V2 approvals, creates CLOB credentials, and refreshes the V2 CLOB's balance-allowance cache:
```typescript TypeScript theme={null}
import { PolyNodeTrader } from 'polynode-sdk';
const trader = new PolyNodeTrader({
polynodeKey: 'pn_live_...',
exchangeVersion: 'v2',
});
// Required before any wrap/order/cancel call:
await trader.ensureReady('0xYOUR_EOA_PRIVATE_KEY');
```
```python Python theme={null}
import asyncio
from polynode.trading import PolyNodeTrader, TraderConfig, ExchangeVersion
trader = PolyNodeTrader(TraderConfig(
polynode_key="pn_live_...",
exchange_version=ExchangeVersion.V2,
))
# Required before any wrap/order/cancel call:
await trader.ensure_ready("0xYOUR_EOA_PRIVATE_KEY")
```
```rust Rust theme={null}
use polynode::trading::{PolyNodeTrader, TraderConfig, ExchangeVersion, PrivateKeySigner};
let mut trader = PolyNodeTrader::new(TraderConfig {
polynode_key: "pn_live_...".into(),
exchange_version: ExchangeVersion::V2,
..Default::default()
})?;
// Required before any wrap/order/cancel call:
let signer = PrivateKeySigner::from_hex("0xYOUR_EOA_PRIVATE_KEY")?;
trader.ensure_ready(Box::new(signer), None).await?;
```
Before placing orders on V2 markets, wrap USDC.e into PolyUSD. Amounts are in raw 6-decimal integers (`1_000_000` = \$1; TS uses `bigint`):
```typescript TypeScript theme={null}
// Wrap 100 USDC.e → PolyUSD
await trader.wrapToPolyUsd(100_000_000n);
const balanceRaw = await trader.getPolyUsdBalance();
console.log(`PolyUSD: $${Number(balanceRaw) / 1e6}`);
```
```python Python theme={null}
# Wrap 100 USDC.e → PolyUSD
tx_hash = await trader.wrap_to_polyusd(100_000_000)
polyusd_raw = trader.get_polyusd_balance() # sync, returns int
print(f"PolyUSD: ${polyusd_raw / 1e6:.6f}")
```
```rust Rust theme={null}
// Wrap 100 USDC.e → PolyUSD
let tx_hash = trader.wrap_to_polyusd(100_000_000).await?;
let balance_raw = trader.get_polyusd_balance().await?;
println!("PolyUSD: ${:.6}", balance_raw as f64 / 1e6);
```
Pass your builder code via the `builder` field on each V2 order. Mint one at [polymarket.com/settings?tab=builder](https://polymarket.com/settings?tab=builder):
```typescript TypeScript theme={null}
await trader.order({
tokenId: "...",
side: "BUY",
price: 0.42,
size: 5,
type: "GTC",
builder: "0x5472fdd7...", // bytes32 builder code
});
```
```python Python theme={null}
from polynode.trading import OrderParams
await trader.order(OrderParams(
token_id="...",
side="BUY",
price=0.42,
size=5,
type="GTC",
builder="0x5472fdd7...", # bytes32 builder code
))
```
```rust Rust theme={null}
use polynode::trading::{OrderParams, OrderSide, OrderType};
trader.order(OrderParams {
token_id: "...".into(),
side: OrderSide::Buy,
price: 0.42,
size: 5.0,
order_type: OrderType::GTC,
builder: Some("0x5472fdd7...".into()),
..Default::default()
}).await?;
```
On V2, `ensureReady()` / `ensure_ready()` also refreshes the CLOB's cached balance-allowance view after setting approvals. If you change approvals outside that flow, call `trader.refreshBalanceAllowance()` / `trader.refresh_balance_allowance()` before placing orders — otherwise the CLOB can reject with `"not enough balance / allowance"` until its cache catches up.
All other trading methods (`order()`, `cancelOrder()`, `split()`, `merge()`, etc.) work the same on V2 markets. The `order_type` accepts `"GTC"` (default — rest on book), `"GTD"` (with `expiration` unix seconds), `"FOK"` (fill-or-kill, fully match or cancel), or `"FAK"` (fill-and-kill, match what you can, cancel the rest). `size` is in shares of the outcome token, `price` is the per-share USD price between 0 and 1.
See the [V2 Migration Guide](/guides/v2-migration) for full details on the Polymarket V2 exchange upgrade, and the [PolyUSD Guide](/guides/polyusd) for wrapping and unwrapping mechanics.
## Fee Escrow
Charge your platform's per-order fee via on-chain escrow. Fees are pulled before the order, distributed on fill, and refunded on cancel. This is **your** fee — independent of Polymarket's protocol fee and V2 builder rev share (which are charged separately by the CLOB). See the [Fee Escrow Guide](/guides/fee-escrow) for the full architecture and the three-fee breakdown.
```typescript theme={null}
const trader = new PolyNodeTrader({
polynodeKey: 'pn_live_...',
feeConfig: { feeBps: 50 }, // 0.5% platform fee on every order (yours; not Polymarket's)
});
const result = await trader.order({
tokenId: '...',
side: 'BUY',
price: 0.55,
size: 100,
});
console.log(result.feeEscrowTxHash); // on-chain pullFee TX
console.log(result.feeAmount); // "0.0275" USDC
// Cancel → fee is automatically refunded
await trader.cancelOrder(result.orderId);
```
Set `feeBps: 0` or omit `feeConfig` to skip your platform fee entirely. Zero overhead, zero behavior change. Note: this only turns off **your** fee — Polymarket's protocol fee and V2 builder rev share are controlled independently by the CLOB.
## API Reference
### `PolyNodeTrader.generateWallet()`
Static async method. Generates a fresh wallet. **You must `await` this call.**
```typescript theme={null}
const { privateKey, address } = await PolyNodeTrader.generateWallet();
// IMPORTANT: Save privateKey securely. It is not stored anywhere by the SDK.
```
### `ensureReady(signer, opts?)`
One-call onboarding. Detects wallet type (Safe, proxy, or deposit wallet), deploys contracts if needed, sets all approvals, creates CLOB credentials.
**Signer types accepted:**
* `string` — hex private key (most common)
* `createPrivySigner(...)` — Privy server wallet
* ethers v5/v6 Signer
* viem WalletClient
**Returns:** `ReadyStatus` with `funderAddress` (where to send collateral), `signatureType`, `approvalsSet`, `credentials`, and `actions` (what it did).
**Wallet types detected automatically:**
| `signatureType` | Value | Description |
| ------------------ | ----- | ----------------------------------------------------- |
| `POLY_GNOSIS_SAFE` | 2 | Gnosis Safe proxy (most existing Polymarket accounts) |
| `POLY_PROXY` | 1 | Legacy proxy wallet |
| `POLY_1271` | 3 | Deposit wallet (newer Polymarket accounts, V2 only) |
| `EOA` | 0 | Direct EOA signing |
Calling it again on a ready wallet is instant (loads from local DB).
For deposit wallet users (`POLY_1271`), order signing uses the ERC-7739 TypedDataSign wrapper automatically. No code changes needed on your side — `order()` detects the wallet type from stored credentials and signs accordingly.
### `order(params)`
Place an order on Polymarket.
```typescript theme={null}
const result = await trader.order({
tokenId: '...', // Market token ID (from /v1/events/search)
side: 'BUY', // 'BUY' or 'SELL'
price: 0.55, // Limit price (0 to 1)
size: 100, // Number of shares
type: 'GTC', // 'GTC' | 'GTD' | 'FOK' | 'FAK' (default: GTC)
});
if (result.success) {
console.log('Order placed:', result.orderId);
// When feeConfig is set:
console.log('Fee TX:', result.feeEscrowTxHash); // on-chain escrow TX
console.log('Fee:', result.feeAmount); // USDC charged
} else {
console.log('Failed:', result.error);
}
```
### `cancelOrder(orderId)` / `cancelAll(market?)`
Cancel a specific order or all orders.
### `split(params)`
Split USDC into YES + NO outcome tokens. Gasless for Safe wallets. Auto-detects neg-risk vs standard markets.
```typescript theme={null}
const result = await trader.split({
conditionId: '0x895e01db...', // market condition ID
amount: 100, // $100 USDC
});
// result: { success: true, txHash: '0x...' }
```
### `merge(params)`
Merge YES + NO outcome tokens back into USDC. Gasless for Safe wallets.
```typescript theme={null}
const result = await trader.merge({
conditionId: '0x895e01db...',
amount: 50,
});
```
### `convert(params)`
Convert NO positions on selected outcomes into USDC + YES on complementary outcomes. Only works on neg-risk multi-outcome markets. Gasless for Safe wallets.
```typescript theme={null}
const result = await trader.convert({
marketId: '0xc7d902c4...', // negRiskMarketID
outcomeIndices: [0, 1], // which outcomes to convert
amount: 100, // $100 per outcome
});
```
See the [Position Management guide](/guides/position-management) for a full explanation of how conversions work and when to use each operation.
### `checkBalance(wallet?)`
Returns USDC.e and MATIC balance for the funder address.
### `checkApprovals(wallet?)`
Check if all required token approvals are set on-chain (6 Polymarket approvals + 1 fee escrow approval).
### `getOpenOrders(params?)`
Fetch open orders from the CLOB. Filters: `market`, `assetId`.
### `getOrderHistory(params?)`
Query local order history from SQLite. Filters: `limit`, `offset`, `tokenId`, `side`.
### `linkCredentials(opts)` / `linkWallet(signer)`
Import existing credentials or link a wallet manually.
### `exportWallet(wallet?)` / `exportAll()` / `importWallet(exported)`
Export and import wallet state for backup. Private keys are never included.
### `unlinkWallet(address?)` / `getLinkedWallets()`
Remove or list stored wallets.
### `close()`
Close the SQLite connection. Call this when shutting down.
## How It Works
1. SDK signs the order locally (EIP-712)
2. SDK sends to polynode's relay at `trade.polynode.dev`
3. Relay adds builder attribution and forwards to Polymarket's CLOB
4. Response returned to SDK, logged in local SQLite
If the relay is down and `fallbackDirect` is true, orders go directly to Polymarket. Your trading never stops.
## Wallet Model & Signing
Polymarket uses a **two-address model** for each trader:
* **EOA (signer)** — a plain private key. Signs every order and every on-chain intent. Holds no trading funds.
* **Safe (funder)** — a Gnosis Safe smart wallet whose address is deterministically derived from the EOA (CREATE2 via the Polymarket Safe Factory). This is where USDC.e / pUSD / CTF positions live, and it's the `maker` on every order.
`ensureReady(privateKey)` on a fresh EOA does all four things for you, **gasless** via the Polymarket relayer:
1. Derives the Safe address from the EOA via CREATE2
2. Deploys the Safe if it doesn't exist yet on-chain
3. Sets all required approvals (USDC.e/pUSD to exchanges + collateral adapters, CTF to exchanges)
4. Creates CLOB API credentials (L2 HMAC) by signing an EIP-712 `ClobAuth` message from the EOA
After this, the SDK stores the CLOB API key, secret, and passphrase in its local SQLite DB keyed by the EOA address. You never send them to polynode.
### How orders are signed
Every V2 order struct includes `maker` (the Safe address) and `signer` (the EOA address). The EOA signs the EIP-712 hash; the CLOB verifies the signature against `signer` and trusts that the `signer` is a 1-of-1 owner of the `maker` Safe (it is, because the Safe's sole owner is the EOA by construction). At settlement time, the V2 exchange calls the Safe's `execTransaction` with your signed payload as authorization.
### How wrap/unwrap, approvals, and split/merge are signed
For any on-chain action that needs to move funds out of the Safe (`wrapToPolyUsd`, `unwrapFromPolyUsd`, `setApprovals`, `split`, `merge`, `convert`), the SDK:
1. Builds the raw calldata for the target contract call
2. Wraps it in a Safe `execTransaction` EIP-712 payload with the current Safe nonce
3. Has your EOA `personal_sign` that payload (via eth\_account / alloy / ethers)
4. POSTs the signed payload to `https://relayer-v2.polymarket.com/submit` with your **builder HMAC headers**
5. Polls the relayer until the tx is mined on Polygon
The relayer pays the gas. You never need MATIC. This is why `wrapToPolyUsd()` / `unwrapFromPolyUsd()` require `builderCredentials` in `TraderConfig` for Safe wallets — the HMAC authenticates your submit to the relayer.
### Linking credentials vs ensureReady
Three ways to get a wallet ready to trade, depending on what you already have:
| Situation | Call | What it does |
| ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Fresh EOA (no Safe yet) | `trader.ensureReady(privateKey)` | Derives Safe, deploys, approves, creates CLOB creds, stores in local DB |
| Existing EOA + Safe already set up (you re-start your service) | `trader.ensureReady(privateKey)` | No-op if the local DB already has creds; otherwise re-derives and re-fetches |
| You already have CLOB creds from somewhere else (DB export, Polymarket account) | `trader.linkCredentials({ wallet, apiKey, apiSecret, apiPassphrase, signatureType, funderAddress })` then `trader.linkWallet(privateKey)` to attach the signer | Imports the creds into the local DB and attaches the signer for order signing. Skips approvals + deploys entirely. |
| Multi-user platform (one SDK instance, many users) | `trader.linkCredentials(userA)` → `trader.linkWallet(userA.pk)` → trade; then switch with `trader.linkWallet(userB.pk)` | Each `linkWallet` call swaps the active signer. `linkCredentials` writes into the local DB so you can return to a user later by calling `linkWallet` again with their key. Consider using one `PolyNodeTrader` per user for concurrency. |
### Multi-user / server wallet patterns
For a backend that trades on behalf of many users:
* **Privy / server-managed wallets:** use `createPrivySigner(privy, walletId, address)` instead of passing a raw private key to `ensureReady`. Same onboarding flow, but Privy holds the key and signs remotely.
* **Your own KMS:** implement the `RouterSigner` interface (or `TradingSigner` in Rust / a signer callable in Python). The SDK only needs two operations from you: `getAddress()` and `signTypedData(payload)`. Everything else is local.
* **Per-user DB file:** pass a different `dbPath` per user to keep their credentials isolated on disk. Or use a shared DB and call `linkWallet(userPk)` to swap active signers.
## Credential Custody
Credentials are stored in a local SQLite database. The SDK never sends your private key or CLOB credentials to polynode's servers. Back up with `exportWallet()`.
## Migrating from Dome
The polynode trading module is a drop-in replacement for Dome. See the [Dome Migration guide](/dome-migration#order-placement) for a line-by-line comparison.
# TypeScript — Configuration
Source: https://docs.polynode.dev/sdks/ts/config
## Configuration
```typescript theme={null}
const pn = new PolyNode({
apiKey: 'pn_live_...', // required
baseUrl: 'https://api.polynode.dev', // default
v3BaseUrl: 'https://api.polynode.dev', // optional override for pn.v3
wsUrl: 'wss://ws.polynode.dev/ws', // default
obUrl: 'wss://ob.polynode.dev/ws', // default
rpcUrl: 'https://rpc.polynode.dev', // default
timeout: 10000, // ms, default 10s
});
```
# TypeScript — Error Handling
Source: https://docs.polynode.dev/sdks/ts/errors
## Error Handling
```typescript theme={null}
import { PolyNode, ApiError, WsError } from 'polynode-sdk';
try {
await pn.market('invalid-id');
} catch (err) {
if (err instanceof ApiError) {
console.log(err.status); // 404
console.log(err.message); // "Market not found"
}
}
```
# TypeScript — Orderbook
Source: https://docs.polynode.dev/sdks/ts/orderbook
## Orderbook Streaming
The SDK includes a dedicated orderbook client for real-time book data from `ob.polynode.dev`. This is a separate WebSocket connection from the event stream, with its own URL, protocol, and message format.
### Subscribe
```typescript theme={null}
// Lazy-initialized, connects on first subscribe
await pn.orderbook.subscribe(['token_id_1', 'token_id_2']);
```
### Event Handlers
```typescript theme={null}
pn.orderbook.on('snapshot', (snap) => {
// Full book snapshot on subscribe
console.log(snap.asset_id, snap.bids.length, 'bids', snap.asks.length, 'asks');
});
pn.orderbook.on('update', (delta) => {
// Incremental delta. A level with size "0" means removal.
console.log(delta.asset_id, delta.bids.length, 'bid changes');
});
pn.orderbook.on('price', (change) => {
// Summary price movement across assets in a market
for (const asset of change.assets) {
console.log(asset.outcome, asset.price);
}
});
pn.orderbook.on('snapshots_done', (msg) => {
console.log(`All ${msg.total} snapshots received`);
});
// Catch-all for snapshot, update, and price events
pn.orderbook.on('*', (update) => {
console.log(update.type, update);
});
```
### LocalOrderbook Helper
Maintain a sorted local copy of the book:
```typescript theme={null}
import { LocalOrderbook } from 'polynode-sdk';
const book = new LocalOrderbook();
pn.orderbook.on('snapshot', (snap) => book.applySnapshot(snap));
pn.orderbook.on('update', (delta) => book.applyUpdate(delta));
// Query state
const fullBook = book.getBook(tokenId); // { bids, asks }
const bestBid = book.getBestBid(tokenId); // { price, size }
const bestAsk = book.getBestAsk(tokenId); // { price, size }
const spread = book.getSpread(tokenId); // number
```
### Compression
Zlib compression is enabled by default (\~50% bandwidth savings). No configuration needed.
```typescript theme={null}
// To disable (not recommended):
const ob = pn.configureOrderbook({ compress: false });
```
### Configuration
```typescript theme={null}
const pn = new PolyNode({
apiKey: 'pn_live_...',
obUrl: 'wss://ob.polynode.dev/ws', // default
});
// Or configure options separately
pn.configureOrderbook({
compress: true,
autoReconnect: true,
maxReconnectAttempts: Infinity,
reconnectBaseDelay: 1000,
reconnectMaxDelay: 30000,
});
```
### System Events
```typescript theme={null}
pn.orderbook.onConnect(() => console.log('connected'));
pn.orderbook.onDisconnect((reason) => console.log('disconnected:', reason));
pn.orderbook.onReconnect((attempt) => console.log('reconnected, attempt', attempt));
pn.orderbook.onError((err) => console.error(err));
```
### Cleanup
```typescript theme={null}
pn.orderbook.unsubscribe(); // unsubscribe from all markets
pn.orderbook.disconnect(); // close connection
```
# TypeScript — Orderbook Engine
Source: https://docs.polynode.dev/sdks/ts/orderbook-engine
## OrderbookEngine
The `OrderbookEngine` is a higher-level wrapper around the orderbook WebSocket. It manages one connection, maintains local state for all subscribed tokens, and lets you create **filtered views** that only deliver updates for specific token subsets.
This is useful when your app has multiple components that each need different slices of the orderbook — a trade page showing one market, a sidebar showing another, a portfolio view watching 20 positions. One connection, one shared state, multiple filtered outputs.
### Create and Subscribe
```typescript theme={null}
import { OrderbookEngine } from 'polynode-sdk';
const engine = new OrderbookEngine({
apiKey: 'pn_live_...',
compress: true,
});
// Subscribe with token IDs, slugs, or condition IDs
await engine.subscribe([
'114694726451307654528948558967898493662917070661203465131156925998487819889437',
'66255671088804707681511323064315150986307471908131081808279119719218775249892',
]);
// Wait for all initial snapshots to load
engine.on('ready', () => {
console.log(`${engine.size} books loaded`);
});
```
### Query State
The engine exposes computed helpers that read from the shared local orderbook:
```typescript theme={null}
engine.midpoint(tokenId); // number | undefined — (bestBid + bestAsk) / 2
engine.spread(tokenId); // number | undefined — bestAsk - bestBid
engine.bestBid(tokenId); // { price, size } | undefined
engine.bestAsk(tokenId); // { price, size } | undefined
engine.book(tokenId); // { bids: [...], asks: [...] } | undefined
```
### Filtered Views
Create lightweight views that only receive updates for specific tokens. No extra connections are opened — views are just filters over the shared state.
```typescript theme={null}
const tradeView = engine.view([tokenA, tokenB]);
const portfolioView = engine.view(myPositionTokenIds);
// Callbacks only fire for this view's tokens
tradeView.on('update', (update) => {
console.log(update.asset_id, 'book changed');
});
tradeView.on('price', (change) => {
console.log('price moved:', change.assets);
});
// Views have the same query helpers as the engine
tradeView.midpoint(tokenA);
tradeView.spread(tokenA);
tradeView.book(tokenA);
```
### Swap or Destroy Views
When a user navigates to a different page or the tracked tokens change:
```typescript theme={null}
// Swap to different tokens (keeps the view, changes the filter)
tradeView.setTokens([newTokenX, newTokenY]);
// Or destroy the view entirely (removes handlers, detaches from engine)
tradeView.destroy();
```
### Engine-Level Events
Listen to all updates across all tokens (unfiltered):
```typescript theme={null}
engine.on('update', (update) => {
// Fires for every book snapshot and delta, all tokens
});
engine.on('price', (change) => {
// All price change events
});
engine.on('ready', () => {
// All initial snapshots loaded
});
```
### Connection Events
Access the underlying WebSocket for connection lifecycle events:
```typescript theme={null}
engine.connection.onConnect(() => console.log('connected'));
engine.connection.onDisconnect((reason) => console.log('disconnected:', reason));
engine.connection.onReconnect((attempt) => console.log('reconnected'));
engine.connection.onError((err) => console.error(err));
```
### Cleanup
```typescript theme={null}
engine.close(); // disconnects WS, destroys all views, clears state
```
# TypeScript — Overview
Source: https://docs.polynode.dev/sdks/ts/overview
**Placing orders?** See [Trading](/sdks/ts/trading) for `PolyNodeTrader`, V2 order placement (`exchangeVersion: 'v2'`), builder attribution, and PolyUSD wrapping.
## Install
```bash theme={null}
npm install polynode-sdk ws
```
Requires Node.js 18+. The `ws` package provides WebSocket support for Node.js.
## Quick Start
```typescript theme={null}
import { PolyNode } from 'polynode-sdk';
const pn = new PolyNode({ apiKey: 'pn_live_...' });
// Fetch top markets
const markets = await pn.markets({ count: 10 });
console.log(`${markets.count} markets, ${markets.total} total`);
// Search
const results = await pn.search('bitcoin');
console.log(results.results[0].question);
```
## V3 Quick Start
The original `pn.market`, `pn.wallet`, and other top-level methods keep their legacy behavior. Use `pn.v3` when you want the newer paginated historical data endpoints.
```typescript theme={null}
// Wallet P&L and enriched positions
const summary = await pn.v3.wallet('0xa9857c7bcb9bcfafd2c132ab053f34f678610058');
const positions = await pn.v3.walletPositions(summary.address, {
status: 'open',
sort: 'size',
limit: 25,
});
// Builder-attributed fills
const trades = await pn.v3.builderTrades('0x4898df15ec6590495dc6c0fedf951ade3e64001d47f9caf44a64e86fc11959df', {
marketSlug: 'dota2-tundra-xtreme-2026-05-22-game1',
limit: 50,
});
// Resolve wallet identifiers
const resolved = await pn.resolve('alice123');
console.log(resolved.safe, resolved.eoa, resolved.username, resolved.type);
```
# TypeScript — REST API
Source: https://docs.polynode.dev/sdks/ts/rest-api
## REST Methods
The TypeScript SDK preserves the original top-level REST methods on their legacy V1/V2 paths. New historical data, builder analytics, and Polymarket profile methods live under the opt-in `pn.v3` namespace.
```typescript theme={null}
// System
await pn.healthz();
await pn.status();
await pn.createKey('my-bot');
// Markets
await pn.markets({ count: 10 });
await pn.market(tokenId);
await pn.marketBySlug('bitcoin-100k');
await pn.marketByCondition(conditionId);
await pn.marketsList({ count: 20, sort: 'volume' });
await pn.search('ethereum', { limit: 5 });
// Pricing
await pn.candles(tokenId, { resolution: '1h', limit: 100 });
await pn.stats(tokenId);
// Settlements
await pn.recentSettlements({ count: 20 });
await pn.tokenSettlements(tokenId, { count: 10 });
await pn.walletSettlements(address, { count: 10 });
// Wallets
await pn.wallet(address);
await pn.resolve('0xabc...'); // wallet, EOA, or username
await pn.walletOnchainPositions(address, {
since: 1778000000,
tagSlug: 'Crypto',
});
// Enriched Data (1 req/sec rate limit)
await pn.leaderboard({ period: 'monthly', sort: 'profit' });
await pn.trending();
await pn.activity();
await pn.movers();
await pn.traderProfile('0xc2e7...');
await pn.traderPnl('0xc2e7...', { period: '1W' });
await pn.event('how-many-fed-rate-cuts-in-2026');
await pn.searchEvents('recession', { limit: 5 });
await pn.marketsByCategory('crypto');
// RPC (rpc.polynode.dev)
await pn.rpc('eth_blockNumber');
await pn.rpc('eth_getBlockByNumber', ['latest', false]);
```
## V3 Data API
Use `pn.v3` for the newer paginated historical endpoints. This keeps existing integrations stable while letting new code opt into V3 explicitly.
```typescript theme={null}
// Global data
await pn.v3.stats();
await pn.v3.trades({ limit: 100, minAmount: 1_000_000 });
await pn.v3.positions({ status: 'open', sort: 'size', tokenId });
await pn.v3.fees({ after: 1778000000 });
await pn.v3.resolutions({ limit: 25 });
// Wallet analytics
await pn.v3.wallet(address);
await pn.v3.walletPnl(address, { period: '30d', includeUnrealized: true });
await pn.v3.walletPnlEvents(address, { period: '7d', group: 'day' });
await pn.v3.walletTrades(address, {
side: 'both',
marketSlug: 'bitcoin-100k',
groupBy: 'user_trade',
});
await pn.v3.walletPositions(address, {
status: 'redeemable',
sort: 'recent',
});
// Market lookups
await pn.v3.searchMarkets({ query: 'bitcoin', limit: 10 });
await pn.v3.marketByCondition(conditionId);
await pn.v3.marketPrice(tokenId);
await pn.v3.marketTrades(tokenId, { limit: 50 });
await pn.v3.marketPositionsBySlug('bitcoin-100k', { status: 'open' });
await pn.v3.token(tokenId);
// Builders
await pn.v3.builders({ sort: 'volume', limit: 20 });
await pn.v3.builder(builderCode);
await pn.v3.builderTrades(builderCode, {
eventSlug: 'who-will-win-the-2026-world-cup',
side: 'buy',
limit: 100,
});
```
## Polymarket Profiles
The profile helpers wrap the V3 username flow. Create the challenge on your backend, have the user sign both payloads in your frontend, then submit the signatures from your backend.
```typescript theme={null}
const available = await pn.v3.polymarketUsernameAvailable('alice123');
const challenge = await pn.v3.createPolymarketUsernameChallenge({
address: userEoa,
username: 'alice123',
});
// Browser wallet signs:
// - challenge.polymarket.message with personal_sign
// - challenge.consent with eth_signTypedData_v4
const result = await pn.v3.completePolymarketUsername({
challenge_id: challenge.challenge_id,
address: userEoa,
username: 'alice123',
polymarket_signature: polymarketSignature,
consent_signature: consentSignature,
});
const profile = await pn.v3.polymarketProfile(result.deposit_wallet);
```
# TypeScript — Source
Source: https://docs.polynode.dev/sdks/ts/source
## Source
[GitHub](https://github.com/joinQuantish/polynode-sdk-ts) | [NPM](https://www.npmjs.com/package/polynode-sdk)
# TypeScript — Trading
Source: https://docs.polynode.dev/sdks/ts/trading
## Trading
**Polymarket V2 cutover: April 28, 2026 at \~11am UTC.** Set `exchangeVersion: "v2"` in your `TraderConfig` before cutover. V1 orders submitted after April 28 will be rejected with `order_version_mismatch`. Pass your V2 builder code (mint at [polymarket.com/settings?tab=builder](https://polymarket.com/settings?tab=builder)) via the `builder` field on each order. See the [V2 Migration Guide](/guides/v2-migration).
Order placement is available through the `PolyNodeTrader` class, which supports the Polymarket V2 exchange. See the [Trading reference](/sdks/trading) for full documentation, including wallet setup, order placement, and V2 PolyUSD wrapping.
See the [V2 Migration Guide](/guides/v2-migration) for details on the Polymarket V2 exchange upgrade.
# TypeScript — Types
Source: https://docs.polynode.dev/sdks/ts/types
## TypeScript Types
All event types are exported and fully typed:
```typescript theme={null}
import type {
SettlementEvent,
TradeEvent,
StatusUpdateEvent,
BlockEvent,
PositionChangeEvent,
DepositEvent,
OracleEvent,
PriceFeedEvent,
PolyNodeEvent, // union of all events
// Orderbook types
OrderbookLevel,
BookSnapshot,
BookUpdate,
PriceChange,
OrderbookUpdate, // union of snapshot | update | price_change
OrderbookOptions,
// Enriched data types
LeaderboardResponse,
LeaderboardTrader,
TrendingResponse,
ActivityResponse,
ActivityTrade,
MoversResponse,
MoverMarket,
TraderProfile,
TraderPnlResponse,
EventDetailResponse,
EventSearchResponse,
EventSearchResult,
EventSearchMarket,
MarketsByCategoryResponse,
// Wallet helpers
ResolveResult,
WalletOnchainPositionsParams,
WalletOnchainPositionsResponse,
WalletOnchainPosition,
// V3 data API
V3Trade,
V3GroupedOrderTrade,
V3TradesResponse,
V3Position,
V3PositionsResponse,
V3WalletSummary,
V3WalletPnl,
V3WalletPnlEventsResponse,
V3MarketMetadata,
V3MarketSearchRow,
V3MarketPosition,
V3MarketPrice,
V3TokenInfo,
V3Builder,
V3BuilderTradesResponse,
V3FeeEvent,
V3ResolutionEvent,
// V3 Polymarket profiles
V3PolymarketUsernameAvailableResponse,
V3PolymarketUsernameChallengeResponse,
V3PolymarketUsernameCompleteResponse,
V3PolymarketProfile,
} from 'polynode-sdk';
```
V3 response envelopes use the same pagination fields as the REST API: `rows_returned`, `has_more`, `offset`, `limit`, and `elapsed_ms`.
# TypeScript — V3 Data & Profiles
Source: https://docs.polynode.dev/sdks/ts/v3-data
V3 endpoints are available from the same `PolyNode` client under `pn.v3`.
Top-level methods such as `pn.market()`, `pn.wallet()`, and `pn.leaderboard()` keep their legacy behavior. Use `pn.v3` when you want the newer paginated historical data API, builder analytics, and Polymarket profile helpers.
```typescript theme={null}
import { PolyNode } from 'polynode-sdk';
const pn = new PolyNode({ apiKey: process.env.POLYNODE_KEY || 'pn_live_YOUR_KEY' });
```
## Wallets
```typescript theme={null}
const wallet = '0xa9857c7bcb9bcfafd2c132ab053f34f678610058';
const summary = await pn.v3.wallet(wallet);
const pnl = await pn.v3.walletPnl(wallet, {
period: '30d',
includeUnrealized: true,
});
const pnlEvents = await pn.v3.walletPnlEvents(wallet, {
period: '7d',
group: 'day',
});
const positions = await pn.v3.walletPositions(wallet, {
status: 'open',
sort: 'size',
order: 'desc',
limit: 25,
});
const trades = await pn.v3.walletTrades(wallet, {
side: 'both',
groupBy: 'user_trade',
marketSlug: 'dota2-aur1-liquid-2026-05-13',
limit: 100,
});
```
`walletPositions()` returns lifecycle statuses: `open`, `closed`, `redeemable`, and `redeemed`. Use `redeemable` for resolved positions that still hold claimable payout.
## Global Feeds
```typescript theme={null}
const recentTrades = await pn.v3.trades({
after: 1778000000,
minAmount: 1_000_000,
order: 'desc',
limit: 100,
});
const globalPositions = await pn.v3.positions({
status: 'open',
sort: 'size',
tokenId: '75783394880030392863380883800697645018418815910449662777195626260206142035810',
limit: 50,
});
const resolutions = await pn.v3.resolutions({ limit: 25 });
const feeEvents = await pn.v3.fees({ after: 1778000000, limit: 100 });
```
All list responses include `rows_returned`, `has_more`, `offset`, `limit`, and `elapsed_ms`.
## Markets
```typescript theme={null}
const matches = await pn.v3.searchMarkets({
query: 'bitcoin',
limit: 10,
});
const condition = await pn.v3.marketByCondition(
'0x6c4d221b3cf2c8d17467c70a8aa3c714e30299b6e57cd3e4269dc8a41d2b0cd8'
);
const price = await pn.v3.marketPrice(
'75783394880030392863380883800697645018418815910449662777195626260206142035810'
);
const marketTrades = await pn.v3.marketTradesBySlug(
'dota2-aur1-liquid-2026-05-13',
{ limit: 50 }
);
const holders = await pn.v3.marketPositionsBySlug(
'dota2-aur1-liquid-2026-05-13',
{ status: 'open', sort: 'pnl', limit: 50 }
);
```
`marketByCondition()` and `token()` return the standard V3 `data: [...]` envelope.
```typescript theme={null}
const token = await pn.v3.token(
'75783394880030392863380883800697645018418815910449662777195626260206142035810'
);
const row = token.data[0];
console.log(row.price, row.question, row.slug);
```
## Builders
```typescript theme={null}
const builders = await pn.v3.builders({
sort: 'volume',
limit: 20,
});
const builder = await pn.v3.builder(
'0x4898df15ec6590495dc6c0fedf951ade3e64001d47f9caf44a64e86fc11959df'
);
const builderTrades = await pn.v3.builderTrades(builder.builder_code ?? builder.builder, {
eventSlug: 'who-will-win-the-2026-world-cup',
side: 'buy',
minAmount: 1_000_000,
limit: 100,
});
```
`builderTrades()` supports `tokenId`, `conditionId`, `marketSlug`, `eventSlug`, `side`, `category`, `tagSlug`, `minAmount`, `after`, `before`, `limit`, and `offset`.
## Resolve Identifiers
Use `resolve()` to turn any Polymarket wallet, EOA, or username into the full identity triple.
```typescript theme={null}
const resolved = await pn.resolve('Fredi9999');
console.log(resolved.safe); // Polymarket trading wallet
console.log(resolved.eoa); // controlling EOA, or null
console.log(resolved.username); // public username, or null
console.log(resolved.type); // "safe", "deposit_wallet", or "proxy"
```
## On-chain Positions
The V2 on-chain positions helper is still top-level because it predates V3 and is already used by the local cache.
```typescript theme={null}
const onchain = await pn.walletOnchainPositions(wallet, {
since: 1778000000,
until: 1778600000,
tagSlug: 'Crypto',
});
console.log(onchain.total_realized_pnl);
console.log(onchain.total_unrealized_pnl);
for (const position of onchain.positions) {
console.log(
position.condition_id,
position.market,
position.outcome,
position.current_price,
position.unrealized_pnl
);
}
```
## Polymarket Username Flow
The profile helpers let your app create or change a user's Polymarket username from an EOA wallet. Your backend calls PolyNode with your API key. Your frontend asks the user wallet to sign the returned messages.
Do not expose your PolyNode API key in the browser. Do not send private keys to PolyNode.
### Backend: create challenge
```typescript theme={null}
import { PolyNode } from 'polynode-sdk';
const pn = new PolyNode({ apiKey: process.env.POLYNODE_KEY || 'pn_live_YOUR_KEY' });
async function createProfileChallenge(input) {
return pn.v3.createPolymarketUsernameChallenge({
address: input.address,
username: input.username,
action: 'set_username',
});
}
```
The challenge contains two payloads:
* `challenge.polymarket.message` — sign with `personal_sign`
* `challenge.consent` — sign with `eth_signTypedData_v4`
### Frontend: request signatures
This example uses the raw EIP-1193 provider shape so the same payload works with browser wallets, wallet connectors, and embedded wallet providers.
```typescript theme={null}
type EthereumProvider = {
request(args: { method: string; params?: unknown[] }): Promise;
};
declare const ethereum: EthereumProvider;
const challenge = await fetch('/api/polymarket-profile/challenge', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ address, username }),
}).then((r) => r.json());
const polymarketSignature = await ethereum.request({
method: 'personal_sign',
params: [challenge.polymarket.message, address],
});
const consentSignature = await ethereum.request({
method: 'eth_signTypedData_v4',
params: [
address,
JSON.stringify({
domain: challenge.consent.domain,
types: challenge.consent.types,
primaryType: challenge.consent.primaryType,
message: challenge.consent.message,
}),
],
});
```
### Backend: complete username action
```typescript theme={null}
export async function completeProfileUsername(input: {
challenge_id: string;
address: string;
username: string;
polymarket_signature: string;
consent_signature: string;
}) {
return pn.v3.completePolymarketUsername(input);
}
```
Completion returns the created or updated profile state:
```typescript theme={null}
const result = await pn.v3.completePolymarketUsername({
challenge_id: challenge.challenge_id,
address,
username,
polymarket_signature: String(polymarketSignature),
consent_signature: String(consentSignature),
});
console.log(result.deposit_wallet);
console.log(result.safe_address);
console.log(result.profile_url);
```
### Public profile lookup
```typescript theme={null}
const profile = await pn.v3.polymarketProfile(result.deposit_wallet);
console.log(profile.username);
console.log(profile.x_username);
console.log(profile.verified_badge);
```
For the full security model and raw HTTP examples, see [Polymarket Profile Setup](/guides/polymarket-profile-setup).
# TypeScript — WebSocket
Source: https://docs.polynode.dev/sdks/ts/websocket
## WebSocket Streaming
Subscribe to real-time events with a builder pattern:
```typescript theme={null}
const sub = await pn.ws.subscribe('settlements')
.minSize(100)
.status('pending')
.snapshotCount(20)
.send();
```
### Event Callbacks
```typescript theme={null}
sub.on('settlement', (event) => {
console.log(`${event.taker_side} $${event.taker_size} on ${event.market_title}`);
});
sub.on('status_update', (event) => {
console.log(`Confirmed in ${event.latency_ms}ms`);
});
// Catch-all
sub.on('*', (event) => {
console.log(event.event_type, event);
});
```
### Async Iterator
```typescript theme={null}
for await (const event of sub) {
if (event.event_type === 'settlement') {
console.log(event.taker_wallet, event.taker_size);
}
}
```
### Subscription Filters
All filters from the [Subscriptions & Filters](/websocket/subscribing) page are supported:
```typescript theme={null}
pn.ws.subscribe('settlements')
.wallets(['0xabc...']) // by wallet
.tokens(['21742633...']) // by token ID
.slugs(['bitcoin-100k']) // by market slug
.conditionIds(['0xabc...']) // by condition ID
.side('BUY') // BUY or SELL
.status('pending') // pending, confirmed, or all
.minSize(100) // min USD size
.maxSize(10000) // max USD size
.eventTypes(['settlement']) // override event types
.snapshotCount(50) // initial snapshot (max 200)
.feeds(['BTC/USD']) // chainlink feeds
.send();
```
### Subscription Types
```typescript theme={null}
pn.ws.subscribe('settlements') // pending + confirmed settlements
pn.ws.subscribe('trades') // all trade activity
pn.ws.subscribe('prices') // price-moving events
pn.ws.subscribe('blocks') // new Polygon blocks
pn.ws.subscribe('wallets') // all wallet activity
pn.ws.subscribe('markets') // all market activity
pn.ws.subscribe('large_trades') // $1K+ trades
pn.ws.subscribe('oracle') // UMA resolution events
pn.ws.subscribe('chainlink') // real-time price feeds
```
### Multiple Subscriptions
Subscriptions stack on the same connection:
```typescript theme={null}
const whales = await pn.ws.subscribe('large_trades')
.minSize(5000).send();
const myWallet = await pn.ws.subscribe('wallets')
.wallets(['0xabc...']).send();
// Both active simultaneously, events deduplicated
```
### Compression
Zlib compression is enabled by default for all WebSocket connections (\~50% bandwidth savings). No configuration needed.
To disable compression (not recommended):
```typescript theme={null}
const ws = pn.configureWs({ compress: false });
```
### Auto-Reconnect
Enabled by default. The SDK reconnects with exponential backoff and replays all active subscriptions:
```typescript theme={null}
const ws = pn.configureWs({
autoReconnect: true, // default: true
maxReconnectAttempts: Infinity, // default: unlimited
reconnectBaseDelay: 1000, // default: 1s
reconnectMaxDelay: 30000, // default: 30s
});
ws.onConnect(() => console.log('connected'));
ws.onDisconnect((reason) => console.log('disconnected:', reason));
ws.onReconnect((attempt) => console.log('reconnected, attempt', attempt));
ws.onError((err) => console.error(err));
```
### Cleanup
```typescript theme={null}
sub.unsubscribe(); // remove one subscription
pn.ws.unsubscribeAll(); // remove all
pn.ws.disconnect(); // close connection
```
# WebSocket Streaming
Source: https://docs.polynode.dev/sdks/websocket-streaming
Real-time event subscriptions with builder pattern, auto-reconnect, and compression
The SDK wraps the PolyNode WebSocket API with typed subscriptions, automatic reconnection, and transparent zlib compression. One connection handles multiple subscriptions simultaneously.
## Subscribe
```typescript theme={null}
import { PolyNodeWS } from 'polynode-sdk';
const ws = new PolyNodeWS('pn_live_...', 'wss://ws.polynode.dev/ws');
const sub = await ws.subscribe('settlements')
.minSize(100)
.status('pending')
.snapshotCount(20)
.send();
```
```rust theme={null}
use polynode::ws::{Subscription, SubscriptionType, StreamOptions};
let mut stream = client.stream(StreamOptions::default()).await?;
stream.subscribe(
Subscription::new(SubscriptionType::Settlements)
.min_size(100.0)
.status("pending")
.snapshot_count(20)
).await?;
```
## Subscription Types
| Type | What you get |
| -------------- | --------------------------------------------------- |
| `settlements` | Pending detection + confirmed settlements |
| `trades` | All trade activity (settlements + confirmed trades) |
| `prices` | Price-moving events for specific markets |
| `blocks` | New Polygon blocks |
| `wallets` | All activity for specified wallets |
| `markets` | All activity for specified markets |
| `large_trades` | Trades >= \$1,000 |
| `oracle` | UMA resolution events (proposals, disputes) |
| `chainlink` | Real-time Chainlink price feeds (\~1/sec) |
## Filters
All filters can be chained on any subscription:
```typescript theme={null}
ws.subscribe('settlements')
.wallets(['0xabc...']) // by wallet address
.tokens(['21742633...']) // by token ID
.slugs(['bitcoin-100k']) // by market slug
.conditionIds(['0xabc...']) // by condition ID
.side('BUY') // BUY or SELL
.status('pending') // pending, confirmed, or all
.minSize(100) // min USD size
.maxSize(10000) // max USD size
.eventTypes(['settlement']) // specific event types
.snapshotCount(50) // initial history (tier-limited)
.feeds(['BTC/USD']) // Chainlink feed names
.send();
```
```rust theme={null}
Subscription::new(SubscriptionType::Settlements)
.wallets(vec!["0xabc...".into()])
.tokens(vec!["21742633...".into()])
.slugs(vec!["bitcoin-100k".into()])
.condition_ids(vec!["0xabc...".into()])
.side("BUY")
.status("pending")
.min_size(100.0)
.max_size(10000.0)
.event_types(vec!["settlement".into()])
.snapshot_count(50)
.feeds(vec!["BTC/USD".into()])
```
## Consuming Events
```typescript theme={null}
// Typed event callbacks
sub.on('settlement', (event) => {
console.log(`${event.taker_side} $${event.taker_size} on ${event.market_title}`);
});
sub.on('status_update', (event) => {
console.log(`Confirmed in ${event.latency_ms}ms`);
});
// Catch-all
sub.on('*', (event) => {
console.log(event.event_type, event);
});
// Or use async iterator
for await (const event of sub) {
console.log(event.event_type);
}
```
```rust theme={null}
while let Some(msg) = stream.next().await {
match msg? {
WsMessage::Event(event) => {
match event {
PolyNodeEvent::Settlement(s) => {
println!("{} ${:.2} on {}",
s.taker_side, s.taker_size,
s.market_title.as_deref().unwrap_or("?"));
}
PolyNodeEvent::StatusUpdate(u) => {
println!("Confirmed in {}ms", u.latency_ms);
}
_ => {}
}
}
WsMessage::Snapshot(events) => {
println!("Snapshot: {} events", events.len());
}
WsMessage::Heartbeat { .. } => {}
WsMessage::Error { message, .. } => eprintln!("{}", message),
_ => {}
}
}
```
## Multiple Subscriptions
Subscriptions stack on the same connection. Events are deduplicated.
```typescript theme={null}
const whales = await ws.subscribe('large_trades')
.minSize(5000).send();
const myWallet = await ws.subscribe('wallets')
.wallets(['0xabc...']).send();
// Both active simultaneously
```
```rust theme={null}
stream.subscribe(
Subscription::new(SubscriptionType::LargeTrades)
.min_size(5000.0)
).await?;
stream.subscribe(
Subscription::new(SubscriptionType::Wallets)
.wallets(vec!["0xabc...".into()])
).await?;
```
## Auto-Reconnect
Enabled by default. On disconnect, the SDK reconnects with exponential backoff and replays all active subscriptions.
```typescript theme={null}
ws.onConnect(() => console.log('connected'));
ws.onDisconnect((reason) => console.log('disconnected:', reason));
ws.onReconnect((attempt) => console.log('reconnected, attempt', attempt));
ws.onError((err) => console.error(err));
```
```rust theme={null}
let mut stream = client.stream(StreamOptions {
auto_reconnect: true,
max_reconnect_attempts: None, // unlimited
initial_backoff: Duration::from_secs(1),
max_backoff: Duration::from_secs(30),
..Default::default()
}).await?;
```
## Compression
Zlib compression is enabled by default for all connections, saving \~50% bandwidth. Binary frames are transparently decompressed. No configuration needed.
## Cleanup
```typescript theme={null}
sub.unsubscribe(); // remove one subscription
ws.unsubscribeAll(); // remove all
ws.disconnect(); // close connection
```
```rust theme={null}
stream.unsubscribe(Some("sub_id".into())).await?;
stream.unsubscribe(None).await?; // all
stream.close().await?;
```
# builder_trade
Source: https://docs.polynode.dev/webhooks/events/builder-trade
Fires when a trade is routed through a specific builder. Monitor builder volume, attribution, and fee flow.
**polynode exclusive** — no other Polymarket data provider exposes builder attribution on webhooks.
## Filters
| Filter | Type | Required | Description |
| ---------------- | ---------- | -------- | --------------------------- |
| `builder_codes` | `string[]` | **Yes** | Builder addresses to watch. |
| `min_amount_usd` | `number` | No | Minimum trade size in USD. |
## Payload
Same schema as [`trade`](/webhooks/events/trade), with `"type": "builder_trade"`.
## Use Cases
* Builders monitoring their own attributed trade volume
* Builder competition analysis
* Fee revenue tracking per builder
# fee_earned
Source: https://docs.polynode.dev/webhooks/events/fee-earned
Fires when a fee receiver gets paid. Track builder revenue, market maker fees, or protocol fee flows.
**polynode exclusive** — no other Polymarket data provider offers this event type.
## Filters
| Filter | Type | Description |
| -------------------- | ---------- | ---------------------------------------------------------------- |
| `receiver_addresses` | `string[]` | Fee receiver addresses to watch. Max 500. Empty = all receivers. |
| `min_fee_usd` | `number` | Minimum fee amount in USD. |
## Payload
```json theme={null}
{
"id": "evt_c1d2e3f4-5a6b-7c8d-9e0f-1a2b3c4d5e6f",
"type": "fee_earned",
"created_at": "2026-05-14T15:09:13.123456789+00:00",
"data": {
"receiver": "0x115f48dc2a731aa16251c6d6e1befc42f92accc9",
"fee_usd": 0.00069,
"transaction_hash": "0x8a76e72ea10587303469998405da67323d33cbab8bda053db3091b3935011b42",
"timestamp": "1778770048"
}
}
```
## Fields
| Field | Type | Description |
| ------------------ | -------- | --------------------------- |
| `receiver` | `string` | Fee receiver wallet address |
| `fee_usd` | `number` | Fee amount in USD |
| `transaction_hash` | `string` | On-chain transaction hash |
| `timestamp` | `string` | Unix timestamp |
## Use Cases
* Builders monitoring their own fee revenue in real-time
* Market makers tracking fee income across markets
* Protocol analytics tracking fee distribution
* Revenue dashboards with instant updates
## Example
```bash theme={null}
curl -X POST https://api.polynode.dev/v3/webhooks \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/hooks/fees",
"event_types": ["fee_earned"],
"filters": {
"receiver_addresses": ["0x115f48dc2a731aa16251c6d6e1befc42f92accc9"],
"min_fee_usd": 1.0
}
}'
```
# first_trade
Source: https://docs.polynode.dev/webhooks/events/first-trade
Fires when a wallet makes its first-ever Polymarket trade. Detect new market participants.
## Filters
| Filter | Type | Description |
| ---------------- | -------- | --------------------------------------- |
| `min_amount_usd` | `number` | Minimum trade size for the first trade. |
## Payload
Same schema as [`trade`](/webhooks/events/trade), with `"type": "first_trade"`.
## Use Cases
* Growth analytics: track new user onboarding rate
* Welcome flows: trigger onboarding automations for new traders
* Smart money detection: flag large first trades (potential whale entry)
# large_trade
Source: https://docs.polynode.dev/webhooks/events/large-trade
Fires when any trade exceeds your USD threshold. No wallet filter required — catches whale trades across all markets.
## Filters
| Filter | Type | Required | Description |
| ---------------- | ---------- | -------- | ---------------------------- |
| `min_amount_usd` | `number` | **Yes** | Minimum trade size in USD |
| `condition_ids` | `string[]` | No | Restrict to specific markets |
| `event_slugs` | `string[]` | No | Restrict to specific events |
| `tags` | `string[]` | No | Restrict to tagged markets |
## Payload
Same schema as [`trade`](/webhooks/events/trade), with `"type": "large_trade"`.
```json theme={null}
{
"id": "evt_b3f2a1c0-8d4e-4f2a-9c1b-2e3f4a5b6c7d",
"type": "large_trade",
"created_at": "2026-05-14T14:56:30.123456789+00:00",
"data": {
"amount_usd": 1205.28,
"price": 0.87,
"direction": "BUY",
"market": "Will Hyperliquid reach $48 in May?",
"slug": "will-hyperliquid-reach-48-in-may",
"outcome": "No",
"maker": "0x337b7fca244bf2ef3272469ee02a2bd541097e26",
"taker": "0x04b6d7e930cf9e493c5e6ef24b496294f95594c8",
"condition_id": "0x...",
"transaction_hash": "0x...",
"order_hash": "0x...",
"builder": "0x0000000000000000000000000000000000000000000000000000000000000000",
"fee_usd": 0.0,
"timestamp": "1778770590"
}
}
```
## Use Cases
* Whale alert bots (Discord, Telegram)
* Smart money tracking
* Unusual activity detection
* Market sentiment signals
## Example
```bash theme={null}
curl -X POST https://api.polynode.dev/v3/webhooks \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-bot.com/whale-alerts",
"event_types": ["large_trade"],
"filters": {
"min_amount_usd": 500,
"tags": ["politics"]
}
}'
```
## Verified Behavior
* Threshold is inclusive: a \$500.00 trade fires on `min_amount_usd: 500`
* Trades below the threshold are silently skipped
* Enrichment (market, outcome, slug) included on every delivery
# market_created
Source: https://docs.polynode.dev/webhooks/events/market-created
Fires when a new prediction market appears on Polymarket.
## Filters
| Filter | Type | Description |
| ------------- | ---------- | ------------------------------------- |
| `tags` | `string[]` | Only markets with these tags. Max 20. |
| `event_slugs` | `string[]` | Only markets in these events. Max 50. |
Empty filters = all new markets.
## Payload
```json theme={null}
{
"id": "evt_f6a7b8c9-0d1e-2f3a-4b5c-6d7e8f9a0b1c",
"type": "market_created",
"created_at": "2026-05-14T15:11:00.000000000+00:00",
"data": {
"condition_id": "0x9622da56d800d10c93c58fe2f6fc19521b3152bc7f9e21fe97979b39749bc58b",
"question": "Will Ethereum reach $5000 by June 2026?",
"slug": "will-ethereum-reach-5000-june-2026",
"event_slug": "crypto-june-2026",
"tags": "{crypto,ethereum}",
"image_url": "https://polymarket-upload.s3.us-east-2.amazonaws.com/...",
"created_at": "2026-05-14 15:10:45.123456"
}
}
```
## Use Cases
* Market discovery: automatically list new markets on your trading UI
* Analytics: track market creation patterns and categories
* Alert bots: notify users when markets matching their interests appear
## Example
```bash theme={null}
curl -X POST https://api.polynode.dev/v3/webhooks \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/hooks/new-markets",
"event_types": ["market_created"],
"filters": {
"tags": ["crypto"]
}
}'
```
# market_resolved
Source: https://docs.polynode.dev/webhooks/events/market-resolved
Fires when a Polymarket market is resolved. Get the winning outcome, payouts, and market details instantly.
## Filters
| Filter | Type | Description |
| --------------- | ---------- | ---------------------------------------------------------- |
| `condition_ids` | `string[]` | Specific markets to watch. Max 100. |
| `event_slugs` | `string[]` | Events to watch (e.g. `us-presidential-election`). Max 50. |
| `tags` | `string[]` | Tags to watch (e.g. `politics`, `crypto`). Max 20. |
Empty filters = all resolutions.
## Payload
```json theme={null}
{
"id": "evt_e5f6a7b8-9c0d-1e2f-3a4b-5c6d7e8f9a0b",
"type": "market_resolved",
"created_at": "2026-05-14T15:10:30.000000000+00:00",
"data": {
"condition_id": "0x0ecb24de3a8b875cf80ad087a40be0c86ba47e455e89519cf46ee98361769316",
"question": "Will Bitcoin reach $100k in May 2026?",
"slug": "will-bitcoin-reach-100k-may-2026",
"payout_numerators": "{1,0}",
"payout_denominator": "1",
"resolved_at": "1778770810000",
"event_slug": "crypto-may-2026",
"tags": "{crypto,bitcoin}"
}
}
```
## Fields
| Field | Type | Description |
| -------------------- | -------- | ---------------------------------------------------------- |
| `condition_id` | `string` | Market condition ID |
| `question` | `string` | Market question (enriched) |
| `slug` | `string` | Market slug (enriched) |
| `payout_numerators` | `string` | Payout array — `{1,0}` means Yes won, `{0,1}` means No won |
| `payout_denominator` | `string` | Denominator for payout calculation |
| `resolved_at` | `string` | Resolution timestamp (unix milliseconds) |
| `event_slug` | `string` | Parent event slug |
| `tags` | `string` | Market tags |
## Use Cases
* Auto-redemption bots: trigger redemptions immediately after resolution
* Settlement alerts: notify users when their markets resolve
* Analytics pipelines: track resolution patterns and outcomes
## Example
```bash theme={null}
curl -X POST https://api.polynode.dev/v3/webhooks \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/hooks/resolutions",
"event_types": ["market_resolved"],
"filters": {
"tags": ["politics"]
}
}'
```
# merge
Source: https://docs.polynode.dev/webhooks/events/merge
Fires when a wallet merges conditional tokens. Inverse of split — tokens combined back into collateral.
**polynode exclusive** — no other Polymarket data provider offers merge event webhooks.
## Filters
| Filter | Type | Description |
| ------------------ | ---------- | -------------------------- |
| `wallet_addresses` | `string[]` | Wallets to watch. Max 500. |
| `condition_ids` | `string[]` | Markets to watch. Max 100. |
## Payload
Same schema as [`split`](/webhooks/events/split), with `"type": "merge"`.
# neg_risk_conversion
Source: https://docs.polynode.dev/webhooks/events/neg-risk-conversion
Fires on neg-risk conversion events. Track hedging activity in multi-outcome markets.
**polynode exclusive** — no other Polymarket data provider offers NRC event webhooks.
## Filters
| Filter | Type | Description |
| ------------------ | ---------- | -------------------------- |
| `wallet_addresses` | `string[]` | Wallets to watch. Max 500. |
## Payload
```json theme={null}
{
"id": "evt_e5f6a7b8-9c0d-1e2f-3a4b-5c6d7e8f9a0b",
"type": "neg_risk_conversion",
"created_at": "2026-05-14T15:18:00.000000000+00:00",
"data": {
"wallet": "0xdb27bf2ac5d428a9c63dbc910c5e6e75560c0ae5",
"market_id": "0xabc123...",
"timestamp": "1778771000"
}
}
```
## Use Cases
* Risk tracking: NRC events signal hedging in multi-outcome markets (elections, sports)
* Portfolio reconciliation: NRC changes on-chain token balances
* Derivatives analytics: track neg-risk position flows
# order_complete
Source: https://docs.polynode.dev/webhooks/events/order-complete
Fires when a multi-fill order finishes. One aggregated event per order instead of N individual fills.
**polynode exclusive** — no other Polymarket data provider aggregates partial fills into order-level events.
## Filters
| Filter | Type | Description |
| ------------------ | ---------- | ------------------------------------------------------- |
| `wallet_addresses` | `string[]` | Wallets to watch (maker or taker on any fill). Max 500. |
| `min_total_usd` | `number` | Minimum total order size in USD. |
## How It Works
When an order is filled across multiple transactions, polynode buffers the individual fills. After 30 seconds with no new fills for that `order_hash`, the buffered fills are aggregated and delivered as a single event.
## Payload
```json theme={null}
{
"id": "evt_f6a7b8c9-0d1e-2f3a-4b5c-6d7e8f9a0b1c",
"type": "order_complete",
"created_at": "2026-05-14T15:19:00.000000000+00:00",
"data": {
"order_hash": "0x608029a5926da1fc3496ba08062c7e64f66b41a2c7383b5125f3867a68ca0b08",
"fill_count": 12,
"total_usd": 4850.75,
"fills": [
{
"amount_usd": 500.0,
"price": 0.62,
"market": "Will Bitcoin hit $100k by 2026?",
"outcome": "Yes",
"timestamp": "1778770502"
}
]
}
}
```
## Fields
| Field | Type | Description |
| ------------ | -------- | ---------------------------------- |
| `order_hash` | `string` | The CLOB order hash |
| `fill_count` | `number` | Number of partial fills aggregated |
| `total_usd` | `number` | Total USD value across all fills |
| `fills` | `array` | Individual fill details |
## Use Cases
* Order tracking: get one notification per order, not per fill
* Execution analysis: see total fill count, slippage, and average price
* Copy-trading: detect when a leader's order is fully filled
# pnl_change
Source: https://docs.polynode.dev/webhooks/events/pnl-change
Fires when a wallet's realized PnL changes. Track profit milestones, loss alerts, and performance shifts.
## Filters
| Filter | Type | Required | Description |
| -------------------- | ---------- | ----------- | ----------------------------- |
| `wallet_addresses` | `string[]` | **Yes** | Wallets to watch. Max 500. |
| `min_pnl_change_usd` | `number` | Recommended | Minimum PnL delta to trigger. |
## Payload
```json theme={null}
{
"id": "evt_b2c3d4e5-6f7a-8b9c-0d1e-2f3a4b5c6d7e",
"type": "pnl_change",
"created_at": "2026-05-14T15:15:00.000000000+00:00",
"data": {
"wallet": "0xdb27bf2ac5d428a9c63dbc910c5e6e75560c0ae5",
"token_id": "83970135942868582996575481879514955574496723508450032076038091728966335782753",
"realized_pnl": 22053845.83,
"market": "Will Bitcoin hit $100k by 2026?",
"outcome": "Yes"
}
}
```
## Use Cases
* Leader tracking: alert when top traders cross PnL milestones
* Risk monitoring: detect sudden losses in watched portfolios
* Copy-trading signals: PnL changes indicate trade activity
# position_status_change
Source: https://docs.polynode.dev/webhooks/events/position-status-change
Fires when a watched wallet's CTF position changes state, including first observed opens.
**Beta / polynode exclusive** — this event watches explicit wallet filters and detects CTF position lifecycle changes from on-chain ERC-1155 transfer deltas.
## What It Detects
`position_status_change` tracks state transitions for watched wallets:
* `none` -> `open` when the wallet first receives shares for a token after the webhook is active
* `open` -> `closed` when the wallet's active-market balance returns to zero
* `open` -> `redeemable` or `none` -> `redeemable` when the position is in a resolved market and the wallet holds shares
* `redeemable` -> `redeemed` when the resolved-market balance returns to zero
Existing positions are seeded when a new watched wallet is first seen, so the webhook does not backfill historical opens that happened before registration. It emits future transitions from that point forward.
For this beta, `wallet_addresses` is required. Unfiltered/global position-status webhooks are intentionally not enabled.
## Filters
| Filter | Type | Description |
| ------------------ | ---------- | -------------------------------------------------------------------------------- |
| `wallet_addresses` | `string[]` | Required. Wallets to watch. Use lowercase `0x` addresses. Max 500. |
| `token_ids` | `string[]` | Optional outcome token IDs. |
| `condition_ids` | `string[]` | Optional market condition IDs. |
| `event_slugs` | `string[]` | Optional parent event slugs. |
| `tags` | `string[]` | Optional tag filters. |
| `status_from` | `string` | Optional previous status: `none`, `open`, `closed`, `redeemable`, or `redeemed`. |
| `status_to` | `string` | Optional new status: `open`, `closed`, `redeemable`, or `redeemed`. |
Use `status_from: "none"` and `status_to: "open"` to detect a wallet opening a new position after registration.
## Payload
```json theme={null}
{
"id": "evt_c3d4e5f6-7a8b-9c0d-1e2f-3a4b5c6d7e8f",
"type": "position_status_change",
"created_at": "2026-05-26T12:00:00.000000000+00:00",
"data": {
"wallet": "0xdb27bf2ac5d428a9c63dbc910c5e6e75560c0ae5",
"token_id": "83970135942868582996575481879514955574496723508450032076038091728966335782753",
"condition_id": "0x3b7ad6a803daeb1994c840027b95d711d634e79a09ff3b02625600dc9d31a8bc",
"from_status": "none",
"to_status": "open",
"previous_amount": 0,
"new_amount": 150,
"delta_amount": 150,
"transfer_amount": 150,
"source": "erc1155_transfer",
"raw_event_type": "TransferSingle",
"source_event_id": "0x4f606b3d4c58c723e4aeb3051e3ba92100f9b0d9116e5d60f7668c34087c8167_621",
"transaction_hash": "0x4f606b3d4c58c723e4aeb3051e3ba92100f9b0d9116e5d60f7668c34087c8167",
"block_number": 87481303,
"timestamp": "2026-05-26T10:58:22.000000+00:00",
"market": "Will Bitcoin hit $100k by 2026?",
"slug": "will-bitcoin-hit-100k-by-2026",
"event_slug": "bitcoin-100k-2026",
"outcome": "Yes",
"tags": "{Crypto,Bitcoin}"
}
}
```
## Fields
| Field | Type | Description |
| ------------------ | -------- | -------------------------------------------------------------------- |
| `wallet` | `string` | Watched wallet address. |
| `token_id` | `string` | ERC-1155 outcome token ID. |
| `condition_id` | `string` | Market condition ID. |
| `from_status` | `string` | Previous tracked state. `none` means no prior tracked balance/state. |
| `to_status` | `string` | New tracked state. |
| `previous_amount` | `number` | Previous share balance, scaled to normal token units. |
| `new_amount` | `number` | New share balance, scaled to normal token units. |
| `delta_amount` | `number` | Signed balance change for the watched wallet. |
| `transfer_amount` | `number` | Absolute transfer amount from the ERC-1155 event. |
| `source` | `string` | `erc1155_transfer`, `split`, `merge`, or `redemption`. |
| `raw_event_type` | `string` | Raw ERC-1155 event type. |
| `source_event_id` | `string` | Source CTF transfer-delta ID. |
| `transaction_hash` | `string` | Polygon transaction hash. |
| `block_number` | `number` | Polygon block number. |
| `timestamp` | `string` | Source ingestion timestamp. |
| `market` | `string` | Market question, when enrichment is available. |
| `slug` | `string` | Market slug, when enrichment is available. |
| `event_slug` | `string` | Parent event slug. |
| `outcome` | `string` | Outcome label. |
| `tags` | `string` | Market tag array as text. |
## Example
```bash theme={null}
curl -X POST https://api.polynode.dev/v3/webhooks \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/hooks/position-opened",
"event_types": ["position_status_change"],
"filters": {
"wallet_addresses": ["0xdb27bf2ac5d428a9c63dbc910c5e6e75560c0ae5"],
"status_from": "none",
"status_to": "open"
},
"description": "Notify when a watched wallet opens a new Polymarket position"
}'
```
## Use Cases
* Copy-trading: detect when a leader opens a new position, including opens that come from ERC-1155 transfers
* Portfolio automation: react when a watched wallet fully exits a position
* Settlement workflows: detect redemption-style zero-balance transitions for resolved markets
# price_change
Source: https://docs.polynode.dev/webhooks/events/price-change
Fires when a token's price changes. Track price movements across any market.
## Filters
| Filter | Type | Required | Description |
| --------------- | ---------- | ----------- | ------------------------------------------------- |
| `token_ids` | `string[]` | No | Specific tokens to watch. Max 100. |
| `condition_ids` | `string[]` | No | Markets to watch (all tokens in market). Max 100. |
| `min_delta_pct` | `number` | Recommended | Minimum price change percentage to trigger. |
Without `min_delta_pct`, this event fires on every price update — potentially thousands per minute. Set a threshold to avoid overwhelming your endpoint.
## Payload
```json theme={null}
{
"id": "evt_a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"type": "price_change",
"created_at": "2026-05-14T15:12:00.000000000+00:00",
"data": {
"token_id": "83970135942868582996575481879514955574496723508450032076038091728966335782753",
"price": 0.81,
"updated_at": "2026-05-14 15:11:55.123456",
"market": "Will Al Fateh Saudi Club vs. Al Najmah Saudi Club end in a draw?",
"slug": "spl-fat-njm-2026-05-14-draw",
"outcome": "No",
"condition_id": "0x3b7ad6a803daeb1994c840027b95d711d634e79a09ff3b02625600dc9d31a8bc"
}
}
```
## Use Cases
* Price alert bots: notify when a market moves significantly
* Arbitrage monitoring: detect price discrepancies
* Portfolio dashboards: real-time position value updates
## Example
```bash theme={null}
curl -X POST https://api.polynode.dev/v3/webhooks \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/hooks/prices",
"event_types": ["price_change"],
"filters": {
"condition_ids": ["0x9622da56d800d10c93c58fe2f6fc19521b3152bc7f9e21fe97979b39749bc58b"],
"min_delta_pct": 5.0
}
}'
```
# redemption
Source: https://docs.polynode.dev/webhooks/events/redemption
Fires when a wallet redeems shares from a resolved market. Know instantly when a leader cashes out.
**polynode exclusive** — no other Polymarket data provider offers this event type.
## Filters
| Filter | Type | Description |
| ------------------ | ---------- | ----------------------------------- |
| `wallet_addresses` | `string[]` | Wallet addresses to watch. Max 500. |
| `condition_ids` | `string[]` | Market condition IDs. Max 100. |
## Payload
```json theme={null}
{
"id": "evt_d4e5f6a7-8b9c-0d1e-2f3a-4b5c6d7e8f9a",
"type": "redemption",
"created_at": "2026-05-14T15:10:00.000000000+00:00",
"data": {
"wallet": "0xdb27bf2ac5d428a9c63dbc910c5e6e75560c0ae5",
"condition_id": "0x7fb0835c4c761fa690ec4603eb444d080e472193ae477aa3c57f273d44dbddd9",
"timestamp": "1778771000",
"market": "Will Bitcoin hit $100k by 2026?",
"outcome": "Yes"
}
}
```
## Use Cases
* Copy-trading bots: know when a leader redeems (position fully closed at settlement)
* Settlement tracking: monitor which wallets are claiming payouts
* Capital flow analysis: redemptions unlock USDC that may be reinvested
## Example
```bash theme={null}
curl -X POST https://api.polynode.dev/v3/webhooks \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/hooks/redemptions",
"event_types": ["redemption"],
"filters": {
"wallet_addresses": ["0xdb27bf2ac5d428a9c63dbc910c5e6e75560c0ae5"]
}
}'
```
# split
Source: https://docs.polynode.dev/webhooks/events/split
Fires when a wallet splits conditional tokens. Track collateral movements and position restructuring.
**polynode exclusive** — no other Polymarket data provider offers split event webhooks.
## Filters
| Filter | Type | Description |
| ------------------ | ---------- | -------------------------- |
| `wallet_addresses` | `string[]` | Wallets to watch. Max 500. |
| `condition_ids` | `string[]` | Markets to watch. Max 100. |
## Payload
```json theme={null}
{
"id": "evt_d4e5f6a7-8b9c-0d1e-2f3a-4b5c6d7e8f9a",
"type": "split",
"created_at": "2026-05-14T15:17:00.000000000+00:00",
"data": {
"wallet": "0xdb27bf2ac5d428a9c63dbc910c5e6e75560c0ae5",
"condition_id": "0x7fb0835c4c761fa690ec4603eb444d080e472193ae477aa3c57f273d44dbddd9",
"amount": 5000.0,
"timestamp": "1778771000",
"market": "Will Bitcoin hit $100k by 2026?"
}
}
```
## Use Cases
* Position tracking: splits indicate position restructuring
* Portfolio reconciliation: track collateral movements
* Copy-trading: detect complex position management by leaders
# trade
Source: https://docs.polynode.dev/webhooks/events/trade
Fires when a fill occurs involving wallets or markets you're watching.
## Filters
| Filter | Type | Description |
| ------------------ | ---------- | ------------------------------------------------------ |
| `wallet_addresses` | `string[]` | Wallet addresses to watch (maker or taker). Max 500. |
| `token_ids` | `string[]` | Outcome token IDs. Max 100. |
| `condition_ids` | `string[]` | Market condition IDs. Max 100. |
| `event_slugs` | `string[]` | Event slugs (e.g. `us-presidential-election`). Max 50. |
| `tags` | `string[]` | Tag filters (e.g. `politics`, `crypto`). Max 20. |
| `min_amount_usd` | `number` | Minimum trade size in USD. |
| `side` | `string` | Filter by side: `"BUY"` or `"SELL"`. |
If a filter is empty or omitted, it matches all values for that dimension. Multiple filters are AND'd together.
## Payload
```json theme={null}
{
"id": "evt_a60234f0-fe5d-4223-bc6a-75f1e4263eda",
"type": "trade",
"created_at": "2026-05-14T14:55:08.448161187+00:00",
"data": {
"id": "0x4f606b3d4c58c723e4aeb3051e3ba92100f9b0d9116e5d60f7668c34087c8167_621",
"maker": "0x6d3c5bd13984b2de47c3a88ddc455309aab3d294",
"taker": "0xd5bba58c09d18f48a71a3aefc4ff3a66954c0fe1",
"amount_usd": 39.99,
"price": 0.81,
"fee_usd": 0.0,
"direction": "BUY",
"token_id": "83970135942868582996575481879514955574496723508450032076038091728966335782753",
"market": "Will Al Fateh Saudi Club vs. Al Najmah Saudi Club end in a draw?",
"slug": "spl-fat-njm-2026-05-14-draw",
"outcome": "No",
"condition_id": "0x3b7ad6a803daeb1994c840027b95d711d634e79a09ff3b02625600dc9d31a8bc",
"event_slug": "spl-fat-njm-2026-05-14",
"transaction_hash": "0x4f606b3d4c58c723e4aeb3051e3ba92100f9b0d9116e5d60f7668c34087c8167",
"order_hash": "0x608029a5926da1fc3496ba08062c7e64f66b41a2c7383b5125f3867a68ca0b08",
"builder": "0x0000000000000000000000000000000000000000000000000000000000000000",
"timestamp": "1778770502"
}
}
```
## Fields
| Field | Type | Description |
| ------------------ | -------- | -------------------------------------------------------------- |
| `id` | `string` | Fill ID (`{tx_hash}_{log_index}`) |
| `maker` | `string` | Maker wallet address |
| `taker` | `string` | Taker wallet address |
| `amount_usd` | `number` | Trade size in USD |
| `price` | `number` | Token price (0.0 to 1.0) |
| `fee_usd` | `number` | Fee in USD |
| `direction` | `string` | `"BUY"` or `"SELL"` (maker's limit order direction) |
| `token_id` | `string` | ERC-1155 outcome token ID |
| `market` | `string` | Market question (enriched) |
| `slug` | `string` | Market slug (enriched) |
| `outcome` | `string` | Outcome name — `"Yes"`, `"No"`, or specific outcome (enriched) |
| `condition_id` | `string` | Market condition ID |
| `event_slug` | `string` | Parent event slug |
| `transaction_hash` | `string` | On-chain transaction hash |
| `order_hash` | `string` | CLOB order hash |
| `builder` | `string` | Builder address (all zeros if no builder) |
| `timestamp` | `string` | Unix timestamp |
## Example
```bash theme={null}
curl -X POST https://api.polynode.dev/v3/webhooks \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/hooks/trades",
"event_types": ["trade"],
"filters": {
"wallet_addresses": ["0x6d3c5bd13984b2de47c3a88ddc455309aab3d294"],
"min_amount_usd": 50
}
}'
```
# wallet_activity
Source: https://docs.polynode.dev/webhooks/events/wallet-activity
Fires on any action by a watched wallet — trades, redemptions, splits, merges, and neg-risk conversions. One webhook replaces five.
**polynode exclusive** — no other Polymarket data provider offers this event type.
## Filters
| Filter | Type | Required | Description |
| ------------------ | ---------- | -------- | ----------------------------------- |
| `wallet_addresses` | `string[]` | **Yes** | Wallet addresses to watch. Max 500. |
## Payload
The payload matches the underlying event type, with an additional `activity_type` field.
```json theme={null}
{
"id": "evt_f8a21c4d-3e5b-4a2f-b1c0-9d8e7f6a5b4c",
"type": "wallet_activity",
"created_at": "2026-05-14T14:55:08.123456789+00:00",
"data": {
"activity_type": "trade",
"id": "0x6c70140012dea826e1d0d9e5371b85387f6307da03166f5c0146efbfa2636644_1005",
"maker": "0x6d3c5bd13984b2de47c3a88ddc455309aab3d294",
"taker": "0x84cfffc3f16dcc353094de30d4a45226eccd2f63",
"amount_usd": 1.45,
"price": 0.36,
"direction": "BUY",
"market": "Will US Lecce win on 2026-05-17?",
"slug": "sea-sas-lec-2026-05-17-lec",
"outcome": "Yes",
"condition_id": "0x9622da56d800d10c93c58fe2f6fc19521b3152bc7f9e21fe97979b39749bc58b",
"event_slug": "sea-sas-lec-2026-05-17",
"transaction_hash": "0x6c70140012dea826e1d0d9e5371b85387f6307da03166f5c0146efbfa2636644",
"order_hash": "0x7631f2317323ce2cdb1c2851d8f5ea898cd0e8711f03c2e1c1698ce7eb77b0db",
"builder": "0x0000000000000000000000000000000000000000000000000000000000000000",
"fee_usd": 0.0,
"timestamp": "1778770705"
}
}
```
## Activity Types
The `activity_type` field tells you which underlying event fired:
| Value | Activity | Description |
| --------------------- | --------------------- | ------------------------- |
| `trade` | trade fill | Wallet was maker or taker |
| `redemption` | redemption | Wallet redeemed shares |
| `split` | split | Wallet split tokens |
| `merge` | merge | Wallet merged tokens |
| `neg_risk_conversion` | neg\_risk\_conversion | NRC event |
## Use Cases
* Copy-trading bots that need a single feed per leader wallet
* Portfolio trackers monitoring all wallet activity
* Alert systems that fire on any wallet movement
## Example
```bash theme={null}
curl -X POST https://api.polynode.dev/v3/webhooks \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/hooks/wallet",
"event_types": ["wallet_activity"],
"filters": {
"wallet_addresses": [
"0x6d3c5bd13984b2de47c3a88ddc455309aab3d294",
"0xdb27bf2ac5d428a9c63dbc910c5e6e75560c0ae5"
]
}
}'
```
# Create Webhook
Source: https://docs.polynode.dev/webhooks/management/create
Register a new webhook to receive event notifications.
## Endpoint
```
POST /v3/webhooks
```
## Request Body
| Field | Type | Required | Description |
| ------------- | ---------- | -------- | ------------------------------------------------------------------------------- |
| `url` | `string` | **Yes** | Delivery URL (must start with `https://`) |
| `event_types` | `string[]` | **Yes** | Event types to subscribe to (see [event types](/webhooks/overview#event-types)) |
| `filters` | `object` | No | Filter criteria (see below) |
| `secret` | `string` | No | HMAC signing secret. Auto-generated if omitted. |
| `description` | `string` | No | Human-readable label |
### Filter Object
All filter fields are optional. Empty or omitted filters match everything for that dimension. Multiple filters are AND'd together.
```json theme={null}
{
"wallet_addresses": ["0x1234..."],
"token_ids": ["12345..."],
"condition_ids": ["0xabcd..."],
"event_slugs": ["us-presidential-election"],
"tags": ["politics", "crypto"],
"builder_codes": ["0x5678..."],
"receiver_addresses": ["0x9abc..."],
"min_amount_usd": 100,
"min_fee_usd": 1.0,
"min_delta_pct": 5.0,
"min_pnl_change_usd": 50.0,
"min_total_usd": 500,
"side": "BUY"
}
```
## Example
```bash theme={null}
curl -X POST https://api.polynode.dev/v3/webhooks \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/hooks/polymarket",
"event_types": ["trade", "redemption"],
"filters": {
"wallet_addresses": ["0x6d3c5bd13984b2de47c3a88ddc455309aab3d294"],
"min_amount_usd": 100
},
"description": "Track whale trades and redemptions"
}'
```
## Response (201 Created)
```json theme={null}
{
"id": "wh_451e06ca0534",
"url": "https://your-app.com/hooks/polymarket",
"event_types": ["trade", "redemption"],
"filters": {
"wallet_addresses": ["0x6d3c5bd13984b2de47c3a88ddc455309aab3d294"],
"min_amount_usd": 100
},
"secret": "whsec_0e4535d1565e4e4d9e24debf3e40f836",
"active": true,
"description": "Track whale trades and redemptions",
"created_at": "2026-05-14T14:54:08.019035820+00:00"
}
```
Save the `secret` value — it is only returned on creation. You'll need it to verify webhook signatures.
# Delete Webhook
Source: https://docs.polynode.dev/webhooks/management/delete
Permanently delete a webhook and its delivery history.
## Endpoint
```
DELETE /v3/webhooks/:id
```
## Example
```bash theme={null}
curl -X DELETE https://api.polynode.dev/v3/webhooks/wh_451e06ca0534 \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response
```json theme={null}
{
"deleted": true
}
```
# Delivery History
Source: https://docs.polynode.dev/webhooks/management/deliveries
View the delivery history for a webhook — status codes, latency, errors, and retry attempts.
## Endpoint
```
GET /v3/webhooks/:id/deliveries?limit=50&offset=0
```
## Query Parameters
| Param | Default | Description |
| -------- | ------- | ----------------------- |
| `limit` | 50 | Max results (up to 200) |
| `offset` | 0 | Pagination offset |
## Example
```bash theme={null}
curl https://api.polynode.dev/v3/webhooks/wh_451e06ca0534/deliveries \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response
```json theme={null}
{
"deliveries": [
{
"event_type": "trade",
"event_id": "evt_a1b2c3d4",
"status_code": 200,
"latency_ms": 142,
"attempt": 1,
"success": true,
"error": null,
"delivered_at": "2026-05-14T14:59:58+00:00"
},
{
"event_type": "trade",
"event_id": "evt_b2c3d4e5",
"status_code": null,
"latency_ms": 5001,
"attempt": 2,
"success": false,
"error": "error sending request for url (https://your-app.com/hooks): timeout",
"delivered_at": "2026-05-14T14:59:45+00:00"
}
],
"count": 2
}
```
## Fields
| Field | Type | Description |
| -------------- | -------------- | ----------------------------------------------------------------------------------------------------------- |
| `event_type` | `string` | The event type that triggered this delivery |
| `event_id` | `string` | Unique event identifier |
| `status_code` | `number\|null` | HTTP response status. `null` if the request failed before getting a response (timeout, connection refused). |
| `latency_ms` | `number` | Round-trip time in milliseconds |
| `attempt` | `number` | Delivery attempt number (1 = first try, 2+ = retries) |
| `success` | `boolean` | `true` if HTTP 2xx received within 5 seconds |
| `error` | `string\|null` | Error message on failure |
| `delivered_at` | `string` | ISO timestamp of delivery attempt |
Delivery history is retained for 7 days.
# List Event Types
Source: https://docs.polynode.dev/webhooks/management/event-types
Retrieve all available webhook event types with descriptions and supported filters.
## Endpoint
```
GET /v3/webhooks/event-types
```
## Example
```bash theme={null}
curl https://api.polynode.dev/v3/webhooks/event-types \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response
```json theme={null}
{
"event_types": [
{
"name": "trade",
"description": "Fill involving watched wallets or markets",
"filters": ["wallet_addresses", "token_ids", "condition_ids", "event_slugs", "tags", "min_amount_usd", "side"],
"exclusive": false
},
{
"name": "redemption",
"description": "Wallet redeems shares from resolved market",
"filters": ["wallet_addresses", "condition_ids"],
"exclusive": true
},
{
"name": "position_status_change",
"description": "Watched-wallet CTF position transitions, including first observed opens",
"filters": ["wallet_addresses", "token_ids", "condition_ids", "event_slugs", "tags", "status_from", "status_to"],
"exclusive": true
}
]
}
```
Events with `"exclusive": true` are only available through polynode.
# Get Webhook
Source: https://docs.polynode.dev/webhooks/management/get
Retrieve one webhook subscription by id.
```text theme={null}
GET /v3/webhooks/{id}
```
Returns the current configuration for a single webhook.
## Example
```bash theme={null}
curl "https://api.polynode.dev/v3/webhooks/wh_451e06ca0534" \
-H "x-api-key: pn_live_..."
```
```json theme={null}
{
"id": "wh_451e06ca0534",
"url": "https://your-app.com/hooks/polymarket",
"event_types": ["trade", "market_resolved"],
"filters": {
"wallet_addresses": ["0x6d3c5bd13984b2de47c3a88ddc455309aab3d294"],
"min_amount_usd": 100
},
"description": "Track whale trades",
"active": true,
"created_at": "2026-05-14T14:55:08Z",
"updated_at": "2026-05-14T14:55:08Z"
}
```
## Errors
| Status | Meaning |
| ------ | ---------------------------------- |
| `401` | Missing or invalid API key |
| `404` | Webhook not found for this API key |
# List Webhooks
Source: https://docs.polynode.dev/webhooks/management/list
List all webhooks registered to your API key.
## Endpoint
```
GET /v3/webhooks
```
## Response
```json theme={null}
{
"webhooks": [
{
"id": "wh_451e06ca0534",
"url": "https://your-app.com/hooks/polymarket",
"event_types": ["trade", "redemption"],
"filters": {
"wallet_addresses": ["0x6d3c..."],
"min_amount_usd": 100
},
"active": true,
"description": "Track whale trades",
"has_secret": true,
"created_at": "2026-05-14T14:54:08+00:00",
"updated_at": "2026-05-14T14:54:08+00:00"
}
],
"count": 1
}
```
# Rotate Secret
Source: https://docs.polynode.dev/webhooks/management/rotate-secret
Generate a new HMAC signing secret for a webhook.
## Endpoint
```
POST /v3/webhooks/:id/rotate-secret
```
Generates a new signing secret. The old secret is immediately invalidated. Update your verification code before the next delivery arrives.
## Example
```bash theme={null}
curl -X POST https://api.polynode.dev/v3/webhooks/wh_451e06ca0534/rotate-secret \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response
```json theme={null}
{
"secret": "whsec_new_secret_value_here_32chars"
}
```
Save this value immediately. It is only returned once.
# Test Webhook
Source: https://docs.polynode.dev/webhooks/management/test
Send a test delivery to verify your endpoint is reachable and configured correctly.
## Endpoint
```
POST /v3/webhooks/:id/test
```
Sends a synthetic event with `"test": true` in the payload. The event type is the first one in your webhook's `event_types` list.
## Example
```bash theme={null}
curl -X POST https://api.polynode.dev/v3/webhooks/wh_451e06ca0534/test \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response
```json theme={null}
{
"delivered": true,
"status_code": 200,
"latency_ms": 89
}
```
## Test Payload
Your endpoint receives:
```json theme={null}
{
"id": "test_a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"type": "trade",
"created_at": "2026-05-14T15:00:00+00:00",
"data": {
"test": true,
"message": "This is a test delivery from polynode webhooks"
}
}
```
The delivery includes all standard webhook headers (`X-Webhook-ID`, `X-Webhook-Event`, `X-Webhook-Timestamp`, `X-Webhook-Signature`) so you can verify your signature validation logic.
# Update Webhook
Source: https://docs.polynode.dev/webhooks/management/update
Update a webhook's URL, filters, event types, or active status.
## Endpoint
```
PUT /v3/webhooks/:id
```
## Request Body
All fields are optional. Only provided fields are updated.
| Field | Type | Description |
| ------------- | ---------- | ---------------------------------- |
| `url` | `string` | New delivery URL |
| `event_types` | `string[]` | Replace event type subscriptions |
| `filters` | `object` | Replace filter criteria |
| `active` | `boolean` | Pause (`false`) or resume (`true`) |
| `description` | `string` | Update label |
## Example — Pause a webhook
```bash theme={null}
curl -X PUT https://api.polynode.dev/v3/webhooks/wh_451e06ca0534 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"active": false}'
```
## Example — Change filters
```bash theme={null}
curl -X PUT https://api.polynode.dev/v3/webhooks/wh_451e06ca0534 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"filters": {
"wallet_addresses": ["0xnewwallet..."],
"min_amount_usd": 500
}
}'
```
## Response
Returns the full updated webhook object (same schema as [List](/webhooks/management/list)).
# Webhooks
Source: https://docs.polynode.dev/webhooks/overview
Push notifications for Polymarket events — trades, price changes, market resolutions, and more.
## Overview
polynode webhooks deliver real-time event notifications to your HTTP endpoint. Instead of polling the API, register a webhook and receive a POST request whenever matching events occur.
* **16 event types** covering trades, prices, markets, positions, and lifecycle events
* **Enriched payloads** — every delivery includes market names, outcomes, slugs, and condition IDs
* **HMAC-SHA256 signed** — verify every delivery with your webhook secret
* **3-30 second polling** — event cadence depends on the event type
* **Delivery transparency** — full history of every delivery attempt, status code, and latency
## Quick Start
### 1. Create a webhook
```bash theme={null}
curl -X POST https://api.polynode.dev/v3/webhooks \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/hooks/polymarket",
"event_types": ["trade", "market_resolved"],
"filters": {
"wallet_addresses": ["0x6d3c5bd13984b2de47c3a88ddc455309aab3d294"],
"min_amount_usd": 100
},
"description": "Track whale trades"
}'
```
### 2. Receive deliveries
Every delivery is a POST to your URL with this structure:
```json theme={null}
{
"id": "evt_a60234f0-fe5d-4223-bc6a-75f1e4263eda",
"type": "trade",
"created_at": "2026-05-14T14:55:08.448161187+00:00",
"data": {
"maker": "0x6d3c5bd13984b2de47c3a88ddc455309aab3d294",
"taker": "0xd5bba58c09d18f48a71a3aefc4ff3a66954c0fe1",
"amount_usd": 39.99,
"price": 0.81,
"direction": "BUY",
"market": "Will Al Fateh Saudi Club vs. Al Najmah Saudi Club end in a draw?",
"slug": "spl-fat-njm-2026-05-14-draw",
"outcome": "No",
"condition_id": "0x3b7ad6a803daeb1994c840027b95d711d634e79a09ff3b02625600dc9d31a8bc",
"event_slug": "spl-fat-njm-2026-05-14",
"transaction_hash": "0x4f606b3d4c58c723e4aeb3051e3ba92100f9b0d9116e5d60f7668c34087c8167",
"order_hash": "0x608029a5926da1fc3496ba08062c7e64f66b41a2c7383b5125f3867a68ca0b08",
"builder": "0x0000000000000000000000000000000000000000000000000000000000000000",
"fee_usd": 0.0,
"timestamp": "1778770502"
}
}
```
### 3. Verify the signature
Every delivery includes HMAC-SHA256 headers:
```
X-Webhook-ID: wh_451e06ca0534
X-Webhook-Event: trade
X-Webhook-Timestamp: 1778770328
X-Webhook-Signature: sha256=3357c641560d55fb46edccb85d3daad3c8099806b7d5d5f60088afb759c0db29
```
Verify with:
```python theme={null}
import hmac, hashlib
expected = hmac.new(
secret.encode(),
f"{timestamp}.{body}".encode(),
hashlib.sha256
).hexdigest()
assert hmac.compare_digest(f"sha256={expected}", signature_header)
```
## Event Types
| Event | Description | Polling | Exclusive |
| ------------------------------------------------------------------- | ----------------------------------------- | ------- | ------------- |
| [`trade`](/webhooks/events/trade) | Fill involving watched wallets or markets | 3s | |
| [`large_trade`](/webhooks/events/large-trade) | Fill exceeding USD threshold | 3s | |
| [`builder_trade`](/webhooks/events/builder-trade) | Fill routed through a specific builder | 3s | polynode only |
| [`first_trade`](/webhooks/events/first-trade) | Wallet's first-ever Polymarket trade | 3s | |
| [`price_change`](/webhooks/events/price-change) | Token price moves beyond threshold | 10s | |
| [`market_resolved`](/webhooks/events/market-resolved) | Market resolved with payouts | 10s | |
| [`market_created`](/webhooks/events/market-created) | New market appears | 30s | |
| [`pnl_change`](/webhooks/events/pnl-change) | Wallet PnL changes beyond threshold | 15s | |
| [`position_status_change`](/webhooks/events/position-status-change) | Watched-wallet position opens/exits | 30s | polynode only |
| [`redemption`](/webhooks/events/redemption) | Wallet redeems shares | 15s | polynode only |
| [`split`](/webhooks/events/split) | Wallet splits conditional tokens | 15s | polynode only |
| [`merge`](/webhooks/events/merge) | Wallet merges conditional tokens | 15s | polynode only |
| [`neg_risk_conversion`](/webhooks/events/neg-risk-conversion) | Neg-risk conversion event | 15s | polynode only |
| [`fee_earned`](/webhooks/events/fee-earned) | Fee receiver gets paid | 15s | polynode only |
| [`wallet_activity`](/webhooks/events/wallet-activity) | Any action by a watched wallet | 3-15s | polynode only |
| [`order_complete`](/webhooks/events/order-complete) | Multi-fill order finishes | \~33s | polynode only |
Events marked **polynode only** are exclusive to polynode — no other Polymarket data provider offers them.
## Management API
| Method | Endpoint | Description |
| -------- | -------------------------------- | ---------------------------------------------------- |
| `POST` | `/v3/webhooks` | [Create webhook](/webhooks/management/create) |
| `GET` | `/v3/webhooks` | [List webhooks](/webhooks/management/list) |
| `GET` | `/v3/webhooks/:id` | [Get webhook](/webhooks/management/get) |
| `PUT` | `/v3/webhooks/:id` | [Update webhook](/webhooks/management/update) |
| `DELETE` | `/v3/webhooks/:id` | [Delete webhook](/webhooks/management/delete) |
| `POST` | `/v3/webhooks/:id/test` | [Send test delivery](/webhooks/management/test) |
| `POST` | `/v3/webhooks/:id/rotate-secret` | [Rotate secret](/webhooks/management/rotate-secret) |
| `GET` | `/v3/webhooks/:id/deliveries` | [Delivery history](/webhooks/management/deliveries) |
| `GET` | `/v3/webhooks/event-types` | [List event types](/webhooks/management/event-types) |
## Retry Policy
| Attempt | Delay |
| ------- | ---------- |
| 1 | Immediate |
| 2 | 10 seconds |
| 3 | 60 seconds |
| 4 | 5 minutes |
| 5 | 1 hour |
A delivery is successful if your endpoint returns HTTP 2xx within 5 seconds. Failed deliveries are visible in the [delivery history](/webhooks/management/deliveries) endpoint.
## Limits
| Tier | Max Webhooks | Max Deliveries/day |
| ---------- | ------------ | ------------------ |
| Starter | 10 | 50,000 |
| Pro | 50 | 500,000 |
| Enterprise | 500 | Unlimited |
# Combo Stream
Source: https://docs.polynode.dev/websocket/combos
Real-time Polymarket combo executions and enriched confirmation events.
PolyNode streams Polymarket combo activity from on-chain combo transactions. The stream follows the same lifecycle as standard settlements: pending events come from mempool calldata, then confirmation events come from on-chain receipts.
This is the on-chain combo stream. Polymarket's RFQ gateway is an off-chain maker/quoter channel. Use this stream to monitor combo executions, fills, confirmations, and enriched lifecycle actions as they hit Polygon.
## Subscribe
```json theme={null}
{
"action": "subscribe",
"type": "combos"
}
```
The default `combos` preset emits:
| Event | Timing | Purpose |
| --------------------- | ------------------- | ----------------------------------------------------------------------------------- |
| `combo_execution` | Pending / mempool | Combo order execution decoded before confirmation. |
| `combo_status_update` | Confirmed / receipt | Confirmation, exact receipt fills, fees, transfers, lifecycle details, and latency. |
Every customer-facing combo event is enrichment-gated. If a combo leg cannot be resolved to market metadata, the event is held rather than emitted partially.
## Full combo surface
Advanced users can explicitly request lifecycle and approval events:
```json theme={null}
{
"action": "subscribe",
"type": "combos",
"filters": {
"event_types": [
"combo_execution",
"combo_status_update",
"combo_lifecycle",
"combo_approval"
]
}
}
```
Lifecycle events are emitted when their legs are present and enriched. AutoRedeemer redemption confirmations are also emitted when they are condition-only, because the receipt provides the user wallet, condition ID, exact payout, normalized payout, and log index without requiring a follow-up lookup.
PM2 AutoRedeemer redemptions are available two ways: as `combo_lifecycle` / `combo_status_update.lifecycle_events[]` for combo-native consumers, and as standard `redemption` events for existing redemption and wallet subscribers.
## Covered on-chain surface
The combo stream covers the Polymarket on-chain contracts used for combo execution and position lifecycle activity:
| Contract | Address | Surface |
| ------------------- | -------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| ExchangeV3 | `0xe3333700cA9d93003F00f0F71f8515005F6c00Aa` | Combo order execution and confirmation. |
| PositionManager | `0x006F54F7f9A22e0000CC2AB60031000000ae9fEF` | Position transfers and approvals. |
| BinaryModule | `0x1000008dD9001B968442c1000017eaE6E0dA00Ba` | Split, merge, redeem, migration, and result lifecycle calls. |
| NegRiskModule | `0x200000900045e3B6259600682756002200028933` | Negative-risk split, merge, redeem, convert, migration, and result calls. |
| CombinatorialModule | `0x30000034706C7d8e12009DAB006Be20000c031A8` | Prepare, split, merge, extract, inject, wrap, unwrap, compress, redeem, and basket conversion. |
| Router | `0x12121212006e4CD160D18e3f00711DA5c3372600` | Router split, merge, redeem, horizontal split/merge, and convert. |
| AutoRedeemer | `0xa1200000d0002264C9a1698e001292D00E1b00af` | Auto redeem calls and confirmations. |
PositionManager approvals for ExchangeV3 and AutoRedeemer are available through `combo_approval` when requested explicitly. Collateral ERC20 allowance approvals are not part of the default combo stream.
## Example
```javascript theme={null}
import WebSocket from "ws";
const ws = new WebSocket("wss://ws.polynode.dev/ws?key=pn_live_YOUR_KEY");
const pending = new Map();
ws.on("open", () => {
ws.send(JSON.stringify({ action: "subscribe", type: "combos" }));
});
ws.on("message", (raw) => {
const msg = JSON.parse(raw);
if (msg.type === "combo_execution") {
pending.set(msg.data.tx_hash, msg.data);
console.log("pending combo", msg.data.tx_hash, msg.data.legs.length, "legs");
}
if (msg.type === "combo_status_update") {
const original = pending.get(msg.data.tx_hash);
console.log("confirmed combo", msg.data.tx_hash, {
latency_ms: msg.data.latency_ms,
pending_seen: Boolean(original),
fills: msg.data.confirmed_fills?.length ?? 0
});
}
});
```
## Filters
Combo subscriptions support the standard WebSocket filters plus combo-specific identifiers:
```json theme={null}
{
"action": "subscribe",
"type": "combos",
"filters": {
"wallets": ["0xMakerOrTaker..."],
"leg_position_ids": ["123456789..."],
"tokens": ["123456789..."],
"combo_condition_ids": ["0xcombo..."],
"condition_ids": ["0xleg_or_combo_condition..."],
"event_ids": ["0xevent..."],
"module_ids": [3],
"side": "YES",
"direction": "BUY",
"status": "confirmed",
"min_size": 100
}
}
```
`tokens` and `leg_position_ids` both match combo position IDs and leg position IDs. Use `leg_position_ids` when you want the filter to be self-documenting.
To receive only combo lifecycle redemptions:
```json theme={null}
{
"action": "subscribe",
"type": "combos",
"filters": {
"event_types": ["combo_lifecycle"],
"action": "Redeem"
}
}
```
## Event reference
* [`combo_execution`](/websocket/events/combo-execution)
* [`combo_status_update`](/websocket/events/combo-status-update)
* [`combo_lifecycle`](/websocket/events/combo-lifecycle)
* [`combo_approval`](/websocket/events/combo-approval)
# Compression
Source: https://docs.polynode.dev/websocket/compression
Reduce WebSocket bandwidth by ~60% with built-in zlib compression.
**Recommended for production.** Compression reduces bandwidth by \~60% with no latency impact.
Every event is compressed once on the server and delivered to all subscribers — zero per-connection overhead.
Enable it by adding `&compress=zlib` to your connection URL.
## How it works
When you connect with `&compress=zlib`, the server sends settlement events as **deflate-compressed binary frames** instead of JSON text frames. Your client decompresses each binary frame with standard zlib `inflateRaw` — available in every language.
Control messages (subscribe acknowledgments, errors, heartbeats, pong) are still sent as normal JSON text, so you can always read them directly.
```
wss://ws.polynode.dev/ws?key=pn_live_YOUR_KEY&compress=zlib
```
## Bandwidth savings
Measured in production on real settlement data:
| Mode | Per-connection | Max connections (1 Gbps) | Max connections (10 Gbps) |
| ---------------- | -------------- | ------------------------ | ------------------------- |
| **Uncompressed** | \~0.78 Mbps | \~1,000 | \~10,000 |
| **Compressed** | \~0.32 Mbps | \~2,500 | \~25,000 |
Compression is applied **once per event** on the server, then the same compressed bytes are fanned out to all compressed subscribers. There is no per-connection compression overhead — it scales the same as uncompressed.
## Client examples
```javascript theme={null}
import WebSocket from "ws";
import { inflateRawSync } from "zlib";
const ws = new WebSocket(
"wss://ws.polynode.dev/ws?key=pn_live_YOUR_KEY&compress=zlib"
);
ws.on("open", () => {
ws.send(JSON.stringify({ action: "subscribe", type: "settlements" }));
});
ws.on("message", (data, isBinary) => {
let event;
if (isBinary) {
// Compressed event — decompress with inflateRaw
const json = inflateRawSync(data).toString();
event = JSON.parse(json);
} else {
// Control message (subscribe ack, heartbeat, error)
event = JSON.parse(data.toString());
}
console.log(event.type, event.data?.market_title);
});
```
```python theme={null}
import json
import zlib
import websocket
def on_message(ws, message, is_binary=False):
if isinstance(message, bytes):
# Compressed event — decompress with raw deflate
json_str = zlib.decompress(message, -zlib.MAX_WBITS).decode()
event = json.loads(json_str)
else:
# Control message (subscribe ack, heartbeat, error)
event = json.loads(message)
print(event["type"], event.get("data", {}).get("market_title"))
ws = websocket.WebSocketApp(
"wss://ws.polynode.dev/ws?key=pn_live_YOUR_KEY&compress=zlib",
on_open=lambda ws: ws.send(json.dumps({
"action": "subscribe", "type": "settlements"
})),
on_message=on_message,
)
ws.run_forever()
```
With `websockets` (async):
```python theme={null}
import asyncio
import json
import zlib
import websockets
async def main():
async with websockets.connect(
"wss://ws.polynode.dev/ws?key=pn_live_YOUR_KEY&compress=zlib"
) as ws:
await ws.send(json.dumps({
"action": "subscribe", "type": "settlements"
}))
async for message in ws:
if isinstance(message, bytes):
event = json.loads(
zlib.decompress(message, -zlib.MAX_WBITS)
)
else:
event = json.loads(message)
print(event["type"])
asyncio.run(main())
```
```go theme={null}
package main
import (
"bytes"
"compress/flate"
"encoding/json"
"fmt"
"io"
"log"
"github.com/gorilla/websocket"
)
func main() {
c, _, err := websocket.DefaultDialer.Dial(
"wss://ws.polynode.dev/ws?key=pn_live_YOUR_KEY&compress=zlib", nil,
)
if err != nil { log.Fatal(err) }
defer c.Close()
c.WriteJSON(map[string]string{
"action": "subscribe", "type": "settlements",
})
for {
msgType, data, err := c.ReadMessage()
if err != nil { log.Fatal(err) }
var event map[string]interface{}
if msgType == websocket.BinaryMessage {
// Compressed — decompress with raw deflate
r := flate.NewReader(bytes.NewReader(data))
decompressed, _ := io.ReadAll(r)
r.Close()
json.Unmarshal(decompressed, &event)
} else {
json.Unmarshal(data, &event)
}
fmt.Println(event["type"])
}
}
```
```rust theme={null}
use flate2::read::DeflateDecoder;
use std::io::Read;
use tungstenite::{connect, Message};
fn main() {
let (mut ws, _) = connect(
"wss://ws.polynode.dev/ws?key=pn_live_YOUR_KEY&compress=zlib"
).unwrap();
ws.send(Message::Text(
r#"{"action":"subscribe","type":"settlements"}"#.into()
)).unwrap();
loop {
let msg = ws.read().unwrap();
let json_str = match msg {
Message::Binary(data) => {
// Compressed — decompress with raw deflate
let mut decoder = DeflateDecoder::new(&data[..]);
let mut s = String::new();
decoder.read_to_string(&mut s).unwrap();
s
}
Message::Text(s) => s.to_string(),
_ => continue,
};
let event: serde_json::Value = serde_json::from_str(&json_str).unwrap();
println!("{}", event["type"]);
}
}
```
```html theme={null}
```
## Key details
Only live event frames (settlements, status updates, trades) are sent as compressed binary.
These are always sent as normal JSON text:
* `{"type": "snapshot", ...}` — initial event snapshot
* `{"type": "subscribed", ...}` — subscription acknowledgment
* `{"type": "unsubscribed", ...}` — unsubscribe confirmation
* `{"type": "heartbeat", ...}` — server heartbeat
* `{"type": "pong"}` — ping response
* `{"type": "error", ...}` — error messages
Check the WebSocket frame type:
* **Binary frame** → compressed event, decompress with `inflateRaw`
* **Text frame** → normal JSON, parse directly
Every WebSocket library exposes this distinction (e.g. `isBinary` in Node.js `ws`, `isinstance(msg, bytes)` in Python).
Raw deflate (RFC 1951) via the `flate2` Rust crate with fast compression level.
Decompress with `inflateRaw` / `zlib.decompress(data, -zlib.MAX_WBITS)` — **not** `inflate` or `gunzip`.
No measurable impact. Each event is compressed once on the server (\~0.05ms for a 1.2 KB message),
then the same compressed bytes are sent to all compressed subscribers via zero-copy fan-out.
Decompression on the client is equally fast.
Compression is set per connection at connect time. You cannot toggle it mid-session.
Different connections from the same API key can use different modes.
## When to use compression
| Scenario | Recommendation |
| ---------------------------------------------- | ------------------------------------------------------------------------------------- |
| **Production bots / trading systems** | Use compression — saves bandwidth, no downside |
| **Prototyping / debugging** | Skip compression — raw JSON is easier to inspect |
| **High-frequency firehose (full data stream)** | Use compression — critical at scale |
| **Browser dashboards** | Use compression with [pako](https://github.com/nichn4nd/pako) — reduces data transfer |
# Block Event
Source: https://docs.polynode.dev/websocket/events/block
New Polygon block with Polymarket-specific statistics.
Emitted for every new Polygon block. Includes Polymarket-specific aggregates — settlement count, trade volume, and how many of the block's transactions involved Polymarket.
```json theme={null}
{
"type": "block",
"timestamp": 1774515147000,
"data": {
"event_type": "block",
"block_number": 84697556,
"block_hash": "0x7b7c3146decd46ca8d271620470455a44066b5e2fac557f2290bfbee5c7c5bb1",
"timestamp": 1774515147000,
"parent_hash": "0xd2cace3840df7c061d68b14c49d420fa6a32d7173eeffd966236c2c0dfc60738",
"tx_count": 268,
"polymarket_tx_count": 0,
"settlement_count": 0,
"trade_volume_usd": 8717.458152
}
}
```
## Fields
Always `"block"`.
Block timestamp in Unix milliseconds.
Block height.
Block hash (0x-prefixed).
Block timestamp in Unix milliseconds.
Parent block hash.
Total transactions in the block.
Transactions involving Polymarket contracts.
Number of Polymarket settlements in this block.
Total Polymarket trade volume in this block (USD).
## Use cases
* **Network monitoring** — track block production rate and Polymarket's share of network activity
* **Volume dashboards** — aggregate `trade_volume_usd` over time for real-time volume charts
* **Block explorer** — correlate settlements with their containing blocks
# Combo Approval Event
Source: https://docs.polynode.dev/websocket/events/combo-approval
PositionManager approval events relevant to combo settlement and auto-redemption.
Emitted for approvals that affect combo settlement or redeemability.
The default combo stream does not include approvals. Request `combo_approval` explicitly if you need approval monitoring. Collateral ERC20 allowance approvals are intentionally not part of the default customer stream.
```json theme={null}
{
"type": "combo_approval",
"timestamp": 1781054112400,
"data": {
"event_type": "combo_approval",
"tx_hash": "0xabc...",
"status": "confirmed",
"detected_at": 1781054112400,
"block_number": 88232739,
"approval_type": "position_manager_approval_for_all",
"owner": "0xOwner...",
"operator": "0xe3333700cA9d93003F00f0F71f8515005F6c00Aa",
"approved": true,
"target_role": "ExchangeV3"
}
}
```
## Subscribing
```json theme={null}
{
"action": "subscribe",
"type": "combos",
"filters": {
"event_types": ["combo_approval"]
}
}
```
## Fields
Polygon transaction hash.
`pending` or `confirmed`.
Unix milliseconds when PolyNode first saw the approval transaction.
Polygon block number once confirmed.
Approval class. Currently `position_manager_approval_for_all` or `collateral_allowance` when decoded.
Wallet granting the approval.
Approved operator or spender.
PositionManager approval status.
Raw approved amount when the decoded approval is allowance-based.
Known combo role for the operator, such as `ExchangeV3` or `AutoRedeemer`.
# Combo Execution Event
Source: https://docs.polynode.dev/websocket/events/combo-execution
Pending Polymarket combo execution decoded from ExchangeV3 calldata.
Emitted when PolyNode detects an ExchangeV3 combo execution before the transaction confirms on-chain.
```json theme={null}
{
"type": "combo_execution",
"timestamp": 1781054110000,
"data": {
"event_type": "combo_execution",
"tx_hash": "0xb0411705aaefc17991e4121acecd8aad03901ddc359cfdb7cf8773f46942927a",
"status": "pending",
"detected_at": 1781054110000,
"block_number": null,
"combo_condition_id": "0xcombo...",
"yes_position_id": "123...",
"no_position_id": "456...",
"leg_position_ids": ["111...", "222..."],
"legs_total": 2,
"legs": [
{
"position_id": "111...",
"module_id": 3,
"module_name": "CombinatorialModule",
"condition_id": "0xleg...",
"outcome_index": 0,
"leg_outcome_label": "Yes",
"market": {
"title": "Will Team A win?",
"slug": "will-team-a-win",
"image": "https://...",
"event_title": "Match Winner",
"event_slug": "match-winner",
"outcomes": ["Yes", "No"],
"token_ids": ["111...", "333..."]
}
}
],
"direction": "BUY",
"side": "YES",
"price_e6": "450000",
"size_e6": "1000000",
"amount_usdc": 1,
"notional_usdc": 1,
"maker_address": "0xMaker...",
"signer_address": "0xSigner...",
"taker_address": "0xTaker...",
"order_hashes": ["0xorder..."]
}
}
```
## Fields
Always `"combo_execution"`.
Unix milliseconds when PolyNode emitted the pending event.
Polygon transaction hash.
`"pending"` for mempool-detected combo executions.
Unix milliseconds when PolyNode first saw the transaction.
Derived combinatorial condition ID.
Polymarket RFQ ID when it can be linked to the on-chain execution.
Polymarket quote ID when it can be linked to the on-chain execution.
YES combo position ID.
NO combo position ID.
Constituent leg position IDs for the combo.
Enriched leg metadata. Customer-facing combo executions are emitted only when all legs are enriched.
`"BUY"` or `"SELL"`.
`"YES"` or `"NO"`.
Six-decimal fixed-point quote price.
Six-decimal fixed-point share size.
USDC-normalized amount when derivable from calldata.
Maker wallet.
Address that signed the ExchangeV3 order.
Taker / transaction participant.
Exchange signature type when present in the decoded order.
ExchangeV3 order hashes decoded from the call.
Per-position fill details when available before confirmation.
## Subscribing
```json theme={null}
{
"action": "subscribe",
"type": "combos",
"filters": {
"event_types": ["combo_execution"]
}
}
```
# Combo Lifecycle Event
Source: https://docs.polynode.dev/websocket/events/combo-lifecycle
Split, merge, redeem, convert, wrap, unwrap, and related combo lifecycle actions.
Emitted for combo lifecycle actions when the event can be represented with enriched legs. AutoRedeemer redemption confirmations can also be emitted without legs when the receipt itself provides the user wallet, condition ID, exact payout, normalized payout, module name, and log index.
```json theme={null}
{
"type": "combo_lifecycle",
"timestamp": 1781054112400,
"data": {
"event_type": "combo_lifecycle",
"tx_hash": "0xabc...",
"status": "confirmed",
"detected_at": 1781054112400,
"block_number": 88232739,
"log_index": 44,
"action": "Redeem",
"user": "0xUser...",
"combo_condition_id": "0xcombo...",
"condition_ids": ["0xcombo..."],
"module_id": 3,
"module_name": "CombinatorialModule",
"position_ids": ["123..."],
"source_position_ids": ["123..."],
"leg_position_ids": ["111...", "222..."],
"legs": [
{
"position_id": "111...",
"leg_outcome_label": "Yes",
"market": {
"title": "Will Team A win?",
"slug": "will-team-a-win"
}
}
],
"amount_e6": "1000000",
"amount_usdc": 1,
"payout_e6": "1000000",
"payout_usdc": 1
}
}
```
## Actions
`action` can be:
`Prepare`, `Split`, `Merge`, `HorizontalSplit`, `HorizontalMerge`, `Convert`, `Redeem`, `Wrap`, `Unwrap`, `Transfer`, `Compress`, `Extract`, `Inject`, `ConvertToYesBasket`, or `MergeFromYesBasket`.
## Subscribing
Lifecycle events are not included in the default `combos` preset. Request them explicitly:
```json theme={null}
{
"action": "subscribe",
"type": "combos",
"filters": {
"event_types": ["combo_lifecycle"]
}
}
```
Filter by lifecycle action:
```json theme={null}
{
"action": "subscribe",
"type": "combos",
"filters": {
"event_types": ["combo_lifecycle"],
"action": "Redeem"
}
}
```
AutoRedeemer redemptions can be consumed through the combo lifecycle stream:
```json theme={null}
{
"type": "combo_lifecycle",
"data": {
"event_type": "combo_lifecycle",
"tx_hash": "0x...",
"status": "confirmed",
"block_number": 88462228,
"log_index": 27,
"action": "Redeem",
"user": "0xde0a3c61f87da595b215f88254a9473e6152f102",
"condition_ids": ["0xd7e68ef0dfabc386f97ffdc330ecff4908cda4d08c5fc5931503f1e60cac0dfe"],
"module_name": "AutoRedeemer",
"payout_e6": "4500000000",
"payout_usdc": 4500,
"diagnostics": {
"decoded_function": "AutoRedeemer.NegRiskRedemption"
}
}
}
```
## Fields
Combo lifecycle action.
Wallet that initiated or received the lifecycle action.
Receipt log index for confirmed lifecycle events.
Combo condition ID when available.
Condition IDs involved in the lifecycle action.
Event IDs for combo legs when available.
Combo module ID. Common values are `1` for binary, `2` for negative-risk, and `3` for combinatorial legs.
Human-readable module name when available.
YES combo position ID when the lifecycle action creates or affects a combo position.
NO combo position ID when the lifecycle action creates or affects a combo position.
Position IDs involved in the lifecycle action.
Positions consumed by the action.
Positions created by the action.
Enriched legs when available. AutoRedeemer redemptions can be emitted without legs when the receipt includes the wallet, condition ID, and payout directly.
Raw six-decimal amount.
USDC-normalized amount.
Exact raw payout in six-decimal USDC units for redeem actions.
USDC-normalized payout for redeem actions when available.
Combo side when available. Values: `YES` or `NO`.
Position transfer logs decoded from the confirmed receipt.
# Combo Status Update Event
Source: https://docs.polynode.dev/websocket/events/combo-status-update
Confirmation event for a combo execution or lifecycle transaction.
Emitted when a combo transaction confirms on-chain. This event links back to a pending `combo_execution` when one was observed, and includes exact receipt data when available.
Receipt-only lifecycle activity, including PM2 AutoRedeemer redemptions, can appear in `lifecycle_events[]`. AutoRedeemer redemptions are included there even when the lifecycle event is condition-only, because the receipt contains the wallet, condition ID, exact payout, normalized payout, and log index.
```json theme={null}
{
"type": "combo_status_update",
"timestamp": 1781054112400,
"data": {
"event_type": "combo_status_update",
"tx_hash": "0xb0411705aaefc17991e4121acecd8aad03901ddc359cfdb7cf8773f46942927a",
"block_number": 88232739,
"confirmed_at": 1781054112400,
"execution_status": "CONFIRMED",
"pending_detected_at": 1781054110000,
"latency_ms": 2400,
"combo_condition_id": "0xcombo...",
"yes_position_id": "123...",
"no_position_id": "456...",
"leg_position_ids": ["111...", "222..."],
"legs": [
{
"position_id": "111...",
"leg_outcome_label": "Yes",
"market": {
"title": "Will Team A win?",
"slug": "will-team-a-win",
"outcomes": ["Yes", "No"],
"token_ids": ["111...", "333..."]
}
}
],
"maker_address": "0xMaker...",
"signer_address": "0xSigner...",
"taker_address": "0xTaker...",
"confirmed_fills": [
{
"position_id": "123...",
"order_hash": "0xorder...",
"maker_address": "0xMaker...",
"taker_address": "0xTaker...",
"side": "BUY",
"outcome_side": "YES",
"price": 0.45,
"size": 1,
"maker_amount_e6": "450000",
"taker_amount_e6": "1000000"
}
],
"fees": [],
"confirmed_transfers": [],
"lifecycle_events": [],
"gas_used": "450000",
"effective_gas_price": "30000000000"
}
}
```
## Fields
Always `"combo_status_update"`.
Transaction hash. Match this to `combo_execution.data.tx_hash` when a pending event was observed.
Polygon block number where the transaction confirmed.
`CONFIRMED`, `FAILED`, `MATCHED`, `MINED`, or `RETRYING`.
Unix milliseconds from the pending event. Present when PolyNode saw the transaction before confirmation.
Time between pending detection and confirmation.
Derived combo condition ID.
YES combo position ID.
NO combo position ID.
Constituent leg position IDs for the combo.
Enriched combo legs. Customer-facing status updates are emitted only when all legs are enriched.
Maker wallet when available from the pending order or receipt.
Address that signed the order when available.
Taker / transaction participant when available.
USDC-normalized amount when derivable.
USDC-normalized notional when derivable.
Exchange order hashes associated with the transaction.
Exact ExchangeV3 fill logs decoded from the receipt.
OrdersMatched-style execution summaries decoded from the receipt.
FeeCharged logs, normalized to USDC where possible.
PositionManager transfer logs for combo positions.
Receipt-derived combo lifecycle events associated with the same transaction. AutoRedeemer redemption entries use the same shape as `combo_lifecycle` and include `action: "Redeem"`, `module_name: "AutoRedeemer"`, `user`, `condition_ids`, `payout_e6`, `payout_usdc`, and `log_index`.
## Subscribing
`combo_status_update` is included in the default combo stream:
```json theme={null}
{
"action": "subscribe",
"type": "combos"
}
```
# Deposit Event
Source: https://docs.polynode.dev/websocket/events/deposit
USDC and PolyUSD deposits and withdrawals on Polymarket.
Emitted when USDC.e or PolyUSD moves into or out of Polymarket contracts — deposits (funding a trading account) and withdrawals.
**V2 update:** Polymarket V2 uses PolyUSD (a 1:1 USDC.e wrapper) as collateral. Deposit events now include PolyUSD wrapping (USDC.e → PolyUSD via Onramp) and unwrapping (PolyUSD → USDC.e via Offramp). The event format is identical — `amount` is in USD with 6 decimal precision regardless of whether the underlying transfer is USDC.e or PolyUSD. See the [V2 Migration Guide](/guides/v2-migration) for details.
```json theme={null}
{
"type": "deposit",
"timestamp": 1774515147000,
"data": {
"event_type": "deposit",
"tx_hash": "0x4ec78046c86dc95e432ad938185dfff4f72644d1c4328483b6a1bcbd024e9d5c",
"block_number": 84697556,
"log_index": 2150,
"timestamp": 1774515147000,
"from": "0x53a26c4ee0776948f9fc645a6e5184f1af3deb92",
"to": "0x4bfb41d5b3570defd03c39a9a4d8de6bd8b8982e",
"amount": 27.489,
"direction": "deposit"
}
}
```
## Fields
Always `"deposit"`.
Block timestamp in Unix milliseconds.
Transaction hash.
Block number.
Log index within the transaction.
Block timestamp in Unix milliseconds.
Sender address.
Receiver address.
USDC amount (6 decimal precision).
`"deposit"` (to Polymarket) or `"withdrawal"` (from Polymarket).
## Use cases
* **Whale tracking** — large deposits often precede big trades
* **Fund flow analysis** — track capital entering and leaving Polymarket
* **Wallet monitoring** — know when a tracked wallet funds their account
# Oracle Event
Source: https://docs.polynode.dev/websocket/events/oracle
UMA Optimistic Oracle events — market initializations, proposals, disputes, resolutions, settlements, and admin actions on Polymarket.
Oracle events stream the full lifecycle of Polymarket's UMA-based market resolution system. Every Polymarket market is resolved through UMA's Optimistic Oracle, which uses a propose-challenge-resolve cycle on the Polygon blockchain.
**The only WebSocket stream of enriched UMA oracle events.** Decoded from on-chain event logs, enriched with full market metadata (title, outcomes, images, token IDs), and pushed to your connection in real-time.
## How Polymarket Resolution Works
1. A market is **initialized** on-chain (QuestionInitialized on the UMA adapter)
2. A **proposer** submits an answer (YES or NO) via ProposePrice on UMA's Optimistic Oracle V2 and posts a USDC bond
3. A 2-hour **liveness window** opens where anyone can dispute
4. If no dispute, the oracle **settles** (Settle event on OO V2) and the market **resolves** (QuestionResolved on the adapter)
5. For neg-risk markets (the majority), a second transaction calls `resolveQuestion()` on the NegRiskOperator, which triggers **condition\_resolution** on the Conditional Tokens contract. **Positions become redeemable at this point.**
6. If disputed, the question **resets** and goes to UMA's DVM for a token-holder vote
7. Admins can **flag**, **pause**, or **manually resolve** markets in edge cases
## Subscribe
```json theme={null}
{
"action": "subscribe",
"type": "oracle"
}
```
## Oracle Event Types
Every oracle event has an `oracle_type` field indicating what happened:
Market initialized on-chain. The UMA adapter has registered a new question. This is the first on-chain event in a market's lifecycle.
Resolution proposed on UMA Optimistic Oracle V2 (ProposePrice). A proposer has submitted an answer and posted a bond. The 2-hour liveness countdown begins. Includes `proposer` address, `proposed_price` (1.0 = YES, 0.0 = NO), and `expiration_timestamp`.
Resolution disputed on UMA Optimistic Oracle V2 (DisputePrice). Someone challenged the proposed outcome. The market re-enters the proposal phase or escalates to UMA's DVM for a token-holder vote. **High signal, rare event.** Price swings are likely. Includes `proposer`, `disputer`, and `proposed_price`.
Settled on UMA Optimistic Oracle V2 after the liveness period expires. The oracle has accepted the proposed answer. Includes `resolved_price` and `proposer`.
Market officially resolved via UMA oracle (QuestionResolved on the adapter). The most common event type (\~200+ per hour during active sports periods). Includes the final `resolved_price`, `payouts` array, and `resolved_outcome`.
Question reset after a dispute. The market goes back to the proposal phase.
Market flagged by admin. Something may be wrong with the resolution.
Market manually resolved by admin, bypassing the normal oracle flow. Includes `payouts` array.
Condition resolved on the Conditional Tokens contract (ConditionResolution event). **This is the moment positions become redeemable.** For neg-risk markets (the majority on Polymarket), this fires in a separate transaction \~2-3 minutes after `resolution`. For standard markets, it fires atomically with `resolution`. Includes `resolved_price`, `payouts`, `condition_id`, and full market metadata. Use this event to trigger redemption workflows.
## Resolution Event (real payload)
A tennis market resolved — Baena won the first set:
```json theme={null}
{
"type": "oracle",
"timestamp": 1773755193000,
"data": {
"adapter_address": "0x65070be91477460d8a7aeeb94ef92fe056c2f2a7",
"block_number": 84317580,
"condition_id": "0xb0b51e92993bf204c76f10a49bdaa595d817f1768a9ae79e546e83695f1cb142",
"event_title": "Murcia: Nikolas Sanchez Izquierdo vs Roberto Carballes Baena",
"event_type": "oracle",
"log_index": 197,
"market_image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/atp-tour-b4390c4fb8.jpg",
"market_slug": "atp-izquier-baena-2026-03-16-first-set-winner-Izquierdo-vs-Baena",
"market_title": "Set 1 Winner: Izquierdo vs Baena",
"neg_risk": false,
"oracle_type": "resolution",
"outcomes": ["Izquierdo", "Baena"],
"payouts": [0, 1],
"question_id": "0x34241c96d35a8ffbd4093f9a360e08f97c5691f66e9bfe29dd46e4763dae833e",
"resolved_outcome": "Baena",
"resolved_price": 0,
"timestamp": 1773755193000,
"token_ids": [
"100092605222559788173670687007064638617594421724580870112622623059744413023416",
"82111831612810431769649657775457339958533284370449876495200499593908955489467"
],
"tokens": {
"100092605222559788173670687007064638617594421724580870112622623059744413023416": "Izquierdo",
"82111831612810431769649657775457339958533284370449876495200499593908955489467": "Baena"
},
"tx_hash": ""
}
}
```
## Proposal Event (ProposePrice from OO V2)
A proposer submits YES for an XRP price market, with a 2-hour liveness window. Proposals include full market metadata:
```json theme={null}
{
"type": "oracle",
"timestamp": 1773755537000,
"data": {
"adapter_address": "0x65070be91477460d8a7aeeb94ef92fe056c2f2a7",
"block_number": 84317752,
"condition_id": "0xc9d0815d7f12e601ac3ce457de5b5d6f18ba5f48df5ffd246cec4c19a9254200",
"event_title": "What price will XRP hit on March 17?",
"event_type": "oracle",
"expiration_timestamp": 1773762737000,
"log_index": 372,
"market_image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/XRP-logo.png",
"market_slug": "will-xrp-dip-to-1pt5-on-march-17",
"market_title": "Will XRP dip to $1.50 on March 17?",
"neg_risk": false,
"oracle_type": "proposal",
"outcomes": ["Yes", "No"],
"proposed_price": 1,
"proposer": "0xedc1e7a3a9275c512f669cb9236f1d03c2fc2e39",
"question_id": "oo_v2:0x65070be91477460d8a7aeeb94ef92fe056c2f2a7:1773720049",
"timestamp": 1773755537000,
"token_ids": [
"29532348047030596543075496892560093525999458628692812631919395372535371275514",
"43096817250499798050326473461465682146308168398097552897166323529988349492396"
],
"tokens": {
"29532348047030596543075496892560093525999458628692812631919395372535371275514": "Yes",
"43096817250499798050326473461465682146308168398097552897166323529988349492396": "No"
},
"tx_hash": ""
}
}
```
## Field Reference
### Core Fields (all oracle events)
Type of oracle event: `initialization`, `proposal`, `dispute`, `settled`, `resolution`, `condition_resolution`, `reset`, `flag`, `unflag`, `pause`, `unpause`, `manual_resolution`.
UMA's unique identifier for this market question.
Which UMA adapter contract this event relates to.
Polygon block number where the event was emitted.
Position of this event within the block's logs.
Block timestamp in milliseconds.
### Proposal Fields (proposal only)
Address of the account that proposed the resolution.
The proposed answer. `1.0` = YES (first outcome), `0.0` = NO (second outcome).
When the 2-hour liveness window expires (unix milliseconds). After this time, the proposal can be settled if not disputed.
### Dispute Fields (dispute only)
Address of the account that disputed the proposal.
Address of the original proposer whose answer is being challenged.
The proposed answer that was disputed.
### Resolution Fields (resolution, condition\_resolution, settled, and manual\_resolution)
The settled price from UMA. `1.0` = first outcome wins (usually YES), `0.0` = second outcome wins (usually NO), `0.5` = split/tie.
Human-readable winning outcome (e.g. "Yes", "No", "L1ga Team"). Derived from `resolved_price` + market metadata. Returns `"Split"` for voided markets where payouts are `[1, 1]`.
Payout array from UMA. `[1, 0]` means the first outcome wins. `[0, 1]` means the second outcome wins. `[1, 1]` means a split (both outcomes pay out). Present on `resolution`, `condition_resolution`, and `manual_resolution`.
### Enrichment Fields (from Polymarket metadata)
These fields are populated automatically when the market's metadata is available.
Polymarket's condition identifier for this market.
Human-readable market question (e.g. "Will Bitcoin reach \$100K?").
URL slug for the market on polymarket.com.
Parent event name if this market is part of a group (e.g. "Dota 2: Pipsqueak+4 vs L1ga Team (BO3)").
Market image URL from Polymarket.
All outcome names for this market (e.g. `["Yes", "No"]` or `["Pipsqueak+4", "L1ga Team"]`).
All conditional token IDs for this market.
Map of token ID to outcome name (e.g. `{"2174263...": "Yes", "4839315...": "No"}`).
Whether this market uses the neg-risk framework (multi-outcome markets like elections).
## Frequency
Oracle events are **not evenly distributed**. They come in bursts when UMA's resolver bot processes batches of markets.
| Period | Typical Volume |
| -------------------------- | ----------------------------------------------------- |
| Active sports hours | 200+ resolutions/hour, proposals flowing continuously |
| Quiet periods | 10-50 resolutions/hour |
| Proposals (ProposePrice) | \~30/hour, continuous flow |
| Settlements (Settle) | \~30/hour, fires after 2-hour liveness |
| Disputes | A few per month (high signal) |
| Flags / Manual resolutions | Very rare |
## Trading Implications
| Event | What It Means | Action |
| ------------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------ |
| **initialization** | New market registered on-chain. | First signal of a new tradeable market |
| **proposal** | Answer proposed, 2-hour countdown started. | Monitor for disputes, position accordingly |
| **dispute** | Proposed outcome challenged. Uncertainty returns. | Watch for price swings, potential arb |
| **settled** | Oracle accepted the answer after liveness. | Resolution imminent |
| **resolution** | Market outcome decided on UMA adapter. For neg-risk markets, positions are not yet redeemable. | Outcome known, prepare for redemption |
| **condition\_resolution** | Condition resolved on CTF contract. **Positions are now redeemable.** | Trigger redemption, notify users |
| **reset** | Market re-enters proposal phase after dispute. | Resolution delayed, reassess positions |
| **flag** | Admin flagged an issue. Market may be paused. | Caution, reduce exposure |
| **manual\_resolution** | Admin override. Could be controversial. | Check outcome, compare to expectations |
# Position Change Event
Source: https://docs.polynode.dev/websocket/events/position-change
Polymarket position transfers between wallets.
Emitted when conditional tokens are transferred between wallets (ERC-1155 `TransferSingle` or `TransferBatch` events on Polymarket's CTF contract).
```json theme={null}
{
"data": {
"amount": 0.050921,
"block_number": 84697556,
"condition_id": "0xc288913391d7d3b484d9e63f60996671a1888207e0d9917691c5257cbab09309",
"event_title": "Bitcoin Up or Down - March 26, 4:50AM-4:55AM ET",
"event_type": "position_change",
"from": "0xe3f18acc55091e2c48d883fc8c8413319d4ab7b0",
"log_index": 2147,
"market_image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/BTC+fullsize.png",
"market_slug": "btc-updown-5m-1774515000",
"market_title": "Bitcoin Up or Down - March 26, 4:50AM-4:55AM ET",
"neg_risk": false,
"outcome": "Up",
"outcomes": ["Up", "Down"],
"taker_base_fee": 1000.0,
"tick_size": 0.01,
"timestamp": 1774515147000,
"to": "0xb27bc932bf8110d8f78e55da7d5f0497a18b5b82",
"token_id": "58779171378753040273547833889411100281640930430837040192014777442237090167078",
"token_ids": [
"58779171378753040273547833889411100281640930430837040192014777442237090167078",
"44910406836464179003614524044201701268382942629198430107881894265432491492514"
],
"tokens": {
"44910406836464179003614524044201701268382942629198430107881894265432491492514": "Down",
"58779171378753040273547833889411100281640930430837040192014777442237090167078": "Up"
},
"transfer_type": "single",
"tx_hash": "0x2b72f18ba36846fa72c7083e89d1a66438e0b9b15e170325f4efde5d08cc602e"
},
"timestamp": 1774515147000,
"type": "position_change"
}
```
## Fields
Always `"position_change"`.
Block timestamp in Unix milliseconds.
Transaction hash.
Block number.
Log index within the transaction.
Block timestamp in Unix milliseconds.
Sender wallet address.
Receiver wallet address.
Conditional token ID being transferred.
Number of tokens transferred.
`"single"` (TransferSingle) or `"batch"` (TransferBatch).
Human-readable market question. Enriched from metadata.
Outcome name (e.g. "Yes", "No"). Enriched from metadata.
Market URL slug. Enriched from metadata.
Parent event title. Enriched from metadata.
Market image URL. Enriched from metadata.
Whether this market uses the negRisk contract type. Enriched from metadata.
Minimum price increment (e.g. `0.01` or `0.001`). Enriched from metadata.
Taker fee rate in basis points. `200` = 2%, `1000` = 10%, `null` = no fee. Short-term crypto markets (5-minute ETH/BTC) typically have `1000` (10%), while standard markets have `200` (2%). Enriched from Polymarket metadata.
Market condition ID (CTF contract identifier). Enriched from metadata.
Ordered list of outcome names (e.g. `["Yes", "No"]`). Index-aligned with `token_ids`. Enriched from metadata.
Ordered list of token IDs for this market. Index-aligned with `outcomes`. Enriched from metadata.
Map of token ID → outcome name for all outcomes in this market. Enriched from metadata.
Position changes from settlements are **not** duplicated here — the `settlement` event already contains the trade details. This event tracks non-trade transfers: wallet migrations, OTC deals, and smart contract interactions.
# Position Merge Event
Source: https://docs.polynode.dev/websocket/events/position-merge
Outcome tokens merged back into collateral on Polymarket.
Emitted when outcome tokens (YES + NO shares) are merged back into collateral (USDC) via the Conditional Tokens Framework `mergePositions` function. The inverse of a [position split](/websocket/events/position-split).
Merges are much rarer than splits (\~20x less frequent). They happen when a trader holds both YES and NO tokens for a condition and redeems them back to USDC.
```json theme={null}
{
"data": {
"amount": 1414.0,
"block_number": 84090704,
"collateral_token": "0x3a3bd7bb9528e159577f7c2e685cc81a765002e2",
"condition_id": "0xd1747284d6048b2a3dcfbee489db405f36de18f9590197dd0e5c9f0c246bf050",
"event_title": "Republican Presidential Nominee 2028",
"event_type": "position_merge",
"log_index": 1004,
"market_image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/Byron_Donalds.png",
"market_slug": "will-byron-donalds-win-the-2028-republican-presidential-nomination",
"market_title": "Will Byron Donalds win the 2028 Republican presidential nomination?",
"neg_risk": true,
"stakeholder": "0xd91e80cf2e7be2e162c6513ced06f1dd0da35296",
"taker_base_fee": 0.0,
"tick_size": 0.001,
"timestamp": 1773301441000,
"tokens": {
"311624663652221737215322113380496984764966764039692273354641152455298576851": "Yes",
"43292374563904271719486871569114922516560719431752184778538513591321835161630": "No"
},
"tx_hash": "0x92fa6de980f6cad9eae9ef28e908d19faa10838a07f01352b0cae63b5f8299fb"
},
"timestamp": 1773301441000,
"type": "position_merge"
}
```
## Fields
Always `"position_merge"`.
Block timestamp in Unix milliseconds.
Transaction hash.
Block number.
Log index within the transaction.
Block timestamp in Unix milliseconds.
Address that triggered the merge.
Collateral token address. USDC.e (`0x2791bca1f2de4661ed88a30c99a7a9449aa84174`) for standard markets, wrapped collateral (`0x3a3bd7bb...`) for neg\_risk markets.
Market condition ID — identifies which market's outcome tokens were burned.
USDC amount received from merging outcome tokens (6 decimal precision).
Human-readable market question. Populated when metadata is available.
URL slug for the market.
Market image URL. Enriched from metadata.
Parent event title. Enriched from metadata.
Whether this market uses the negRisk contract type. Enriched from metadata.
Minimum price increment (e.g. `0.01` or `0.001`). Enriched from metadata.
Taker fee rate in basis points. `200` = 2%, `1000` = 10%, `null` = no fee. Short-term crypto markets (5-minute ETH/BTC) typically have `1000` (10%), while standard markets have `200` (2%). Enriched from Polymarket metadata.
Map of token ID → outcome name for all outcomes in this market. Enriched from metadata.
## Use cases
* **Position unwinding** — detect when traders are closing positions by merging outcome tokens
* **Liquidity withdrawal tracking** — merges reduce the total supply of outcome tokens in a market
* **Arbitrage detection** — merges after buying both outcomes at favorable prices signal arbitrage
Not included in `global` firehose (full data stream) by default. Subscribe explicitly with `event_types: ["position_merge"]` or use the `wallets`/`markets` subscription types which include it automatically.
# Position Split Event
Source: https://docs.polynode.dev/websocket/events/position-split
Collateral split into outcome tokens on Polymarket.
Emitted when collateral (USDC) is split into outcome tokens (YES/NO shares) via the Conditional Tokens Framework `splitPosition` function.
99%+ of splits happen as part of trade execution. This event provides supplementary data: how much collateral was split and into which market condition.
```json theme={null}
{
"data": {
"amount": 5.190306,
"block_number": 84090704,
"collateral_token": "0x2791bca1f2de4661ed88a30c99a7a9449aa84174",
"condition_id": "0xab3eea0d5b61cd443affca5fcd19d3541e92f7c96f7e92111982fbcbff56bb55",
"event_title": "Bitcoin Up or Down - March 12, 3:40AM-3:45AM ET",
"event_type": "position_split",
"log_index": 763,
"market_image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/BTC+fullsize.png",
"market_slug": "btc-updown-5m-1773301200",
"market_title": "Bitcoin Up or Down - March 12, 3:40AM-3:45AM ET",
"neg_risk": false,
"stakeholder": "0x4bfb41d5b3570defd03c39a9a4d8de6bd8b8982e",
"taker_base_fee": 1000.0,
"tick_size": 0.01,
"timestamp": 1773301441000,
"tokens": {
"47635040450827094965464370243227927750353541381049355332417378831540287352201": "Up",
"57924371746740845060329991315866785058480643549619724503463886953768356812176": "Down"
},
"tx_hash": "0x74dbde22f31ce8dcbc5b3c187c33cd131d9bb5791874e139b41c1049ae3589fc"
},
"timestamp": 1773301441000,
"type": "position_split"
}
```
## Fields
Always `"position_split"`.
Block timestamp in Unix milliseconds.
Transaction hash.
Block number.
Log index within the transaction.
Block timestamp in Unix milliseconds.
Address that triggered the split (usually the exchange contract, sometimes a direct user).
Collateral token address. USDC.e (`0x2791bca1f2de4661ed88a30c99a7a9449aa84174`) for standard markets, wrapped collateral (`0x3a3bd7bb...`) for neg\_risk markets.
Market condition ID — identifies which market's outcome tokens were minted.
USDC amount that was split into outcome tokens (6 decimal precision).
Human-readable market question. Populated when metadata is available.
URL slug for the market.
Market image URL. Enriched from metadata.
Parent event title. Enriched from metadata.
Whether this market uses the negRisk contract type. Enriched from metadata.
Minimum price increment (e.g. `0.01` or `0.001`). Enriched from metadata.
Taker fee rate in basis points. `200` = 2%, `1000` = 10%, `null` = no fee. Short-term crypto markets (5-minute ETH/BTC) typically have `1000` (10%), while standard markets have `200` (2%). Enriched from Polymarket metadata.
Map of token ID → outcome name for all outcomes in this market. Enriched from metadata.
## Use cases
* **Collateral flow analysis** — track how much USDC is being converted into outcome tokens
* **Market liquidity creation** — splits indicate new shares being minted for a market
* **Trade enrichment** — pair with `trade` events to see the full lifecycle of a trade (split → fill)
Not included in `global` firehose (full data stream) by default due to high volume (splits fire on most trades). Subscribe explicitly with `event_types: ["position_split"]` or use the `wallets`/`markets` subscription types which include it automatically.
# Positions Converted Event
Source: https://docs.polynode.dev/websocket/events/positions-converted
Position conversion on neg-risk multi-outcome markets.
Emitted when a user calls `convertPositions` on the NegRiskAdapter contract. This converts NO positions on selected outcomes into USDC plus YES positions on the complementary outcomes within the same multi-outcome market.
Conversions are unique to neg-risk markets (multi-outcome events like "Republican Presidential Nominee" or "FIFA World Cup Winner"). They allow traders to rebalance positions across outcomes without going through the orderbook.
```json theme={null}
{
"data": {
"amount": 98,
"block_number": 85007631,
"converted_outcomes": [
"Fidesz-KDNP",
"TISZA"
],
"event_title": "Hungary Parliamentary Election Winner",
"event_type": "positions_converted",
"index_set": "0x0000000000000000000000000000000000000000000000000000000000000021",
"log_index": 943,
"market_id": "0x355e7310dd6e18ef5fa456de7ce1331bd8c7540c4c39028db05325be19050100",
"neg_risk": true,
"stakeholder": "0x30cecdf29f069563ea21b8ae94492e41e53a6b2b",
"taker_base_fee": null,
"timestamp": 1775135299000,
"tx_hash": "0x74fcb24732007ec58042c6e2ed7596c44a12f2fb34dec8bef9bbd3eaeb020718"
},
"timestamp": 1775135299000,
"type": "positions_converted"
}
```
## Fields
Always `"positions_converted"`.
Block timestamp in Unix milliseconds.
Transaction hash.
Block number.
Log index within the transaction.
Block timestamp in Unix milliseconds.
Wallet address that performed the conversion.
The neg-risk market group ID (bytes32). All outcomes within the same multi-outcome event share this ID.
Bitmask (hex) of which outcome indices were converted. Each bit position corresponds to an outcome in the market. For example, `0x03` (binary `11`) means outcomes at index 0 and 1 were converted.
USDC amount per outcome that was converted (6 decimal precision).
Human-readable names of the outcomes that were converted. Decoded from the `index_set` bitmask using market metadata. For example: `["Fidesz-KDNP", "TISZA"]`.
Parent event title (e.g. "Hungary Parliamentary Election Winner"). Enriched from metadata.
Always `true` for conversion events (conversions only exist on neg-risk markets).
Market question. Enriched from metadata when available.
URL slug for the market. Enriched from metadata.
Market image URL. Enriched from metadata.
Taker fee rate in basis points. Enriched from metadata.
Map of token ID to outcome name for all outcomes in this market. Enriched from metadata.
## How conversions work
On a neg-risk market with N outcomes, a user holding NO positions on selected outcomes can convert them:
1. The user's NO tokens on the selected outcomes are burned
2. The user receives **(number of NO positions - 1) x amount** in USDC
3. The user receives YES tokens on all complementary outcomes (the ones NOT in the index\_set)
This enables efficient position rebalancing without using the orderbook.
## Use cases
* **Whale tracking** — large conversions signal sophisticated repositioning across outcomes
* **Market structure analysis** — conversion flow reveals which outcomes traders are moving between
* **Liquidity detection** — conversions create new YES token supply on complementary outcomes
* **Arbitrage monitoring** — conversions can indicate perceived mispricing between outcomes
Not included in `global` firehose by default (same as `position_split`/`position_merge`). Subscribe explicitly with `event_types: ["positions_converted"]` or use the `wallets`/`markets` subscription types which include it automatically.
# Price Feed Event
Source: https://docs.polynode.dev/websocket/events/price-feed
Real-time crypto price updates -- BTC, ETH, SOL, BNB, XRP, DOGE, HYPE.
Real-time crypto price data. Updates arrive approximately once per second per feed, streamed directly over WebSocket.
Subscribe with `"type": "chainlink"` to receive price feed events.
For the full guide including all 7 available feeds, filtering, and examples, see the **[Crypto Prices](/crypto/overview)** section.
```json theme={null}
{
"type": "price_feed",
"feed": "BTC/USD",
"timestamp": 1774672089,
"data": {
"feed": "BTC/USD",
"price": 66236.61,
"bid": 66229.77,
"ask": 66237.94,
"timestamp": 1774672089
}
}
```
## Subscribing
```json theme={null}
{"action": "subscribe", "type": "chainlink"}
```
With feed filter (only receive specific feeds):
```json theme={null}
{
"action": "subscribe",
"type": "chainlink",
"filters": {
"feeds": ["BTC/USD", "ETH/USD"]
}
}
```
Omit the `feeds` filter to receive all available price feeds.
## Response
```json theme={null}
{
"type": "subscribed",
"subscriber_id": "550e8400-e29b-41d4-a716-446655440000",
"subscription_type": "chainlink"
}
```
Price feed subscriptions do not include a snapshot. Events start arriving with the next price observation (\~1 second).
## Fields
Always `"price_feed"`.
Feed name (e.g. `"BTC/USD"`, `"ETH/USD"`).
Observation timestamp in Unix seconds.
Feed name (e.g. `"BTC/USD"`).
Mid price in USD.
Bid price in USD.
Ask price in USD.
Observation timestamp in Unix seconds.
## Available feeds
| Feed | Update rate |
| ---------- | ----------- |
| `BTC/USD` | \~1/second |
| `ETH/USD` | \~1/second |
| `SOL/USD` | \~1/second |
| `BNB/USD` | \~1/second |
| `XRP/USD` | \~1/second |
| `DOGE/USD` | \~1/second |
| `HYPE/USD` | \~1/second |
See **[Available Feeds](/crypto/feeds)** for detailed per-feed documentation.
## Dual subscription
Price feeds run on the same WebSocket connection as settlement subscriptions. Send both subscribe messages to receive both:
```json theme={null}
{"action": "subscribe", "type": "settlements"}
{"action": "subscribe", "type": "chainlink"}
```
Events arrive interleaved. Use the `type` field (`"settlement"` vs `"price_feed"`) to route them in your application.
# Redemption Event
Source: https://docs.polynode.dev/websocket/events/redemption
Position payout redemptions on Polymarket.
Emitted when a user redeems their outcome tokens for collateral after a market resolves. The `redeemPositions` function on the Conditional Tokens contract burns the user's shares and pays out USDC based on the resolved outcome.
Every resolved market generates redemption events as holders claim their payouts. A non-zero `payout` means the user held winning shares. A zero payout means the user redeemed losing shares (no collateral returned).
Recent redemption events also include the market's resolved payout vector when available. Use `winning_outcome`, `winning_token_id`, or the ordered `payouts` / `outcome_prices` arrays to identify which outcome won without making a second market lookup.
PM2 AutoRedeemer payouts are bridged into this same event class. For those transactions, `redeemer` is the user wallet from the AutoRedeemer log, not the AutoRedeemer contract, and `source` identifies the AutoRedeemer redemption function.
```json theme={null}
{
"data": {
"block_number": 85244342,
"collateral_token": "0x2791bca1f2de4661ed88a30c99a7a9449aa84174",
"condition_id": "0x487f28579354d6c4408eea243888cdedbe03e83a219a4708523ef455dcbc5e31",
"event_title": "Bitcoin Up or Down - April 7, 8:15PM-8:20PM ET",
"event_type": "redemption",
"index_sets": [1, 2],
"log_index": 1282,
"market_image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/BTC+fullsize.png",
"market_slug": "btc-updown-5m-1775607300",
"market_title": "Bitcoin Up or Down - April 7, 8:15PM-8:20PM ET",
"neg_risk": false,
"outcome_prices": [1.0, 0.0],
"outcomes": ["Up", "Down"],
"payout": 7.566434,
"payout_e6": "7566434",
"payouts": [1, 0],
"redeemer": "0x8858aae9c92a82b00986a0f1a66338e8b0cc8641",
"taker_base_fee": 1000,
"tick_size": 0.01,
"timestamp": 1775608721000,
"token_ids": [
"81518481532502107093994258923994884503752044066839262771005198927094175386674",
"112731399428633109059368520083498445904538970112239739400592554575174180266461"
],
"tokens": {
"112731399428633109059368520083498445904538970112239739400592554575174180266461": "Down",
"81518481532502107093994258923994884503752044066839262771005198927094175386674": "Up"
},
"tx_hash": "0xcfb51c25a8f6ebb0b5a7484de7812d66f2cbf9935f29c0b56561e74722c64fac",
"winning_outcome": "Up",
"winning_outcome_index": 0,
"winning_token_id": "81518481532502107093994258923994884503752044066839262771005198927094175386674"
},
"timestamp": 1775608721000,
"type": "redemption"
}
```
## Fields
Always `"redemption"`.
Block timestamp in Unix milliseconds.
Transaction hash.
Block number.
Log index within the transaction.
Block timestamp in Unix milliseconds.
Address that redeemed positions. This is the wallet that held the outcome tokens and called `redeemPositions`.
Collateral token address. USDC.e (`0x2791bca1f2de4661ed88a30c99a7a9449aa84174`) for standard markets.
Market condition ID — identifies which resolved market's positions were redeemed.
Which outcome slots were redeemed. `[1]` = first outcome only, `[2]` = second outcome only, `[1, 2]` = both outcomes. Most redemptions include all outcomes (`[1, 2]`).
USDC amount paid out to the redeemer (6 decimal precision). `0` means the user redeemed losing shares and received nothing.
Exact payout amount in six-decimal integer units. Use this for accounting-safe comparisons.
Decoder path for bridged redemptions. PM2 AutoRedeemer events use values such as `AutoRedeemer.Redemption`, `AutoRedeemer.BinaryRedemption`, or `AutoRedeemer.NegRiskRedemption`.
Human-readable market question. Enriched from metadata.
URL slug for the market.
Market image URL. Enriched from metadata.
Parent event title. Enriched from metadata.
Whether this market uses the negRisk contract type. Enriched from metadata.
Minimum price increment (e.g. `0.01` or `0.001`). Enriched from metadata.
Taker fee rate in basis points. `200` = 2%, `1000` = 10%, `null` = no fee. Enriched from Polymarket metadata.
Map of token ID to outcome name for all outcomes in this market. Enriched from metadata.
Ordered token IDs for this market. Same order as `outcomes`, `payouts`, and `outcome_prices`.
Ordered outcome labels for this market. Same order as `token_ids`, `payouts`, and `outcome_prices`.
Resolved payout numerators for each outcome. Binary markets usually resolve to `[1, 0]` or `[0, 1]`.
Normalized payout value for each outcome. Binary winners are `1.0`, losing outcomes are `0.0`; split outcomes can be fractional.
Index of the winning outcome when exactly one outcome has a non-zero payout.
Label of the winning outcome when exactly one outcome has a non-zero payout.
Token ID of the winning outcome when exactly one outcome has a non-zero payout.
## Use cases
* **Payout tracking** — monitor who is cashing out resolved positions and how much they receive
* **Wallet PnL** — combine with trade data to calculate realized profit/loss per wallet
* **Market lifecycle** — track the full arc from trading to resolution to redemption
* **Winning wallet alerts** — filter for large `payout` values to spot big winners as they redeem
* **Outcome-aware automations** — use `winning_outcome` or `outcome_prices` directly instead of waiting on slower market metadata refreshes
## Subscribing
Subscribe directly to redemptions:
```json theme={null}
{
"action": "subscribe",
"type": "redemptions"
}
```
Or request redemptions from the settlement stream:
```json theme={null}
{
"action": "subscribe",
"type": "settlements",
"filters": {
"event_types": ["redemption"]
}
}
```
PM2 AutoRedeemer redemptions also appear here and can be matched to combo-native lifecycle data by `tx_hash`, `condition_id`, `payout_e6`, and `log_index`.
Filter to only winning redemptions using `min_size`:
```json theme={null}
{
"action": "subscribe",
"type": "settlements",
"filters": {
"event_types": ["redemption"],
"min_size": 0.01
}
}
```
Track redemptions for specific wallets:
```json theme={null}
{
"action": "subscribe",
"type": "wallets",
"filters": {
"wallets": ["0x8858aae9c92a82b00986a0f1a66338e8b0cc8641"],
"event_types": ["redemption"]
}
}
```
# Settlement Event
Source: https://docs.polynode.dev/websocket/events/settlement
Polymarket settlement — pending (pre-chain) or confirmed (on-chain).
The core polynode event. A Polymarket settlement detected before or after on-chain confirmation.
**Pending settlements arrive 3–5 seconds before on-chain confirmation (1–2 Polygon blocks).** This is the data that makes polynode unique.
**V2 compatible.** Settlement events work identically for both V1 and V2 Polymarket exchanges. V2 settlements are detected automatically — no subscription changes needed. The event format, trade fields, and enrichment are the same. V2 settlement events may include `condition_id` directly from the V2 matchOrders calldata. See the [V2 Migration Guide](/guides/v2-migration).
```json theme={null}
{
"data": {
"block_number": null,
"condition_id": "0xb564e5e20a0d3ab9b07d24ff25b19001d9d6b2d1d3121d4b9a2a0691713643cb",
"detected_at": 1774515004952,
"event_slug": "will-mara-corina-machado-enter-venezuela-by-january-31",
"event_title": "María Corina Machado enters Venezuela by...?",
"event_type": "settlement",
"market_image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/will-mara-corina-machado-enter-venezuela-by-january-31-2JimdK2AkPpQ.jpg",
"market_slug": "will-mara-corina-machado-enter-venezuela-by-april-30",
"market_title": "Will María Corina Machado enter Venezuela by April 30?",
"neg_risk": false,
"outcome": "No",
"outcomes": ["Yes", "No"],
"status": "pending",
"taker_base_fee": null,
"taker_price": 0.85,
"taker_side": "BUY",
"taker_size": 12.1647,
"taker_token": "105661833938076784158167307763414073000325182187633954283290589114908891807534",
"taker_wallet": "0x5b78254f3e639788fed14221bf8a8bd15c2fa88a",
"tick_size": 0.01,
"token_ids": [
"52054255865962303053567273154012826122522952121382457828722214406382382900565",
"105661833938076784158167307763414073000325182187633954283290589114908891807534"
],
"tokens": {
"105661833938076784158167307763414073000325182187633954283290589114908891807534": "No",
"52054255865962303053567273154012826122522952121382457828722214406382382900565": "Yes"
},
"trades": [
{
"maker": "0x0bfb8009df6c46c1fdd79b65896cf224dc4526a7",
"maker_amount": "96000000",
"order_hash": "0x2ca8ef3e7a72fac98a54fba5d92cdbf352633e3f7d929252f3204c35ca65ae80",
"outcome": "Yes",
"price": 0.16,
"side": "BUY",
"signer": "0x9b4371d8fe1fad11c0a2209ebbc4ca97a040b3f2",
"size": 600.0,
"taker": "0x5b78254f3e639788fed14221bf8a8bd15c2fa88a",
"taker_amount": "600000000",
"token_id": "52054255865962303053567273154012826122522952121382457828722214406382382900565"
},
{
"maker": "0x5b78254f3e639788fed14221bf8a8bd15c2fa88a",
"maker_amount": "10340000",
"order_hash": "0x42961ee60f3ad3bea6df8fb1f98abcf090a05cd03b70d697ab899ad4a960059d",
"outcome": "No",
"price": 0.85,
"side": "BUY",
"signer": "0x28167f8e9da7739f338056e52cea318892cad166",
"size": 12.1647,
"taker": "0x5b78254f3e639788fed14221bf8a8bd15c2fa88a",
"taker_amount": "12164700",
"token_id": "105661833938076784158167307763414073000325182187633954283290589114908891807534"
}
],
"tx_hash": "0xa5afa27aa8a7c1f75e24202f4a92d266b0ca965a306e3846574d0c1004a41b63"
},
"timestamp": 1774515004952,
"type": "settlement"
}
```
Every WebSocket event is wrapped with three top-level fields: `type`, `timestamp`, and `data`. The `data` object also contains an `event_type` field that always matches the top-level `type` — use whichever is convenient for your parsing logic.
## Fields
Always `"settlement"`.
Unix milliseconds. Use this as the canonical event time.
Transaction hash (0x-prefixed).
`"pending"` — detected pre-chain, not yet confirmed.
`"confirmed"` — included in a block.
Unix milliseconds when the transaction was first detected by PolyNode.
Block number. `null` when `status` is `"pending"`, set when `"confirmed"`.
Taker (settler) wallet address.
Polymarket conditional token ID.
`"BUY"` or `"SELL"` — the taker's side of the trade.
Fill price (0.0–1.0 for binary markets).
Fill size in shares.
Human-readable market question. Enriched from Polymarket metadata.
Outcome name (e.g. "Yes", "No", "Donald Trump"). Enriched from metadata.
Market URL slug (e.g. `"bitcoin-100k-2026"`). Enriched from metadata.
Parent event title. Enriched from metadata.
Market image URL. Enriched from metadata.
Whether this market uses the negRisk contract type. Enriched from metadata.
Minimum price increment (e.g. `0.01` or `0.001`). Enriched from metadata.
Taker fee rate in basis points. `200` = 2%, `1000` = 10%, `null` = no fee. Short-term crypto markets (5-minute ETH/BTC) typically have `1000` (10%), while standard markets have `200` (2%). Enriched from Polymarket metadata.
Market condition ID (CTF contract identifier). Enriched from metadata.
Parent event URL slug (e.g. `"will-mara-corina-machado-enter-venezuela-by-january-31"`). Enriched from metadata.
Ordered list of outcome names (e.g. `["Yes", "No"]`). Index-aligned with `token_ids`. Enriched from metadata.
Ordered list of token IDs for this market. Index-aligned with `outcomes`. Enriched from metadata.
Map of token ID → outcome name for all outcomes in this market (e.g. `{"123...": "Yes", "456...": "No"}`). Enriched from metadata.
Array of matched maker orders in this settlement.
Maker wallet address.
Order signer address (may differ from maker for smart contract wallets).
Taker wallet address.
Conditional token ID.
`"BUY"` or `"SELL"`.
Fill price (0.0–1.0).
Fill size in shares.
Raw maker amount (6 decimal USDC or tokens).
Raw taker amount (6 decimal USDC or tokens).
Outcome name for this trade's token (e.g. "Yes", "No", "Up"). Enriched from metadata.
EIP-712 order hash for this maker's limit order. Persistent across partial fills. Computed from calldata pre-chain, so this is available on pending settlements before block confirmation.
Exchange contract version: `"v2"` for trades settled on Polymarket's V2 exchange contracts. Absent for V1 trades (current default). Use this to distinguish which exchange system processed the settlement.
## Ordering guarantees
**Confirmed settlements** are delivered in strict block order. All confirmed events from block N arrive before any from block N+1. Within a block, events are ordered by log index. Polygon uses Bor consensus with single-validator sprints, making chain reorganizations effectively nonexistent.
**Pending settlements** are best-effort ordered by detection time. Because they are extracted from the mempool before block inclusion, there is no guaranteed global sequence. Two pending events detected within the same second may arrive in either order. Do not assume pending ordering reflects eventual on-chain ordering.
**Pending-to-confirmed pairing:** every pending settlement will be followed by a corresponding `status_update` event when the same `tx_hash` confirms on-chain. You will never receive a confirmed update without a prior pending event for the same transaction.
**A note on sender and nonce:** Polymarket trades are submitted by Polymarket's relayer EOAs, not by the traders themselves. Users sign EIP-712 orders off-chain, and the relayer submits the on-chain transaction. The transaction sender and nonce reflect the relayer's state, not the trader's intent or ordering. For this reason, sender address and nonce are not included in settlement events — they would be misleading to build on.
## The full lifecycle
For every Polymarket settlement, polynode emits up to three events on the WebSocket stream as the transaction moves from mempool to confirmed block:
| Event | When it fires | Source | What it contains |
| --------------------------------------- | ------------------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| **`settlement` (pending)** | 3–5s before block confirmation | Mempool calldata | All fills in `data.trades[]`, decoded from the matchOrders calldata. Per-maker prices are estimated from aggregate amounts. |
| **`status_update`** | At block confirmation | Receipt logs | Confirmation metadata (block, latency) **plus** `data.confirmed_fills[]` — exact per-fill data from the on-chain `OrderFilled` logs. |
| **`settlement` (confirmed)** *(legacy)* | At block confirmation | Calldata replay | Same shape as the pending settlement, with `status: "confirmed"` and `block_number` set. Same calldata-derived prices. |
The pending settlement gives you speed (2–5 second lead before the block). The `status_update` with `confirmed_fills` gives you exactness — those are the canonical prices Polymarket itself reads from the chain.
### Pending → confirmed pairing
Every pending settlement is followed by a corresponding `status_update` event when the same `tx_hash` confirms on-chain. Match them by `tx_hash`:
```javascript theme={null}
const pending = new Map();
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === "settlement" && msg.data.status === "pending") {
pending.set(msg.data.tx_hash, msg.data);
return;
}
if (msg.type === "status_update") {
const original = pending.get(msg.data.tx_hash);
pending.delete(msg.data.tx_hash);
// The status_update has confirmed_fills with the EXACT prices from
// the on-chain OrderFilled logs. Compare against the pending estimate
// if you want, or just use confirmed_fills as the source of truth.
for (const fill of msg.data.confirmed_fills || []) {
console.log(`Confirmed: ${fill.side} ${fill.size} @ ${fill.price} (block ${msg.data.block_number})`);
}
}
};
```
### When to use which
* **Pending `settlement` only** — you want the 2–5 second lead time and you don't mind \~0.01–0.04 estimation error on the rare multi-maker fill (e.g. copy trading, frontend price ticks).
* **`status_update.confirmed_fills` only** — you need exact prices and don't care about pre-confirmation. (e.g. analytics, P\&L, bookkeeping).
* **Both together (pending settlement + status\_update with confirmed\_fills)** — you want the speed of pending detection AND the exact prices once confirmed. The recommended pattern for most production integrations.
For the full schema of `confirmed_fills[]` and a longer treatment of when to use each layer, see the [Trade Tracking guide](/guides/trade-tracking) and the [Status Update event reference](/websocket/events/status-update).
## Pending vs confirmed (field-level)
| Field | Pending | Confirmed |
| -------------- | ------------------------------- | --------------------------------- |
| `status` | `"pending"` | `"confirmed"` |
| `block_number` | `null` | block number |
| `detected_at` | when polynode first detected TX | same value |
| Latency | \~0ms from detection | 2–5s after detection (1–2 blocks) |
Subscribe with `"status": "pending"` to get the 2–5 second edge. You'll receive a separate `status_update` event when the same transaction confirms on-chain, including `latency_ms` and `confirmed_fills[]`.
## Use cases
* **Copy trading** — detect whale trades 1–2 blocks before confirmation and execute your own order
* **Market making** — adjust spreads based on incoming settlements
* **Analytics** — track real-time volume and price movements
* **Alerts** — notify on large trades or specific wallet activity
## Tracking a specific wallet's trades
**To find a wallet's trades, iterate `data.trades[]` and match by `maker`. Never match by `taker`.** The `maker` field on each entry is the wallet that placed the order. The `taker` field is the counterparty, which is not what you want.
Each entry in `data.trades[]` is one fill, and the `maker` field on that entry is the wallet whose trade it is. To track a specific wallet, find the entries in `trades[]` where `maker === your_wallet`:
```javascript theme={null}
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type !== "settlement") return;
for (const fill of msg.data.trades) {
if (fill.maker.toLowerCase() === MY_WALLET.toLowerCase()) {
console.log("My trade:", fill.side, fill.size, fill.outcome, "@", fill.price);
}
}
};
```
That's it. One rule: **`maker === your_wallet`**.
### Why not match by `taker`
If you match by `taker === your_wallet`, you get **counterparty fills**, not the wallet's actual trade. The `taker` field on each fill is the opposite party, so matching it gives you the wrong wallet's perspective — including the **opposite token** and the **complement price**. For example, if your wallet bought Up at 0.80, matching by `taker` returns a fill showing the counterparty buying Down at 0.20.
For a deeper explanation of why the on-chain `OrderFilled` events are structured this way, see the [Trade Event reference](/websocket/events/trade#tracking-a-specific-wallets-trades) or the [Dome migration guide](/dome-migration#order-perspective).
# Status Update Event
Source: https://docs.polynode.dev/websocket/events/status-update
Notification when a pending settlement confirms on-chain, with exact fill data from receipt logs.
Fired when a previously pending settlement is confirmed in a block. Links back to the original `settlement` event via `tx_hash`, includes the confirmation latency, and attaches **confirmed fill data** from on-chain `OrderFilled` receipt logs.
The `confirmed_fills` array contains the exact execution prices and sizes from the blockchain — the same data source Polymarket's own activity API reads. This gives you both speed (pre-confirmation detection) and accuracy (on-chain execution data) in a single subscription.
```json theme={null}
{
"data": {
"block_number": 85296338,
"condition_id": "0xcd1b782d94423552e19db1ca2bec7bcf5a445de8586638e41da75fae3d138ac4",
"confirmed_at": 1775713418000,
"confirmed_fills": [
{
"order_hash": "0x8238c0666c...",
"maker": "0x48ac40fc...",
"taker": "0xc51f90d3...",
"token_id": "97486603884638701907147078120258734284412618667021950609483989581295500093074",
"side": "BUY",
"price": 0.56,
"size": 2.25,
"maker_amount": "1260000",
"taker_amount": "2250000",
"fee": 0.176785
},
{
"order_hash": "0x3d388dd54a...",
"maker": "0x25f4707c...",
"taker": "0xc51f90d3...",
"token_id": "97486603884638701907147078120258734284412618667021950609483989581295500093074",
"side": "BUY",
"price": 0.56,
"size": 7.0,
"maker_amount": "3920000",
"taker_amount": "7000000",
"fee": 0.55
}
],
"event_title": "Augusta National Invitational - Winner ",
"event_type": "status_update",
"latency_ms": 3928,
"maker_wallets": [
"0x48ac40fc...",
"0x25f4707c..."
],
"market_slug": "will-charl-schwartzel-win-the-2026-masters-tournament",
"market_title": "Will Charl Schwartzel win the 2026 Masters tournament?",
"neg_risk": true,
"order_hashes": [
"0x8238c0666c...",
"0x3d388dd54a..."
],
"outcome": "No",
"outcomes": ["Yes", "No"],
"pending_detected_at": 1775713414072,
"taker_wallet": "0xc51f90d3...",
"tick_size": 0.001,
"token_id": "97486603884638701907147078120258734284412618667021950609483989581295500093074",
"tx_hash": "0x7599f442e4..."
},
"timestamp": 1775713418000,
"type": "status_update"
}
```
## Fields
Always `"status_update"`.
Unix milliseconds of the confirmation.
Transaction hash — matches the original `settlement` event.
Conditional token ID.
Block where the transaction was included.
Unix milliseconds of block confirmation.
Unix milliseconds when the transaction was first detected by polynode.
Time between initial detection and block confirmation, in milliseconds. Typical values: 2000–5000ms (median \~2900ms). Corresponds to 1–2 Polygon blocks.
Taker wallet address from the original settlement.
Maker wallet addresses from the original settlement.
EIP-712 order hashes from the original pending settlement. Each hash uniquely identifies a maker's limit order on Polymarket's order book.
Exact fill data from on-chain `OrderFilled` receipt logs. Each entry represents one maker's fill with the precise execution price and size as settled by the contract. Present when receipt decoding succeeds (vast majority of blocks). `null` on the rare occasion receipt data is unavailable.
EIP-712 order hash for this specific fill.
Maker wallet address.
Taker wallet address (or exchange contract address for the taker's own fill event).
Conditional token ID for this specific fill.
`"BUY"` or `"SELL"` from the maker's perspective.
Exact execution price from the on-chain settlement. This is the canonical price — the same value Polymarket's own APIs report.
Gross token amount filled (before fees). Polymarket's activity API reports net-of-fee sizes, so this value will be slightly larger than what Polymarket shows. Use the `fee` field to compute the net: `net_size = size - fee / price`.
Raw maker amount from the OrderFilled event (integer string, 6 decimal precision for USDC).
Raw taker amount from the OrderFilled event (integer string).
Fee charged on this fill in USDC. `null` if no fee.
Market question. Enriched from metadata.
Outcome name. Enriched from metadata.
Market image URL. Enriched from metadata.
Market URL slug. Enriched from metadata.
Parent event title. Enriched from metadata.
Whether this market uses the negRisk contract type. Enriched from metadata.
Minimum price increment (e.g. `0.01` or `0.001`). Enriched from metadata.
Taker fee rate in basis points. `200` = 2%, `1000` = 10%, `null` = no fee. Short-term crypto markets (5-minute ETH/BTC) typically have `1000` (10%), while standard markets have `200` (2%). Enriched from Polymarket metadata.
Market condition ID (CTF contract identifier). Enriched from metadata.
Ordered list of outcome names (e.g. `["Yes", "No"]`). Index-aligned with `token_ids`. Enriched from metadata.
Ordered list of token IDs for this market. Index-aligned with `outcomes`. Enriched from metadata.
Map of token ID to outcome name for all outcomes in this market (e.g. `{"123...": "Yes", "456...": "No"}`). Enriched from metadata.
`status_update` events arrive in bursts. A single Polygon block typically contains 30–80 Polymarket settlements, so expect 30–80 `status_update` messages within \~100ms each time a block confirms (\~2s interval). If you only need pending detection, subscribe with `"status": "pending"` or exclude `"status_update"` from your `event_types` filter.
## How to use
Match `status_update` events back to their `settlement` by `tx_hash`:
```javascript theme={null}
const pending = new Map(); // tx_hash → settlement data
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === "settlement" && msg.data.status === "pending") {
pending.set(msg.data.tx_hash, msg.data);
}
if (msg.type === "status_update") {
const original = pending.get(msg.data.tx_hash);
console.log(`Confirmed in ${msg.data.latency_ms}ms`, original);
// Use confirmed_fills for exact on-chain prices
if (msg.data.confirmed_fills) {
for (const fill of msg.data.confirmed_fills) {
console.log(` ${fill.side} ${fill.size} @ ${fill.price} (fee: ${fill.fee})`);
}
}
pending.delete(msg.data.tx_hash);
}
};
```
# Trade Event
Source: https://docs.polynode.dev/websocket/events/trade
Confirmed on-chain trade execution from Polymarket exchange contracts.
A confirmed on-chain trade from Polymarket's exchange contracts.
**Don't confuse `trade` events with the `trades[]` array inside `settlement` events.** They are related but not the same thing.
A `trade` event is one of three ways polynode exposes the same fill data, depending on what you need:
| Shape | When it fires | Source | Format |
| --------------------------------- | ---------------------------------------- | ------------------------------------ | -------------------------------- |
| `settlement.trades[]` | Pre-confirmation (3-5s before the block) | Calldata (estimated for multi-maker) | Array inside one event |
| `trade` events (this page) | At block confirmation | Receipt logs (exact) | One separate WS message per fill |
| `status_update.confirmed_fills[]` | At block confirmation | Receipt logs (exact) | Array inside one event |
`trade` events and `confirmed_fills[]` contain the **same exact data** — they're both decoded from the on-chain `OrderFilled` logs, just packaged differently. If you already subscribe to `settlement` events, you can read `confirmed_fills[]` from the `status_update` instead of also subscribing to `trade` events. If you only want the post-confirmation per-fill stream, subscribe to `trade` events directly.
`settlement.trades[]` is the **pre-confirmation equivalent** — it has the same shape but the per-maker prices are estimated from the calldata's aggregate amounts. Use it for speed (3-5 second lead), use `trade` events or `confirmed_fills[]` for exactness.
**V2 compatible.** Trade events work identically for both V1 and V2 Polymarket exchanges. The `fee` field is decoded the same way for V2 trades. No changes to your subscription or parsing logic are needed.
```json theme={null}
{
"data": {
"block_number": 84705489,
"condition_id": "0x158fa5bd542d4982a5b09eee29c5d23f8f1304367adc1a8e4396b692eb550466",
"event_slug": "eth-updown-5m-1774530900",
"event_title": "Ethereum Up or Down - March 26, 9:15AM-9:20AM ET",
"event_type": "trade",
"exchange": "ctf_exchange",
"fee": 0.122535,
"log_index": 1996,
"maker": "0x4b8cf80092c60b9b23d22133eb63eb4508fe4d31",
"maker_amount": "2130000",
"market_image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/ETH+fullsize.jpg",
"market_slug": "eth-updown-5m-1774530900",
"market_title": "Ethereum Up or Down - March 26, 9:15AM-9:20AM ET",
"neg_risk": false,
"order_hash": "0x916fa5c2728c93e19ea5d7c254b07234815ad01e59597573c09d852fe53ee564",
"outcome": "Up",
"outcomes": ["Up", "Down"],
"price": 0.71,
"side": "BUY",
"size": 3.0,
"taker": "0x536be02af900fe046fa708c8059c04f737a2cee3",
"taker_amount": "3000000",
"taker_base_fee": 1000.0,
"tick_size": 0.01,
"timestamp": 1774531013000,
"token_id": "67838338591118719610402948028043822510398368770316608063235371949564584838291",
"token_ids": [
"67838338591118719610402948028043822510398368770316608063235371949564584838291",
"106640979554519903529443899392736422684573887313790829687503639341656103167080"
],
"tokens": {
"106640979554519903529443899392736422684573887313790829687503639341656103167080": "Down",
"67838338591118719610402948028043822510398368770316608063235371949564584838291": "Up"
},
"tx_hash": "0xaf3d097f3b2e799c0a14cca4e7709122b397c9b920c276c0fd3caa4848ea9808"
},
"timestamp": 1774531013000,
"type": "trade"
}
```
## Fields
Always `"trade"`.
Unix milliseconds of the block.
Transaction hash.
Block number.
Log index within the transaction. `(tx_hash, log_index)` is globally unique.
Block timestamp in Unix milliseconds.
`"ctf_exchange"` or `"neg_risk_ctf_exchange"`.
Maker wallet address.
Taker wallet address.
Conditional token ID.
`"BUY"` or `"SELL"`.
Fill price (0.0–1.0 for binary markets).
Fill size in shares.
Raw maker amount (6 decimal precision).
Raw taker amount (6 decimal precision).
Fee charged on this trade in USDC. Decoded from the OrderFilled log. `null` when fee is zero or not present.
EIP-712 order hash. Uniquely identifies the maker's limit order on Polymarket's CLOB. **Persistent across partial fills** — the same `order_hash` appears every time a slice of that limit order is matched.
Exchange contract version: `"v2"` for trades on Polymarket's V2 exchange contracts. Absent for V1 trades (current default).
Market question. Enriched from metadata.
Outcome name. Enriched from metadata.
Market URL slug. Enriched from metadata.
Market image URL. Enriched from metadata.
Parent event title. Enriched from metadata.
Whether this market uses the negRisk contract type. Enriched from metadata.
Minimum price increment (e.g. `0.01` or `0.001`). Enriched from metadata.
Taker fee rate in basis points. `200` = 2%, `1000` = 10%, `null` = no fee. Short-term crypto markets (5-minute ETH/BTC) typically have `1000` (10%), while standard markets have `200` (2%). Enriched from Polymarket metadata.
Market condition ID (CTF contract identifier). Enriched from metadata.
Parent event URL slug. Enriched from metadata.
Ordered list of outcome names (e.g. `["Yes", "No"]`). Index-aligned with `token_ids`. Enriched from metadata.
Ordered list of token IDs for this market. Index-aligned with `outcomes`. Enriched from metadata.
Map of token ID → outcome name for all outcomes in this market (e.g. `{"123...": "Yes", "456...": "No"}`). Enriched from metadata.
## Ordering guarantees
Trade events are delivered in strict block order. All trades from block N arrive before any trades from block N+1. Within a block, trades are ordered by `log_index`. The tuple `(tx_hash, log_index)` is globally unique and can be used as a deduplication key.
Polygon uses Bor consensus with single-validator sprints, making chain reorganizations effectively nonexistent. You can safely assume that once you receive a trade from block N+1, you have received all trades from block N.
## Settlement vs trade
| | Settlement | Trade |
| -------- | ------------------------------------ | -------------------------- |
| Source | Pre-chain detection | On-chain confirmation |
| Timing | Pending (pre-chain) or confirmed | Confirmed only |
| Contains | Full settlement with all maker fills | Single maker-taker fill |
| Use case | Early detection, copy trading | Accurate historical record |
A single settlement can contain multiple trades (one per matched maker order). If you subscribe to both `settlement` and `trade`, you'll see the settlement first (pending), then individual trades (confirmed).
**Tracking limit orders across fills:** Use `order_hash` to group partial fills of the same limit order. When a maker places a 1,000-share order and it fills in three separate transactions, all three trades share the same `order_hash`. This is the EIP-712 hash of the signed order struct — it's deterministic from the order parameters and matches Polymarket's CLOB order hash.
## Tracking a specific wallet's trades
**TL;DR — Always filter by `maker`, never by `taker`.** A single transaction emits multiple `trade` events as separate WebSocket messages. The wallet's actual fill is the event where `data.maker === your_wallet`. The other events show the counterparties' perspective.
This is the most common source of confusion when integrating with the trade stream. Once you understand the on-chain structure, the fix is one line.
**`trade` events have no sub-array.** Each fill arrives as its own WebSocket message with top-level `maker`, `taker`, `side`, `price`, `token_id`, etc. Inspect each incoming message individually. If you're looking for a `trades[]` array, you're thinking of [settlement events](/websocket/events/settlement), which bundle all fills from a single `matchOrders` transaction into one message with a nested `data.trades[]` array.
### How `OrderFilled` events work on-chain
Every match on Polymarket emits one `OrderFilled` log per maker order, plus one additional log for the taker's own order. **All fields on each log — `side`, `token_id`, `price`, `size` — are recorded from the maker's perspective on that specific log.**
* `maker` = whoever placed the limit order that got filled (the resting order)
* `taker` = whoever's order matched into it
* `side` = whether the maker was buying or selling
When a user submits an order that matches against N existing maker orders, the on-chain settlement emits **N + 1 events**:
1. **N events with the user as `taker`** — one per counterparty maker order they matched against. These show the counterparties' tokens, prices, and sides, not the user's.
2. **One additional event with the user as `maker`** and the exchange contract as `taker` (`0x4bfb41d5b3570defd03c39a9a4d8de6bd8b8982e` for standard markets, `0xc5d563a36ae78145c45a50134d48a1215220f80a` for neg-risk markets). **This is the user's actual fill.**
Polymarket's activity API and Dome both follow this same convention, because they read directly from these `OrderFilled` logs. If you've used Dome before with `event.user === yourWallet`, the equivalent here is `event.maker === yourWallet`. See the [Dome migration guide](/dome-migration#order-perspective) for the same explanation in Dome's terminology.
### Real example
Live data from transaction `0x910466c40141da9784fe0dd6b8e7a81d0b46ed3667100b83c714ee70eaf39ab2` on April 9, 2026. The user `0xf5a77527f5154e4cd84ef32c73fab4ba58ec4be0` placed a market BUY for the Up token on an Ethereum 5-minute market. The transaction emitted **4 trade events**:
```
log_index=586 maker=0x0b9353ae taker=0xf5a77527 side=BUY price=0.21 size=5 token=Down
log_index=596 maker=0xfcdc071d taker=0xf5a77527 side=BUY price=0.20 size=12 token=Down
log_index=606 maker=0x2e9b93fa taker=0xf5a77527 side=BUY price=0.19 size=1.79 token=Down
log_index=610 maker=0xf5a77527 taker=0xExchange side=BUY price=0.7983 size=18.79 token=Up ← USER'S ACTUAL TRADE
```
The first three events show counterparties (other wallets) buying the **Down** token at 0.21, 0.20, 0.19. The user happens to appear as the `taker` on each of those because the contract matched their order against them, but those events represent the counterparties' trades, not the user's.
The user's actual trade is the fourth event: **`BUY Up at 0.7983 size=18.79`**, where they appear as `maker` and the CTF Exchange contract appears as `taker`. Polymarket's activity API confirms this exact trade: `side=BUY outcome=Up price=0.7983 size=18.51` (the slight size difference is the fee deduction).
If you filter by `taker == wallet`, you get the first three events (counterparty perspective on the Down token). If you filter by `maker == wallet`, you get the user's actual fourth event (the Up token at 0.7983).
### The right way to filter
```javascript theme={null}
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type !== "trade") return;
const t = msg.data;
// CORRECT — match by maker. This is the user's actual fill.
if (t.maker.toLowerCase() === MY_WALLET.toLowerCase()) {
console.log("My trade:", t.side, t.size, t.outcome, "@", t.price);
}
// WRONG — match by taker. Returns counterparty fills with the OPPOSITE
// token and the COMPLEMENT price (e.g. BUY Down 0.20 when you actually
// bought Up at 0.80).
// if (t.taker.toLowerCase() === MY_WALLET.toLowerCase()) { ... }
};
```
### Subscribing only to a specific wallet
If you only want events involving a specific wallet, use the `wallets` filter on the subscription. The server-side filter matches **both** `maker` and `taker` fields, so you receive every event the wallet appears in. Apply `maker === wallet` on the client side to get only the wallet's actual trades:
```javascript theme={null}
ws.send(JSON.stringify({
action: "subscribe",
type: "trades",
filters: { wallets: ["0xYourWallet"] }
}));
```
# Examples
Source: https://docs.polynode.dev/websocket/examples
Complete WebSocket examples in JavaScript, Python, and wscat.
## JavaScript / Node.js
### Track all pending settlements
```javascript theme={null}
const WebSocket = require("ws");
const ws = new WebSocket("wss://ws.polynode.dev/ws?key=pn_live_YOUR_KEY");
ws.on("open", () => {
ws.send(JSON.stringify({
action: "subscribe",
type: "settlements",
filters: { status: "pending" }
}));
});
ws.on("message", (data) => {
const msg = JSON.parse(data);
switch (msg.type) {
case "snapshot":
console.log(`Loaded ${msg.count} recent settlements`);
break;
case "settlement":
const d = msg.data;
console.log(
`${d.status.toUpperCase()} | ${d.taker_side} ${d.taker_size} shares @ ${d.taker_price}`,
`| "${d.market_title}" (${d.outcome})`
);
break;
case "status_update":
console.log(`Confirmed: ${msg.data.tx_hash} in ${msg.data.latency_ms}ms`);
break;
case "heartbeat":
break; // ignore
}
});
// Reconnect on close
ws.on("close", () => setTimeout(() => process.exit(1), 1000));
```
### Whale alerts (\$5K+)
```javascript theme={null}
const ws = new WebSocket("wss://ws.polynode.dev/ws?key=pn_live_YOUR_KEY");
ws.on("open", () => {
ws.send(JSON.stringify({
action: "subscribe",
type: "large_trades",
filters: { min_size: 5000 }
}));
});
ws.on("message", (data) => {
const msg = JSON.parse(data);
if (msg.type === "settlement") {
const d = msg.data;
console.log(`WHALE ALERT: ${d.taker_side} $${d.taker_size.toFixed(0)} @ ${d.taker_price}`);
console.log(` Market: ${d.market_title} (${d.outcome})`);
console.log(` Wallet: ${d.taker_wallet}`);
console.log(` TX: ${d.tx_hash}`);
}
});
```
### Monitor a specific market by slug
```javascript theme={null}
ws.send(JSON.stringify({
action: "subscribe",
type: "markets",
filters: {
slugs: ["will-pete-hegseth-win-the-2028-us-presidential-election"],
snapshot_count: 100 // Get last 100 events
}
}));
```
### Track a wallet
```javascript theme={null}
const TRACKED_WALLET = "0xabcdef1234567890abcdef1234567890abcdef12";
ws.send(JSON.stringify({
action: "subscribe",
type: "wallets",
filters: { wallets: [TRACKED_WALLET] }
}));
// To find the wallet's actual trades inside settlement events,
// iterate data.trades[] and match by maker. NEVER match by taker.
ws.on("message", (raw) => {
const msg = JSON.parse(raw);
if (msg.type !== "settlement") return;
for (const fill of msg.data.trades) {
if (fill.maker.toLowerCase() === TRACKED_WALLET.toLowerCase()) {
console.log(`Trade: ${fill.side} ${fill.size} ${fill.outcome} @ ${fill.price}`);
}
}
});
```
Always match by `fill.maker`, never by `fill.taker`. The `taker` field is the counterparty — matching it returns the wrong wallet's perspective with the **opposite token** and the **complement price**. See the [Settlement Event reference](/websocket/events/settlement#tracking-a-specific-wallets-trades) for full details.
### Chainlink price feed
```javascript theme={null}
const ws = new WebSocket("wss://ws.polynode.dev/ws?key=pn_live_YOUR_KEY");
ws.on("open", () => {
ws.send(JSON.stringify({
action: "subscribe",
type: "chainlink",
filters: { feeds: ["BTC/USD"] }
}));
});
ws.on("message", (data) => {
const msg = JSON.parse(data);
if (msg.type === "price_feed") {
const d = msg.data;
console.log(`BTC/USD: $${d.price.toFixed(2)} (bid: $${d.bid.toFixed(2)}, ask: $${d.ask.toFixed(2)})`);
}
});
```
### Settlements + price feeds on same connection
```javascript theme={null}
const ws = new WebSocket("wss://ws.polynode.dev/ws?key=pn_live_YOUR_KEY");
ws.on("open", () => {
// Subscribe to both — they're independent streams
ws.send(JSON.stringify({ action: "subscribe", type: "settlements" }));
ws.send(JSON.stringify({ action: "subscribe", type: "chainlink" }));
});
ws.on("message", (data) => {
const msg = JSON.parse(data);
switch (msg.type) {
case "settlement":
console.log(`TRADE: ${msg.data.taker_side} ${msg.data.taker_size} @ ${msg.data.taker_price}`);
break;
case "price_feed":
console.log(`BTC: $${msg.data.price.toFixed(2)}`);
break;
}
});
```
## Python
### asyncio client with reconnection
```python theme={null}
import asyncio
import json
import websockets
API_KEY = "pn_live_YOUR_KEY"
URL = f"wss://ws.polynode.dev/ws?key={API_KEY}"
async def connect():
while True:
try:
async with websockets.connect(URL) as ws:
# Subscribe to pending settlements
await ws.send(json.dumps({
"action": "subscribe",
"type": "settlements",
"filters": {"status": "pending", "min_size": 100}
}))
async for message in ws:
msg = json.loads(message)
if msg["type"] == "snapshot":
print(f"Loaded {msg['count']} recent events")
elif msg["type"] == "settlement":
d = msg["data"]
print(f"{d['taker_side']} {d['taker_size']:.0f} @ {d['taker_price']:.2f}"
f" | {d.get('market_title', 'unknown')}")
elif msg["type"] == "status_update":
print(f"Confirmed: {msg['data']['tx_hash'][:16]}..."
f" ({msg['data']['latency_ms']}ms)")
except (websockets.ConnectionClosed, ConnectionError) as e:
print(f"Disconnected: {e}. Reconnecting in 3s...")
await asyncio.sleep(3)
asyncio.run(connect())
```
### Multiple wallet subscriptions
```javascript theme={null}
const ws = new WebSocket("wss://ws.polynode.dev/ws?key=pn_live_YOUR_KEY");
const subscriptionIds = [];
ws.on("open", () => {
// Subscribe to multiple wallet groups — each creates an independent subscription
ws.send(JSON.stringify({
action: "subscribe",
type: "wallets",
filters: { wallets: ["0xWallet1...", "0xWallet2..."] }
}));
ws.send(JSON.stringify({
action: "subscribe",
type: "wallets",
filters: { wallets: ["0xWallet3...", "0xWallet4..."] }
}));
});
ws.on("message", (data) => {
const msg = JSON.parse(data);
if (msg.type === "subscribed") {
// Save subscription_id for targeted unsubscribe later
subscriptionIds.push(msg.subscription_id);
console.log(`Subscribed: ${msg.subscription_id}`);
}
if (msg.type === "settlement") {
console.log(`${msg.data.taker_side} ${msg.data.taker_size} @ ${msg.data.taker_price}`);
}
});
// Later: remove just one subscription
// ws.send(JSON.stringify({ action: "unsubscribe", subscription_id: subscriptionIds[0] }));
// Or remove all
// ws.send(JSON.stringify({ action: "unsubscribe" }));
```
## wscat (CLI)
Quick testing from the command line:
```bash theme={null}
# Install
npm install -g wscat
# Connect
wscat -c "wss://ws.polynode.dev/ws?key=pn_live_YOUR_KEY"
# Once connected, paste:
{"action": "subscribe", "type": "settlements", "filters": {"status": "pending"}}
```
```bash theme={null}
# Subscribe to Chainlink BTC/USD prices
{"action": "subscribe", "type": "chainlink"}
# Subscribe to whale trades (stacks with previous subscriptions)
{"action": "subscribe", "type": "large_trades", "filters": {"min_size": 10000}}
# Subscribe by market slug
{"action": "subscribe", "type": "markets", "filters": {"slugs": ["bitcoin-100k-2026"]}}
# Ping
{"action": "ping"}
# Unsubscribe a specific subscription (use the subscription_id from the ack)
{"action": "unsubscribe", "subscription_id": "abc123:1"}
# Unsubscribe all
{"action": "unsubscribe"}
```
## curl (WebSocket upgrade)
```bash theme={null}
# Using websocat (Rust CLI tool)
websocat "wss://ws.polynode.dev/ws?key=pn_live_YOUR_KEY"
# Then type:
{"action":"subscribe","type":"settlements"}
```
## Common patterns
### Reconnection with exponential backoff
```javascript theme={null}
function createConnection() {
let delay = 1000;
function connect() {
const ws = new WebSocket("wss://ws.polynode.dev/ws?key=pn_live_YOUR_KEY");
ws.onopen = () => {
delay = 1000; // Reset backoff
ws.send(JSON.stringify({
action: "subscribe",
type: "settlements"
}));
};
ws.onclose = () => {
console.log(`Reconnecting in ${delay}ms...`);
setTimeout(connect, delay);
delay = Math.min(delay * 2, 30000); // Max 30s
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type !== "heartbeat") {
handleEvent(msg);
}
};
}
connect();
}
```
### Pending-to-confirmed tracking
```javascript theme={null}
const pending = new Map();
function handleEvent(msg) {
if (msg.type === "settlement" && msg.data.status === "pending") {
pending.set(msg.data.tx_hash, {
...msg.data,
received_at: Date.now()
});
console.log(`PENDING: ${msg.data.taker_side} ${msg.data.taker_size} @ ${msg.data.taker_price}`);
}
if (msg.type === "status_update") {
const original = pending.get(msg.data.tx_hash);
if (original) {
console.log(`CONFIRMED: ${msg.data.latency_ms}ms latency, block ${msg.data.block_number}`);
pending.delete(msg.data.tx_hash);
}
}
}
```
# WebSocket Overview
Source: https://docs.polynode.dev/websocket/overview
Real-time Polymarket trades, 3-5 seconds before on-chain confirmation.
polynode's WebSocket is the fastest way to get Polymarket trade data. Every fill is delivered **3-5 seconds before on-chain confirmation** (1-2 blocks early). Subscribe to `fills` to get one clean event per trade, or `settlements` for the full transaction lifecycle.
Individual trades, one per message. Pre-confirmation, flat format, no array parsing. The fastest way to track trades.
Full transaction bundles with all fills in a `trades[]` array plus status lifecycle tracking.
Confirmed on-chain fills. Same data as settlements but after block confirmation with exact receipt values.
Polymarket combo executions and confirmations with enriched leg metadata.
Settlements and fills detected before on-chain confirmation. Typically 3-5 seconds (1-2 blocks) early.
Subscribe by wallet, token, market slug, side, size, or event type. Only receive what you need.
Every event includes market title, outcome name, slug, and image. No secondary lookups needed.
Real-time prices for BTC, ETH, SOL, BNB, XRP, DOGE, and HYPE (\~1/second each). Subscribe with `"type": "chainlink"` on the same connection.
UMA Optimistic Oracle events: market resolutions, disputes, proposals, and admin actions. Subscribe with `"type": "oracle"` to track the full resolution lifecycle.
**Save \~60% bandwidth with compression.** Add `&compress=zlib` to your connection URL and decompress binary frames with standard `inflateRaw`. Zero latency impact, recommended for production. [See compression docs →](/websocket/compression)
## Quick start
```bash theme={null}
curl -s -X POST https://api.polynode.dev/v1/keys \
-H "Content-Type: application/json" \
-d '{"name": "my-app"}'
```
Save the `pn_live_...` key from the response.
Save this as `stream.js` and run with `API_KEY=pn_live_... node stream.js`:
```javascript Node.js theme={null}
const WebSocket = require("ws");
const API_KEY = process.env.API_KEY || "YOUR_API_KEY";
const DURATION = 30000;
const ws = new WebSocket("wss://ws.polynode.dev/ws?key=" + API_KEY);
ws.on("open", () => {
ws.send(JSON.stringify({ action: "subscribe", type: "fills" }));
console.log("Streaming trades for " + (DURATION / 1000) + "s...\n");
});
let count = 0;
ws.on("message", (raw) => {
const msg = JSON.parse(raw);
if (msg.type !== "event") return;
count++;
const d = msg.data;
console.log({
outcome: d.token_label,
side: d.side,
price: d.price,
shares: d.shares_normalized,
user: d.user,
market: d.title
});
});
ws.on("close", () => console.log("\nDone. " + count + " trades."));
setTimeout(() => ws.close(), DURATION);
```
```python Python theme={null}
import asyncio, json, websockets, time
API_KEY = "YOUR_API_KEY"
DURATION = 30
async def stream():
url = f"wss://ws.polynode.dev/ws?key={API_KEY}"
async with websockets.connect(url) as ws:
await ws.send(json.dumps({"action": "subscribe", "type": "fills"}))
print(f"Streaming trades for {DURATION}s...\n")
count = 0
end = time.time() + DURATION
async for message in ws:
if time.time() > end:
break
msg = json.loads(message)
if msg.get("type") != "event":
continue
count += 1
d = msg["data"]
print(f"{d['token_label']:5} {d['side']:4} {d['shares_normalized']:>8.2f} shares @ {d['price']:<8} | {d['user'][:12]}... | {d['title'][:50]}")
print(f"\nDone. {count} trades.")
asyncio.run(stream())
```
You'll see individual trades streaming in, one per line. Each event is a single fill with clear fields for outcome, side, price, shares, and the wallet involved.
Every message is one flat event with everything you need. Here's a real captured fill:
```json theme={null}
{
"type": "event",
"data": {
"side": "BUY",
"price": 0.85,
"shares_normalized": 1.67,
"token_label": "Up",
"title": "Bitcoin Up or Down - April 30, 2:50PM-2:55PM ET",
"user": "0xe54bbd9dcef861af50bf2c4dcc354a87e87bff96",
"taker": "0x70412cbf28288d47ece30002be45efff5d2d5c73",
"order_hash": "0x40169bc2d2af4dded1fbe4d2655838f2b79ba4a9aabc548b8fec3649e66ed9f4",
"tx_hash": "0xef710f81783210de00d90fd477d3c190c3acd657cf7dd5dae31188c53ded2e4e",
"market_slug": "btc-updown-5m-1777575000",
"condition_id": "0x6fe03c3c781f62d456518a3a6b2b95c0d0bee427243bcef06f5b37b3f1c395b7",
"token_id": "28011179493432352359106676407817901302975340324989108630179591254583544078282",
"shares": 1670000,
"timestamp": 1777575164,
"block_number": null,
"log_index": null
}
}
```
`block_number: null` means this was detected **before** the block confirmed. Pre-confirmation delivery, 3-5 seconds ahead of on-chain settlement. One event per fill, no arrays to iterate, all fields included.
**`fills` vs `settlements`** — both stream the same trades at the same speed. `fills` gives you one flat event per trade (simplest). `settlements` gives you the full transaction bundle with a `trades[]` array and lifecycle tracking via `status_update` events. Start with `fills`, move to `settlements` when you need the full lifecycle. See [Subscriptions](/websocket/subscribing) for all options.
If you need the full transaction format with nested fills and status lifecycle, subscribe to `settlements` instead:
```javascript theme={null}
const ws = new WebSocket("wss://ws.polynode.dev/ws?key=pn_live_YOUR_KEY");
ws.onopen = () => {
ws.send(JSON.stringify({ action: "subscribe", type: "fills" }));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === "settlement") {
const d = msg.data;
console.log(`${d.taker_side} ${d.taker_size} @ ${d.taker_price} | ${d.market_title}`);
for (const fill of d.trades) {
console.log(` ${fill.side} ${fill.size} @ ${fill.price} | maker: ${fill.maker}`);
}
}
};
```
See [Settlement Event](/websocket/events/settlement) for the full field reference.
## Connection URL
```
wss://ws.polynode.dev/ws?key=pn_live_YOUR_KEY
```
With compression (\~60% bandwidth savings):
```
wss://ws.polynode.dev/ws?key=pn_live_YOUR_KEY&compress=zlib
```
The API key is passed as a query parameter. Keys starting with `pn_live_` or `qm_live_` are accepted.
## Connection limits
Most apps only need one Event WebSocket connection. A single connection can carry multiple filtered subscriptions at once, including fills, settlements, trades, blocks, wallet filters, market filters, oracle events, and Chainlink price feeds.
| Tier | Concurrent Event WebSockets | Filter items per subscription | Session limit |
| ------------------------- | --------------------------- | ----------------------------- | ------------- |
| **Free** | 1 | 10,000 | 1 hour/day |
| **Starter** (\$50/mo) | 10 | 10,000 | Unlimited |
| **Growth** (\$200/mo) | 50 | 10,000 | Unlimited |
| **Enterprise** (\$750/mo) | Unlimited | Custom | Unlimited |
Event WebSocket limits apply to `ws.polynode.dev`. Orderbook WebSocket limits for `ob.polynode.dev` are listed separately in the [Orderbook Stream](/orderbook/overview) docs.
## Multiple streams, one connection
PolyNode supports multiple subscription types on the same WebSocket:
| Stream | Purpose | Subscribe |
| ---------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| **Fills** | Individual pre-confirmation trades, one flat event per fill. Simplest format. | `{"action":"subscribe","type":"fills"}` |
| **Settlements** | Pre-confirmation transaction bundles with all fills in a `trades[]` array (default) | `{"action":"subscribe","type":"settlements"}` |
| **Trades** | Confirmed on-chain fills with exact receipt values | `{"action":"subscribe","type":"trades"}` |
| **Prices** | Price-moving settlement and trade events for specific markets | `{"action":"subscribe","type":"prices"}` |
| **Combos** | Polymarket combo executions and confirmations with enriched legs | `{"action":"subscribe","type":"combos"}` |
| **Blocks** | New Polygon block notifications with Polymarket stats | `{"action":"subscribe","type":"blocks"}` |
| **Wallets** | All activity for specified wallets | `{"action":"subscribe","type":"wallets","filters":{"wallets":["0x..."]}}` |
| **Markets** | All activity for specified markets | `{"action":"subscribe","type":"markets","filters":{"tokens":["..."]}}` |
| **Deposits** | USDC.e and Polymarket USD deposit/withdrawal activity | `{"action":"subscribe","type":"deposits"}` |
| **Large Trades** | Whale alerts (\$1K+ by default) | `{"action":"subscribe","type":"large_trades"}` |
| **Global** | Full firehose of the default public event set | `{"action":"subscribe","type":"global"}` |
| **Oracle** | UMA resolution lifecycle (resolutions, disputes, flags) | `{"action":"subscribe","type":"oracle"}` |
| **Chainlink** | Real-time crypto prices (7 feeds, \~1/sec each) | `{"action":"subscribe","type":"chainlink"}` |
All can run simultaneously on the same connection. See [Subscriptions & Filters](/websocket/subscribing) for full details on each type, including default event types and available filters.
## Heartbeat
The server sends a heartbeat every 30 seconds:
* A WebSocket-level **Ping** frame (handled automatically by WS clients)
* A text message: `{"type": "heartbeat", "ts": 1772386305181}`
If no heartbeat arrives within \~35 seconds, the connection is dead — reconnect.
The server monitors client liveness via incoming messages and Pong frames. If no activity is received from the client within 5 minutes, the server closes the connection with a close frame explaining the reason. Standard WebSocket clients handle Pong automatically, which counts as activity.
### Application-level keepalive
Send a ping message to confirm the connection is alive:
```json theme={null}
{"action": "ping"}
```
Response:
```json theme={null}
{"type": "pong", "ts": 1774429169820}
```
Any message you send (subscribe, unsubscribe, ping) resets the server's liveness timer.
**Running behind a reverse proxy (Railway, Render, Heroku, AWS ALB, etc.)?** Some cloud platform proxies intercept WebSocket Ping/Pong control frames at the proxy layer and don't forward them to your application. This means the server never receives your client's automatic Pong responses, and the connection gets dropped for inactivity.
**The fix:** Send `{"action": "ping"}` every 30-60 seconds from your application code. These are regular text frames that pass through any proxy. This is the recommended keepalive method for any containerized or cloud-hosted deployment.
```python theme={null}
# Python example — keepalive for cloud-hosted clients
import asyncio, json, websockets
async def stream():
url = "wss://ws.polynode.dev/ws?key=pn_live_YOUR_KEY"
async with websockets.connect(url, ping_interval=None, ping_timeout=None) as ws:
await ws.send(json.dumps({"action": "subscribe", "type": "fills"}))
async def keepalive():
while True:
await asyncio.sleep(30)
await ws.send(json.dumps({"action": "ping"}))
ping_task = asyncio.create_task(keepalive())
try:
async for message in ws:
msg = json.loads(message)
if msg["type"] in ("pong", "heartbeat"):
continue
# handle events...
finally:
ping_task.cancel()
asyncio.run(stream())
```
```javascript theme={null}
// Node.js example — keepalive for cloud-hosted clients
const ws = new WebSocket("wss://ws.polynode.dev/ws?key=pn_live_YOUR_KEY");
ws.onopen = () => {
ws.send(JSON.stringify({ action: "subscribe", type: "fills" }));
setInterval(() => ws.send(JSON.stringify({ action: "ping" })), 30000);
};
```
## Connection lifecycle
```
Connect → wss://ws.polynode.dev/ws?key=pn_live_...
↓
Send → {"action": "subscribe", "type": "fills"}
↓
Receive ← {"type": "subscribed", "subscriber_id": "...", "subscription_id": "...:1"}
↓
Send → {"action": "subscribe", ...} (additional subscriptions stack)
Receive ← {"type": "subscribed", "subscription_id": "...:2"}
↓
Receive ← {"type": "event", "data": {...}} (live fills)
Receive ← Ping frame (every 30s)
Send → Pong frame (automatic)
Receive ← {"type": "heartbeat", ...} (every 30s)
Send → {"action": "ping"} (optional, recommended if cloud-hosted)
Receive ← {"type": "pong", "ts": ...} (keepalive response)
↓
Send → {"action": "unsubscribe", "subscription_id": "...:1"} (remove one)
Send → {"action": "unsubscribe"} (remove all)
Receive ← {"type": "unsubscribed"}
```
## Error handling
Invalid messages return structured errors:
```json theme={null}
{
"type": "error",
"code": "invalid_json",
"message": "Message is not valid JSON."
}
```
| Error code | Cause |
| -------------------------- | ----------------------------------------- |
| `invalid_json` | Message is not valid JSON |
| `invalid_message` | Unknown action or missing required fields |
| `unresolved_slugs` | Slug not found in market metadata |
| `unresolved_condition_ids` | Condition ID not found in metadata |
## Reconnection
PolyNode does not send a close frame before disconnecting. Implement reconnection with exponential backoff and use the `since` filter to fill any gaps:
```javascript theme={null}
let delay = 1000;
let lastEventTs = null;
function connect() {
const ws = new WebSocket("wss://ws.polynode.dev/ws?key=pn_live_...");
ws.onopen = () => {
delay = 1000;
const filters = lastEventTs ? { since: lastEventTs } : {};
ws.send(JSON.stringify({ action: "subscribe", type: "fills", filters }));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.timestamp) lastEventTs = msg.timestamp;
if (msg.type === "snapshot") {
// Process snapshot events (gap-fill from last disconnect)
for (const e of msg.events) {
if (e.timestamp) lastEventTs = Math.max(lastEventTs || 0, e.timestamp);
}
}
};
ws.onclose = () => setTimeout(connect, Math.min(delay *= 2, 30000));
}
```
The `since` filter returns all events after that timestamp within your tier's lookback window (free: 30s, starter: 2min, growth/enterprise: 5min). When `since` is set, it overrides `snapshot_count`. For outages longer than your lookback window, use the REST API to backfill.
# Subscriptions & Filters
Source: https://docs.polynode.dev/websocket/subscribing
Subscription types, filter options, and how event matching works.
## Subscribe message
Send a JSON message after connecting to start receiving events:
```json theme={null}
{
"action": "subscribe",
"type": "settlements",
"filters": {
"wallets": ["0x1234..."],
"tokens": ["21742633..."],
"slugs": ["bitcoin-100k-2026"],
"condition_ids": ["0xabc..."],
"side": "BUY",
"status": "pending",
"min_size": 100,
"max_size": 10000,
"event_types": ["settlement", "status_update"],
"snapshot_count": 50,
"since": 1774412600000
}
}
```
All fields except `action` are optional.
## Subscribe response
Successful subscriptions return an acknowledgement:
```json theme={null}
{
"type": "subscribed",
"subscriber_id": "connection-id",
"subscription_id": "connection-id:0",
"subscription_type": "combos"
}
```
Event messages use this envelope:
```json theme={null}
{
"type": "combo_execution",
"timestamp": 1781054110000,
"data": {
"event_type": "combo_execution",
"tx_hash": "0xb0411705aaefc17991e4121acecd8aad03901ddc359cfdb7cf8773f46942927a"
}
}
```
## Subscription types
The `type` field selects a preset of event types. If you also set `filters.event_types`, it overrides the preset.
Events: `settlement` (reformatted as one flat event per fill)
The simplest way to stream Polymarket trades. Each fill arrives as its own flat event with clear fields: `user` (the wallet whose fill this is), `side`, `price`, `shares_normalized`, `token_label` (outcome name), and `title` (market name). No arrays to iterate, no nested objects to unpack.
Pre-confirmation delivery, 3-5 seconds before on-chain settlement. Same speed as `settlements`, just a cleaner format for per-trade tracking.
```javascript theme={null}
const ws = new WebSocket("wss://ws.polynode.dev/ws?key=YOUR_KEY");
ws.on("open", () => {
ws.send(JSON.stringify({ action: "subscribe", type: "fills" }));
});
ws.on("message", (raw) => {
const msg = JSON.parse(raw);
if (msg.type !== "event") return;
const fill = msg.data;
console.log({
outcome: fill.token_label,
side: fill.side,
price: fill.price,
shares: fill.shares_normalized,
user: fill.user,
market: fill.title
});
});
```
**Example event:**
```json theme={null}
{
"type": "event",
"data": {
"order_hash": "0x7b281ecba532e471...",
"user": "0x0c6367b4e0022ca702ad6f33cd2cf504b2cc3cbb",
"taker": "0x4bfb41d5b3570defd03c39a9a4d8de6bd8b8982e",
"tx_hash": "0xf7d09a5f24609e82...",
"side": "BUY",
"price": 0.59,
"shares": 33898304,
"shares_normalized": 33.898304,
"token_id": "111868385514416639...",
"token_label": "Astralis",
"condition_id": "0xd546a1f7ff24a008...",
"market_slug": "cs2-ast10-eye-2026-04-09-game1",
"title": "Counter-Strike: Astralis vs EYEBALLERS - Map 1 Winner",
"timestamp": 1775731431,
"block_number": null,
"log_index": null
}
}
```
`block_number: null` means pre-confirmation (detected before the block). Once confirmed, a `status_update` fires on the `settlements` subscription if you have one active. If you only need trades and don't care about the confirmation lifecycle, `fills` is all you need.
Events: `settlement`, `status_update`
Pending and confirmed Polymarket settlements. The default subscription type. Each event contains a `trades[]` array with all fills in the transaction. Use this when you need the full settlement lifecycle (pending detection, block confirmation via `status_update`, and all fills grouped by transaction).
If you just want individual trades in a flat format, use [`fills`](#fills) instead.
Events: `settlement`, `trade`, `status_update`
All trade activity including confirmed on-chain trades.
Events: `settlement`, `trade`
Price-moving events only. Useful for building price feeds.
Events: `combo_execution`, `combo_status_update`
Polymarket combo executions and confirmations. Pending `combo_execution` events are decoded before on-chain confirmation; confirmed `combo_status_update` events include receipt-derived fills, transfers, fees, gas fields, and confirmation latency when the pending event was seen first.
Combo events are enrichment-gated. Customer-facing combo executions and status updates include enriched `legs[]` metadata; events without enough leg metadata are held instead of emitted partially.
Lifecycle and approval events are available by explicit request:
```json theme={null}
{
"action": "subscribe",
"type": "combos",
"filters": {
"event_types": [
"combo_execution",
"combo_status_update",
"combo_lifecycle",
"combo_approval"
]
}
}
```
See [Combo Stream](/websocket/combos) for the full field reference and filters.
Events: `settlement` (reformatted as per-fill flat events)
Dome API-compatible feed. Each settlement is exploded into individual flat events — one per order fill — matching the exact field names and structure of Dome's WebSocket API. Designed as a drop-in replacement for Dome with 3-5 second faster delivery. See [Dome Migration](/dome-migration) for full field reference and copy trading examples.
This is the simplest subscription type for **per-wallet trade tracking**. Each fill arrives as its own flat event with a `user` field. Filter incoming events by `event.data.user === your_wallet` to get the wallet's actual trades — no array iteration, no maker/taker confusion.
```javascript theme={null}
import WebSocket from "ws";
const TRACKED_WALLET = "0x...";
const ws = new WebSocket("wss://ws.polynode.dev/ws?key=YOUR_KEY");
ws.on("open", () => {
ws.send(JSON.stringify({
action: "subscribe",
type: "dome"
}));
});
ws.on("message", (raw) => {
const msg = JSON.parse(raw);
if (msg.type !== "event") return;
const fill = msg.data;
if (fill.user.toLowerCase() !== TRACKED_WALLET.toLowerCase()) return;
console.log(
fill.side, fill.shares_normalized, fill.token_label,
"@", fill.price, "in", fill.title
);
});
```
**Real captured event** (live data from April 9, 2026):
```json theme={null}
{
"type": "event",
"data": {
"order_hash": "0x7b281ecba532e47147e9919e073635b44ce0361527f46ee1067f2ca152aea765",
"user": "0x0c6367b4e0022ca702ad6f33cd2cf504b2cc3cbb",
"taker": "0x4bfb41d5b3570defd03c39a9a4d8de6bd8b8982e",
"tx_hash": "0xf7d09a5f24609e8209298d7be35fa33ba8a3749ebb51f8a13d10d547467d104d",
"side": "BUY",
"price": 0.59,
"shares": 33898304,
"shares_normalized": 33.898304,
"token_id": "111868385514416639450414573730782830447093166389406898626712932606174471659926",
"token_label": "Astralis",
"condition_id": "0xd546a1f7ff24a008a90bf80a06a7f7aa034abb33311d6727d325f9b848e93940",
"market_slug": "cs2-ast10-eye-2026-04-09-game1",
"title": "Counter-Strike: Astralis vs EYEBALLERS - Map 1 Winner",
"timestamp": 1775731431,
"block_number": null,
"log_index": null
}
}
```
In this event, `user` is the wallet whose trade this is (`0x0c6367b4...`), and `taker` is the CTF Exchange contract (`0x4bfb41d5...`) — that pattern means this is the user's own fill, not a counterparty fill. The user bought Astralis at 0.59 for 33.9 shares.
The same transaction also produces a separate event for the counterparty (with `user = counterparty_wallet, taker = real_taker_wallet`). To track only the wallet you care about, filter by `event.data.user === your_wallet` and ignore everything else.
Events: `block`
New Polygon block notifications with Polymarket-specific stats (settlement count, trade volume).
Events: `settlement`, `trade`, `position_change`, `deposit`, `status_update`, `position_split`, `position_merge`, `positions_converted`, `redemption`
All activity for specific wallets. Best used with the `wallets` filter.
Events: `redemption`
Direct feed for payout redemptions. Includes normal Conditional Tokens redemptions and PM2 AutoRedeemer redemptions bridged into the same `redemption` shape.
Events: `settlement`, `trade`, `position_change`, `status_update`, `position_split`, `position_merge`, `positions_converted`, `redemption`
All activity for specific markets. Best used with `tokens`, `slugs`, or `condition_ids` filters.
Events: `deposit`
USDC.e and Polymarket USD deposit/withdrawal activity involving known Polymarket contracts.
Events: `settlement`, `trade`
Whale alerts. Defaults to `min_size: 1000` (\$1K+) unless you set your own.
Events: `settlement`, `trade`, `position_change`, `deposit`, `block`, `status_update`, `oracle`, `redemption`
Unfiltered stream of the default public event set (**firehose**). High throughput — use with caution. This counts toward your plan's firehose connection limit. Combo events are not included in `global` by default; use `type: "combos"` for combo activity.
**Firehose** refers to any WebSocket subscription with no filters applied — receiving every event at full throughput. Filtered subscriptions (specifying wallets, tokens, slugs, or size thresholds) don't count toward your firehose limit and use far less bandwidth.
Events: `oracle`
UMA Optimistic Oracle events: market resolutions, proposals, disputes, flags, and admin actions. See [Oracle Event](/websocket/events/oracle).
Events: `price_feed`
Real-time crypto prices (\~1 update/second per feed). 7 feeds available: BTC, ETH, SOL, BNB, XRP, DOGE, HYPE. Use the `feeds` filter to select specific feeds (e.g. `["BTC/USD", "ETH/USD"]`). See [Crypto Prices](/crypto/overview).
## Filters
All filters are optional. Omit the `filters` object entirely to receive all events matching the subscription type.
### Market identification
Filter by Polymarket conditional token IDs. Match if **any** token in the list appears in the event.
```json theme={null}
"tokens": ["21742633143463906290569404...", "98765432..."]
```
Filter by market URL slugs. Resolved to token IDs at subscribe time — zero performance cost after that.
```json theme={null}
"slugs": ["bitcoin-100k-2026", "trump-2024"]
```
If a slug isn't found in metadata, you'll get a warning in the `subscribed` response. The subscription still activates for any resolved slugs.
Filter by Polymarket condition IDs. Resolved to token IDs at subscribe time.
```json theme={null}
"condition_ids": ["0x123abc..."]
```
### Combo identification
These filters apply to `type: "combos"` subscriptions.
Filter by derived combo condition IDs.
```json theme={null}
"combo_condition_ids": ["0xcombo..."]
```
Filter by constituent leg position IDs. `tokens` also matches combo and leg position IDs, but `leg_position_ids` makes the intent explicit.
```json theme={null}
"leg_position_ids": ["123456789..."]
```
Filter by combo leg event IDs when available.
Filter by combo module ID. Common values are `1` for binary, `2` for negative-risk, and `3` for combinatorial legs.
Filter `combo_lifecycle` events by lifecycle action. Values include `Prepare`, `Split`, `Merge`, `HorizontalSplit`, `HorizontalMerge`, `Convert`, `Redeem`, `Wrap`, `Unwrap`, `Transfer`, `Compress`, `Extract`, `Inject`, `ConvertToYesBasket`, and `MergeFromYesBasket`.
Filter `combo_execution` events by requester direction.
Values: `"BUY"` or `"SELL"`
For `type: "combos"`, `condition_ids` are matched directly against combo and leg condition IDs. They are not resolved through standard market metadata at subscription time.
### Wallet filtering
Filter by wallet addresses (case-insensitive). Matches if the wallet is involved as maker **or** taker.
```json theme={null}
"wallets": ["0xabcdef1234567890..."]
```
**Tracking a specific wallet's trades?** Always match by `maker`, never by `taker`.
For `settlement` events: iterate `data.trades[]` and find entries where `fill.maker === your_wallet`.
For `trade` events: filter incoming events by `data.maker === your_wallet`.
The `maker` field on each fill is the wallet that placed the order. The `taker` field is the counterparty — matching against it returns the wrong wallet's perspective with the **opposite token** and the **complement price** (e.g. shows `BUY Down 0.20` when the wallet actually bought `Up at 0.80`). This is how on-chain `OrderFilled` events are structured. See the [Settlement Event](/websocket/events/settlement#tracking-a-specific-wallets-trades) or [Trade Event](/websocket/events/trade#tracking-a-specific-wallets-trades) reference for full code examples.
### Trade filtering
Filter by trade side. Case-insensitive.
Values: `"BUY"` or `"SELL"`
Minimum trade size in USD. Events below this size are dropped.
```json theme={null}
"min_size": 1000
```
Maximum trade size in USD. Events above this size are dropped.
Filter by settlement status.
| Value | Description |
| ------------- | ------------------------------------ |
| `"pending"` | Only pre-chain detections |
| `"confirmed"` | Only confirmed in a block |
| `"all"` | Both pending and confirmed (default) |
### Event control
Override the subscription type's default event list. Valid values:
`"settlement"`, `"trade"`, `"status_update"`, `"block"`, `"position_change"`, `"deposit"`, `"position_split"`, `"position_merge"`, `"positions_converted"`, `"redemption"`, `"oracle"`, `"combo_execution"`, `"combo_status_update"`, `"combo_lifecycle"`, `"combo_approval"`
Number of recent matching events to receive immediately after subscribing. Max varies by tier (free: 20, starter: 100, growth: 200, enterprise: 500).
UNIX timestamp in milliseconds. When set, the initial snapshot returns all events **after** this timestamp instead of just the most recent N events. Useful for gap-filling after a reconnect or cold start.
```json theme={null}
"since": 1774412600000
```
The server keeps a rolling window of recent events in memory. Timestamps older than your tier's lookback window are clamped to the window edge.
| Tier | Max lookback |
| ---------- | ------------ |
| Free | 30 seconds |
| Starter | 2 minutes |
| Growth | 5 minutes |
| Enterprise | 5 minutes |
**`since` vs `snapshot_count`** — these are two separate snapshot modes. Use `snapshot_count` when you just want the last N events (capped by tier). Use `since` when you need every event after a specific timestamp (e.g. reconnecting after a drop). If you pass both, `since` takes priority and `snapshot_count` is ignored. See [tier limits](/guides/rate-limits#tier-limits) for the full breakdown.
### Price feed filtering
Filter by price feed names. Only applies to `chainlink` subscriptions. Available feeds: `BTC/USD`, `ETH/USD`, `SOL/USD`, `BNB/USD`, `XRP/USD`, `DOGE/USD`, `HYPE/USD`.
```json theme={null}
"feeds": ["BTC/USD", "ETH/USD", "SOL/USD"]
```
Omit to receive all 7 feeds. See [Crypto Prices](/crypto/overview) for details.
## Matching logic
Filters use **AND** logic between different filter types and **OR** logic within each filter:
* An event must match **all** active filter categories (wallets AND tokens AND side AND size range)
* Within a category, matching **any** value is sufficient (wallet A OR wallet B)
* Empty/omitted filters match everything in that category
## Multiple subscriptions
You can send multiple `subscribe` messages on the same connection — they **stack**. Each subscribe creates an independent subscription with its own filters. Events matching *any* of your active subscriptions are delivered, deduplicated so you never receive the same event twice.
```javascript theme={null}
// Subscribe to two different wallet groups independently
ws.send(JSON.stringify({
action: "subscribe",
type: "wallets",
filters: { wallets: ["0xaaa..."] }
}));
ws.send(JSON.stringify({
action: "subscribe",
type: "wallets",
filters: { wallets: ["0xbbb..."] }
}));
// Both subscriptions are active — events for either wallet are delivered
```
## Subscribe response
After subscribing, you receive two messages:
**1. Snapshot** — recent events matching your filters:
```json theme={null}
{
"type": "snapshot",
"count": 20,
"events": [...]
}
```
**2. Confirmation** — your subscription ID and any warnings:
```json theme={null}
{
"type": "subscribed",
"subscriber_id": "550e8400-e29b-41d4-a716-446655440000",
"subscription_id": "550e8400-e29b-41d4-a716-446655440000:1",
"subscription_type": "settlements",
"warnings": []
}
```
Save the `subscription_id` if you need to remove a specific subscription later.
## Unsubscribe
### Remove a specific subscription
```json theme={null}
{"action": "unsubscribe", "subscription_id": "550e8400-...:1"}
```
Response:
```json theme={null}
{
"type": "unsubscribed",
"subscriber_id": "550e8400-...",
"subscription_id": "550e8400-...:1"
}
```
### Remove all subscriptions
```json theme={null}
{"action": "unsubscribe"}
```
Response:
```json theme={null}
{"type": "unsubscribed", "subscriber_id": "550e8400-..."}
```
To change filters on an existing subscription, unsubscribe by `subscription_id` and send a new subscribe with the updated filters.