> ## 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.

# Build with the L2 Order Book Diff

> Build cross-asset depth dashboards, market scanners, and arbitrage engines that track live aggregated book state across one coin, a fixed list, or every Hyperliquid asset on a single subscription - with bandwidth proportional to what changed each block, not the size of the book.

The `l2BookDiff` channel on `wss://hypercore.goldrushdata.com/ws?key=<GOLDRUSH_API_KEY>` is a GoldRush-exclusive stream - it has no equivalent on `wss://api.hyperliquid.xyz/ws`. After subscribing, the server emits one `Snapshot` per subscribed coin, then per-block `Updates` carrying only the price levels that changed. The shape per level is the same `{px, sz, n}` you already get from `l2Book`, so book-state code only needs to learn how to apply diffs - and `coin` accepts a single asset, a list, or can be omitted to stream every asset on one subscription. For the raw subscription shape see the [`l2BookDiff` reference](/api-reference/hyperliquid-websocket/l2-book-diff); for the connection model see the [WebSocket API overview](/goldrush-hyperliquid/websocket-api/overview).

## What you get

* **Snapshot + diff transport.** One full `Snapshot` per subscribed coin on subscribe, then per-block `Updates` containing only the levels that changed. Apply diffs to local state.
* **Aggregated `{px, sz, n}` shape.** Identical to `l2Book` levels - reuse your existing aggregated-book types and just add a level-apply function.
* **Multi-coin / wildcard friendly.** `coin` accepts a single asset, an array of assets, or can be omitted to stream every asset on one subscription. When `coin` is omitted, `marketTypes` defaults to `["perp"]` - pass `["spot"]`, `["outcome"]`, a mix, or `["*"]` to opt into spot, outcome, and future market types. `l4Book` is one-coin-per-subscription; `l2Book` requires per-asset fan-out on the public feed.
* **Bandwidth proportional to change.** Quiet markets cost almost nothing; only the levels that actually moved arrive over the wire, instead of a full re-snapshot every tick.
* **GoldRush-exclusive.** Public Hyperliquid has no L2-diff transport - `l2Book` is full-snapshot only.

## Pair address format

Hyperliquid markets use a **deployer-prefix naming scheme**. The exact string differs by surface, so pass the value each API expects - matching is case- and format-sensitive.

| Where it's used                                                       | Format                                                                   | Examples                                    |
| --------------------------------------------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------- |
| WebSocket `coin` (`l2Book`, `l2BookDiff`, `l4Book`) - canonical perps | `<symbol>`                                                               | `BTC`, `ETH`, `HYPE`                        |
| WebSocket `coin` - HIP-3 builder markets                              | `<deployer>:<symbol>`                                                    | `xyz:GOLD`, `flx:OIL`                       |
| Streaming `pair_addresses` (`ohlcvCandlesForPair`)                    | `<deployer>:<symbol>-<quote>`, or `<symbol>-<quote>` for canonical pairs | `xyz:GOLD-USDC`, `flx:OIL-USDH`, `BTC-USDC` |
| Streaming `token_addresses` (`ohlcvCandlesForToken`)                  | `<symbol>` (no prefix)                                                   | `GOLD`, `OIL`, `BTC`                        |

* `deployer` - wallet address of the HIP-3 builder that deployed the market. Omitted for canonical Hyperliquid markets.
* `symbol` - market ticker (e.g. `GOLD`, `OIL`).
* `quote` - the margin / quote currency, usually `USDC` (some HIP-3 markets use `USDH`).

<Note>HIP-4 outcome markets use a separate `#<encoding>` scheme (e.g. `#1230`), not the deployer-prefix - see [HIP-4 Markets](/goldrush-hyperliquid/streaming/hip4-markets).</Note>

**Discovering markets:** a dedicated market-list endpoint (the `perpDexs` Info API type) is on the roadmap. Until it ships, call [`metaAndAssetCtxs`](/api-reference/hyperliquid-info/meta-and-asset-ctxs) - the `name` field on each `universe` entry is the canonical pair address.

## Subscribe and maintain book state

The pattern below keeps a `Map<coin, { bids: Map<px, Level>, asks: Map<px, Level> }>`. Each `Snapshot` seeds the per-coin entry; each `book_diff` inside `Updates` either deletes a level (when `sz === "0"`) or replaces it with the new `{sz, n}`.

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

  type Level = { px: string; sz: string; n: number };
  type Side = Map<string, Level>;
  type Book = { bids: Side; asks: Side };

  const books = new Map<string, Book>();

  function applyLevels(side: Side, levels: Level[]) {
    for (const lvl of levels) {
      if (lvl.sz === "0") side.delete(lvl.px);
      else side.set(lvl.px, lvl);
    }
  }

  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: "l2BookDiff", coin: "HYPE" },
    }));
  });

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

    if (msg.data.Snapshot) {
      const { coin, levels: [bids, asks] } = msg.data.Snapshot;
      const book: Book = { bids: new Map(), asks: new Map() };
      for (const l of bids) book.bids.set(l.px, l);
      for (const l of asks) book.asks.set(l.px, l);
      books.set(coin, book);
      return;
    }

    if (msg.data.Updates) {
      for (const diff of msg.data.Updates.book_diffs) {
        const book = books.get(diff.coin);
        if (!book) continue;
        const [bidLevels, askLevels] = diff.levels;
        applyLevels(book.bids, bidLevels);
        applyLevels(book.asks, askLevels);
      }
    }
  });
  ```

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

  books: dict[str, dict] = {}

  def apply_levels(side: dict, levels: list[dict]) -> None:
      for lvl in levels:
          if lvl["sz"] == "0":
              side.pop(lvl["px"], None)
          else:
              side[lvl["px"]] = lvl

  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": "l2BookDiff", "coin": "HYPE"},
          }))
          async for raw in ws:
              msg = json.loads(raw)
              if msg.get("channel") != "l2BookDiff":
                  continue
              data = msg["data"]

              if "Snapshot" in data:
                  snap = data["Snapshot"]
                  bids, asks = snap["levels"]
                  books[snap["coin"]] = {
                      "bids": {l["px"]: l for l in bids},
                      "asks": {l["px"]: l for l in asks},
                  }
                  continue

              if "Updates" in data:
                  for diff in data["Updates"]["book_diffs"]:
                      book = books.get(diff["coin"])
                      if not book:
                          continue
                      bid_levels, ask_levels = diff["levels"]
                      apply_levels(book["bids"], bid_levels)
                      apply_levels(book["asks"], ask_levels)

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

## Patterns

### Single coin, list, or wildcard

All three subscription shapes share the same handler - only the payload sent at `subscribe` time changes. Use whichever matches your coverage requirements.

```typescript TypeScript theme={null}
// Single coin
ws.send(JSON.stringify({
  method: "subscribe",
  subscription: { type: "l2BookDiff", coin: "HYPE" },
}));

// Fixed list of coins
ws.send(JSON.stringify({
  method: "subscribe",
  subscription: { type: "l2BookDiff", coin: ["HYPE", "BTC", "ETH"] },
}));

// Every perp asset on one subscription (perps is the default when coin is omitted)
ws.send(JSON.stringify({
  method: "subscribe",
  subscription: { type: "l2BookDiff" },
}));

// Spot only, or any mix; pass ["*"] for perps + spot + outcomes plus auto-opt-in to future types
ws.send(JSON.stringify({
  method: "subscribe",
  subscription: { type: "l2BookDiff", marketTypes: ["spot"] },
}));
ws.send(JSON.stringify({
  method: "subscribe",
  subscription: { type: "l2BookDiff", marketTypes: ["*"] },
}));
```

A list or wildcard subscription receives one `Snapshot` per live coin before diffs begin, so the `books` map fills in over the first few messages rather than all at once. `marketTypes` is only valid when `coin` is omitted; it defaults to `["perp"]`, so spot and outcome markets require explicit opt-in. A second `subscribe` with a different `marketTypes` value replaces the previous filter rather than coexisting with it.

### Reconstruct a top-of-book stream

After each diff is applied, the best bid is the highest `px` in `book.bids` and the best ask is the lowest `px` in `book.asks`. Emit only when the top level changes to avoid noise.

```typescript TypeScript theme={null}
function bestLevels(book: Book) {
  let bestBid: string | null = null;
  for (const px of book.bids.keys()) if (!bestBid || Number(px) > Number(bestBid)) bestBid = px;
  let bestAsk: string | null = null;
  for (const px of book.asks.keys()) if (!bestAsk || Number(px) < Number(bestAsk)) bestAsk = px;
  return { bestBid, bestAsk };
}
```

For frequent top-of-book reads, cache the best levels and update them only when an incoming diff touches the current best `px` or improves on it.

### Compare against `l2Book`

`l2BookDiff` levels carry the same `{px, sz, n}` shape as `l2Book`, so any code that consumes `l2Book` snapshots can consume `l2BookDiff` state once you maintain the per-coin map. The difference is transport - `l2Book` re-sends the full book on every tick, while `l2BookDiff` ships only what changed. For the same coin in both modes the book state will agree at every block boundary; pick the diff variant when wide multi-asset coverage or quiet markets make full-snapshot bandwidth wasteful.

## Handling reconnects

On reconnect, resend the same `subscribe` payload. The first messages back are fresh `Snapshot`s - one per coin in scope - so drop your local `books` map for the affected coins and re-seed from those snapshots. **Do not attempt to replay missed `Updates`** - the snapshot is authoritative and supersedes anything you held before the disconnect.

```typescript TypeScript theme={null}
function connect() {
  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: "l2BookDiff", coin: "HYPE" },
  })));
  ws.on("close", () => {
    books.clear();
    setTimeout(connect, 1000);
  });
  return ws;
}
```

## Related

* [`l2BookDiff` API reference](/api-reference/hyperliquid-websocket/l2-book-diff) - full subscription, snapshot, and update schema.
* [`l2Book` reference](/api-reference/hyperliquid-websocket/l2-book) - full-snapshot transport when diff replay isn't desirable.
* [`l4Book` reference](/api-reference/hyperliquid-websocket/l4-book) - order-level stream with `user`, `oid`, `cloid`, `tif`, and trigger metadata.
* [WebSocket API overview](/goldrush-hyperliquid/websocket-api/overview) - endpoint URL, auth, and limits.
