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

# Order book (L4)

> The l4Book websocket feed — Hyperliquid's full L4 order book for one coin, every resting order with its wallet plus untriggered stop and take-profit orders, as one snapshot followed by per-block diffs. Pro and above.

`l4Book` streams the **full L4 order book of one coin** — every resting order with the wallet address behind it, plus the trigger orders (stops and take-profits) resting but not yet triggered — as one snapshot followed by a per-block stream of the diffs that apply to it. The server holds no book: the snapshot and every event are the node's own bytes, split per coin and forwarded. **The book lives in your process.**

```json theme={null}
→ {"method":"subscribe","subscription":{"type":"l4Book","coin":"BTC"}}

← {"channel":"subscriptionResponse","data":{"method":"subscribe","subscription":{"coin":"BTC","type":"l4Book"},"id":"7d1e4c9a2f5b8306"}}
← {"channel":"l4Book","coin":"BTC","snapshot":true,"height":100,"time_ms":1757815848097,"seq":100,"data":{"book_orders":[[…],[…]],"untriggered_orders":[…]}}
← {"channel":"l4Book","coin":"BTC","seq":101,"height":101,"prev":100,"time_ms":1757815848173,"partial":false,"data":{"raw_book_diffs":[…],"order_statuses":[…]}}
```

## Who gets it

`l4Book` is included on **Pro and above**. Below that, a subscribe answers `feed not in tier` on the `error` channel. The [`connected` frame](/websocket/overview#the-first-frame) lists `l4Book` in `limits.feeds` when your account may subscribe — read that rather than assuming from the plan table, since enterprise limits are resolved per account.

```json theme={null}
← {"channel":"connected","data":{"tier":"pro","limits":{"connections":5,"subscriptions":100,"feeds":["trades","tradesByCoin","tradesByUser","tradesByUsers","liquidations","liquidationsByUser","liquidationsByUsers","leverageUpdates","leverageUpdatesByUser","leverageUpdatesByUsers","candle","allCandles","l4Book","twapSliceFills","twapSliceFillsByUser","twapSliceFillsByUsers","twapOrders","twapOrdersByUser","twapOrdersByUsers","twapStatuses","twapStatusesByUser"]}}}
```

## Subscribing

One argument, `coin`, and it is **case-sensitive** — one of only [two argument values on this socket that are](/websocket/feeds#subscribing), the other being a candle's `interval`. It must be the market exactly as the node spells it, builder-dex prefix and case included, because that is the only name the node's book export answers to:

```json theme={null}
{"method":"subscribe","subscription":{"type":"l4Book","coin":"BTC"}}
{"method":"subscribe","subscription":{"type":"l4Book","coin":"xyz:GOLD"}}
{"method":"subscribe","subscription":{"type":"l4Book","coin":"#28200"}}
```

`btc` is not normalised to `BTC`: it names no market, and the subscription is [dropped](#when-a-book-subscription-is-dropped) with `unknown coin`. Take the spelling from the node's own market list. Every `l4Book` message echoes the coin **as you sent it**, and the subscription `id` hashes it as sent.

<Warning>
  **Each subscription starts with the whole book as one message.** BTC's snapshot is about **30 MB**; HYPE's and ETH's are about 15 and 11 MB; the median coin is about 5 KB. Set your library's read limit accordingly before you subscribe — there is no depth option, and the [generated clients](/websocket/quickstart#order-book) raise it for you.
</Warning>

`l4Book` subscriptions count against the tier's per-connection [subscription cap](/websocket/limits) like any other; there is no separate book-only limit. Going over answers `too many subscriptions`. Unsubscribing frees the slot immediately, and a book subscription that ends in an error frees it too.

## The snapshot

Exactly one per subscription, first, sent only to the subscriber. It is fetched when you subscribe, after the subscribe ack.

```json theme={null}
{"channel":"l4Book","coin":"BTC","snapshot":true,"height":H,"time_ms":T,"seq":H,"data":{"book_orders":[bids,asks],"untriggered_orders":[…]}}
```

| Field      | Meaning                                                                                               |
| ---------- | ----------------------------------------------------------------------------------------------------- |
| `channel`  | `l4Book`.                                                                                             |
| `coin`     | The coin, spelled as you sent it.                                                                     |
| `snapshot` | `true`. Only the snapshot carries this key.                                                           |
| `height`   | The block the snapshot reflects, **inclusive**: the book is the state after block `H`.                |
| `time_ms`  | That block's time, milliseconds since the Unix epoch.                                                 |
| `seq`      | Equal to `height` — see [seq versus prev](#seq-versus-prev).                                          |
| `data`     | The node's `l4Snapshots` export for the coin, compacted (whitespace removed) and otherwise untouched. |

Inside `data`:

* `book_orders[0]` is every **bid**, `book_orders[1]` every **ask**.
* `untriggered_orders` is the resting **trigger orders** — stops and take-profits waiting on their trigger price.
* Every entry in all three is a positional **`[wallet, {order}]` pair**. Wallet addresses are always included.
* The order object is passed through as the node writes it — nothing is added, renamed or reordered. Read its fields from the node's export rather than from a mapping of ours; there is none.

The snapshot is the node's most recent export, which is normally some blocks — up to about a minute's worth — **behind the live stream**. The server closes that distance before it sends you anything: it buffers live updates until they span a run of blocks wide enough to join a snapshot onto (**105 source blocks**, roughly 7.6 s), then fetches the snapshot and discards the buffered updates at or before it. What you read is **one unbroken chain from `H+1`** with nothing lost and nothing repeated.

**This is why the first message takes a while.** One interval is about 10 seconds of chain, and attempt *N* waits for *N* intervals — so a cold subscription commonly waits **10–60 seconds** before the snapshot lands, and up to about **100 seconds** if all ten attempts are needed. That is expected, not a stall.

You pay it only on a **cold** coin. The buffer belongs to the feed group, not to you: if anyone else is already subscribed to that coin — or was within the last 60 seconds — the group is already buffered and your subscription passes straight through to the snapshot.

Two different errors end the wait, and they mean different things:

* `snapshot buffer did not fill` — one attempt waited 3 minutes and the source never produced enough blocks. The feed has stalled.
* `snapshot is older than retained history` — all ten attempts ran and the snapshot still predated the buffer. The node's export is further behind than the server is willing to bridge.

You never receive a second snapshot for a subscription: a snapshot is always the first message, and only the first.

## Updates

Then one message per block, on the same channel, **whether or not the block touched this coin**.

```json theme={null}
{"channel":"l4Book","coin":"BTC","seq":N,"height":H+k,"prev":H+k-1,"time_ms":T,"partial":false,"data":{"raw_book_diffs":[…],"order_statuses":[…]}}
```

| Field                 | Meaning                                                                                                                                                                                                                                                                          |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `seq`                 | The source block height — the same value as `height` on this feed.                                                                                                                                                                                                               |
| `height`              | The block this update is for — the node's `block_number`.                                                                                                                                                                                                                        |
| `prev`                | The height of the block the node produced this one after — the node's `prev_block_number`, as the gateway sequenced them. **Not** the last message you happened to receive, so a block missing anywhere between the node and you is visible in `prev` without counting messages. |
| `time_ms`             | The block's time.                                                                                                                                                                                                                                                                |
| `partial`             | `true` marks a block for which the node delivered only one of the two event streams; the other array is `[]`.                                                                                                                                                                    |
| `data.raw_book_diffs` | This coin's diff events for the block, each `{"user","oid","coin","side","px","raw_book_diff":…}` where `raw_book_diff` is one of `{"new":{…}}`, `{"update":{…}}` or `"remove"`.                                                                                                 |
| `data.order_statuses` | This coin's status events for the block, each `{"time","user","hash","builder","status","order":{"coin",…}}`.                                                                                                                                                                    |

Both arrays are the node's event bytes exactly, in the node's order. A block with nothing for this coin carries **two empty arrays** — the height still advances, so silence is detectable.

A subscription therefore reads, in order (`data` elided) — `seq` and `height` agree on every line, snapshot included:

```json theme={null}
{"channel":"l4Book","coin":"BTC","snapshot":true,"height":100,"time_ms":1757815848097,"seq":100,"data":{…}}
{"channel":"l4Book","coin":"BTC","seq":101,"height":101,"prev":100,"time_ms":1757815848173,"partial":false,"data":{…}}
{"channel":"l4Book","coin":"BTC","seq":102,"height":102,"prev":101,"time_ms":1757815848249,"partial":false,"data":{"raw_book_diffs":[],"order_statuses":[]}}
{"channel":"l4Book","coin":"BTC","seq":103,"height":103,"prev":102,"time_ms":1757815848325,"partial":false,"data":{…}}
{"channel":"l4Book","coin":"BTC","seq":104,"height":104,"prev":103,"time_ms":1757815848401,"partial":false,"data":{…}}
```

## seq versus prev

They answer different questions.

* **`seq`** is the source block height every feed carries, and your [reconnect cursor](/websocket/protocol#reconnecting). On this feed it equals `height`, so it says the same thing `height` does.
* **`prev`** is the chain of blocks: it names the **exact predecessor**, so a `prev` you have not applied means a block is missing anywhere between the node and you, whatever the cause — a dropped frame, a server-side reconnect, a frame the node could not split.

**Gate on `prev`.** `seq` tells you where you are; `prev` tells you whether you got there without skipping anything, which is the question a book has to answer. Keep the last `seq` you applied anyway — it is what you pass back to resume after a disconnect.

## The client rule

From the wire contract, verbatim:

> Apply an update only if its `prev` equals the last applied height; on any mismatch, or `partial:true` when attribution matters, reconnect with the last successfully applied `seq`. If that cursor is no longer retained, subscribe without it to rebuild from a fresh snapshot.

The first update after the snapshot has `prev == H`, whether it was replayed or live; you cannot tell the two apart by `prev`, and need not. A server-side reconnect that could not resume where it left off shows up the same way, as a `prev` you have not applied. The generated [order-book clients](/websocket/quickstart#order-book) implement the rule.

## When a book subscription is dropped

A book subscription that cannot be started, or cannot be kept, is **dropped** — and you are told on the ordinary [`error` channel](/websocket/protocol#errors-on-an-open-socket):

```json theme={null}
{"channel":"error","data":"unknown coin"}
```

<Warning>
  The frame does **not** name the coin. A connection holding several `l4Book` subscriptions cannot tell which one ended from the frame alone — re-check every book you hold, or keep one book per connection if attribution matters.
</Warning>

| `data`                                                                   | What it means                                                                                                           |
| ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
| `unknown coin`                                                           | The node has no such market, or you spelled it in the wrong case. **Do not retry as is.**                               |
| `gateway busy`, `gateway unavailable`, `snapshot timed out`, `cancelled` | Transient. Retry with backoff.                                                                                          |
| `book gateway not configured`                                            | The feed is off on this server.                                                                                         |
| `snapshot buffer did not fill`                                           | One attempt waited 3 minutes and the source never produced enough blocks to join a snapshot onto. The feed has stalled. |
| `snapshot is older than retained history`                                | All ten attempts ran and the node's export still predated the buffer.                                                   |
| `snapshot failed: <Code>`                                                | Any other gRPC status code from the gateway, named.                                                                     |
| `snapshot failed`                                                        | The gateway's bytes were not a book.                                                                                    |

The subscription is gone, it releases its slot under the per-connection cap, and the subscribe ack you already received no longer stands. Any snapshot already sent for it should be **discarded**. An `unsubscribe` while the snapshot is in flight is honoured silently.

The refusals that never start a subscription at all arrive on the same channel: `feed not in tier` below Pro, `missing argument coin`, and `too many subscriptions` when the connection is at its cap.

## What it is not

* **No history to seek into.** There is no arbitrary start height to ask for. The snapshot is the node's most recent export, and the only cursor you can resume from is one you were actually sent.
* **Resume is bounded, not unlimited.** Passing the last applied `seq` back on resubscribe replays what you missed with no snapshot — but only within the retained window (10,000 messages per feed, and 60 seconds of warmth once a group empties). Past that you get `sequence is not retained` and a fresh snapshot is the only way back; see [reconnecting](/websocket/protocol#reconnecting).
* **No book is held anywhere but in your process.** The server splits and forwards the node's bytes; applying diffs, and deciding when to throw the book away and start over, is your client's job.
