> ## 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 L4 Order Book

> Build per-trader flow attribution, queue-position estimators, smart-money trackers, and microstructure analytics from individual Hyperliquid orders - with `user`, `oid`, `cloid`, `tif`, and trigger metadata exposed on every resting order.

The `l4Book` 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 sends a single `Snapshot` of every resting order, then per-block `Updates` carrying lifecycle events and per-order book changes. Each order arrives with its `user`, `oid`, `cloid`, `tif`, and trigger metadata, so you can reconstruct queue position and price-time priority, attribute flow to specific wallets, and run microstructure analytics that `l2Book`'s aggregated `{px, sz, n}` view hides. For the raw subscription shape see the [`l4Book` reference](https://goldrush.dev/docs/api-reference/hyperliquid-websocket/l4-book); for the connection model see the [WebSocket API overview](https://goldrush.dev/docs/goldrush-hyperliquid/websocket-api/overview.md).

## What you get

* **Per-order visibility.** Every level in the snapshot is an individual order keyed by `oid`, with `user`, `cloid`, `tif`, `orderType`, and trigger metadata attached. `l2Book` only exposes `{px, sz, n}` per price level.
* **Snapshot + diff transport.** One full `Snapshot` on subscribe, then per-block `Updates` containing `order_statuses` (lifecycle events) and `book_diffs` (per-order changes). Apply diffs to local state.
* **Per-block cadence.** Updates fire on each HyperCore block where the book for the subscribed `coin` changed. The `time` and `block_height` fields anchor each message to a specific block.
* **GoldRush-exclusive.** Not available on the public Hyperliquid WebSocket - this stream surfaces user attribution and per-order metadata the public feed never exposes.
* **One coin per subscription.** Unlike `l2Book`, `coin` is required. To cover multiple assets, open one `l4Book` subscription per asset on the same connection.

## 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](https://goldrush.dev/docs/goldrush-hyperliquid/streaming/hip4-markets.md).

**Discovering markets:** a dedicated market-list endpoint (the `perpDexs` Info API type) is on the roadmap. Until it ships, call [`metaAndAssetCtxs`](https://goldrush.dev/docs/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<oid, Order>` per coin. The snapshot seeds the map; each `Updates` message applies `book_diffs` against it. Reconnects drop the map and re-seed from the next snapshot.

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

type Order = {
  user: string | null;
  coin: string;
  side: "B" | "A";
  limitPx: string;
  sz: string;
  oid: number;
  timestamp: number;
  triggerCondition: string;
  isTrigger: boolean;
  triggerPx: string;
  isPositionTpsl: boolean;
  reduceOnly: boolean;
  orderType: string;
  tif: string;
  cloid: string | null;
};

const orders = new Map<number, Order>();

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: "l4Book", coin: "BTC" },
  }));
});

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

  if (msg.data.Snapshot) {
    orders.clear();
    const [bids, asks] = msg.data.Snapshot.levels;
    for (const o of [...bids, ...asks]) orders.set(o.oid, o);
    console.log("seeded from snapshot:", orders.size, "orders");
    return;
  }

  if (msg.data.Updates) {
    const { order_statuses, book_diffs } = msg.data.Updates;

    for (const s of order_statuses) {
      // Re-attach the parent `user` since the nested order has user=null.
      orders.set(s.order.oid, { ...s.order, user: s.user });
    }

    for (const d of book_diffs) {
      const existing = orders.get(d.oid);
      if (!existing) continue;
      if (d.raw_book_diff.new) {
        orders.set(d.oid, { ...existing, sz: d.raw_book_diff.new.sz });
      }
      // Other raw_book_diff shapes (deletes, modifies) belong here.
    }
  }
});
```

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

orders: dict[int, dict] = {}

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

            if "Snapshot" in data:
                orders.clear()
                bids, asks = data["Snapshot"]["levels"]
                for o in bids + asks:
                    orders[o["oid"]] = o
                print("seeded from snapshot:", len(orders), "orders")
                continue

            if "Updates" in data:
                for s in data["Updates"]["order_statuses"]:
                    o = {**s["order"], "user": s["user"]}
                    orders[o["oid"]] = o
                for d in data["Updates"]["book_diffs"]:
                    existing = orders.get(d["oid"])
                    if not existing:
                        continue
                    new = d["raw_book_diff"].get("new")
                    if new:
                        existing["sz"] = new["sz"]

asyncio.run(main())
```

## Patterns

### Track individual orders by `oid`

`oid` is the Hyperliquid order id, stable for the lifetime of the order - use it as the primary key in your local map. `cloid` is the client-supplied id (may be `null`); index on it when you need to correlate fills back to a specific trading bot's instructions.

### Per-user flow attribution

Every order entry carries `user`. Group orders by wallet to surface market-maker behavior, identify spoofing patterns, or build a per-trader heatmap of resting size. Pair this with `clearinghouseState` for per-user position and margin context.

```typescript TypeScript theme={null}
function sizeByUser(orders: Map<number, Order>) {
  const totals = new Map<string, number>();
  for (const o of orders.values()) {
    if (!o.user) continue;
    totals.set(o.user, (totals.get(o.user) ?? 0) + Number(o.sz));
  }
  return totals;
}
```

### Reconstruct aggregated price levels

If a downstream consumer expects an `l2Book`-style aggregated view, sum `sz` across all orders sharing a `limitPx` on the same `side`. Going the other way isn't possible - `l2Book` collapses the per-order detail you'd lose.

```typescript TypeScript theme={null}
function aggregate(orders: Map<number, Order>) {
  const bids = new Map<string, number>();
  const asks = new Map<string, number>();
  for (const o of orders.values()) {
    const book = o.side === "B" ? bids : asks;
    book.set(o.limitPx, (book.get(o.limitPx) ?? 0) + Number(o.sz));
  }
  return { bids, asks };
}
```

## Handling reconnects

On reconnect, resend the same `subscribe` payload. The first message back is always a fresh `Snapshot` - drop your local `orders` map and re-seed from it. **Do not attempt to replay missed `Updates`** - the snapshot is authoritative and supersedes anything you held before the disconnect.

Back off exponentially between attempts. A reconnect opens a new session, and a new session is a new subscription that re-pays the [one-minute minimum](#billing), so a fixed one-second retry against a flapping connection bills a minute of `l4Book` every second.

```typescript TypeScript theme={null}
let backoff = 1_000;

function connect() {
  const ws = new WebSocket(
    `wss://hypercore.goldrushdata.com/ws?key=${process.env.GOLDRUSH_API_KEY}`,
  );
  ws.on("open", () => {
    backoff = 1_000;
    ws.send(JSON.stringify({
      method: "subscribe",
      subscription: { type: "l4Book", coin: "BTC" },
    }));
  });
  ws.on("close", () => {
    orders.clear();
    setTimeout(connect, backoff);
    backoff = Math.min(30_000, backoff * 2);
  });
  return ws;
}
```

## Billing

`l4Book` bills **per coin, per minute, in whole minutes**. There is no proration and no partial minute.

| Subscription | Credits per minute |
| - | - |
| `"coin": "BTC"` | 60 |
| Any other coin | 3 |

> [!WARNING]
>
> **The minimum charge is one full minute.** Sending `subscribe` charges the first minute immediately, before the `Snapshot` is delivered. Subscribing, reading the snapshot, and disconnecting 400 ms later still costs a full minute per coin.

After the first minute, the total is `ceil(seconds_subscribed / 60)` counted from the moment you subscribed. 60 seconds bills 1 minute; 61 seconds bills 2.

Because `coin` is required, every coin you track is its own billable subscription. A snapshot-only sweep across 168 coins costs `60 + (167 x 3) = 561` credits whether the connection lives 400 milliseconds or a full 60 seconds. Repeating that sweep ten times costs ten times as much.

For the rest of the model - what counts as one subscription, why reconnects are not free, and the `unsubscribe` shape-matching rule - see [Billing](https://goldrush.dev/docs/goldrush-hyperliquid/websocket-api/limits.md#billing) on the rate limits page. The same rules apply to every order-book channel, and the whole-minute rounding applies to the wallet-activity channels too.

## Related

* [`l4Book` API reference](https://goldrush.dev/docs/api-reference/hyperliquid-websocket/l4-book) - full subscription, snapshot, and update schema.
* [`l2Book` reference](https://goldrush.dev/docs/api-reference/hyperliquid-websocket/l2-book) - aggregated price-level snapshots when per-order detail isn't needed.
* [WebSocket API overview](https://goldrush.dev/docs/goldrush-hyperliquid/websocket-api/overview.md) - endpoint URL, auth, and limits.
* [Billing](https://goldrush.dev/docs/goldrush-hyperliquid/websocket-api/limits.md#billing) - the whole-minute model shared by every WebSocket channel.
* [`clearinghouseState`](https://goldrush.dev/docs/api-reference/hyperliquid-info/clearinghouse-state) - pair per-user resting orders with position and margin state.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.