API Documentation

Base URL: https://api.predmktdata.com

Quick start

Get data flowing in 3 steps:

  1. Sign in at predmktdata.com with Google and subscribe to Lite. You get your API key instantly.
  2. Test your key:
curl -H "x-api-key: YOUR_KEY" https://api.predmktdata.com/status
  1. Download data — list available Parquet dumps and start downloading:
# 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.

How Polymarket works

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.

What this API provides

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.

Authentication

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

Endpoints

GET /status LITE

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, ...
  }
}
GET /dumps LITE

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
GET /dumps/{path} LITE

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).

Available tables

TableRowsParquetType
order_filled_events~1.2B~37 GBAppend-only (v1 exchanges, Nov 2022 → Apr 28, 2026)
order_filled_events_v2~440M~24 GBAppend-only (v2/v3 exchanges, Apr 2026+, growing)
payout_redemptions~222M~6.8 GBAppend-only
position_splits~118M~5 GBAppend-only
position_merges~24M~1 GBAppend-only
position_conversions~3.4M~150 MBAppend-only
combinatorial_redemptions~370K~20 MBAppend-only (parlays, Jun 2026+)
positions~318M~8.6 GBMutable (UPSERT)
markets~1.7M~220 MBLookup (overwritten daily)
parlay_legsgrowingsmallLookup (overwritten daily) — parlay → leg markets
daily_token_pricesgrowingdaily filesLast traded price per token, one file per UTC day
perps_* (7 streams)growingdaily filesOff-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.

Schema

order_filled_events (v1 — legacy CTF + NegRisk, Nov 2022 → Apr 28, 2026)

ColumnTypeNotes
timestamptimestampBlock timestamp
transaction_hashtext
block_numberbigint
makertextMaker wallet address
takertextTaker wallet address
maker_asset_idtextUsually "0" (USDC side)
taker_asset_idtextOutcome token ID
maker_amount_filledbigintRaw units (divide by 1e6 for USDC)
taker_amount_filledbigintRaw units (divide by 1e6)
feebigintRaw units
sidetextbuy or sell

order_filled_events_v2 (CTF v2 + NegRisk v2 + Exchange v3 parlays, since April 28, 2026)

ColumnTypeNotes
timestamptimestampBlock timestamp
transaction_hashtext
block_numberbigint
exchangetextctf_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_hashtextOrder identifier emitted by the contract
makertextMaker wallet address
takertextTaker wallet address
sideinteger0 = buy, 1 = sell (int, not text — different from v1)
token_idtextOutcome token ID (unified — replaces maker_asset_id/taker_asset_id)
maker_amount_filledbigintRaw units (divide by 1e6 for USDC)
taker_amount_filledbigintRaw units (divide by 1e6)
feebigintRaw units
buildertextAddress that built the order (often 0x0…0)
metadatatextOrder-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.

combinatorial_redemptions (parlays — multi-leg bets, since June 2026)

ColumnTypeNotes
timestamptimestampBlock timestamp
transaction_hashtext
block_numberbigint
recipienttextWallet that owned the parlay — the real winner, even for relayer-assisted auto-redemptions
position_idtextParlay 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
amountbigintParlay tokens burned (raw, divide by 1e6)
payoutbigintUSDC 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.

parlay_legs (lookup: parlay → underlying markets, overwritten daily)

ColumnTypeNotes
parlay_condition_idtextThe parlay's condition id
position_id_yestextParlay YES token — joins order_filled_events_v2.token_id and combinatorial_redemptions.position_id
position_id_notextParlay NO token (same joins)
n_legsintegerNumber of legs in the parlay
leg_indexinteger0-based leg position
leg_position_idtextRaw leg position id (audit)
leg_moduletextbinary or neg_risk
leg_condition_idtextThe leg's market — joins markets.condition_id for question/outcomes. Empty for markets not yet in the lookup (self-heals daily)
leg_outcometextyes 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.

positions

ColumnTypeNotes
user_addresstextWallet address
token_idtextOutcome token ID
amountbigintCurrent position size (raw)
avg_pricebigintAverage entry price (divide by 1e6)
realized_pnlbigintRealized PnL (raw units)
total_boughtbigintTotal tokens bought (raw)
last_blockbigintLast 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.

daily_token_prices (one file per UTC day)

ColumnTypeNotes
daytextUTC day, YYYYMMDD
token_idtextOutcome token ID
pricedoubleLast 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.

markets

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.

ColumnTypeNotes
condition_idtextUnique identifier for the market condition
questiontextThe market question (e.g. "Will Bitcoin hit $100k?")
outcome_yestextLabel for the Yes side (e.g. "Yes", "Bitcoin")
outcome_notextLabel for the No side (e.g. "No", "Ethereum")
yes_token_idtextToken ID for the "Yes" outcome
no_token_idtextToken ID for the "No" outcome
market_slugtextURL slug on polymarket.com
end_date_isotextMarket end date (ISO 8601)
neg_riskbooleanUses NegRisk exchange (multi-outcome markets)
To look up which market a trade belongs to, join the event's 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).

resolution_proposals (UMA Oracle)

Who proposed the outcome of each market. Indexed from UMA OptimisticOracleV2, filtered to Polymarket adapters only. Full snapshot updated daily in uma/ folder.

ColumnTypeNotes
transaction_hashtextOn-chain transaction
block_numberbigintPolygon block number
requestertextAdapter contract address (V1/V2/V3)
proposertextAddress that proposed the outcome
proposed_pricetext1e18 = Yes, 0 = No, 0.5e18 = Unknown
ancillary_datatextQuestion text (human-readable)
request_timestampbigintUMA request timestamp (unix)
expiration_timestampbigintChallenge window end (unix)

resolution_disputes (UMA Oracle)

When someone challenges a proposed outcome. ~2% of proposals are disputed.

ColumnTypeNotes
transaction_hashtextOn-chain transaction
block_numberbigintPolygon block number
disputertextAddress that disputed
proposertextOriginal proposer being disputed
proposed_pricetextThe price being challenged
ancillary_datatextQuestion text

resolution_settlements (UMA Oracle)

Final resolved outcome and bond payouts.

ColumnTypeNotes
transaction_hashtextOn-chain transaction
block_numberbigintPolygon block number
proposertextOriginal proposer
disputertextDisputer (0x0 if undisputed)
settled_pricetextFinal outcome (1e18=Yes, 0=No)
payouttextBond payout to winner
ancillary_datatextQuestion text

Perps (off-chain perpetual futures)

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.

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, 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):

ColumnTypeNotes
timestamp_receivedtimestamptzCapture time (UTC) — also the file's day bucket
tstimestamptzExchange message timestamp (UTC)
sqbigintServer-global sequence — ordering metadata only, not a per-channel loss detector
iidintegerInstrument 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.

StreamCadenceColumns beyond the common four (type)
perps_tradesevent-driventid (bigint), side (varchar: long/short), price (varchar), quantity (varchar), hash (varchar: always 0x)
perps_book~10 Hz snapshotbids (varchar JSON), asks (varchar JSON) — [["price","qty"], ...]
perps_bboevent-drivenbp, bq, ap, aq (varchar) — best bid/ask price & quantity
perps_tickers~10 Hz snapshotidx, mark, last, mid, oi, fr (varchar); nxf (timestamptz). fr is the live funding rate, not the settled one
perps_statistics~1 Hz snapshotvol (varchar), open (varchar), klines (varchar JSON passthrough — use perps_klines for canonical candles)
perps_klines1-minuteinterval (varchar, always 1m), candle_ts (timestamptz), open, high, low, close, volume (varchar), trades (bigint)
perps_fundinghourly (REST)own shape: iid (integer), funding_rate (varchar), ts (timestamptz, settlement time), fetched_at (timestamptz). No timestamp_received/sq

Conventions a quant needs

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.

Error responses

All errors return JSON with a detail field:

StatusMeaningExample
401Missing or invalid API key{"detail": "Unauthorized"}
403No active Lite subscription — /dumps and /dumps/{path} require Lite—
400Bad request{"detail": "Invalid path"}
404No such dump file{"detail": "File not found"}
410Endpoint discontinued — the real-time API (/events, /user/*, /ws/*) was discontinued on 2026-10-09 and returns 410. Use the daily dumps.—
429Too many requests, or daily download cap reached{"detail": "Rate limit exceeded"}

Sync patterns

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 ↗

1. Backfill (once)

  1. GET /dumps to list every file your key can download
  2. Download the daily event files (<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 once
  3. Download the snapshot files: positions/positions_current.parquet, markets/markets.parquet, parlay_legs/parlay_legs.parquet, and uma/resolution_*.parquet
  4. Query directly with DuckDB, or INSERT into your database (UPSERT for positions)

2. Daily incremental (after 05:00 UTC)

  1. GET /dumps with If-None-Match set to the ETag of your last listing. A 304 means nothing new — you're done for the day
  2. Otherwise compare the listing with what you already have, by path 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)
  3. INSERT the new event rows; replace your positions and lookup tables with the fresh snapshots
Event tables are append-only — use INSERT. Positions are mutable — replace the table with the new file, or UPSERT every row (INSERT ... ON CONFLICT (user_address, token_id) DO UPDATE).

Positions snapshot model: a single rolling file 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.

Skipping unchanged big files: keep the 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.

Compression

JSON responses such as the /dumps listing support two compression algorithms via the Accept-Encoding header (Parquet files are already compressed internally):

Units and scaling

Coverage

Plans

FeatureLite ($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
UpdatesEnd-of-day (daily at 05:00 UTC)
Dump downloads/day20,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).