Hyperliquid runs its order books on its own chain, and the chain publishes the complete order-event stream (replica_cmds: every placement, modify, cancel and fill). We replay that stream, block by block, into three tables per coin per day — top of book, the 25-level book and the fills tape — for every perp, spot pair and HIP-3 market from 2025-01-25, and continue it daily. It is sold per day with a coin filter: included with an Explorer or Premium plan bought for Hyperliquid, $50/mo as an add-on on a Polymarket plan, and any account can pull the fixed sample day (2026-08-12, up to 1.0 GB). Pull from /catalog/hyperliquid or the API below.
Why buy it rather than build it: Building this yourself — pulling the chain's order-event archive out of requester-pays S3 and replaying it into books — runs to around $10,000 in egress and compute.
One parquet per table per coin per day, in a table-first tree. The bundle keeps that layout, so a single read_parquet on a table directory reads every coin and day you pulled, with date injected from the path. The coin directory is the venue's coin name made path-safe (km:GOLD → km-GOLD, PURR/USDC → PURR_USDC) and is deliberately not a coin= partition: every file already carries a coin column with the native name.
hyperliquid-2026-09-01-to-2026-09-07/ README.md hl_l1/date=2026-09-01/BTC/l1.parquet hl_l1/date=2026-09-01/ETH/l1.parquet hl_l2/date=2026-09-01/BTC/l2.parquet hl_trades/date=2026-09-01/BTC/trades.parquet quality/date=2026-09-01/BTC.json # the replay's scorecard for that coin-day …
import pandas as pd
l2 = pd.read_parquet("hl_l2") # date injected from the path; coin is a column
btc = l2[l2.coin == "BTC"].sort_values("msg_seq")
# strict UTC day (a date= folder is block-aligned, see caveats)
day = btc[(btc.ts_src_ms >= 1756684800000) & (btc.ts_src_ms < 1756771200000)]
# depth-weighted mid from the top 5 levels
bid_sz = day[[f"bid_sz_{i}" for i in range(1, 6)]].sum(axis=1)
ask_sz = day[[f"ask_sz_{i}" for i in range(1, 6)]].sum(axis=1)
day["microprice"] = (day.best_bid * ask_sz + day.best_ask * bid_sz) / (bid_sz + ask_sz)
trades = pd.read_parquet("hl_trades")
takers = trades.groupby("user").size().sort_values(ascending=False)Three tables, all keyed the same way: ts_src_ms is the block time, msg_seq the block height, coin the native name. Choose any of l1, l2, trades.
Best bid and ask with sizes and the spread, one row per block in which the top of book changed, replayed from the chain's own order events — not sampled from a websocket. No consecutive duplicates. ~1.0 GB · all coins.
The 25-level book on both sides — price, size and the number of resting orders at each level — one row per block in which any of it changed. Wide columns (bid_px_1 … ask_n_25), unused levels null. This is the table the venue's own websocket cannot give you: Hyperliquid pushes l2Book snapshots only, throttled to seconds. ~16 GB · all coins.
Every fill the chain executed: price, size, taker side, order id, the taker's wallet, fee and fee token, closed PnL and start position, and the settlement transaction hash. From 2025-03-22 straight from the node's fills tape; before that synthesized from the taker's own fill status (see caveats). ~350 MB · all coins.
Every coin-day ships quality/date=<D>/<coin>.json, the replay's own scorecard. score.bb_px / score.ba_px are the share of Hyperliquid's own published l2Book snapshots (score.n of them, roughly every 5 s) whose best bid / best ask price our reconstructed book matched exactly at that instant; b5_px, a5_px, b20_px, a20_px the same for the top 5 and top 20 levels; *_sz_exact for size as well as price. seed_source says where the day's opening book came from (hl_archive = the venue's snapshot, state = our previous day's close, none = unseeded). A score.n of 0 means the venue published no snapshots for that coin that day — typical for spot and HIP-3 — not a bad book. On BTC 2026-09-10 the top-of-book price agreement was 99.6% over 16,051 snapshots.
One row per block, not per message. Hyperliquid blocks land about every 70 ms. hl_l1 has a row for every block in which the top of book changed, hl_l2 for every block in which any of the 25 levels changed (no consecutive duplicates in either), hl_trades one row per fill. Within a block, order is by ts_recv_ns.
A day is block-aligned, not midnight-aligned. Each date= folder is the chain's day folder, which starts a few minutes after 00:00 UTC and ends a few minutes after the next midnight, so a few thousand rows per coin carry a ts_src_ms past midnight. Filter on ts_src_ms for strict UTC days; a multi-day pull is contiguous.
ts_recv_ns is not a second clock. On l1/l2 it is ts_src_ms × 1e6 plus a sub-millisecond offset that preserves within-block order; on trades it is exactly ts_src_ms × 1e6. Use it to sort, never to measure latency.
Placeholder rows. A coin listed on the venue but with no book that day (delisted, or a HIP-3 market before its first quote) ships as a single all-null l1/l2 row and an empty trades file, so the coin set is the venue's listing rather than our selection.
Coin names are the venue's. Perps are bare (BTC, kPEPE), spot pairs are indexes (@107; PURR/USDC is the one named pair), HIP-3 markets are dex:coin (xyz:CL, km:GOLD). HIP-3 dexes are chained from their launch day: xyz 2025-10-13, flx 2025-11-13, vntl 2025-11-13, hyna 2025-12-03, km 2026-01-12, cash 2026-01-20, para 2026-04-01, mkts 2026-07-01, io 2026-08-19. Perp books are seeded each day from the venue's own published snapshot; spot and HIP-3 books carry over from the previous day's close.
Fills before 2025-03-22 are synthesized. No fills tape exists upstream before 2025-03-22, so taker fills in that era are derived from each taker order's own filled size and average price against the resting book (source_event_type = l3_taker_status). price, size, side, oid and user are set; transaction_hash, tid, dir, fee, fee_token, closed_pnl and start_position are null. Resting orders that expired or were cancelled without an event can linger in the book until the order-id-gap inference catches them.
Spot books start unseeded. The archive has no snapshot before its first day, so resting spot depth is thinner than steady state for the first day or two.
A 924-block hole on 2025-07-19. Heights 668379075 onward, roughly the last minutes of one source object: the object is truncated in Hyperliquid's own bucket. Every coin is affected for that interval; nothing else that day.
Same bearer key and rate limits as the rest of /api/v1. One pull covers at most 92 days and 10 GB; the enqueue prices your exact selection from the per-file listing and refuses over the cap with the figure, so narrow the coins, drop l2, or split the range. A whole-venue day is ~17 GB, so "every coin" only fits for l1 + trades on one day. No CSV twin.
GET /api/v1/hyperliquid
→ { access, demo_date, demo_max_bytes, first_date, last_date, days, bytes, limits,
datasets:[{dataset, label, days, first_date, last_date, bytes, files}],
families:{perp, spot, hip3}, hip3_dexes:[{dex, first_date}],
coins:[{coin, family, first_date, last_date, days, avg_bytes_per_day:{hl_l1,hl_l2,hl_trades}}],
known_caveats:[…] }
access: plan | addon | demo | requires_addon
POST /api/v1/hyperliquid/downloads
body { start, end, datasets:["hl_l1" | "hl_l2" | "hl_trades"], coins?:["BTC", "ETH", "@107", "xyz:CL"] }
→ { status:"staging", id, poll, days, files, bytes, via }
poll GET /api/v1/downloads/{id} as for any other downloadimport requests, time
API = "https://tickfoundry.com/api/v1"; H = {"Authorization": "Bearer tf_live_…"}
job = requests.post(f"{API}/hyperliquid/downloads", headers=H, json={
"start": "2026-06-01", "end": "2026-08-31",
"datasets": ["hl_l1", "hl_l2", "hl_trades"],
"coins": ["BTC", "ETH"],
}).json()
while (s := requests.get(f"{API}/downloads/{job['id']}", headers=H).json())["status"] == "staging":
time.sleep(10)
open("hyperliquid.zip", "wb").write(requests.get(s["url"], headers=H).content)