# l2Diff: the incremental book

> The Hyperliquid l2Diff channel on Supanode: the same order book as l2Book, sent as one snapshot and then only the levels that changed. About 113x less traffic at 1000 levels, rebuilt book identical level for level.

`l2Diff` carries the same order book as `l2Book`, with the same parameters, built by the same flush. The difference is delivery: one snapshot when you subscribe, and after that only the levels that changed. Subscribe to it instead of `l2Book` whenever you want depth. At 1000 levels it is roughly a hundred times less traffic, and the book you rebuild from it is identical, level for level.

## Why it exists

A 1000-level BTC book is about 72 KB of JSON, and it is rebuilt about 14 times a second, one flush per block. Resending it whole is about 1 MB/s (8 Mbit/s) for a single coin on a single subscriber.

Measured over 120 s on one connection carrying both channels, BTC at `nLevels: 1000`:

| | Frames | Mean frame | Total |
|---|---|---|---|
| `l2Book` | 1,674 | 71,711 B | 120.04 MB |
| `l2Diff` | 1 snapshot + 1,674 updates | 592 B per update | 1.06 MB |

**113× less over the run, 121× less per update.** A second server on a second node, same run: 104× and 111×. The book rebuilt from those 1,674 updates matched all 1,674 `l2Book` frames.

## Subscribe

```json
{ "method": "subscribe", "subscription": { "type": "l2Diff", "coin": "BTC", "nLevels": 1000 } }
```

The parameters are exactly those of `l2Book`:

| Parameter | Values | Meaning |
|---|---|---|
| `coin` | e.g. `"BTC"`, `"@107"` | required |
| `nLevels` | 1-1000, omit for 20 | levels per side |
| `nSigFigs` | 2-5 | price bucketing, as on the public Hyperliquid API |
| `mantissa` | 2 or 5, requires `nSigFigs: 5` | finer bucketing |

<Warning>
**The validator is strict about two things.** Passing `nLevels: 20` explicitly is rejected: omit the field to get the default. And `mantissa` without `nSigFigs: 5` is rejected too.
</Warning>

Unsubscribe with the same subscription object and `"method": "unsubscribe"`.

## The two frames

`data` holds exactly one of two keys, `Snapshot` or `Updates`.

**Snapshot** is the whole book at your depth. It is sent when you subscribe, and again whenever the server cannot produce a correct diff for you (see [Continuity](#continuity)):

```json
{"channel":"l2Diff","data":{"Snapshot":{
  "coin":"BTC","time":1789558634545,"height":1149553971,"nLevels":1000,
  "levels":[
    [{"px":"75899.0","sz":"1.85926","n":11}, {"px":"75898.0","sz":"0.4","n":2}],
    [{"px":"75900.0","sz":"5.68367","n":38}, {"px":"75901.0","sz":"0.9","n":3}]
  ]}}}
```

`levels` is `[bids, asks]`, bids descending and asks ascending, exactly as in `l2Book`. `px` and `sz` are decimal strings. `n` is the number of resting orders at that level.

**Updates** carry only what moved since the previous frame on this subscription:

```json
{"channel":"l2Diff","data":{"Updates":{
  "coin":"BTC","time":1789558634744,"height":1149553974,"prevHeight":1149553971,
  "nLevels":1000,
  "bids":{"upd":[["75899.0","2.10012",13]],"del":["75898.0"]},
  "asks":{"upd":[["75900.0","5.1",35],["75902.0","0.25",1]],"del":[]}
}}}
```

Each side carries two lists:

- **`upd`** holds levels to insert or overwrite, addressed by price. Each one is a positional triple `[px, sz, n]`, not an object: at 1000 levels the field names would be about 60% of the frame. `px` and `sz` are strings, `n` is a number.
- **`del`** holds prices that are no longer in the book **at your depth**.

<Warning>
**A level has two shapes.** In a snapshot it is an object, `{"px":"75899.0","sz":"1.85926","n":11}`. In an update it is an array, `["75899.0","2.10012",13]`. In `del` it is a bare price string. Write one parser per shape.
</Warning>

`nLevels` is always echoed back. `nSigFigs` and `mantissa` are echoed on both frames when you set them.

## Continuity

A coin is not dirty at every block, so `height` skips legitimately. `height - 1` proves nothing. **`prevHeight` is the check.**

<Note>
**If `prevHeight` does not equal the last `height` you applied, you missed a frame.** Your book is stale. Resubscribe (unsubscribe, then subscribe) to get a fresh `Snapshot`.
</Note>

The server also sends a `Snapshot` unasked in three cases:

- your first frame on the subscription;
- a slow consumer that fell behind the broadcast: the frames it missed are gone, so a diff would be wrong;
- a coin the server has no prior book for.

Treat any `Snapshot` as "replace everything you have for this subscription".

## Depth is part of the diff

Levels are truncated to **your** `nLevels` before the comparison. That has two consequences:

- a price in `del` may simply have been pushed past your depth by a better level, not cancelled or filled;
- two subscribers at different depths receive different diffs for the same book. That is correct behaviour.

## Silence is normal

A coin with nothing to report sends nothing at all. There are no heartbeat or keep-alive frames on this channel. On a quiet market you can go seconds without a frame: the book has not changed, the connection is fine. Send `{"method":"ping"}` (answered with `{"channel":"pong"}`) if you want to check liveness.

## Use `height`, not `time`

`time` is the **block time**, and a block is applied over several flushes. Two consecutive frames can carry the same `time` with different books. Use `height` and `prevHeight` as the frame identity and for ordering. `l2Book` has no `height` field, which is one more reason to prefer this channel. Flushing once per block is on the roadmap.

## Next steps

- [Examples](https://supanode.xyz/docs/hyperliquid/websocket/examples#deep-book-without-the-bandwidth-l2diff): a client that keeps the book in sync from `l2Diff`.
- [Streams](https://supanode.xyz/docs/hyperliquid/websocket/whats-available): every other channel on the connection.
- [Limits](https://supanode.xyz/docs/hyperliquid/websocket/limits): depth, connections and keep-alive.
