Base URL: https://api.predmktdata.com
Get data flowing in 3 steps:
curl -H "x-api-key: YOUR_KEY" https://api.predmktdata.com/status
# List available files curl -H "x-api-key: YOUR_KEY" https://api.predmktdata.com/dumps # Download a Parquet file curl -L -H "x-api-key: YOUR_KEY" -o fills.parquet \ https://api.predmktdata.com/dumps/order_filled_events/20260405.parquet # Or query remotely with DuckDB (no download needed) # Get the presigned URL, then: SELECT * FROM read_parquet('URL')
That's it. You have Polymarket data. For full sync patterns, see Sync patterns below.
Polymarket is a prediction market where people bet on real-world outcomes. Every market is a yes/no question (e.g. "Will Bitcoin hit $100k by July 2026?"). Each side has a token that trades between $0 and $1 — if the event happens, "Yes" tokens pay out $1; otherwise, they're worth $0.
All trading happens on the Polygon blockchain — a public, permanent ledger where every transaction is recorded and verifiable. A wallet is a user's identity on the blockchain, represented by an address like 0xed86.... Every trade, deposit, and withdrawal is tied to a wallet address.
When a user buys an outcome, they receive outcome tokens — ERC-1155 tokens on Polygon, each identified by a large numeric token_id. On its own, a token_id is just a number — the included markets lookup table maps each one to its human-readable question and outcome (e.g. token 123... → "Will Bitcoin hit $100k?" / "Yes"). A user's position is their current holding of a specific outcome token: how many they hold, what they paid on average, and their realized profit/loss from any tokens they've already sold or redeemed.
predmktdata reads every relevant transaction from Polygon and delivers it as Parquet dumps and CSV API responses. You get two types of data:
You don't need to understand Solidity, ABIs, or how to talk to a blockchain node. The API handles all of that and gives you clean, structured data you can load into any database or spreadsheet.
All endpoints require an API key via the x-api-key header. Get your key by signing in with Google at predmktdata.com.
curl -H "x-api-key: YOUR_API_KEY" https://api.predmktdata.com/status
Current indexer state and table row counts. Returns JSON.
{
"last_block": 84570000,
"head_block": 84570000,
"tables": {
"order_filled_events": 854000000,
"order_filled_events_v2": 1500000,
"positions": 150000000,
"payout_redemptions": 93000000, ...
}
}
Fetch events as CSV. One table per request. Supports gzip and zstd compression.
| Param | Default | Description |
|---|---|---|
after_block | required | Return events after this block number |
limit | 5000 | Max blocks to include (1 - 5,000) |
tables | required | Single table name |
Block metadata is returned in response headers:
x-after-block — your requested after_blockx-last-block — last block in this chunk (use as next after_block)x-head-block — current chain headx-reorg — present if you're ahead of the indexer (rollback to this block)# Fetch fills for 500 blocks curl --compressed -H "x-api-key: YOUR_KEY" \ "https://api.predmktdata.com/events?after_block=84420000&limit=500&tables=order_filled_events" # Response: CSV with header row transaction_hash,log_index,block_number,exchange,maker,taker,... 0x7fe2...,210,84420001,ctf,0xed86...,0x9ce4...,...
Available tables: order_filled_events, order_filled_events_v2, position_splits, position_merges, payout_redemptions, position_conversions, combinatorial_redemptions, positions. Dumps additionally include the markets and parlay_legs lookups, UMA resolution snapshots, and order book depth.
order_filled_events covers the legacy CTF and NegRisk exchanges (active since Aug 2022). order_filled_events_v2 covers the new CTF v2 (0xE111…996B) and NegRisk v2 (0xe222…0F59) exchanges and has a different schema: a unified token_id, side as integer (0=buy, 1=sell), plus order_hash, builder, and metadata. Both tables continue to receive new fills — query each independently to capture all activity. See the schema sections below for full column details.All current positions for a wallet address. Returns CSV. Includes unrealized_pnl and total_pnl columns computed from live market prices (updated every minute).
All fills where address is maker or taker, across both v1 (order_filled_events) and v2 (order_filled_events_v2) exchange contracts. v2 fills are normalized to the v1 column shape (maker_asset_id/taker_asset_id, side as buy/sell) so a single client can consume both. The exchange column (ctf/neg_risk/ctf_v2/neg_risk_v2/ctf_v3_v2) tells you which contract emitted the fill. Returns CSV.
| Param | Default | Description |
|---|---|---|
after_block | 0 | Only fills after this block |
limit | 50000 | Max rows (up to 500,000) |
Current PnL summary for a wallet. Realized from closed trades, unrealized mark-to-market on open positions using live prices (updated every minute). All values in USD. Returns JSON.
{
"realized_pnl": -52.34,
"unrealized_pnl": 184.21,
"total_pnl": 131.87,
"portfolio_value": 1250.00,
"total_volume": 8340.50,
"active_positions": 12,
"markets_traded": 47
}
Real-time WebSocket feed of fills and position changes. Subscribe by wallet address or amount/price thresholds. Data pushes to you the moment the indexer commits — no polling needed.
Connect:
wscat -c "wss://api.predmktdata.com/ws/feed?x_api_key=YOUR_KEY"
Subscribe messages (send as JSON after connecting):
# Watch a specific wallet's fills and position changes {"action": "subscribe", "type": "user", "address": "0xabc..."} # Watch large fills (raw units, divide by 1e6 for USDC) {"action": "subscribe", "type": "threshold", "min_fill_amount": 10000000000} # Watch positions with low avg_price (cheap bets) {"action": "subscribe", "type": "threshold", "max_avg_price": 50000} # Unsubscribe {"action": "unsubscribe", "type": "user", "address": "0xabc..."} # Keepalive {"action": "ping"}
Messages you receive:
{
"fills": [{"transaction_hash": "0x...", "maker": "0x...", ...}],
"positions": [{"user_address": "0x...", "amount": 5000000, ...}]
}
Limits: 100 total connections, 5 per API key, 20 user subscriptions per connection. Values are raw i64 (same as REST API — divide by 1e6).
Firehose WebSocket for keeping your own database in sync. Catches up from a recent block, then streams all new events and positions as the indexer commits them. No subscriptions needed — you get everything.
| Param | Default | Description |
|---|---|---|
x_api_key | required | API key (query string) |
start_block | required | Block to start from (max ~900 blocks / 30 min behind head) |
tables | all | Comma-separated tables to stream (optional filter) |
Connect:
wscat -c "wss://api.predmktdata.com/ws/stream?x_api_key=YOUR_KEY&start_block=84650000" # Or stream only specific tables: wscat -c "wss://api.predmktdata.com/ws/stream?x_api_key=YOUR_KEY&start_block=84650000&tables=order_filled_events,positions"
Messages you receive:
# Catchup batches (sent rapidly until caught up) {"type": "batch", "from_block": 84650000, "to_block": 84650100, "order_filled_events": [{...}], "positions": [{...}], ...} # Catchup complete signal {"type": "caught_up", "block": 84651400} # Live batches (every few seconds as indexer commits) {"type": "batch", "from_block": 84651400, "to_block": 84651410, ...}
Limits: 10 concurrent stream connections (shared pool: 100 total, 5 per key). Slow consumers are disconnected (code 4008) — reconnect with a newer start_block.
List available Parquet dump files. Returns JSON with file paths and sizes.
Download a dump file (.parquet). Returns 302 redirect to a time-limited URL (60 min). Parquet files support remote querying with DuckDB via HTTP range requests.
| Table | Rows | Parquet | Type |
|---|---|---|---|
| order_filled_events | ~1.2B | ~37 GB | Append-only (v1 exchanges, Nov 2022 → Apr 28, 2026) |
| order_filled_events_v2 | ~440M | ~24 GB | Append-only (v2/v3 exchanges, Apr 2026+, growing) |
| payout_redemptions | ~222M | ~6.8 GB | Append-only |
| position_splits | ~118M | ~5 GB | Append-only |
| position_merges | ~24M | ~1 GB | Append-only |
| position_conversions | ~3.4M | ~150 MB | Append-only |
| combinatorial_redemptions | ~370K | ~20 MB | Append-only (parlays, Jun 2026+) |
| positions | ~318M | ~8.6 GB | Mutable (UPSERT) |
| markets | ~1.7M | ~220 MB | Lookup (overwritten daily) |
| parlay_legs | growing | small | Lookup (overwritten daily) — parlay → leg markets |
| perps_* (7 streams) | growing | daily files | Off-chain perps capture (dumps only) — see Perps |
Sizes are approximate and grow over time. With Parquet, you can query remotely with DuckDB without downloading — only the columns and rows you need are transferred.
| Column | Type | Notes |
|---|---|---|
| timestamp | timestamp | Block timestamp |
| transaction_hash | text | |
| block_number | bigint | |
| maker | text | Maker wallet address |
| taker | text | Taker wallet address |
| maker_asset_id | text | Usually "0" (USDC side) |
| taker_asset_id | text | Outcome token ID |
| maker_amount_filled | bigint | Raw units (divide by 1e6 for USDC) |
| taker_amount_filled | bigint | Raw units (divide by 1e6) |
| fee | bigint | Raw units |
| side | text | buy or sell |
The /events API returns additional columns: log_index, exchange. The timestamp column is named block_timestamp in API responses.
| Column | Type | Notes |
|---|---|---|
| timestamp | timestamp | Block timestamp |
| transaction_hash | text | |
| block_number | bigint | |
| exchange | text | ctf_v2, neg_risk_v2, or ctf_v3_v2 (v3 = parlay fills — token_id is a combinatorial position id, joins with combinatorial_redemptions.position_id, not with markets) |
| order_hash | text | Order identifier emitted by the contract |
| maker | text | Maker wallet address |
| taker | text | Taker wallet address |
| side | integer | 0 = buy, 1 = sell (int, not text — different from v1) |
| token_id | text | Outcome token ID (unified — replaces maker_asset_id/taker_asset_id) |
| maker_amount_filled | bigint | Raw units (divide by 1e6 for USDC) |
| taker_amount_filled | bigint | Raw units (divide by 1e6) |
| fee | bigint | Raw units |
| builder | text | Address that built the order (often 0x0…0) |
| metadata | text | Order-level metadata emitted by the contract |
Cash-flow semantics match v1: for side=0 (buy), maker pays maker_amount_filled USDC and receives taker_amount_filled outcome tokens. For side=1 (sell), maker delivers tokens and receives USDC. The v1 flow ended on-chain at the cutover (last v1 fill Apr 28, 2026 ~11:00 UTC) — all new fills land here. Union both tables for full history.
Computing volume: each row is one on-chain OrderFilled log, not one unique trade — a match emits one log per maker order plus a taker-side roll-up that restates the same volume, so summing every row double-counts. To count each match once, drop rows where lower(taker) is an exchange contract: 0x4bfb…982e, 0xc5d5…f80a (v1), 0xe111…996b, 0xe222…0f59 (v2), 0xe333…00aa (v3). Full lowercase addresses and worked SQL are in llms.txt. Note Polymarket's published per-market volume counts both sides of every match (≈2× the single-counted metric).
Row identity: the unique key for an event is (transaction_hash, log_index) at the source, but the fills dumps omit log_index (and v1 dumps also omit order_hash). As a result, distinct on-chain fills can appear as byte-identical rows — about 1% of v1 rows, e.g. one order matched in several equal legs in the same transaction. These are real, separate fills: never DISTINCT or deduplicate the fills dumps. If you need a strict per-row key, use /events, which includes log_index.
| Column | Type | Notes |
|---|---|---|
| timestamp | timestamp | Block timestamp |
| transaction_hash | text | |
| block_number | bigint | |
| recipient | text | Wallet that owned the parlay — the real winner, even for relayer-assisted auto-redemptions |
| position_id | text | Parlay outcome token ID — the same ID that appears as token_id in order_filled_events_v2, so parlay entries (trades) and cash-outs join on it |
| amount | bigint | Parlay tokens burned (raw, divide by 1e6) |
| payout | bigint | USDC received (raw, divide by 1e6). payout / amount = settlement price |
A parlay combines several markets into one position that pays out only if every leg wins. This table records the cash-outs; opening a parlay is a regular trade on the new exchange and appears in order_filled_events_v2 with the same position_id as token_id. Voided legs pay 0.5, so fractional settlement prices (0.5, 0.25, 0.75…) are legitimate.
| Column | Type | Notes |
|---|---|---|
| parlay_condition_id | text | The parlay's condition id |
| position_id_yes | text | Parlay YES token — joins order_filled_events_v2.token_id and combinatorial_redemptions.position_id |
| position_id_no | text | Parlay NO token (same joins) |
| n_legs | integer | Number of legs in the parlay |
| leg_index | integer | 0-based leg position |
| leg_position_id | text | Raw leg position id (audit) |
| leg_module | text | binary or neg_risk |
| leg_condition_id | text | The leg's market — joins markets.condition_id for question/outcomes. Empty for markets not yet in the lookup (self-heals daily) |
| leg_outcome | text | yes or no — which side of the market this parlay needs to win |
One row per leg. Lets you name every parlay with plain SQL: join a v3 fill's token_id against position_id_yes/position_id_no, then each leg_condition_id against the markets table. See llms.txt for a worked query.
| Column | Type | Notes |
|---|---|---|
| user_address | text | Wallet address |
| token_id | text | Outcome token ID |
| amount | bigint | Current position size (raw) |
| avg_price | bigint | Average entry price (divide by 1e6) |
| realized_pnl | bigint | Realized PnL (raw units) |
| total_bought | bigint | Total tokens bought (raw) |
| last_block | bigint | Last block this position was updated |
The /events API and /user endpoints add block_timestamp. The /user/*/positions endpoint also adds unrealized_pnl and total_pnl (computed from live market prices).
Lookup table that maps token IDs to human-readable market info. Join with event or position tables on yes_token_id / no_token_id to see which question and outcome a trade or position belongs to. Overwritten daily.
| Column | Type | Notes |
|---|---|---|
| condition_id | text | Unique identifier for the market condition |
| question | text | The market question (e.g. "Will Bitcoin hit $100k?") |
| outcome_yes | text | Label for the Yes side (e.g. "Yes", "Bitcoin") |
| outcome_no | text | Label for the No side (e.g. "No", "Ethereum") |
| yes_token_id | text | Token ID for the "Yes" outcome |
| no_token_id | text | Token ID for the "No" outcome |
| market_slug | text | URL slug on polymarket.com |
| end_date_iso | text | Market end date (ISO 8601) |
| neg_risk | boolean | Uses NegRisk exchange (multi-outcome markets) |
token_id (or taker_asset_id for fills) against yes_token_id or no_token_id in the markets table.
Parlay fills (exchange='ctf_v3_v2') will not match this join — their token is a combinatorial position id, not a market token; treat them as their own category in per-market breakdowns. A parlay position_id with its last byte zeroed identifies the parlay condition (last byte: 0 = YES, 1 = NO of the same parlay).
Who proposed the outcome of each market. Indexed from UMA OptimisticOracleV2, filtered to Polymarket adapters only. Full snapshot updated daily in uma/ folder.
| Column | Type | Notes |
|---|---|---|
| transaction_hash | text | On-chain transaction |
| block_number | bigint | Polygon block number |
| requester | text | Adapter contract address (V1/V2/V3) |
| proposer | text | Address that proposed the outcome |
| proposed_price | text | 1e18 = Yes, 0 = No, 0.5e18 = Unknown |
| ancillary_data | text | Question text (human-readable) |
| request_timestamp | bigint | UMA request timestamp (unix) |
| expiration_timestamp | bigint | Challenge window end (unix) |
When someone challenges a proposed outcome. ~2% of proposals are disputed.
| Column | Type | Notes |
|---|---|---|
| transaction_hash | text | On-chain transaction |
| block_number | bigint | Polygon block number |
| disputer | text | Address that disputed |
| proposer | text | Original proposer being disputed |
| proposed_price | text | The price being challenged |
| ancillary_data | text | Question text |
Final resolved outcome and bond payouts.
| Column | Type | Notes |
|---|---|---|
| transaction_hash | text | On-chain transaction |
| block_number | bigint | Polygon block number |
| proposer | text | Original proposer |
| disputer | text | Disputer (0x0 if undisputed) |
| settled_price | text | Final outcome (1e18=Yes, 0=No) |
| payout | text | Bond payout to winner |
| ancillary_data | text | Question text |
Level-2 order book snapshots for every Polymarket outcome token, sourced from pmxt (CC BY 4.0) and consolidated into 6-hour windows. Each row is an order-book event with best bid/ask and full depth; we enrich it with the market question, slug, and Yes/No outcome. Order book dumps are Parquet, served from the orderbook/ folder (path: orderbook/polymarket_orderbook_YYYYMMDD_HH.parquet), with windows starting at hour 00, 06, 12, 18 UTC. History starts April 2026.
| Column | Type | Notes |
|---|---|---|
| timestamp | timestamptz | Order book event time |
| timestamp_received | timestamptz | When pmxt captured the snapshot |
| market | text | Market condition_id |
| asset_id | text | Outcome token ID (join to markets) |
| event_type | text | book / price_change / tick_size_change |
| bids | text | Full bid side — JSON array of [price, size] levels |
| asks | text | Full ask side — JSON array of [price, size] levels |
| best_bid | decimal | Highest bid price (0–1) |
| best_ask | decimal | Lowest ask price (0–1) |
| price | decimal | Price of the changed level (price_change events) |
| size | decimal | Size at the changed level |
| side | text | buy / sell (for price_change events) |
| fee_rate_bps | smallint | Maker/taker fee in basis points |
| old_tick_size / new_tick_size | decimal | Tick size before/after a tick_size_change |
| transaction_hash | text | On-chain tx (when applicable) |
| question | text | Market question (enriched) |
| slug | text | Market slug (enriched) |
| outcome | text | YES / NO for this asset_id (enriched) |
| spread | decimal | best_ask − best_bid (enriched) |
| mid | double | (best_bid + best_ask) / 2 (enriched) |
Polymarket Perps is a separate product from everything above: leveraged perpetual futures on equity indices, commodities, and crypto, traded on an off-chain central limit order book. Matching happens off-chain, so an individual trade produces no on-chain record — every trade's hash is an empty "0x". Unlike the on-chain tables, there is no ledger to reconstruct this from later, so we capture it live from the exchange's public WebSocket firehose, plus a one-time REST backfill for history predating capture start. Live capture began 2026-07-09 ~12:31 UTC. Either you record this feed as it arrives or the data is gone — there is no way to fetch a missed trade back from a blockchain.
/events, /ws/feed, or /ws/stream. Layout: perps_<stream>/YYYYMMDD.parquet, one file per stream per UTC day (the day boundary is the UTC day of the row's capture time). Listed by GET /dumps and downloaded like any other dump.Instruments are keyed by an integer iid: 1=SP500-USD, 2=GOLD-USD, 3=WTIOIL-USD, 4=NAS100-USD, 5=SILVER-USD, 6=BTC-USD, 7=ETH-USD, 8=SOL-USD, 9=SPCX-USD, 10=HYPE-USD. The list can grow — new instruments appear with a new iid automatically.
Common columns (every stream except perps_funding):
| Column | Type | Notes |
|---|---|---|
| timestamp_received | timestamptz | Capture time (UTC) — also the file's day bucket |
| ts | timestamptz | Exchange message timestamp (UTC) |
| sq | bigint | Server-global sequence — ordering metadata only, not a per-channel loss detector |
| iid | integer | Instrument ID (see above) |
Each stream then adds its own columns. All numeric fields (prices, quantities, rates, volumes) are delivered as strings to preserve raw precision — cast in your query.
| Stream | Cadence | Columns beyond the common four (type) |
|---|---|---|
| perps_trades | event-driven | tid (bigint), side (varchar: long/short), price (varchar), quantity (varchar), hash (varchar: always 0x) |
| perps_book | ~10 Hz snapshot | bids (varchar JSON), asks (varchar JSON) — [["price","qty"], ...] |
| perps_bbo | event-driven | bp, bq, ap, aq (varchar) — best bid/ask price & quantity |
| perps_tickers | ~10 Hz snapshot | idx, mark, last, mid, oi, fr (varchar); nxf (timestamptz). fr is the live funding rate, not the settled one |
| perps_statistics | ~1 Hz snapshot | vol (varchar), open (varchar), klines (varchar JSON passthrough — use perps_klines for canonical candles) |
| perps_klines | 1-minute | interval (varchar, always 1m), candle_ts (timestamptz), open, high, low, close, volume (varchar), trades (bigint) |
| perps_funding | hourly (REST) | own shape: iid (integer), funding_rate (varchar), ts (timestamptz, settlement time), fetched_at (timestamptz). No timestamp_received/sq |
0x — off-chain, no transaction to point to.(iid, candle_ts) has many rows; the closed candle is the row with max(timestamp_received). Because files bucket on timestamp_received, a candle that opened just before UTC midnight can finalize in the next day's file — read day D+1 when reconstructing candles at a day boundary.(iid, ts) settlement. This is the only overlapping stream. When concatenating days, dedup on (iid, ts) keeping max(fetched_at) — this also picks up any retroactively restated rate.timestamp_received == ts (for klines, timestamp_received == candle_ts) and sq IS NULL; live WSS rows have a real receive time and a real sq. For perps_funding the backfill marker is fetched_at == ts.Download with the same /dumps flow as any other file — e.g. GET /dumps/perps_trades/20260709.parquet. Full stream-by-stream detail and worked recipes are in llms.txt.
All errors return JSON with a detail field:
| Status | Meaning | Example |
|---|---|---|
| 401 | Missing or invalid API key | {"detail": "Unauthorized"} |
| 403 | Plan doesn't include this endpoint | {"detail": "Pro plan required"} |
| 400 | Bad request | {"detail": "Specify exactly one table per request."} |
| 422 | Missing required parameter | {"detail": [{"type": "missing", ...}]} |
| 429 | Rate limit exceeded | {"detail": "Rate limit exceeded"} |
Want your own complete, always-updated Polymarket database?
We wrote a step-by-step guide: create the schema, backfill the full history from dumps, and keep it synced in real time. Read the full guide ↗
positions/positions_current.parquet) and markets lookuppositions_current.parquet (overwritten daily)positions/positions_current.parquet that always represents the current state of every position. We update it daily (incremental merge Mon–Sat, full regen Sunday). The URL is stable — point your job at it and re-download once a day.
The API supports two compression algorithms via Accept-Encoding header:
gzip — universally supported, use curl --compressedzstd — ~30% smaller, faster. Send Accept-Encoding: zstd<table>/YYYYMMDD.parquet)| Feature | Lite ($49/mo) | Pro ($149/mo) |
|---|---|---|
| Full history of trades (since 2022) | ✓ | ✓ |
| Daily position snapshot (05:00 UTC) | ✓ | ✓ |
| Markets lookup table | ✓ | ✓ |
| UMA resolution data (proposals, disputes, settlements) | ✓ | ✓ |
| Order book L2 depth (Polymarket, pmxt) | ✓ | ✓ |
| Perps — off-chain perpetual futures (7 streams) | ✓ | ✓ |
| Updates | End-of-day | Real-time (<1s) |
| Real-time API (/events) | — | ✓ |
| Per-wallet queries (/user/*, /user/*/pnl) | — | ✓ |
| WebSocket real-time feed (/ws/feed) | — | ✓ |
| Firehose stream (/ws/stream) | — | ✓ |
| Rate limit (API) | Dumps only | 5 req/s |
| Dump downloads/day | 20,000 | 30,000 |
If you've signed up but haven't subscribed yet, you can preview the data shape before paying. Samples live under samples/ and are exposed by GET /dumps like any other dump file. Cap: 50 downloads/day.
All samples are CSV.gz (paid plans get Parquet for the full history) and cover one busy day — 2025-01-20, US inauguration day — plus a 10k slice of open positions and the markets lookup. Schema matches the paid dumps.
| File | Size | Contents |
|---|---|---|
samples/order_filled_events_sample.csv.gz | ~38 MB | All trades on 2025-01-20 |
samples/payout_redemptions_sample.csv.gz | ~2.7 MB | Redemptions on 2025-01-20 |
samples/position_merges_sample.csv.gz | ~448 KB | Merges on 2025-01-20 |
samples/markets_sample.csv.gz | ~281 KB | Markets lookup snapshot |
samples/position_conversions_sample.csv.gz | ~88 KB | NegRisk conversions on 2025-01-20 |
samples/positions_sample.csv.gz | ~812 KB | Random slice of 10k open positions |
samples/position_splits_sample.csv.gz | ~59 KB | Splits on 2025-01-20 |
Example:
curl -H "x-api-key: $KEY" -L \
https://api.predmktdata.com/dumps/samples/order_filled_events_sample.csv.gz \
-o trades.csv.gz