tickfoundry
capture livesign inget the data →
docs / perps

Polymarket perps — schema & delivery

Polymarket runs a perpetual futures exchange alongside its prediction-market order book (live since 2026-04-21, CFTC-regulated, crypto plus US equities, indices and commodities). We capture it on two independent collectors since 2026-07-31 and normalize six tables per UTC day. It is sold per day with an instrument filter: included with Premium, $29/mo on Explorer, one fixed free sample day (2026-08-12) for any account. Pull from /catalog/perps or the API below.

File layout

One parquet per table per instrument per day, in a Hive tree. The bundle keeps that layout, so a single read_parquet on a table directory reads every instrument and day you pulled, with date and symbol injected as columns from the path. In all six tables symbol is not a file column; the stored key is iid, and perps_instruments — always included — is the iid → symbol dictionary, read the same way as the others.

bundle layouttext
polymarket-perps-2026-08-12/
  README.md
  perps_bbo/date=2026-08-12/symbol=BTC-USD/perps_bbo-2026-08-12-BTC-USD.parquet
  perps_bbo/date=2026-08-12/symbol=ETH-USD/…
  perps_trades/date=2026-08-12/symbol=BTC-USD/…
  perps_instruments/date=2026-08-12/symbol=BTC-USD/…
read a table across every instrument and day in the bundlepython
import pandas as pd

bbo   = pd.read_parquet("perps_bbo")            # date + symbol injected from the path
inst  = pd.read_parquet("perps_instruments")    # same layout, same read
spec  = inst.sort_values("snapshot_ts_ms").groupby("iid").tail(1)[["iid", "symbol", "category", "max_leverage"]]

btc = bbo[bbo.symbol == "BTC-USD"].sort_values("ts_recv_ns")
btc["spread_bps"] = 1e4 * (btc.ask_px - btc.bid_px) / btc.mid

Tables

Five event tables and the dictionary. Every event table carries the same provenance tail (sq, emit_ts_ms, ts_recv_ns, cohort, payload_sha256). Choose any of bbo, book, book_deep, trades, tickers; the dictionary is always added.

perps_bboBest bid/offer (event-driven)

Top of book on every change: bid/ask price and size, mid and spread, straight off the venue's bbo channel. ~17 messages a second per instrument, none of them redundant. ~1.0 GB · ~19M rows across all instruments.

columntypedescription
iidint32Instrument id — join to perps_instruments.
bid_pxdoubleBest bid price.
bid_qtydoubleBest bid size, contracts.
ask_pxdoubleBest ask price.
ask_qtydoubleBest ask size, contracts.
middouble(bid_px + ask_px) / 2 as published.
spreaddoubleask_px − bid_px as published.
sqint64Venue-global event id. Shared ACROSS channels (one value fans out to many frames), so it orders events but is NOT a per-channel sequence and cannot be used for gap detection.
emit_ts_msint64The frame's own ts — server emit time, ms.
ts_recv_nsint64Our receive time, ns since epoch — authoritative ordering.
ts_recv_isostringSame instant, ISO-8601, for eyeballing.
cohortstringWhich of our two collector nodes' copy was kept (dual-node provenance).
payload_sha256stringHash of the exact upstream payload — the cross-cohort dedup key together with (channel, sq).

perps_bookOrder book — 20 levels, ~100 ms snapshots

The websocket ladder, 20 levels a side, as a complete snapshot roughly every 100 ms. The venue publishes no delta stream, so every row is a whole book; about a quarter of consecutive frames are byte-identical and are kept as sent. ~1.6 GB · ~23M rows across all instruments.

columntypedescription
iidint32Instrument id — join to perps_instruments.
bid_pxlist<double>Bid prices, best first, up to 20 levels.
bid_qtylist<double>Bid sizes aligned with bid_px.
ask_pxlist<double>Ask prices, best first, up to 20 levels.
ask_qtylist<double>Ask sizes aligned with ask_px.
bid_levelsint32Number of bid levels present in this frame.
ask_levelsint32Number of ask levels present in this frame.
best_biddoublebid_px[0] for convenience.
best_askdoubleask_px[0] for convenience.
sqint64Venue-global event id. Shared ACROSS channels (one value fans out to many frames), so it orders events but is NOT a per-channel sequence and cannot be used for gap detection.
emit_ts_msint64The frame's own ts — server emit time, ms.
ts_recv_nsint64Our receive time, ns since epoch — authoritative ordering.
ts_recv_isostringSame instant, ISO-8601, for eyeballing.
cohortstringWhich of our two collector nodes' copy was kept (dual-node provenance).
payload_sha256stringHash of the exact upstream payload — the cross-cohort dedup key together with (channel, sq).

perps_book_deepOrder book — full ladder, ~15 s REST snapshots

The full ladder from the REST book endpoint, polled about every 15 seconds — deeper than the 20-level websocket book (deepest observed ~71 levels) and carrying the venue's own sequence and timestamp. A separate table, never merged into perps_book. ~55 MB · ~300k rows across all instruments.

columntypedescription
iidint32Instrument id — join to perps_instruments.
srcstringAlways `rest` — this table never contains websocket frames.
bid_pxlist<double>Bid prices, best first, full ladder (requested depth 100).
bid_qtylist<double>Bid sizes aligned with bid_px.
ask_pxlist<double>Ask prices, best first, full ladder.
ask_qtylist<double>Ask sizes aligned with ask_px.
bid_levelsint32Levels present on the bid side.
ask_levelsint32Levels present on the ask side.
best_biddoublebid_px[0].
best_askdoubleask_px[0].
src_sequenceint64The REST body's own sequence.
src_ts_msint64The REST body's own timestamp, ms.
sqint64Venue-global event id. Shared ACROSS channels (one value fans out to many frames), so it orders events but is NOT a per-channel sequence and cannot be used for gap detection.
emit_ts_msint64The frame's own ts — server emit time, ms.
ts_recv_nsint64Our receive time, ns since epoch — authoritative ordering.
ts_recv_isostringSame instant, ISO-8601, for eyeballing.
cohortstringWhich of our two collector nodes' copy was kept (dual-node provenance).
payload_sha256stringHash of the exact upstream payload — the cross-cohort dedup key together with (channel, sq).

perps_tradesTrades

Every fill the venue printed: taker side, price, size, venue trade id and the settlement transaction hash once mined. The venue is young and thin — tens of thousands of trades a day across all instruments — so the book and ticker tables carry most of the information. ~3 MB · ~40k rows across all instruments.

columntypedescription
tidint64Venue trade id.
iidint32Instrument id — join to perps_instruments.
sidestring`long` or `short` — the TAKER's direction.
pricedoubleFill price.
qtydoubleFill size, contracts.
trade_ts_msint64Venue trade time, ms.
tx_hashstringSettlement transaction hash; `0x` until mined.
sqint64Venue-global event id. Shared ACROSS channels (one value fans out to many frames), so it orders events but is NOT a per-channel sequence and cannot be used for gap detection.
emit_ts_msint64The frame's own ts — server emit time, ms.
ts_recv_nsint64Our receive time, ns since epoch — authoritative ordering.
ts_recv_isostringSame instant, ISO-8601, for eyeballing.
cohortstringWhich of our two collector nodes' copy was kept (dual-node provenance).
payload_sha256stringHash of the exact upstream payload — the cross-cohort dedup key together with (channel, sq).

perps_tickersTickers — mark, index, open interest, funding

Index, mark, last and mid price, open interest, the current funding rate and the next funding time, roughly every 100 ms per instrument. This is the ONLY place funding lives: the venue has no market-wide funding channel. ~1.3 GB · ~24M rows across all instruments.

columntypedescription
iidint32Instrument id — join to perps_instruments.
index_pxdoubleIndex price.
mark_pxdoubleMark price.
last_pxdoubleLast trade price.
mid_pxdoubleMid price.
open_interestdoubleOpen interest, contracts.
funding_ratedoubleCurrent funding rate (hourly interval on every listed instrument).
next_funding_msint64Next funding time, ms.
sqint64Venue-global event id. Shared ACROSS channels (one value fans out to many frames), so it orders events but is NOT a per-channel sequence and cannot be used for gap detection.
emit_ts_msint64The frame's own ts — server emit time, ms.
ts_recv_nsint64Our receive time, ns since epoch — authoritative ordering.
ts_recv_isostringSame instant, ISO-8601, for eyeballing.
cohortstringWhich of our two collector nodes' copy was kept (dual-node provenance).
payload_sha256stringHash of the exact upstream payload — the cross-cohort dedup key together with (channel, sq).

perps_instrumentsInstruments dictionary (iid → symbol, contract spec)

The contract specifications and the iid → symbol mapping every other table is keyed on: category, base/quote asset, max leverage, tick/lot decimals, min notional, liquidation fee, risk tiers. Snapshotted every five minutes. Included with every pull. ~1 MB · ~16k rows across all instruments.

columntypedescription
iidint32Instrument id — the key every other table carries.
instrument_typestringperpetual.
categorystringcrypto · equity · index · commodity.
base_assetstringUnderlying, e.g. BTC, NVDA, GOLD.
quote_assetstringpUSD.
funding_intervalstringe.g. 1h.
max_leverageint32Maximum leverage offered.
price_decimalsint32Tick precision.
quantity_decimalsint32Lot precision.
min_notionaldoubleMinimum order notional.
liquidation_feedoubleLiquidation fee rate.
isolated_onlyboolIsolated-margin only.
risk_tiers_jsonstringThe venue's risk-tier table, verbatim JSON.
snapshot_ts_msint64When this snapshot of the dictionary was taken, ms.
ts_recv_nsint64Our receive time, ns since epoch.
ts_recv_isostringSame instant, ISO-8601.
cohortstringCollector node provenance.
payload_sha256stringHash of the upstream payload.

Read before you pull

Books are snapshots, never deltas. The venue publishes no incremental stream. Every perps_book row is a complete 20-level ladder at ~100 ms and every perps_book_deep row a complete full ladder at ~15 s from REST; the two are separate tables and are never merged. About a quarter of consecutive perps_book frames are byte-identical and are kept as sent — derive a changed flag with df.sort_values("sq").duplicated(subset=["bid_px","bid_qty","ask_px","ask_qty"]).

sq is venue-global, not per-channel. One value fans out to many frames across channels. It orders events; it cannot detect gaps. There is no per-channel sequence number on this venue.

Funding lives only in tickers. funding_rate and next_funding_ms are on every perps_tickers row (~100 ms). There is no market-wide funding channel.

Coverage starts 2026-07-31 at 15:44 UTC, so that first day is partial. The universe grew from 34 to 67 instruments during August 2026; each instrument's own first day is on the catalog page and in the API.

KPEPE-USD is absent 2026-08-042026-08-05. Not captured on either collector for these two days: the venue's 34th listing pushed the subscription request past the 100-channels-per-connection cap and the excess instrument was silently rejected. Fixed by sharding subscriptions; no other symbol was affected.

Trades are thin. Tens of thousands of fills a day across the whole venue; the book and ticker tables carry most of the information.

API

Same bearer key and rate limits as the rest of /api/v1. One pull covers at most 31 days and 10 GB; the enqueue prices your exact selection from the per-instrument file listing and refuses over the cap with the figure, so narrow the instruments, drop a table, or split the range. Perps bundles have no CSV twin.

endpointstext
GET  /api/v1/perps
     → { access, demo_date, first_date, last_date, days, limits,
         datasets:[{dataset, label, days, first_date, last_date, bytes, record_count}],
         instruments:[{symbol, category, first_date, last_date, days}],
         known_gaps:[…] }
     access: plan | addon | demo | requires_addon

POST /api/v1/perps/downloads
     body { start, end, datasets:["perps_bbo" | "perps_book" | "perps_book_deep" | "perps_trades" | "perps_tickers"], symbols?:["BTC-USD", …] }
     → { status:"staging", id, poll, days, files, bytes, via }
     poll GET /api/v1/downloads/{id} as for any other download
pull a week of BTC and ETH books + tradespython
import requests, time
API = "https://tickfoundry.com/api/v1"; H = {"Authorization": "Bearer tf_live_…"}

job = requests.post(f"{API}/perps/downloads", headers=H, json={
    "start": "2026-08-10", "end": "2026-08-16",
    "datasets": ["perps_bbo", "perps_book", "perps_trades"],
    "symbols": ["BTC-USD", "ETH-USD"],
}).json()

while (s := requests.get(f"{API}/downloads/{job['id']}", headers=H).json())["status"] == "staging":
    time.sleep(10)
open("perps.zip", "wb").write(requests.get(s["url"], headers=H).content)