> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dune.com/llms.txt
> Use this file to discover all available pages before exploring further.

# hyperliquid.perp_orderbook_1m

> One-minute L2 order book snapshots for Hyperliquid perpetual markets, with resting bids and asks aggregated by price level, best price first.

export const PremiumDatasetAccessCard = ({href = "https://dune.com/enterprise#contact-form", note = null}) => <Card title="Gated dataset" icon="lock" href={href}>
    Querying this dataset requires an entitlement on your workspace. See <a href="/data-catalog/overview#access-tiers-public-vs-gated-datasets">access tiers</a>, or contact the Dune team to enable access.
    {note && <><br /><br />{note}</>}
  </Card>;

`hyperliquid.perp_orderbook_1m` is the order book table for the Hyperliquid perpetual futures venue. Grain: one row per `(coin, interval_start)`, carrying the whole resting book on both sides as arrays of price levels. Top-of-book, spread, depth and slippage curves therefore all come off a single row, with no join and no self-join across levels. It covers both first-party perps (`coin` = `BTC`) and HIP-3 builder-deployed markets (`coin` = `dex:SYMBOL`, e.g. `xyz:TSLA`); Hyperliquid spot and HIP-4 outcome markets are filtered out.

<PremiumDatasetAccessCard />

<Info>
  **Grain:** one row per (`coin`, `interval_start`) · **Depth:** up to 100 price levels per side · **History:** from 2026-06-29 — a much later floor than the fills tables · **Markets:** 497 — 232 first-party, 265 HIP-3 · **Refresh:** a view over the live feed, so no build lag · Figures as of 2026-08-24
</Info>

## Table schema

| Column              | Type                                            | Description                                                                                                                                                         |
| ------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `coin`              | `VARCHAR`                                       | Venue-native market id, `BTC` or `dex:SYMBOL`. Grain with `interval_start`, and the join key to `perp_market_details`                                               |
| `interval_start`    | `TIMESTAMP(3) WITH TIME ZONE`                   | The minute boundary the book state is as of, UTC. Grain, with `coin`. An instant, not a bucket summarising the minute after it                                      |
| `interval`          | `VARCHAR`                                       | Cadence label of the snapshot series. Constant `1m`                                                                                                                 |
| `block_time`        | `TIMESTAMP(3) WITH TIME ZONE`                   | Timestamp of the L1 block the book was actually read at. At or just before `interval_start`, never after                                                            |
| `height`            | `BIGINT`                                        | L1 block height the book was read at                                                                                                                                |
| `bids`              | `ARRAY(ROW(n INTEGER, px VARCHAR, sz VARCHAR))` | Resting bids, best price first — so descending `px`. `n` is the number of orders at that level, `sz` the total size. Empty array when nothing is resting on the bid |
| `asks`              | `ARRAY(ROW(n INTEGER, px VARCHAR, sz VARCHAR))` | Resting asks, best price first — so ascending `px`. Same shape as `bids`. Empty array when nothing is resting on the ask                                            |
| `staleness_seconds` | `BIGINT`                                        | Whole seconds from `block_time` to `interval_start`: how far back the read sits from the boundary it is labelled with. 0-59                                         |
| `derived_at`        | `TIMESTAMP(3) WITH TIME ZONE`                   | When the snapshot was produced. Not a per-row freshness clock — see below                                                                                           |
| `block_date`        | `DATE`                                          | UTC date of `interval_start`. Partition column                                                                                                                      |

## A row is a boundary instant, not a bucket

`interval_start` labels the moment the book is as of, and the book comes from the last L1 block at or before it — `block_time` is never later than `interval_start`. So a row describes the book *at* that minute boundary; it says nothing about what the book did during the minute that follows, and there is no open/high/low/close aggregation involved.

`staleness_seconds` measures exactly that offset: it equals the whole seconds from `block_time` to `interval_start` on every row. On 2026-08-20 it was 0 on 80.2% of rows, with a 95th percentile of 6 seconds, a 99th of 27, and a maximum of 59 — a fresh market is read essentially at the boundary, and a large value means the last block before the boundary was itself old, which for a quiet market is normal rather than a defect. Read it when you are aligning the book against another table's timestamps to a sub-minute tolerance; ignore it otherwise.

`derived_at` is not a per-row freshness clock. It tracks the minute closely while the feed runs in steady state, but backfilled history carries the timestamp of the batch that produced it, so comparing `derived_at` to `interval_start` measures ingestion history rather than data quality.

## Reading bids and asks

Both arrays are already sorted best-first, so `bids[1]` is the best bid and `asks[1]` the best ask — no `ORDER BY` inside the array, and no window function to pick the top level. `px` and `sz` are `VARCHAR`, carrying the venue's own decimal strings unrounded, so every arithmetic use needs an explicit cast:

```sql theme={null}
SELECT
  coin,
  interval_start,
  CAST(bids[1].px AS DOUBLE) AS best_bid,
  CAST(asks[1].px AS DOUBLE) AS best_ask,
  (CAST(asks[1].px AS DOUBLE) - CAST(bids[1].px AS DOUBLE))
    / ((CAST(asks[1].px AS DOUBLE) + CAST(bids[1].px AS DOUBLE)) / 2) * 10000 AS spread_bps
FROM hyperliquid.perp_orderbook_1m
WHERE block_date = DATE '2026-08-20'
  AND coin = 'BTC'
  AND CARDINALITY(bids) > 0
  AND CARDINALITY(asks) > 0
ORDER BY interval_start
```

Guard the `CARDINALITY` on both sides before indexing: a one-sided book produces an empty array rather than a null row, and `bids[1]` on an empty array raises rather than returning null. It is rare but real — 177 of the 454,797 rows on 2026-08-20 had no resting bid.

For anything summed across levels, unnest instead of indexing. Depth within a distance of the mid, per minute:

```sql theme={null}
SELECT
  interval_start,
  SUM(CAST(b.px AS DOUBLE) * CAST(b.sz AS DOUBLE)) AS bid_notional_within_25bps
FROM hyperliquid.perp_orderbook_1m AS o
CROSS JOIN UNNEST(o.bids) AS b (n, px, sz)
WHERE o.block_date = DATE '2026-08-20'
  AND o.coin = 'BTC'
  AND CAST(b.px AS DOUBLE) >= CAST(o.bids[1].px AS DOUBLE) * (1 - 0.0025)
GROUP BY 1
ORDER BY 1
```

## Absent minutes are not gaps

The feed is event-driven, not a dense grid: a coin-minute is missing exactly when the book was unchanged from the snapshot before it. So forward-filling the previous row is exact rather than an approximation, and a missing minute is never a lost snapshot.

Density splits sharply by how actively a market is quoted. On 2026-08-20, 263 of the 497 markets had all 1,440 minutes and the median market had a row for every minute, while at the 5th percentile a market had a single row for the whole day. Across all markets that day the table held 454,797 rows against 715,680 possible coin-minutes.

Two consequences for queries. First, never read row counts per market as an activity metric without saying what they measure — they count book *changes*, not time covered. Second, to align the book with a fixed grid, or with a timestamp from another table, take the last row at or before your cutoff rather than matching a minute exactly:

```sql theme={null}
SELECT
  coin,
  MAX_BY(bids[1].px, interval_start) AS best_bid,
  MAX_BY(asks[1].px, interval_start) AS best_ask,
  MAX(interval_start) AS book_at
FROM hyperliquid.perp_orderbook_1m
WHERE block_date BETWEEN DATE '2026-08-19' AND DATE '2026-08-20'
  AND interval_start < TIMESTAMP '2026-08-20 12:00' AT TIME ZONE 'UTC'
  AND CARDINALITY(bids) > 0
  AND CARDINALITY(asks) > 0
GROUP BY 1
```

The two-day window is deliberate: a market quoted rarely enough can have its last change on a previous day, so a one-day bound silently drops it.

## Depth is capped at 100 levels per side

Each side holds at most 100 price levels, uniform across first-party and HIP-3 markets. The cap binds routinely rather than exceptionally — on 2026-08-20 it bound on 54.8% of rows, and the median row carried 98 bid levels — so a truncated book looks exactly like a complete one from inside this table.

That matters whenever a query walks the book to a target size or distance. Depth measured near the mid is unaffected; a slippage estimate for a clip large enough to sweep past the hundredth level is bounded by the cap, not by the market. Check whether you reached the end of the data before concluding you reached the end of the book:

```sql theme={null}
SELECT
  interval_start,
  CARDINALITY(bids) AS bid_levels,
  CARDINALITY(bids) = 100 AS bids_possibly_truncated
FROM hyperliquid.perp_orderbook_1m
WHERE block_date = DATE '2026-08-20'
  AND coin = 'BTC'
ORDER BY interval_start
```

The upstream diff stream this table is built from carries up to 200 levels, so a question that genuinely needs the book past 100 levels has to go there instead.

## coin is the only market identifier here

Unlike the other venue-wide tables, this one carries neither `market_symbol` nor `perp_dex`: `coin` is the raw venue string, readable on its own for both perp namespaces. Join [`perp_market_details`](/data-catalog/curated/perpetuals/hyperliquid/perp-market-details) on `coin` for the dex, asset class, size precision and leverage caps.

Use a `LEFT JOIN`. The registry behind that dimension is a poller snapshot that trails a new listing, so a freshly listed market can appear in the book before it appears there — all 497 markets resolved as of 2026-08-20, but an inner join is not guaranteed to be lossless on the current day.

## Bound block\_date on every query

`block_date` is the only partition column, and this table is a view, so there is no `block_month` to fall back on — an unbounded query reads all 24.2M rows and every price level in them. Nothing prunes within a partition either: filtering `coin` still reads the full day.

## Not in this table

Traded prices and sizes are in [`perp_trades`](/data-catalog/curated/perpetuals/hyperliquid/perp-trades); this table is resting liquidity, which is why a level can sit in the book for hours without a fill. Oracle, mark and mid prices, plus Hyperliquid's own impact-notional quotes, are in [`perp_oracle_prices`](/data-catalog/curated/perpetuals/hyperliquid/perp-oracle-prices) — prefer `mid_price` there over deriving a mid here when a first-party market is all you need, since it needs no cast and no cardinality guard. Order-level detail — individual resting orders, their owners, placements and cancellations — is not in this dataset at any grain; the levels here are aggregated, with `n` as the only surviving trace of how many orders composed them.
