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_v2/20261008.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 daily Parquet dumps. Here is what you get:
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, ...
}
}
List the dump files your key can download. Returns JSON: one entry per file with its path, size in bytes, and mtime (unix seconds, when the file was last written).
{
"files": [
{"path": "order_filled_events_v2/20261008.parquet", "size": 171966464, "mtime": 1791522000},
{"path": "positions/positions_current.parquet", "size": 8612345678, "mtime": 1791522000}, ...
],
"base_url": "/dumps/"
}
The listing only changes when new dumps are published, and it comes with an ETag header. Send that value back unchanged (quotes included) as If-None-Match and you get an empty 304 Not Modified when nothing has changed — a cheap way to check for new files.
curl -i -H "x-api-key: YOUR_KEY" -H 'If-None-Match: "ETAG_FROM_LAST_LISTING"' \ https://api.predmktdata.com/dumps # HTTP/1.1 304 Not Modified -> nothing new since your last listing
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. Every call counts toward your daily download cap (see Plans).
| 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 |
| daily_token_prices | growing | daily files | Last traded price per token, one file per UTC day |
| perps_* (7 streams) | growing | daily files | Off-chain perps capture — 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 |
| 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.
| 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 |
Served as one rolling file, positions/positions_current.parquet, regenerated daily. The file carries realized PnL; to mark open positions to market, join token_id to daily_token_prices (below) and value amount at the latest price.
| Column | Type | Notes |
|---|---|---|
| day | text | UTC day, YYYYMMDD |
| token_id | text | Outcome token ID |
| price | double | Last traded price that day, USDC per token (0–1, already decimal) |
Path: daily_token_prices/YYYYMMDD.parquet. A token only has a row on days it traded — for a mark on a given day, take the latest row on or before it.
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 |
Polymarket Perps is a separate Polymarket product from everything above (its data is included in Lite): 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.
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, the exchange's own instrument_id — 89 instruments as of 2026-09-17 (crypto, single stocks, indices, commodities), e.g. 1=SP500-USD, 2=GOLD-USD, 6=BTC-USD, 7=ETH-USD, 19=NVDA-USD. The authoritative, always-current iid → symbol mapping is the exchange's instruments endpoint: curl https://api.perpetuals.polymarket.com/v1/info/instruments (fields instrument_id, symbol, category). The list grows; new listings are captured in every stream 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.perps_tickers/perps_statistics empty from 2026-07-28 ~14:51 UTC and perps_book/perps_bbo without iid ≥ 27 until 2026-09-17 15:06 UTC; a capture-host outage 2026-09-11 ~14:41 → 2026-09-17 15:06 UTC left no book/bbo and no funding settlements between 09-11 14:00 and ~09-16 15:00. perps_trades and perps_klines were repaired from the exchange's REST history for all instruments (trades for iid ≥ 27 from 2026-08-18 ~13:00–14:30, the REST retention floor). REST-sourced trade rows fetched before 2026-09-17 missed ~0.7% of trades (trades sharing a millisecond with a REST page boundary): re-repaired for 2026-08-18 → 2026-09-16; the one-time backfill days 2026-06-09 → 2026-07-09 ~12:31 keep that shortfall, and >100 trades sharing one millisecond exceed the endpoint's page size and cannot be fetched at all (15 such groups in the repaired range). Full detail in llms.txt.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 | No active Lite subscription — /dumps and /dumps/{path} require Lite | — |
| 400 | Bad request | {"detail": "Invalid path"} |
| 404 | No such dump file | {"detail": "File not found"} |
| 410 | Endpoint discontinued — the real-time API (/events, /user/*, /ws/*) was discontinued on 2026-10-09 and returns 410. Use the daily dumps. | — |
| 429 | Too many requests, or daily download cap reached | {"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 current with a small daily job. Read the full guide ↗
<table>/YYYYMMDD.parquet) in parallel. Each /dumps/{path} call is a quick 302 redirect; the file itself downloads straight from storage, so run several downloads at oncepositions/positions_current.parquet, markets/markets.parquet, parlay_legs/parlay_legs.parquet, and uma/resolution_*.parquetIf-None-Match set to the ETag of your last listing. A 304 means nothing new — you're done for the daypath and mtime: download paths you don't have yet (each new day adds one file per table), and re-download any file whose mtime changed — that's how you pick up the overwritten snapshots (positions, markets, parlay legs, UMA)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 it when its mtime changes.ETag the storage returned on your last download and send it as If-None-Match when you fetch the redirect URL again — an unchanged file answers 304 Not Modified with no body. Use GET, not HEAD (the download links are signed for GET only). Every /dumps/{path} call still counts toward your daily download cap.
JSON responses such as the /dumps listing support two compression algorithms via the Accept-Encoding header (Parquet files are already compressed internally):
gzip — universally supported, use curl --compressedzstd — ~30% smaller, faster. Send Accept-Encoding: zstd<table>/YYYYMMDD.parquet). Full history is kept, with no expiry; a completed day file does not change| Feature | Lite ($49/mo, single user) |
|---|---|
| Trades (fills, splits, merges, redemptions, conversions, parlays) | Full history since 2022 |
| Daily position snapshot (05:00 UTC) | ✓ |
| Markets lookup table | ✓ |
| Daily token prices | ✓ |
| UMA resolution data (proposals, disputes, settlements) | ✓ |
Perps — off-chain perpetual futures, 7 daily streams (perps_*) | ✓ Included |
| Updates | End-of-day (daily at 05:00 UTC) |
| Dump downloads/day | 20,000 |
Lite is a single-user subscription, month-to-month via Stripe. A key without an active Lite subscription gets 403 on /dumps and /dumps/{path}. API requests are rate-limited per key (5 requests/second).