tickfoundry
capture livesign inget the data →
§ docs

Documentation

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.

How Polymarket structures markets — and what “a market” means here

Polymarket organizes everything into four layers:

SeriesA template for a repeating question, e.g. “Bitcoin Up or Down — 15 minute”.
EventOne instance of that question, e.g. “Bitcoin Up or Down — May 13, 2:15 PM ET”. One-off events (an election, a championship final) have no series.
MarketA single tradeable binary question inside an event, identified by a condition_id. Most events contain one market; some contain several.
Outcome tokensThe two sides of each market (Yes/No), identified by token_id. These are what actually trade on the orderbook, and what the raw websocket feed is keyed by.

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.

standalone event · examplesmarkets inside
World Cup Winner48 — one per nation
World Cup: Player to score147
Presidential Election Winner 202836
Next French Presidential Election36
Masters London 2026 MVP (Valorant)60
What will Fed Rate hit before 2027?20
Nobel Peace Prize Winner 202620

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.

Collections — logical bundles of units

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:

collection · examplesunits inside
2026 FIFA World Cup215 events
Midterms565 events
Trump284 events
AI98 events
Masters London (Valorant)event group

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.

Schema reference

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.

l1.parquet — top-of-book updates

columntypedescription
ts_recv_nsint64Collector receive time, ns since epoch. Authoritative ordering.
ts_src_msint64Upstream Polymarket timestamp, ms (when present in payload).
ts_recv_isostringISO-8601 UTC mirror of ts_recv_ns.
condition_idstringMarket identifier — joins markets.condition_id.
token_idstringOutcome token — joins tokens.token_id.
best_bid · best_askfloat64Best resting bid / ask price, in probability.
best_bid_size · best_ask_sizefloat64Size resting at the touch.
spreadfloat64ask − bid, 3 decimal places.
source_event_typestringUpstream event that triggered the update: book, price_change, …
msg_seqint64Capture sequence number — gap-audited, breaks timestamp ties.

l2.parquet — full order-book snapshots

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.

columntypedescription
ts_recv_ns · ts_src_ms · ts_recv_isoSame two-clock convention as L1.
condition_id · token_idstringJoin keys, as in L1.
event_kindstringFull book event vs incremental update.
source_event_typestringUpstream trigger event.
bid_levels · ask_levelsint64Number of populated levels this snapshot.
bid_px_1 … bid_px_25float64Bid ladder prices, best first. NaN beyond depth.
bid_sz_1 … bid_sz_25float64Bid ladder sizes.
ask_px_1 … ask_px_25float64Ask ladder prices, best first.
ask_sz_1 … ask_sz_25float64Ask ladder sizes.
best_bid · best_askfloat64Top of our reconstructed book.
payload_best_bid · payload_best_askfloat64Top of book as declared by the upstream payload.
bid_ask_reconcilesboolTrue when our reconstruction matches the payload’s declared top.
msg_seqint64Capture sequence number.

trades.parquet — prints

columntypedescription
ts_recv_ns · ts_src_ms · ts_recv_isoTwo-clock convention.
condition_id · token_idstringJoin keys.
price · sizefloat64Fill price (probability) and size.
sidestringAggressor side.
fee_rate_bpsfloat64Fee rate applied, basis points.
transaction_hashstringOn-chain transaction hash.
msg_seqint64Capture sequence number.

raw.jsonl — capture envelope

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.

columntypedescription
sourcestringAlways polymarket_market_ws.
collector_id · run_id · conn_idstringWhich collector, run and connection captured this message.
msg_seqint64Monotonic per-connection sequence, gap-audited.
ts_recv_ns · ts_recv_iso · ts_srcReceive time (ns + ISO) and upstream timestamp.
event_typestringprice_change · book · tick_size_change · last_trade_price.
condition_id · token_idstringSubject identifiers (token_id may be null).
parse_ok · parse_errorbool · stringWhether the payload parsed cleanly downstream.
payload_sha256stringHash of the original payload — dedup + integrity.
payload_textstringThe verbatim upstream message, as a JSON string.

reference/ — joining the hierarchy

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

File layout & naming

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 messages

Download bundles

What 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 guide

Order bundles do not contain raw.jsonl — the raw capture layer appears only in the free sample bundle and in Enterprise engagements.

spot/ — crypto spot-price ride-along

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.

Delivery

SFTP

Premium and Enterprise accounts get read-only SFTP access to their delivered bundles at sftp.tickfoundry.com, port 2022. Every bundle you pull (dashboard or API) appears in your SFTP home the moment staging completes, organized by date — the same files, no re-download budget. Auth is by ed25519 key only (no passwords): send us your public key via contact and we provision your account and username, normally within a business day. Then: sftp -P 2022 your-username@sftp.tickfoundry.com.

Downloads

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.

REST API

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.

IntegrityDRAFT

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.

REST API

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.

Authentication

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.

Endpoints

All paths are relative to the base URL above.

columntypedescription
GET /meYour plan, limits and current usage — including coverage-start.
GET /catalogqueryList units across the whole venue. q= search, kind=series|event, limit=, offset=.
POST /downloadsbody{unit_id, date, layers?, format?} → {status:"staging", id, poll}, or {status:"ready", url} if already materialized.
GET /downloads/{id}Poll a staging job → {status:"staging"} while building, {status:"ready", url} when done.
GET /downloads/{id}/fileStream the staged bundle. Send your API key — there is no public URL; available once ready.

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.

curlbash
# 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 venue
curl -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 materialized
until [ "$(curl -s "$POLL" -H "Authorization: Bearer $KEY" | jq -r .status)" = "ready" ]; do
sleep 3
done
# 4. download the bundle — the url is this API (key-authed), not a public link
URL=$(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
python · requestspython
import time, requests
API = "https://tickfoundry.com/api/v1"
# key minted in the dashboard, shown once, sha256-stored
S = requests.Session()
S.headers["Authorization"] = "Bearer tf_live_xxxxxxxxxxxxxxxxxxxxxxxx"
# 1. list the whole venue
hits = 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.day
job = 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 url
zipped = S.get(job["url"]).content
open("polymarket-btc-up-or-down-2026-05-19.zip", "wb").write(zipped)

Python SDKDRAFT

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.

Rate limits

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.

planreq / minreq / daydownloads / day
Explorer601,00015
Premium600unlimitedunlimited
Enterprisecustomcustomunlimited

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.

Quickstart — load and join in pandas

Load the L1 file, attach human-readable market context from the reference tables, and index by receive time.

quickstart.pypython
import pandas as pd
l1 = 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 outcome
l1 = (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 time
tok = 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

Worked example — slippage from the L2 ladder

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.

slippage.pypython
import numpy as np
l2 = pd.read_parquet("l2.parquet")
snap = l2[l2.token_id == YES_TOKEN].iloc[-1] # latest snapshot
def fill_vwap(snap, size):
"""Walk the ask ladder; VWAP for a buy of `size`."""
filled, cost = 0.0, 0.0
for i in range(1, 26):
px, sz = snap[f"ask_px_{i}"], snap[f"ask_sz_{i}"]
if np.isnan(px) or filled >= size:
break
take = min(sz, size - filled)
filled += take
cost += take * px
return cost / filled
vwap = fill_vwap(snap, 5_000)
slippage = vwap - snap.best_ask # cost of size, in probability

Worked example — the book at time T

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.

book_at_t.pypython
T = pd.Timestamp("2026-05-19 14:30:00Z").value # ns since epoch
book_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.

Data quality

Dual collectorsTwo independent collectors in separate regions capture the same feed. Each message carries collector_id, run_id and conn_id so provenance is never ambiguous.
Sequence auditingmsg_seq is monotonic per connection. Gaps are detected mechanically, cross-checked against the second collector, and backfilled from it.
Deduplicationpayload_sha256 hashes every upstream message — duplicates across collectors and reconnects are dropped deterministically.
ReconciliationEvery L2 snapshot compares our reconstructed top-of-book against the payload’s declared best bid/ask; bid_ask_reconciles flags any disagreement.
Parse transparencyMessages that fail downstream parsing are kept in raw with parse_ok=false and the error recorded — nothing is silently discarded.
Replayable by designThe raw layer is the system of record; every parquet is reproducible from raw + reference. book_state.json.zst seeds day-boundary replays.

FAQ

What granularity is this, exactly?
Event-level: every websocket message — every book update, price change and trade — receive-timestamped to the nanosecond. Not periodic snapshots; if the book changed, there is a row.
Do I need parquet tooling?
No — every bundle ships a parallel CSV variant (…-csv.zip) with the same tree. Parquet is recommended for L2 at scale.
How do L1 and L2 relate?
L1 rows are derived top-of-book updates; L2 rows are the full 25-level ladder after each update. L1 is smaller and faster for price/spread work; reach for L2 when you need depth.
Can I rebuild your parquets myself?
Yes, under an Enterprise engagement: raw.jsonl.zst plus the reference tables is sufficient to reproduce every normalized file, and the replay-engine source is part of the engagement. Raw is delivered per contract, not through the self-serve API.
What happens at day boundaries?
Each partition closes with book_state.json.zst — the end-of-day book — so the next day’s replay starts from a known state.
Get the free sample →See pricing