We capture Kalshi's market-data websocket on two independent legs, merge and deduplicate them hourly, and seal one UTC day at a time into four tables per series (the 25-level book, top of book, trades and Kalshi's ticker) from 2026-09-27 (11:00 UTC that day), plus the CF Benchmarks index feed as one whole-exchange file a day from 2026-09-28. Before that Kalshi has trades only, sold as kalshi_trades_history from 2021-06-30. It is sold per day with a series filter on the Starter, L10 and L25 plans bought for Kalshi (L10 serves the book at 10 levels; the index feed rides on L25), its own subscription that adds up with plans for other venues, and any account can pull the sample day (2026-10-07, up to 1.0 GB). Pull from /catalog/kalshi or the API below.
One parquet per table per series per day, in a table-first tree, so a single read_parquet on a table directory reads every series and day you pulled with date injected from the path. The series directory is plain (not series=): every file carries market_ticker, and the series is its prefix before the first -. The CF Benchmarks index is one file a day (the collector files it under _cfbenchmarks).
kalshi-kxfeddecision-2026-10-01-to-2026-10-07-l25/ README.md kalshi_l1/date=2026-10-01/KXFEDDECISION/l1.parquet kalshi_l2/date=2026-10-01/KXFEDDECISION/l2.parquet kalshi_trades/date=2026-10-01/KXFEDDECISION/trades.parquet kalshi_ticker/date=2026-10-01/KXFEDDECISION/ticker.parquet kalshi_cfbenchmarks/date=2026-10-01/cfbenchmarks.parquet … kalshi-exchange-2021-07-01-to-2021-07-31-trades/ README.md kalshi_trades_history/date=2021-07-01/trades.parquet …
import pandas as pd
l2 = pd.read_parquet("kalshi_l2") # every series and day pulled
mkt = l2[l2.market_ticker == "KXFEDDECISION-26OCT-H0"].sort_values("ts_recv_ns")
# YES terms: asks are 1 - NO bids, at the NO bids' sizes
def cost_to_buy(row, contracts, levels=25):
left, cost = contracts, 0.0
for i in range(1, levels + 1):
px, sz = row[f"ask_px_{i}"], row[f"ask_sz_{i}"]
if pd.isna(px) or left <= 0:
break
take = min(left, sz)
cost, left = cost + take * px, left - take
return cost if left <= 0 else None # None: not enough depth
trades = pd.read_parquet("kalshi_trades")
history = pd.read_parquet("kalshi_trades_history") # whole exchange, one file a day
fed = history[history.market_ticker.str.startswith("KXFEDDECISION-")]Every table carries ts_src_ms (the exchange's clock), ts_recv_ns (ours, the earlier of the two legs) and market_ticker. Prices are dollars exact to 1e-4, sizes contracts exact to 1e-2.
Best YES bid and ask with sizes and the spread, one row whenever either changed, derived tick by tick from the rebuilt book: not Kalshi's conflated ticker. ~2.4 GB · all series.
The whole ladder after every book event, in YES terms (the NO bids become the asks, at their sizes), 25 levels a side: wide columns bid_px_1 … ask_sz_25, plus the exchange's own hourly snapshots. Each row is self-contained, so you can start reading mid-day. ~23 GB · all series.
Every trade: YES and NO price, size in contracts, the taker's side, block-trade flag, trade id. ~440 MB · all series.
Kalshi's own ticker feed, about one update a second per market: last price, YES bid/ask with sizes, cumulative volume and open interest in contracts and dollars. ~1.1 GB · all series.
The CF Benchmarks reference indexes Kalshi's crypto markets settle on (BRTI and the ETH, SOL, XRP, DOGE … real-time indexes), 1-second and 5 Hz, with Kalshi's 60-second averages. One file a day; on L25. ~72 MB · one file.
Every trade Kalshi reports from 30 June 2021 to 26 September 2026, for the whole exchange, one file a day. Trades only: Kalshi's historical API has no tick-level order book, so these days carry no l1, l2 or ticker. ~4 kB a day in 2021, ~300 MB a day by September 2026.
One book per market, in YES terms. Kalshi quotes YES and NO bids. The NO bids become the asks (price 1 − NO bid, at its size), so the book is one ladder per market and nothing on the NO side is lost. bid_levels / ask_levels count the levels on the exchange's book, which can exceed 25; the columns carry the best 25.
L10 is a cut, not a different capture. On L10 the l2 file keeps levels 1-10, caps the level counts at 10, and drops a row in which only levels beyond 10 moved, so every row is a real change to the levels you have.
The trades history is trades only. Before 2026-09-27 Kalshi publishes trades but no tick-level book; those days are one whole-exchange file each, sorted by market_ticker, with ts_recv_ns null and legs = rest. They are bought as backfill on any Kalshi plan ($7.50 a month), never per series and never at book depth.
The first day starts at 11:00 UTC. Our capture began at 11:00 UTC on 27 September 2026: 13 hours that day, not 24, and no CF Benchmarks index file (that capture starts on the 28th).
A book-quality dip on 2026-09-29. During a pipeline cutover 10,255 exchange snapshots disagreed with the rebuilt book (41 to 1,378 on every other day) and 16,678 negative levels were clamped to zero. The book resynchronises at every exchange snapshot; trades and ticker are unaffected.
GET /api/v1/kalshi lists coverage, the trades history's range, your access and one page of series (?q=, ?category=, ?sort=, ?page=). POST /api/v1/kalshi/downloads requests a range: up to 31 days and 10 GB of captured tables per request, or kalshi_trades_history alone, up to 31 days and 12 GB. Poll GET /api/v1/downloads/{id} like every other download.
import requests, time
API = "https://tickfoundry.com/api/v1"
H = {"Authorization": "Bearer tf_live_…"}
job = requests.post(f"{API}/kalshi/downloads", headers=H, json={
"start": "2026-10-01", "end": "2026-10-07",
"datasets": ["kalshi_l1", "kalshi_l2", "kalshi_trades"],
"series": ["KXFEDDECISION"],
}).json()
while (s := requests.get(f"{API}/downloads/{job['id']}", headers=H).json())["status"] == "staging":
time.sleep(5)
open("kalshi.zip", "wb").write(requests.get(s["url"], headers=H).content)