Everything you need to load, join and replay the data: the full schema, the delivery layout, and worked examples against the free sample — a one-hour slice of the BTC Up or Down 4h series.
Polymarket organizes everything into four layers:
In Polymarket's terms, a “market” is the smallest of these — a single binary question that may live for only 15 minutes. A high-frequency series like the 15-minute Bitcoin one spawns hundreds of such markets every day. Selling data at that granularity would mean thousands of tiny files and an unusable purchasing experience.
When you select “a market” on tickfoundry, you are selecting the series. You get the complete history of every event and every underlying Polymarket market in that series for your chosen dates — all the 15-minute Bitcoin instances, not one of them. For one-off events that don't belong to a series, the unit is that event, again with all of its underlying markets included.
Not everything repeats. One-off questions are standalone events with no series — and they include the marquee markets on the venue. These are selectable exactly like a series: pick the event and you get every underlying market in it for your chosen dates.
The pattern: series are repeating questions (15-minute BTC, daily games); standalone events are one-off questions with a definite resolution date — tournaments, elections, award shows.
You rarely want 215 World Cup events one by one. Collections are curated groupings of units — built on Polymarket's own tag taxonomy, the same one its site navigation uses — so a whole tournament, election cycle or topic is a single selection:
Collections unify both worlds: one can hold series and standalone events — a Crypto collection carries the 15-minute BTC series alongside one-off ETF-approval events. The customer-facing hierarchy is vertical → collection → market (series or event), with the identical delivery unit underneath in every case. Collections will surface in the catalog picker as single selections.
Collections are catalog metadata, resolved to a concrete set of units at purchase time — archived files are never re-cut. They're curated (Polymarket's raw tags are uncurated and overlapping) and reviewed as new tournaments and cycles appear; your subscription picks up the new units automatically.
Every file delivered carries condition_id and token_id columns, and each bundle includes reference tables (events, markets, tokens), so you can always slice back down to a specific instance, a specific question, or a specific Yes/No token.
Coverage under this scheme is complete: roughly 638 active series account for the bulk of venue activity, with ~2,560 standalone events (≈14,400 markets) covering the one-off tail — between the two unit kinds, every market on the venue is selectable.
Three data files per bundle (raw.jsonl additionally in the free sample and Enterprise engagements), plus reference tables. Every record carries two clocks: ts_recv_ns — our collector receive time in nanoseconds since the Unix epoch, authoritative for ordering — and ts_src_ms — Polymarket's upstream timestamp in milliseconds, when present. ts_recv_iso mirrors ts_recv_ns as an ISO-8601 UTC string.
Each row is a complete snapshot of one token's book after an update — 25 levels per side, padded with NaN past the deepest level present. No delta replay is needed to know the book at any moment: take the latest row at or before your timestamp.
The system of record: exact inbound messages from Polymarket's market websocket (wss://ws-subscriptions-clob.polymarket.com/ws/market), one JSON object per line. payload_text preserves the original message byte-for-byte (JSON-safe re-encoded) — parse it to recover the upstream object. All parquet files are reproducible from raw + reference.
Polymarket organizes markets as series → events → markets → outcome tokens. The series level is the bundle itself (a bundle covers one series, or one standalone event) — inside it, three reference CSVs ship: events, markets and tokens. All IDs are Polymarket-native.
events.event_id ──▶ markets.event_id markets.market_id ◀─ tokens.market_id markets.condition_id ◀─ l1 / l2 / trades.condition_id markets.yes_token_id / no_token_id ─▶ tokens.token_id tokens.token_id ◀─ l1 / l2 / trades.token_id
The dataset store is Hive-partitioned by entity and UTC date. Entities are Polymarket series, or standalone events. Partition values (kind / id / date) live in the path, not repeated as columns. Bulk and enterprise delivery mirrors this tree.
normalized/v1/
kind=series/series_id=<ID>/date=YYYY-MM-DD/
l1.parquet # top-of-book updates
l2.parquet # order-book snapshots
trades.parquet # prints / last-trade updates
book_state.json.zst # end-of-day book state — seeds next day's replay
kind=event/event_id=<ID>/date=YYYY-MM-DD/ # same files
atoms/v1/
kind={series,event}/<id>=<ID>/date=YYYY-MM-DD/
raw.jsonl.zst # exact inbound websocket messagesWhat a customer actually downloads: one zip per package, named polymarket-<slug>-<date>-<scope>.zip. A parallel CSV bundle (…-csv.zip) ships alongside for no-parquet users.
polymarket-<slug>-<date>-<scope>.zip
l1.parquet # top-of-book updates
l2.parquet # order-book snapshots
trades.parquet # prints
reference/
markets.csv # condition_ids, yes/no token ids
tokens.csv # outcome tokens — join key into all data files
events.csv # events under the unit
spot/ # Premium & Enterprise — see below
binance.parquet
chainlink.parquet
README.md # schema + row counts + join guideOrder bundles do not contain raw.jsonl — the raw capture layer appears only in the free sample bundle and in Enterprise engagements.
Premium and Enterprise bundles include a spot/ directory with binance.parquet and chainlink.parquet covering the bundle's UTC day — all 13 crypto symbols at roughly 1 Hz — for dates from 2026-06-21 onward (earlier dates ship without spot). Prices are captured from Polymarket's real-time data stream (RTDS), which relays Binance spot and Chainlink oracle prices.
Columns: topic, symbol, value, full_accuracy_value, src_ts_ms, emit_ts_ms, ts_recv_ns, ts_recv_iso. Rows are sorted by (symbol, src_ts_ms) — sort by ts_recv_ns for tape order. Spot ships parquet-only: the …-csv.zip twin carries the same spot/ parquet files.
SFTP delivery is rolling out for Premium and Enterprise: daily drops land in your prefix, mirroring the partitioned layout above. Auth is by ed25519 key, and provisioning is manual for now — send us your public key via contact and we set up your access; your endpoint hostname is issued at provisioning.
Bundles are streamed to you through authenticated endpoints — your dashboard session or your API key — not as public links, so a copied URL is useless to anyone else and there is nothing to leak or share. Catalog pulls land within 60 seconds of purchase. Each delivered bundle can be re-downloaded up to 25 times on Free and Explorer; unlimited on Premium and Enterprise.
The REST read API is live on Explorer and above: list the whole venue, request any series·day, and stream the bundle — fully programmatic, no dashboard round-trip. Full reference, auth and worked curl / requests examples are in API below. A thin Python SDK is coming soon.
Raw-archive integrity is tracked by internal manifests today (per-message payload_sha256, sequence audits). A delivery-side SHA-256 manifest per drop is planned and will appear alongside each bundle.
Everything you can do in the dashboard you can do over HTTP: enumerate the whole venue, request any series·day, and stream the bundle. The API is read-only and available on Explorer and above. The base URL is https://tickfoundry.com/api/v1.
Every request carries an API key as a bearer token: Authorization: Bearer tf_live_…. Mint keys in your dashboard — the secret is shown once at creation and stored only as a sha256 hash, so save it then. Each call resolves the key to your account and active plan, consumes rate-limit budget, and gates on tier capabilities (e.g. the L2 level cap). API access requires an Explorer subscription or higher.
All paths are relative to the base URL above.
A POST /downloads enqueues an on-demand staging job (or reports it ready if that series·day is already staged) and hands back a poll URL. Poll GET /downloads/{id} until status is ready, then GET the returned url (it points at /downloads/{id}/file) with your API key to stream the bundle — it is not a public link. The layers field selects among l1, l2, trades; format is parquet (default) or csv. Raw atoms are available under an Enterprise engagement, delivered per contract — not through this API.
# auth: bearer key, minted in the dashboard, shown once (sha256-stored)API="https://tickfoundry.com/api/v1"KEY="tf_live_xxxxxxxxxxxxxxxxxxxxxxxx"# 1. find a unit — search the whole venuecurl -s "$API/catalog?q=bitcoin+up+or+down&kind=series&limit=5" \-H "Authorization: Bearer $KEY"# 2. request a series.day — returns staging + a poll url (or ready + url)JOB=$(curl -s "$API/downloads" \-H "Authorization: Bearer $KEY" \-H "Content-Type: application/json" \-d '{"unit_id":"series:10637","date":"2026-05-19","layers":["l1","l2","trades"]}')POLL=$(echo "$JOB" | jq -r .poll)# 3. poll until the job is materializeduntil [ "$(curl -s "$POLL" -H "Authorization: Bearer $KEY" | jq -r .status)" = "ready" ]; dosleep 3done# 4. download the bundle — the url is this API (key-authed), not a public linkURL=$(curl -s "$POLL" -H "Authorization: Bearer $KEY" | jq -r .url)curl -L "$URL" -H "Authorization: Bearer $KEY" -o polymarket-btc-up-or-down-2026-05-19.zip
import time, requestsAPI = "https://tickfoundry.com/api/v1"# key minted in the dashboard, shown once, sha256-storedS = requests.Session()S.headers["Authorization"] = "Bearer tf_live_xxxxxxxxxxxxxxxxxxxxxxxx"# 1. list the whole venuehits = S.get(f"{API}/catalog",params={"q": "bitcoin up or down", "kind": "series", "limit": 5}).json()unit = hits["results"][0]["unit_id"]# 2. request a series.dayjob = S.post(f"{API}/downloads",json={"unit_id": unit, "date": "2026-05-19","layers": ["l1", "l2", "trades"]}).json()# 3. poll until ready (immediate if already staged)while job["status"] != "ready":time.sleep(3)job = S.get(job["poll"]).json()# 4. download the bundle — same session carries the API key to the stream urlzipped = S.get(job["url"]).contentopen("polymarket-btc-up-or-down-2026-05-19.zip", "wb").write(zipped)
A thin Python SDK that wraps these endpoints — hiding the poll loop behind a single client.download(unit_id, date) call — is coming soon. Until then the REST API above is the integration surface; the requests example is a ~20-line drop-in.
Limits are enforced per plan. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; exceeding the window returns HTTP 429 with a Retry-After header. Per-day download ceilings apply on top of the request rate.
Free accounts have no API access — keys are mintable from Explorer upward. The same per-day download ceiling that governs dashboard pulls applies to API-driven downloads.
Load the L1 file, attach human-readable market context from the reference tables, and index by receive time.
import pandas as pdl1 = pd.read_parquet("l1.parquet")tokens = pd.read_csv("reference/tokens.csv")markets = pd.read_csv("reference/markets.csv")# label every quote with its market and outcomel1 = (l1.merge(tokens[["token_id", "market_id", "outcome"]], on="token_id").merge(markets[["market_id", "question"]], on="market_id"))# one token's top-of-book, indexed by authoritative receive timetok = l1[l1.token_id == YES_TOKEN].copy()tok.index = pd.to_datetime(tok.ts_recv_ns, unit="ns", utc=True)mid = (tok.best_bid + tok.best_ask) / 2
Because every L2 row is a full 25-level snapshot, fill simulation is a straight walk down the ladder: take liquidity level by level until your order is filled, and compare the volume-weighted price to the touch.
import numpy as npl2 = pd.read_parquet("l2.parquet")snap = l2[l2.token_id == YES_TOKEN].iloc[-1] # latest snapshotdef fill_vwap(snap, size):"""Walk the ask ladder; VWAP for a buy of `size`."""filled, cost = 0.0, 0.0for i in range(1, 26):px, sz = snap[f"ask_px_{i}"], snap[f"ask_sz_{i}"]if np.isnan(px) or filled >= size:breaktake = min(sz, size - filled)filled += takecost += take * pxreturn cost / filledvwap = fill_vwap(snap, 5_000)slippage = vwap - snap.best_ask # cost of size, in probability
No delta replay needed: the book at any instant is each token's last snapshot at or before that instant. ts_recv_ns orders events; msg_seq breaks ties.
T = pd.Timestamp("2026-05-19 14:30:00Z").value # ns since epochbook_at_T = (l2[l2.ts_recv_ns <= T].sort_values(["ts_recv_ns", "msg_seq"]).groupby("token_id").tail(1))# cross-day studies: book_state.json.zst carries the end-of-day book,# so day N+1 starts from a known state instead of a cold book.