Skip to content
datastore.sh

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.

Data Platform17 min read

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

TableKindRowsColumnsCoverage
swapsmarket2.6B37Jul 2025 → Jul 2026
fundingevent1.6B9Sep 2025 → Jul 2026
l2_order_book_snapshotsmarket37.0M14May 2026 → Jun 2026
ledger_updatesevent18.8M30Sep 2025 → Jul 2026
validator_rewardsevent12.6M6Sep 2025 → Jul 2026
delegationsevent162.9K8Sep 2025 → Jul 2026
gossip_auctionsevent115.2K7Apr 2026 → Jul 2026
depositsevent90.7K6Sep 2025 → Jul 2026
withdrawalsevent89.6K7Sep 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 block
  • block_time, DateTime64(3, 'UTC'), millisecond precision, always UTC
  • event_time, DateTime64(3, 'UTC'), when the event occurred
  • hash, 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:

json
{
  "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.

text
tid 752057817590464  0x42fa34ca…  sell  taker  8.68
                     0xffffffff…  buy   maker  8.68
tid  49486923937202  0xcf0a36de…  sell  taker  11.52
                     0xffffffff…  buy   maker  11.52

The 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:

sql
-- 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.

json
{
  "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:

sql
-- 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.

json
{
  "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:

sql
-- 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:

text
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 populated

Nothing 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:

sql
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.

json
{
  "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.

json
{
  "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:

sql
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.

json
{
  "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.

sql
-- 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.

json
{
  "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:

sql
-- 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.

  1. Filter is_taker = 1 when you measure market volume in swaps. Otherwise you double count every fill.
  2. Never cast Decimal(38,18) to a float before aggregating. Sum in exact arithmetic and scale in the final projection.
  3. Group fees by fee_token. Both USDC and HFUN appear.
  4. Filter ledger_updates by delta_type first. Empty columns are the schema working as designed. raw_delta holds the ground truth.
  5. Use <> 'null' instead of IS NOT NULL on gossip_auctions.previous_winner.
  6. Decide whether withdrawals means requested or settled, then filter is_finalized to match.
  7. Check per-table coverage before you design a study across tables. The order-book window is far narrower than the fill window.
  8. Use exchange_time for market analysis. Use receipt_time for 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 $200

Data Platform

datastore.sh engineering

The team that operates capture, decoding, and reconciliation for every dataset in the catalog.