Every table in the Hyperliquid historical dataset, explained
Explore all nine Hyperliquid historical dataset tables, 4.3 billion rows, real sample data, practical queries, coverage details, and common analysis traps.
The Hyperliquid historical dataset holds nine tables and about 4.3 billion rows.
Coverage runs July 2025 through July 2026 at schema v1.0.
You get the complete available fill history, per-user funding payments, L2 order-book snapshots, and the ledger and staking events underneath them. Delivery is partitioned Parquet you keep.
This post covers all nine tables. What each one holds. What a real row looks like. Which questions each one answers. And the details which corrupt your numbers if you miss them.
Every example row below comes from the free 100-row sample published for each table.
The nine tables
| Table | Kind | Rows | Columns | Coverage |
|---|---|---|---|---|
swaps | market | 2.6B | 37 | Jul 2025 → Jul 2026 |
funding | event | 1.6B | 9 | Sep 2025 → Jul 2026 |
l2_order_book_snapshots | market | 37.0M | 14 | May 2026 → Jun 2026 |
ledger_updates | event | 18.8M | 30 | Sep 2025 → Jul 2026 |
validator_rewards | event | 12.6M | 6 | Sep 2025 → Jul 2026 |
delegations | event | 162.9K | 8 | Sep 2025 → Jul 2026 |
gossip_auctions | event | 115.2K | 7 | Apr 2026 → Jul 2026 |
deposits | event | 90.7K | 6 | Sep 2025 → Jul 2026 |
withdrawals | event | 89.6K | 7 | Sep 2025 → Jul 2026 |
Two points before you plan any analysis.
Coverage runs per table, not per dataset. The dataset headline says July 2025
through July 2026. swaps spans the full window. l2_order_book_snapshots
covers May through June 2026. gossip_auctions starts in April 2026. If your
study needs order-book depth alongside fills, your real window is the
order-book window.
Two tables hold most of the data. swaps and funding make up about 98% of
the rows. Every other table fits in memory on a laptop.
Shared conventions
Most tables open with the same four columns:
block_number,UInt64, the Hyperliquid blockblock_time,DateTime64(3, 'UTC'), millisecond precision, always UTCevent_time,DateTime64(3, 'UTC'), when the event occurredhash,String, the source event or transaction hash
Three type conventions matter.
Money uses Decimal(38, 18), stored in Parquet as decimal128(38,18). The
values are exact. Cast to a float and you introduce rounding you never recover.
Aggregate in decimal. Convert in the final projection only.
Enum-style strings use LowCardinality(String). This covers coin,
market_type, direction, liquidity_role, delta_type, and token. They
read as ordinary strings in Parquet and compress well, so filtering on them
costs little.
Timestamps come in several forms. block_time is chain time. event_time is
when the event happened. l2_order_book_snapshots adds receipt_time at
nanosecond precision and exchange_time from the exchange. For latency work,
the gap between those two is your measurement.
swaps, 2.6B rows, 37 columns
The complete available spot and perpetual fill history. Most buyers come for this table.
A real row from the sample:
{
"block_time": "2025-07-27 09:05:40.456+00:00",
"coin": "@1",
"market_type": "SPOT",
"dex": "hyperliquid",
"wallet": "0x42fa34caf53e79fcef0d147da7e1c31123fede21",
"direction": "sell",
"px": "18.745000000000000000",
"sz": "8.680000000000000000",
"notional_usd": "162.706600000000000000",
"closed_pnl": "0E-18",
"fee": "0.103871880000000000",
"fee_token": "USDC",
"liquidity_role": "taker",
"is_taker": 1,
"oid": 121672619760,
"tid": 752057817590464,
"is_twap": 0,
"is_liquidation": 0
}The 37 columns fall into five groups:
- Identity:
coin,market_type,dex,wallet - Execution:
direction,px,sz,notional_usd,oid,tid,cloid,tx_signature,tx_index - Economics:
fee,fee_token,fee_is_rebate,builder,builder_fee,closed_pnl,start_position - Microstructure:
liquidity_role,is_taker,is_twap,twap_id,node_latency_ms - Perp context:
position_action,position_side,is_liquidation,liquidated_user,mark_px,liquidation_method
Every fill appears twice
The swaps sample holds 100 rows. Those 100 rows contain 50 distinct tid
values. Every tid appears exactly twice: same size, opposite direction, one
taker and one maker.
tid 752057817590464 0x42fa34ca… sell taker 8.68
0xffffffff… buy maker 8.68
tid 49486923937202 0xcf0a36de… sell taker 11.52
0xffffffff… buy maker 11.52The design is correct. You need both sides to compute per-wallet flow. But a
plain sum(notional_usd) reports double the real traded volume. Pick one side:
-- Daily traded volume. Taker rows count each fill once.
SELECT date_trunc('day', block_time) AS day,
coin,
count(*) AS fills,
sum(notional_usd)::DECIMAL(38,2) AS volume_usd
FROM read_parquet('hyperliquid/historical/swaps/schema=v1.0/date=*/part-*.parquet')
WHERE is_taker = 1
GROUP BY 1, 2
ORDER BY 1, volume_usd DESC;For per-wallet activity, drop the filter. A wallet's maker fills are real fills.
Filter is_taker = 1 when you measure the market. Keep both sides when you
measure participants.
More details worth knowing
coin is not always a ticker. Spot pairs use Hyperliquid index notation. Every
row in the swaps sample carries coin = "@1". Perpetual markets use the symbol.
Branch on market_type instead of assuming a format.
fee_token varies. The sample shows HFUN on 63 rows and USDC on 37. Summing
fee without grouping by fee_token adds different currencies together. Group
by fee_token, or convert first.
Zeros render as 0E-18. Decimal zero in scientific notation, not a string and
not null. Normal comparison against 0 works.
Perp-only columns hold empty strings on spot rows. position_action and
position_side are empty on all 100 spot sample rows. Empty string, not NULL.
Use = '' instead of IS NULL.
funding, 1.6B rows, 9 columns
Per-user funding payments with the position size and rate behind them. One row per user per funding event per coin, which explains the size.
{
"block_time": "2025-09-27 10:00:00.021+00:00",
"user": "0x004ee5cc832709446498d0c88d256b4ddcfa8fb4",
"coin": "0G",
"funding_amount": "0.600266000000000000",
"szi": "1638.000000000000000000",
"funding_rate": "-0.000102650800000000"
}szi carries a sign. Positive is long. Negative is short. In the sample, a user
long 1,638 at a rate of -0.00010265 received +0.600266. A user short 3,763
at the same instant paid -1.379001. Sign conventions hold, so read
funding_amount directly instead of deriving your own.
Every user at the same timestamp shares one funding_rate. You get the market
rate and the distribution of who carried which side when the rate applied:
-- Realised funding P&L per wallet, and the size they carried.
SELECT user,
coin,
sum(funding_amount)::DECIMAL(38,6) AS net_funding,
avg(szi)::DECIMAL(38,4) AS avg_signed_size,
count(*) AS funding_events
FROM read_parquet('hyperliquid/historical/funding/schema=v1.0/date=*/part-*.parquet')
WHERE coin = 'BTC'
GROUP BY 1, 2
HAVING abs(net_funding) > 1000
ORDER BY net_funding;Funding also gives you open interest by wallet over time without replaying every
fill. szi is the position at the moment of each funding charge, sampled at
every funding interval.
l2_order_book_snapshots, 37M rows, 14 columns
Order-book depth over time, stored as parallel arrays instead of one row per
level. Six arrays: bid_px, bid_sz, bid_n, and the three ask_*
equivalents. The _n arrays hold the order count resting at each level.
{
"exchange_time": "2026-05-01 00:00:15.851+00:00",
"coin": "BTC",
"bid_px": ["76331.0", "76330.0", "76329.0", "76328.0", "…"],
"bid_sz": ["…"],
"bid_n": ["…"],
"ver_num": 1
}Arrays stay arrays. These are native Parquet lists, not JSON strings, so every engine reads them without parsing. Index 0 is top of book on both sides, so spread and depth stay simple:
-- Top-of-book spread, sampled per minute.
SELECT date_trunc('minute', exchange_time) AS minute,
coin,
avg(ask_px[1] - bid_px[1]) AS spread,
avg(bid_sz[1] + ask_sz[1]) AS top_depth
FROM read_parquet('hyperliquid/historical/l2_order_book_snapshots/schema=v1.0/date=*/part-*.parquet')
WHERE coin = 'BTC'
GROUP BY 1, 2
ORDER BY 1;DuckDB list indexing starts at 1. Depth N levels deep is a list_slice plus a
sum. You never explode the rows unless you want to.
Two operational columns need care. receipt_time runs at nanosecond precision
and records when we received the snapshot. exchange_time comes from the
exchange clock. Use exchange_time for market analysis. Use the delta between
the two for capture latency. source_day, source_hour, and ingested_at
describe provenance, not the market.
This table also holds the narrowest window, May through June 2026. Check the window first if your study depends on book state.
ledger_updates, 18.8M rows, 30 columns
Typed balance, transfer, fee, vault, and operation deltas. The widest table in
the dataset, and the one which confuses people first, because the table holds a
union of event types. delta_type tells you which columns carry meaning.
In the 100-row sample, delta_type is accountClassTransfer on 78 rows and
accountActivationGas on 22. Most string columns sit empty for those types:
user, destination, source_dex, destination_dex,
fee_token, vault, operation, dex → empty on all 100 rows
token → empty on 78 of 100
hash, delta_type, raw_delta → always populatedNothing is missing. The schema is wide and population depends on delta_type. A
vault event fills vault. A transfer fills destination. A spot trade fills
token. Filter by delta_type first, then select only the columns the type
uses:
SELECT delta_type,
count(*) AS events,
count(DISTINCT user) AS users,
sum(usdc_value)::DECIMAL(38,2) AS usdc_value
FROM read_parquet('hyperliquid/historical/ledger_updates/schema=v1.0/date=*/part-*.parquet')
GROUP BY 1
ORDER BY events DESC;Run this first on any new coverage window. You get the map of what exists before you write anything specific.
raw_delta is your escape hatch. The column holds the original source ledger
delta and stays populated on every row. When a typed column reads empty and you
expect a value, check raw_delta. A new delta type never loses information
because we have yet to type a column for the field.
validator_rewards, 12.6M rows, 6 columns
Reward events by block and validator. Small, simple, and the base for staking yield work.
{
"block_time": "2025-09-27 09:28:44.771+00:00",
"validator": "0x000000000056f99d36b6f2e0c51fd41496bbacb8",
"reward": "0.255886760000000000"
}Rewards repeat at a steady cadence per validator. Daily totals per validator are a one-line aggregate. Validator share of rewards over time is a window function.
delegations, 162.9K rows, 8 columns
Delegation and undelegation events. is_undelegate carries the direction. The
sample splits 68 delegate and 32 undelegate.
{
"block_time": "2025-09-27 23:15:44.571+00:00",
"user": "0x005844b2ffb2e122cf4244be7dbcb4f84924907c",
"validator": "0xa82fe73bbd768bc15d1ef2f6142a21ff8bd762ad",
"amount": "826.000000000000000000",
"is_undelegate": 1
}Net stake per validator is a running sum with the sign flipped on undelegates:
SELECT validator,
sum(CASE WHEN is_undelegate = 1 THEN -amount ELSE amount END)
::DECIMAL(38,6) AS net_delegated
FROM read_parquet('hyperliquid/historical/delegations/schema=v1.0/date=*/part-*.parquet')
GROUP BY 1
ORDER BY net_delegated DESC;deposits and withdrawals, 90.7K and 89.6K rows
Bridge flow in and out. Deposits carry block_number, block_time,
event_time, hash, user, and amount. Withdrawals add is_finalized. In
the sample, 71 of 100 rows are finalized and 29 are not.
{
"block_time": "2025-09-27 23:16:05.716+00:00",
"user": "0x005844b2ffb2e122cf4244be7dbcb4f84924907c",
"amount": "826.000000000000000000",
"is_finalized": 0
}A withdrawal row is a request. Sum withdrawals without filtering
is_finalized = 1 and you measure intent, not settled outflow. Both questions
are valid. Choose the one you meant to ask.
Join these two tables
Compare the withdrawal above with the delegation row shown earlier. Same wallet. Same
826.000000000000000000. 21 seconds apart. One undelegate, one withdrawal. A
user unstaked and pulled funds out, visible across two tables.
The pattern repeats. Matching on (user, amount) across only the two 100-row
samples, 12 withdrawals line up with an undelegation. Those samples cover tables
of roughly 90K and 163K rows.
-- Unstake then withdraw. How fast does stake leave after undelegating?
SELECT w.user,
d.validator,
d.amount,
d.block_time AS undelegated_at,
w.block_time AS withdrawn_at,
date_diff('second', d.block_time, w.block_time) AS seconds_between
FROM read_parquet('hyperliquid/historical/withdrawals/schema=v1.0/date=*/part-*.parquet') w
JOIN read_parquet('hyperliquid/historical/delegations/schema=v1.0/date=*/part-*.parquet') d
ON d.user = w.user
AND d.amount = w.amount
AND d.is_undelegate = 1
AND d.block_time <= w.block_time
WHERE w.is_finalized = 1
ORDER BY seconds_between;gossip_auctions, 115.2K rows, 7 columns
Gossip auction slots, prior winners, and ending gas values. The most specialised table in the set, and the one with the sharpest trap.
{
"block_time": "2026-04-13 08:48:00.050+00:00",
"slot_id": 0,
"previous_winner": "null",
"end_gas": "0E-18"
}previous_winner holds the four-character string "null", not SQL NULL. In
the sample, 99 of 100 rows hold the string "null" and one holds a JSON object,
{"Ip": "35.75.72.69"}. So:
-- Wrong. Matches nothing, because the column is never SQL NULL.
WHERE previous_winner IS NOT NULL
-- Right.
WHERE previous_winner <> 'null'slot_id runs 0 through 4 in the sample. Slots are a small fixed set, so
grouping by slot_id costs little.
Checklist before you trust a number
Every point below is a property of this data, not general advice.
- Filter
is_taker = 1when you measure market volume inswaps. Otherwise you double count every fill. - Never cast
Decimal(38,18)to a float before aggregating. Sum in exact arithmetic and scale in the final projection. - Group fees by
fee_token. BothUSDCandHFUNappear. - Filter
ledger_updatesbydelta_typefirst. Empty columns are the schema working as designed.raw_deltaholds the ground truth. - Use
<> 'null'instead ofIS NOT NULLongossip_auctions.previous_winner. - Decide whether withdrawals means requested or settled, then filter
is_finalizedto match. - Check per-table coverage before you design a study across tables. The order-book window is far narrower than the fill window.
- Use
exchange_timefor market analysis. Usereceipt_timefor latency.
Getting the data
Every table above publishes a free 100-row sample in the customer schema. You validate types and shapes before you buy. Those samples produced every row in this post.
Pricing runs by coverage window, not by table. One purchase covers all nine tables for the window you choose, delivered as partitioned, versioned Parquet with per-file checksums. The files are yours to keep and query with the engine you already run.
Datasets in this post
Dataset · HyperliquidHyperliquid Historical9 tables · schema v1.0 · from $200Data Platform
datastore.sh engineering
The team that operates capture, decoding, and reconciliation for every dataset in the catalog.