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

# l2BookDiff2 | Hyperliquid WebSocket API

> Hyperliquid l2BookDiff2: subscribe to a diff-only L2 order book stream with per-coin sequence numbers and a server epoch for gap detection, bootstrapped from the l2BookDiffSnapshot REST snapshot.

<CardGroup cols={1}>
  <Card title="Processing"> Realtime</Card>
</CardGroup>

<Info>
  * GoldRush-native. `l2BookDiff2` is not exposed on `wss://api.hyperliquid.xyz/ws`. Pointing a client at the public endpoint with this subscription type will fail.
  * **Diff-only — no inline snapshot.** Unlike [`l2BookDiff`](/docs/api-reference/hyperliquid-websocket/l2-book-diff), this stream never sends a `Snapshot` frame. Every message is an `Updates` batch. Seed your local book from the REST [`l2BookDiffSnapshot`](/docs/api-reference/hyperliquid-info/l2-book-diff-snapshot) info method, then apply diffs on top — see [Bootstrapping & gap detection](#bootstrapping--gap-detection) below.
  * **Sequenced for gap detection.** Each per-coin diff carries a monotonic `seq` and the `prev_seq` it follows; each batch carries a server `epoch` and a `cursor`. This lets a client detect a dropped message or a server reset and re-bootstrap deterministically.
  * `coin` is optional. Omit it to stream every asset on a single subscription; use `marketTypes` to choose which market families are included (defaults to `["perp"]`).
</Info>

## Endpoint

```
wss://hypercore.goldrushdata.com/ws?key=<GOLDRUSH_API_KEY>
```

<ParamField query="key" type="string" required>
  Your GoldRush API key. Passed as a query parameter at connection time - no `Authorization` header is used.
</ParamField>

## Subscribe

Send this JSON message after the connection is established:

<ParamField body="method" type="string" required>
  Always `"subscribe"`.
</ParamField>

<ParamField body="subscription" type="object" required>
  <Expandable title="properties">
    <ResponseField name="type" type="string" required>Always `"l2BookDiff2"`.</ResponseField>

    <ResponseField name="coin" type="string | string[]">
      Asset filter. Accepts three shapes:

      * **String** - a single asset symbol (e.g. `"HYPE"`, `"BTC"`, `"@107"` for spot pairs). For HIP-3 markets, include the deployer prefix.
      * **Array of strings** - a fixed list of asset symbols (e.g. `["HYPE", "BTC", "ETH"]`). Each coin is sequenced independently.
      * **Omitted** - wildcard. Streams diffs across **every perp asset** by default on one subscription (see `marketTypes` to include spot or outcome markets).
    </ResponseField>

    <ResponseField name="marketTypes" type="string[]">
      Optional. Selects which market families a wildcard subscription includes. **Only valid when `coin` is omitted.** **Defaults to `["perp"]`.**

      Accepted values:

      * `"perp"` *(default)* - vanilla perps and HIP-3 deployer-perps.
      * `"spot"` - `@<index>` spot markets and legacy spot pairs. Not included by default.
      * `"outcome"` - HIP-4 prediction-market outcomes. Not included by default.
      * `"*"` - every current type (`"perp"` + `"spot"` + `"outcome"`) and auto-opt-in to any future types.

      Mix and match in one subscription, e.g. `["perp", "outcome"]`. A second `subscribe` with a different `marketTypes` value **replaces** the previous filter.
    </ResponseField>
  </Expandable>
</ParamField>

### Example

| Subscribe with                                       | What you receive                                                    |
| ---------------------------------------------------- | ------------------------------------------------------------------- |
| `{"type":"l2BookDiff2","coin":"HYPE"}`               | Diffs for **HYPE only**                                             |
| `{"type":"l2BookDiff2","coin":["HYPE","BTC","ETH"]}` | Diffs for **a fixed list of coins**                                 |
| `{"type":"l2BookDiff2"}`                             | Diffs for **every perp coin** (default: `marketTypes: ["perp"]`)    |
| `{"type":"l2BookDiff2","marketTypes":["*"]}`         | Diffs for **every coin** (perp + spot + outcome, plus future types) |

<CodeGroup>
  ```bash wscat theme={null}
  wscat -c "wss://hypercore.goldrushdata.com/ws?key=$GOLDRUSH_API_KEY"

  > {"method":"subscribe","subscription":{"type":"l2BookDiff2","coin":"BTC"}}
  ```

  ```typescript TypeScript theme={null}
  import WebSocket from "ws";

  const ws = new WebSocket(
    `wss://hypercore.goldrushdata.com/ws?key=${process.env.GOLDRUSH_API_KEY}`,
  );

  ws.on("open", () => {
    ws.send(JSON.stringify({
      method: "subscribe",
      subscription: { type: "l2BookDiff2", coin: "BTC" },
    }));
  });

  ws.on("message", (raw) => {
    const msg = JSON.parse(raw.toString());
    if (msg.channel !== "l2BookDiff2") return;

    const { time, block_height, epoch, cursor, book_diffs } = msg.data.Updates;
    for (const d of book_diffs) {
      console.log(block_height, epoch, d.coin, "seq", d.prev_seq, "->", d.seq);
      // apply d.levels = [changedBids, changedAsks]; sz "0" removes the level
    }
  });
  ```

  ```python Python theme={null}
  import asyncio, json, os
  import websockets

  async def main():
      uri = f"wss://hypercore.goldrushdata.com/ws?key={os.environ['GOLDRUSH_API_KEY']}"
      async with websockets.connect(uri) as ws:
          await ws.send(json.dumps({
              "method": "subscribe",
              "subscription": {"type": "l2BookDiff2", "coin": "BTC"},
          }))
          async for raw in ws:
              msg = json.loads(raw)
              if msg.get("channel") != "l2BookDiff2":
                  continue
              upd = msg["data"]["Updates"]
              for d in upd["book_diffs"]:
                  print(upd["block_height"], upd["epoch"], d["coin"], "seq", d["prev_seq"], "->", d["seq"])

  asyncio.run(main())
  ```
</CodeGroup>

## Unsubscribe

Send the same `subscription` body with `method: "unsubscribe"`:

```json theme={null}
{
  "method": "unsubscribe",
  "subscription": { "type": "l2BookDiff2", "coin": "BTC" }
}
```

<Note>
  Unsubscribe matches subscriptions by **exact body**. A subscription created with `coin: ["BTC", "ETH"]` is a different subscription from one created with `coin: "BTC"`. To narrow a multi-coin set, unsubscribe the original `coin` array in full, then resubscribe with the smaller list.
</Note>

## Streamed messages

Every message has `channel: "l2BookDiff2"` and a `data` payload containing an `Updates` batch (this stream never emits a `Snapshot`). An `Updates` batch is emitted on each HyperCore block where the book for at least one subscribed coin changed.

```json theme={null}
{
  "channel": "l2BookDiff2",
  "data": {
    "Updates": {
      "time": 1786379817106,
      "block_height": 1105180205,
      "epoch": "466bf60f-bb7a-4591-89a3-a2660234fef5",
      "cursor": "1105180205:1786379817106",
      "book_diffs": [
        {
          "coin": "BTC",
          "seq": 4229978,
          "prev_seq": 4229977,
          "levels": [
            [
              { "px": "57686.0", "sz": "0.09903", "n": 25 }
            ],
            [
              { "px": "64164.0", "sz": "0", "n": 0 }
            ]
          ]
        }
      ]
    }
  }
}
```

Each entry in `levels[0]` (bids) and `levels[1]` (asks) replaces the current state at its `px`. A level with `sz: "0"` and `n: 0` means **the level at that price has been removed**.

## Bootstrapping & gap detection

`l2BookDiff2` gives you diffs only, so you build the book yourself by seeding from a snapshot and applying diffs in order:

1. **Subscribe** to `l2BookDiff2` for your coin(s) and start **buffering** the incoming `Updates`.
2. **Fetch a snapshot** for each coin from the REST [`l2BookDiffSnapshot`](/docs/api-reference/hyperliquid-info/l2-book-diff-snapshot) info method. Note its `height`, `epoch`, and per-coin `seq`.
3. **Discard already-applied diffs.** Drop any buffered batch whose `block_height` is **≤** the snapshot's `height` (the snapshot already includes those blocks). You may equivalently discard per coin by `seq` - drop any diff whose `seq` **≤** the snapshot's `seq` - for the tightest alignment.
4. **Verify the epoch.** Every remaining diff's `epoch` must equal the snapshot's `epoch`. A different `epoch` means the stream reset after your snapshot - re-fetch the snapshot and restart.
5. **Apply in order.** For each coin, the first diff you apply should have `prev_seq == snapshot.seq`. For each level, upsert `{px, sz, n}`, or remove the price when `sz` is `"0"`.
6. **Ongoing gap detection.** Per coin, each diff's `prev_seq` must equal the last `seq` you applied for that coin. On a mismatch (a dropped message) or an `epoch` change (a server reset), re-bootstrap that coin from step 2.

<Tip>
  The `seq`/`prev_seq` are **per coin**, tracked within the current `epoch`. Compare them per coin, not globally across the batch.
</Tip>

## Response fields

<ResponseField name="channel" type="string">
  Always `"l2BookDiff2"`.
</ResponseField>

<ResponseField name="data" type="object">
  Contains an `Updates` batch.

  <Expandable title="Updates (per-block diffs)">
    <ResponseField name="time" type="int">HyperCore block timestamp in milliseconds.</ResponseField>
    <ResponseField name="block_height" type="int">HyperCore block height these diffs were produced at. Align against the snapshot's `height`.</ResponseField>

    <ResponseField name="epoch" type="string">
      Server generation (UUID). Regenerated when the stream resets (e.g. a server restart); a new `epoch` means your snapshot and buffered diffs are stale, so re-bootstrap. Matches the `epoch` in the [`l2BookDiffSnapshot`](/docs/api-reference/hyperliquid-info/l2-book-diff-snapshot) response.
    </ResponseField>

    <ResponseField name="cursor" type="string">
      A per-batch position marker, formatted `"<block_height>:<time>"`. To recover from a disconnect, re-bootstrap from a fresh `l2BookDiffSnapshot` (see [Bootstrapping & gap detection](#bootstrapping--gap-detection)) — the stream does not replay from this value.
    </ResponseField>

    <ResponseField name="book_diffs" type="array<object>">
      Per-coin lists of changed price levels. One entry per coin that changed at this block.

      <Expandable title="book_diff fields">
        <ResponseField name="coin" type="string">Asset symbol the diff applies to.</ResponseField>
        <ResponseField name="seq" type="int">Per-coin monotonic sequence for this diff, within the current `epoch`.</ResponseField>
        <ResponseField name="prev_seq" type="int">The `seq` of the previous diff for this coin. Must equal the last `seq` you applied for the coin (or the snapshot's `seq` for the first diff after a bootstrap). A mismatch signals a gap.</ResponseField>

        <ResponseField name="levels" type="array<array<Level>>">
          Tuple `[changed_bids, changed_asks]`. Each entry replaces the current state at its `px`; an entry with `sz: "0"` and `n: 0` removes the level at that price.

          <Expandable title="level properties">
            <ResponseField name="px" type="string">Price for this level (decimal string).</ResponseField>
            <ResponseField name="sz" type="string">New aggregate size at this level (decimal string). `"0"` means the level is removed.</ResponseField>
            <ResponseField name="n" type="int">New number of orders aggregated into this level. `0` means the level is removed.</ResponseField>
          </Expandable>
        </ResponseField>
      </Expandable>
    </ResponseField>
  </Expandable>
</ResponseField>

## Related endpoints

<CardGroup cols={2}>
  <Card title="l2BookDiffSnapshot" href="/docs/api-reference/hyperliquid-info/l2-book-diff-snapshot">Full-depth L2 snapshot with the height/epoch/seq needed to bootstrap this stream.</Card>
  <Card title="l2BookDiff" href="/docs/api-reference/hyperliquid-websocket/l2-book-diff">Snapshot-then-diff L2 stream (self-seeding, no sequence numbers).</Card>
  <Card title="l2Book" href="/docs/api-reference/hyperliquid-websocket/l2-book">Real-time L2 order book snapshots over WebSocket.</Card>
</CardGroup>
