# l2BookDiff

{% hint style="info" %}
New endpoint - this endpoint is not a part of original Hyperliquid API and is added by us for builder convenience.
{% endhint %}

Stream incremental L2 orderbook changes as they happen. Each message contains all changed price levels across all subscribed coins for a single block, making this far more bandwidth-efficient than full snapshots.

For full snapshots, use [`l2Book`](/readme/websocket/l2book.md). For just top-of-book, use [`bbo`](/readme/websocket/bbo.md).

### Subscribe

```json
{
    "method": "subscribe",
    "subscription": {
        "type": "l2BookDiff",
        "coins": ["ETH", "BTC"]
    }
}
```

**Parameters:**

| Parameter     | Type      | Required | Description                                                                                                                           |
| ------------- | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `coins`       | string\[] | No       | Coins to subscribe to. Omit for all markets (requires `ws:l2BookDiffAll` permission).                                                 |
| `marketTypes` | string\[] | No       | All-markets only. Filters delivery by market type — see [All-markets filter](#all-markets-filter). Rejected if combined with `coins`. |

### All-markets filter

When `coins` is omitted, the optional `marketTypes` field restricts the firehose to specific market types. Each entry is `"perp"`, `"spot"`, `"outcome"`, or the wildcard `"*"` (alone) for "every type the server currently tracks":

```json
{
    "method": "subscribe",
    "subscription": {
        "type": "l2BookDiff",
        "marketTypes": ["perp", "outcome"]
    }
}
```

Omitting `marketTypes` defaults to `["perp"]` — outcome and spot markets do **not** appear unless you opt in. The default never grows; new market types must be added to your `marketTypes` array explicitly. Pass `["*"]` to auto-opt-in to future types.

A second subscribe with a different `marketTypes` value **replaces** the previous filter rather than coexisting with it.

### Unsubscribe

```json
{
    "method": "unsubscribe",
    "subscription": {
        "type": "l2BookDiff",
        "coins": ["ETH", "BTC"]
    }
}
```

### Update data format

Each message contains all changed levels for all subscribed coins in a single block. One message per block (\~14 blocks/sec). Levels with `sz: "0"` indicate that price level has been removed from the book.

```json
{
    "type": "l2BookDiff",
    "channel": "l2BookDiff",
    "seq": 42,
    "cursor": "782007304:1704067200000",
    "data": {
        "height": 782007304,
        "time": 1704067200000,
        "diffs": [
            {
                "coin": "ETH",
                "epoch": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                "seq": 108,
                "prev_seq": 107,
                "levels": [
                    [
                        {"px": "3245.5", "sz": "12.4", "n": 3},
                        {"px": "3244.0", "sz": "0", "n": 0}
                    ],
                    [
                        {"px": "3246.0", "sz": "8.1", "n": 2}
                    ]
                ]
            },
            {
                "coin": "BTC",
                "epoch": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                "seq": 55,
                "prev_seq": 54,
                "levels": [
                    [{"px": "68605", "sz": "96.9", "n": 5}],
                    []
                ]
            }
        ]
    }
}
```

### Resync signal

When a block height gap is detected (e.g., after a service restart or connection loss), the server sends a resync message. Clients must discard their local book and re-bootstrap from a REST snapshot.

```json
{
    "type": "l2BookDiff",
    "channel": "l2BookDiff",
    "data": {
        "type": "resync",
        "coin": "ETH",
        "reason": "height_gap",
        "new_epoch": "f9e8d7c6-b5a4-3210-fedc-ba9876543210"
    }
}
```

### Field reference

**Envelope fields** (on every message):

| Field    | Type   | Description                                                                        |
| -------- | ------ | ---------------------------------------------------------------------------------- |
| `seq`    | number | Per-subscription sequence number (continuous across all coins, for session replay) |
| `cursor` | string | Cursor for session reconnection replay (format: `height:timestamp`)                |

**Batch data fields** (`data`):

| Field    | Type   | Description                                                     |
| -------- | ------ | --------------------------------------------------------------- |
| `height` | number | Block height                                                    |
| `time`   | number | Block timestamp (milliseconds since epoch)                      |
| `diffs`  | array  | Array of per-coin diffs (only coins with changes in this block) |

**Per-coin diff fields** (each item in `diffs`):

| Field           | Type   | Description                                                            |
| --------------- | ------ | ---------------------------------------------------------------------- |
| `coin`          | string | Market symbol (e.g., "ETH", "BTC")                                     |
| `epoch`         | string | Server epoch (UUID) — changes on service restart                       |
| `seq`           | number | Per-coin sequence number — increments by 1 for each diff for this coin |
| `prev_seq`      | number | Previous per-coin sequence number (for gap detection)                  |
| `levels`        | array  | Tuple of `[bids, asks]` — only changed levels are included             |
| `levels[][].px` | string | Price as decimal string                                                |
| `levels[][].sz` | string | Size as decimal string (`"0"` means level removed)                     |
| `levels[][].n`  | number | Number of orders at this level (`0` when level removed)                |

**Resync data fields** (`data` when `type` is `"resync"`):

| Field       | Type   | Description                                |
| ----------- | ------ | ------------------------------------------ |
| `type`      | string | `"resync"`                                 |
| `coin`      | string | Market symbol                              |
| `reason`    | string | Why resync occurred (e.g., `"height_gap"`) |
| `new_epoch` | string | The new server epoch after restart         |

### Building a local orderbook

1. **Subscribe** to `l2BookDiff` for your coin(s) — start buffering messages.
2. **Fetch snapshot** via the REST [`l2BookDiffSnapshot`](/readme/rest-api/market-data/l2bookdiffsnapshot.md) endpoint. Note the `epoch` and `seq` for each coin.
3. **Discard stale diffs**: For each coin in each buffered batch, drop diffs where `epoch` doesn't match or `seq <= snapshot.seq`.
4. **Apply remaining diffs** in order. The first diff for each coin should have `prev_seq == snapshot.seq`.
   * For each level in `bids` and `asks`:
     * If `sz` is `"0"`, remove that price level.
     * Otherwise, upsert the price level with the new `sz` and `n`.
5. **Ongoing gap detection**: Per coin, check `prev_seq == your_current_seq` on each diff. If there's a gap, re-bootstrap.
6. **Resync**: If you receive a resync message, discard local state and re-bootstrap from step 1.

### Examples

{% tabs %}
{% tab title="Python (SDK)" %}

```python
import asyncio
from hydromancer_sdk import L2BookClient

def on_update(client, height):
    for coin in client.get_coins():
        book = client.get_book(coin)
        bid = book.best_bid()
        ask = book.best_ask()
        print(f"{coin} @ {height}: {bid.px if bid else 'n/a'} / {ask.px if ask else 'n/a'}")

async def main():
    client = L2BookClient(coins=["ETH", "BTC"], on_update=on_update)
    await client.run()

asyncio.run(main())
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
const WebSocket = require('ws');

const ws = new WebSocket(`wss://api.hydromancer.xyz/ws?token=${process.env.HYDROMANCER_API_KEY}`);

// Per-coin state: { epoch, seq }
const coinState = {};

ws.on('message', (raw) => {
    const msg = JSON.parse(raw);

    if (msg.type === 'connected') {
        ws.send(JSON.stringify({
            method: 'subscribe',
            subscription: { type: 'l2BookDiff', coins: ['ETH', 'BTC'] }
        }));
    } else if (msg.type === 'ping') {
        ws.send(JSON.stringify({ method: 'pong' }));
    } else if (msg.type === 'l2BookDiff') {
        const batch = msg.data;

        // Handle resync
        if (batch.type === 'resync') {
            console.log(`Resync for ${batch.coin}: ${batch.reason}`);
            delete coinState[batch.coin];
            return;
        }

        for (const diff of batch.diffs) {
            // Check for gaps
            const state = coinState[diff.coin];
            if (state && (diff.epoch !== state.epoch || diff.prev_seq !== state.seq)) {
                console.log(`Gap for ${diff.coin}, re-bootstrapping`);
                delete coinState[diff.coin];
                continue;
            }

            coinState[diff.coin] = { epoch: diff.epoch, seq: diff.seq };

            const [bidChanges, askChanges] = diff.levels;
            console.log(`${diff.coin} @ ${batch.height}: ${bidChanges.length} bid, ${askChanges.length} ask changes`);
        }
    }
});
```

{% endtab %}

{% tab title="Python (raw)" %}

```python
import websocket
import json
import os

coin_state = {}

def on_message(ws, message):
    msg = json.loads(message)

    if msg['type'] == 'connected':
        ws.send(json.dumps({
            "method": "subscribe",
            "subscription": { "type": "l2BookDiff", "coins": ["ETH", "BTC"] }
        }))
    elif msg['type'] == 'ping':
        ws.send(json.dumps({'type': 'pong'}))
    elif msg['type'] == 'l2BookDiff':
        batch = msg['data']

        if batch.get('type') == 'resync':
            print(f"Resync for {batch['coin']}: {batch['reason']}")
            coin_state.pop(batch['coin'], None)
            return

        for diff in batch['diffs']:
            coin = diff['coin']
            state = coin_state.get(coin)
            if state and (diff['epoch'] != state['epoch'] or diff['prev_seq'] != state['seq']):
                print(f"Gap for {coin}, re-bootstrapping")
                del coin_state[coin]
                continue

            coin_state[coin] = {'epoch': diff['epoch'], 'seq': diff['seq']}
            bid_changes, ask_changes = diff['levels']
            print(f"{coin} @ {batch['height']}: {len(bid_changes)} bid, {len(ask_changes)} ask changes")

ws = websocket.WebSocketApp(
    f"wss://api.hydromancer.xyz/ws?token={os.environ.get('HYDROMANCER_API_KEY')}",
    on_message=on_message
)
ws.run_forever()
```

{% endtab %}
{% endtabs %}

### Common errors

1. `Too many coins` - Reduce number of coins or upgrade tier
2. `Subscribing to all markets requires permission` - Needs `ws:l2BookDiffAll` add-on
3. `Rate limit exceeded` - Reduce subscription frequency
4. `Authentication failed` - Check API key


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.hydromancer.xyz/readme/websocket/l2bookdiff.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
