tickfoundry
capture livesign inget the data →
docs / hyperliquid

Hyperliquid — schema & delivery

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.

File layout

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/USDCPURR_USDC) and is deliberately not a coin= partition: every file already carries a coin column with the native name.

bundle layouttext
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
  …
read a table across every coin and day in the bundlepython
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)

Tables

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.

hl_l1Top of book (every change)

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.

columntypedescription
ts_src_msint64Block time from the chain, ms since the Unix epoch, UTC. The row's clock.
ts_recv_nsint64The same instant in ns. On l1/l2 a sub-millisecond offset (0–999,999 ns) preserves within-block order; on trades it is exactly ts_src_ms × 1e6. NOT an independent receive clock.
coinstringThe venue's native coin name: BTC, @107 (spot index), PURR/USDC, xyz:CL (HIP-3 dex:coin).
best_biddoubleBest bid price. Null on a placeholder row (no book that day).
best_bid_sizedoubleSize resting at the best bid.
best_askdoubleBest ask price.
best_ask_sizedoubleSize resting at the best ask.
spreaddoublebest_ask − best_bid, exactly.
source_event_typestringl3_replay for book rows; node_fills_by_block for fills (from 2025-03-22), l3_taker_status for synthesized fills before that.
msg_seqint64Hyperliquid block height. Unique per row on l1/l2 (one row per block); many fills share one on trades.

hl_l2Order book — 25 levels, every change

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.

columntypedescription
ts_src_msint64Block time from the chain, ms since the Unix epoch, UTC. The row's clock.
ts_recv_nsint64The same instant in ns. On l1/l2 a sub-millisecond offset (0–999,999 ns) preserves within-block order; on trades it is exactly ts_src_ms × 1e6. NOT an independent receive clock.
coinstringThe venue's native coin name: BTC, @107 (spot index), PURR/USDC, xyz:CL (HIP-3 dex:coin).
event_kindstringl3_replay — reserved for future row kinds.
bid_levelsint16Bid levels present in this row (0–25).
ask_levelsint16Ask levels present in this row (0–25).
best_biddouble= bid_px_1, for convenience.
best_askdouble= ask_px_1.
bid_px_1 … bid_px_25doubleBid price at level 1 (best) … 25. Null beyond the levels present.
bid_sz_1 … bid_sz_25doubleResting size at that level, in the coin's base unit.
ask_px_1 … ask_px_25doubleAsk price at level 1 (best) … 25.
ask_sz_1 … ask_sz_25doubleResting size at that level.
bid_n_1 … bid_n_25int32Number of resting orders at that bid level.
ask_n_1 … ask_n_25int32Number of resting orders at that ask level.
source_event_typestringl3_replay for book rows; node_fills_by_block for fills (from 2025-03-22), l3_taker_status for synthesized fills before that.
msg_seqint64Hyperliquid block height. Unique per row on l1/l2 (one row per block); many fills share one on trades.

hl_tradesTrades (fills)

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.

columntypedescription
ts_src_msint64Block time from the chain, ms since the Unix epoch, UTC. The row's clock.
ts_recv_nsint64The same instant in ns. On l1/l2 a sub-millisecond offset (0–999,999 ns) preserves within-block order; on trades it is exactly ts_src_ms × 1e6. NOT an independent receive clock.
coinstringThe venue's native coin name: BTC, @107 (spot index), PURR/USDC, xyz:CL (HIP-3 dex:coin).
pricedoubleFill price.
sizedoubleFill size, base unit.
sidestringBUY / SELL — the TAKER's side.
transaction_hashstringSettlement transaction hash. All-zero for fills the chain settles without a user transaction (liquidations, TWAP legs …); null before 2025-03-22.
tidint64Venue trade id. Null before 2025-03-22.
oidint64The taker's order id.
dirstringPerps: Open Long / Close Long / Open Short / Close Short / Long > Short / Short > Long. Spot: Buy / Sell / Spot Dust Conversion. Null before 2025-03-22.
feedoubleFee paid by the taker, in fee_token.
fee_tokenstringUSDC on perps; on spot the base or quote token. Null before 2025-03-22.
closed_pnldoubleRealized PnL on this fill (perps).
start_positiondoubleTaker's position before the fill.
userstringThe taker's wallet address.
builder_feedoubleBuilder fee when a builder code was attached, else null.
source_event_typestringl3_replay for book rows; node_fills_by_block for fills (from 2025-03-22), l3_taker_status for synthesized fills before that.
msg_seqint64Hyperliquid block height. Unique per row on l1/l2 (one row per block); many fills share one on trades.

Quality scorecards

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.

Read before you pull

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.

API

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.

endpointstext
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 download
pull a quarter 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}/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)