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

# l4Book | Hyperliquid WebSocket API

> Hyperliquid l4Book: subscribe to GoldRush's order-level Hyperliquid book - initial snapshot of every resting order plus per-block diffs with full metadata.

<CardGroup cols={2}>
  <Card title="Credit Cost"> 3 per coin per minute (except BTC which is 60 per minute)</Card>
  <Card title="Processing"> Realtime</Card>
</CardGroup>

<Tip>
  Estimate your monthly cost for this API using the [Pricing Calculator](/pricing-calculator?endpoint=%2Fapi-reference%2Fhyperliquid-websocket%2Fl4-book).
</Tip>

<Info>
  * GoldRush-native. `l4Book` is not exposed on `wss://api.hyperliquid.xyz/ws`. Pointing a client at the public endpoint with this subscription type will fail.
  * Perps only. No spot assets.
  * `coin` is required. Unlike `l2Book` and `l2BookDiff`, you cannot omit `coin` to stream every asset. Open one subscription per asset.
  * Snapshot, then diffs. The first message is always a `Snapshot`; every message thereafter is an `Updates`. Clients must seed local book state from the snapshot and apply diffs from there. On reconnect, drop local state and re-seed from the next snapshot.
  * Per-order detail. Each entry exposes `user`, `oid`, `cloid`, `tif`, and trigger metadata - enabling queue-position reconstruction, per-trader flow attribution, and microstructure analytics that are not possible with `l2Book`.
  * See the <a href="https://goldrush.dev/docs/goldrush-hyperliquid/websocket-api/l4-book" target="_blank" rel="noopener noreferrer">L4 Order Book recipe</a> for patterns to maintain book state, attribute flow by `user`, or reconstruct aggregated price levels.
</Info>

**Note:** When `"coin":"BTC"` is used, a credit rate of `60` **credits per minute subscribed** is applied.

## 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 `"l4Book"`.</ResponseField>
    <ResponseField name="coin" type="string" required>Asset symbol - e.g. `"BTC"`, `"ETH"`, `"@107"` for spot pairs. For HIP-3 markets, include the deployer prefix. **Required** - unlike `l2Book`, `l4Book` does not support wildcard subscriptions; each subscription is locked to a single asset.</ResponseField>
  </Expandable>
</ParamField>

### Example

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

  > {"method":"subscribe","subscription":{"type":"l4Book","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: "l4Book", coin: "BTC" },
    }));
  });

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

    if (msg.data.Snapshot) {
      const { coin, time, block_height, levels: [bids, asks] } = msg.data.Snapshot;
      console.log("snapshot", coin, time, block_height, "bids:", bids.length, "asks:", asks.length);
    } else if (msg.data.Updates) {
      const { time, block_height, order_statuses, book_diffs } = msg.data.Updates;
      console.log("updates", time, block_height, "statuses:", order_statuses.length, "diffs:", book_diffs.length);
    }
  });
  ```

  ```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": "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:
                  snap = data["Snapshot"]
                  bids, asks = snap["levels"]
                  print("snapshot", snap["coin"], snap["time"], snap["block_height"], "bids:", len(bids), "asks:", len(asks))
              elif "Updates" in data:
                  upd = data["Updates"]
                  print("updates", upd["time"], upd["block_height"], "statuses:", len(upd["order_statuses"]), "diffs:", len(upd["book_diffs"]))

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

## Unsubscribe

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

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

## Streamed messages

Every message has `channel: "l4Book"`. The `data` payload contains **exactly one** of two variants: a `Snapshot` (emitted once, immediately after subscribe) or an `Updates` (emitted on each subsequent HyperCore block where the book for `coin` changed).

### Initial snapshot

The first message after subscribe carries the full resting book at the current block. Each entry in `levels[0]` (bids) and `levels[1]` (asks) is an **individual order** - not an aggregated price level.

```json theme={null}
{
  "channel": "l4Book",
  "data": {
    "Snapshot": {
      "coin": "BTC",
      "time": 1778865761968,
      "block_height": 997719816,
      "levels": [
        [
          {
            "user": "0xa62b923a112d50d03e1e096bbd53422490dac104",
            "coin": "BTC",
            "side": "B",
            "limitPx": "79242",
            "sz": "0.74831",
            "oid": 427632406005,
            "timestamp": 1778865761305,
            "triggerCondition": "N/A",
            "isTrigger": false,
            "triggerPx": "0.0",
            "isPositionTpsl": false,
            "reduceOnly": false,
            "orderType": "Limit",
            "tif": "Alo",
            "cloid": "0x00000000000000000000019e2c8b7d66"
          }
        ],
        [
          {
            "user": "0xfcf104006bfff47695c1dc21dad3e9de1e72098e",
            "coin": "BTC",
            "side": "A",
            "limitPx": "79250",
            "sz": "0.2961",
            "oid": 427632406032,
            "timestamp": 1778865761305,
            "triggerCondition": "N/A",
            "isTrigger": false,
            "triggerPx": "0.0",
            "isPositionTpsl": false,
            "reduceOnly": false,
            "orderType": "Limit",
            "tif": "Gtc",
            "cloid": null
          }
        ]
      ]
    }
  }
}
```

### Incremental updates

Subsequent messages carry only what changed since the previous block. `order_statuses` describes order lifecycle events (`open`, etc.); `book_diffs` carries the corresponding price-level changes.

```json theme={null}
{
  "channel": "l4Book",
  "data": {
    "Updates": {
      "time": 1778865761768,
      "block_height": 997719813,
      "order_statuses": [
        {
          "time": "2026-05-15T17:22:41.768005701",
          "user": "0x31ca8395cf837de08b24da3f660e77761dfb974b",
          "status": "open",
          "order": {
            "user": null,
            "coin": "BTC",
            "side": "B",
            "limitPx": "79242.0",
            "sz": "0.00867",
            "oid": 427632416336,
            "timestamp": 1778865761768,
            "triggerCondition": "N/A",
            "isTrigger": false,
            "triggerPx": "0.0",
            "isPositionTpsl": false,
            "reduceOnly": false,
            "orderType": "Limit",
            "tif": "Alo",
            "cloid": null
          }
        }
      ],
      "book_diffs": [
        {
          "user": "0x31ca8395cf837de08b24da3f660e77761dfb974b",
          "oid": 427632416336,
          "px": "79242.0",
          "coin": "BTC",
          "raw_book_diff": { "new": { "sz": "0.00867" } }
        }
      ]
    }
  }
}
```

### Response fields

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

<ResponseField name="data" type="object">
  Contains exactly one of `Snapshot` or `Updates`.

  <Expandable title="Snapshot (first message after subscribe)">
    <ResponseField name="coin" type="string">Asset symbol the snapshot belongs to.</ResponseField>
    <ResponseField name="time" type="int">HyperCore block timestamp in milliseconds.</ResponseField>
    <ResponseField name="block_height" type="int">HyperCore block height the snapshot was taken at.</ResponseField>

    <ResponseField name="levels" type="array<array<Order>>">
      Tuple `[bids, asks]`. Each side is an array of individual **Order** objects (see below), in queue order at their respective price.
    </ResponseField>
  </Expandable>

  <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.</ResponseField>

    <ResponseField name="order_statuses" type="array<object>">
      Order lifecycle events at this block.

      <Expandable title="order_status fields">
        <ResponseField name="time" type="string">ISO-8601 timestamp with nanosecond precision.</ResponseField>
        <ResponseField name="user" type="string">Wallet address that owns the order.</ResponseField>
        <ResponseField name="status" type="string">Lifecycle status (e.g. `"open"`).</ResponseField>
        <ResponseField name="order" type="Order">The order, in the same shape as a snapshot entry. `user` inside this nested object is `null` because it duplicates the parent `user`.</ResponseField>
      </Expandable>
    </ResponseField>

    <ResponseField name="book_diffs" type="array<object>">
      Per-order book changes at this block.

      <Expandable title="book_diff fields">
        <ResponseField name="user" type="string">Wallet address that owns the order.</ResponseField>
        <ResponseField name="oid" type="int">Order id the diff applies to.</ResponseField>
        <ResponseField name="px" type="string">Price level the diff applies to (decimal string).</ResponseField>
        <ResponseField name="coin" type="string">Asset symbol.</ResponseField>
        <ResponseField name="raw_book_diff" type="object">The change descriptor. Observed shape: `{ "new": { "sz": "<size>" } }` for a newly resting order. Other shapes may carry size deltas or cancellations - inspect the keys to discriminate.</ResponseField>
      </Expandable>
    </ResponseField>
  </Expandable>
</ResponseField>

### Order object

The **Order** type appears inside `Snapshot.levels[*][*]` and `Updates.order_statuses[*].order`.

<ResponseField name="user" type="string | null">Wallet address that owns the order. `null` when the order is nested inside an `order_status` (the parent already carries it).</ResponseField>
<ResponseField name="coin" type="string">Asset symbol.</ResponseField>
<ResponseField name="side" type="string">`"B"` for bid, `"A"` for ask.</ResponseField>
<ResponseField name="limitPx" type="string">Limit price (decimal string).</ResponseField>
<ResponseField name="sz" type="string">Resting size (decimal string, base units).</ResponseField>
<ResponseField name="oid" type="int">Hyperliquid order id - stable for the lifetime of the order.</ResponseField>
<ResponseField name="timestamp" type="int">Order-placement timestamp in HyperCore milliseconds.</ResponseField>
<ResponseField name="triggerCondition" type="string">Trigger condition string (e.g. `"N/A"` for plain limit orders).</ResponseField>
<ResponseField name="isTrigger" type="boolean">True if this is a stop / take-profit trigger order.</ResponseField>
<ResponseField name="triggerPx" type="string">Trigger price (decimal string, `"0.0"` for non-trigger orders).</ResponseField>
<ResponseField name="isPositionTpsl" type="boolean">True if this is a position-level TP/SL.</ResponseField>
<ResponseField name="reduceOnly" type="boolean">True if the order is flagged reduce-only.</ResponseField>
<ResponseField name="orderType" type="string">Hyperliquid order type (e.g. `"Limit"`).</ResponseField>
<ResponseField name="tif" type="string">Time-in-force (e.g. `"Alo"`, `"Gtc"`, `"Ioc"`).</ResponseField>
<ResponseField name="cloid" type="string | null">Client-supplied order id (hex string), or `null` if none was provided.</ResponseField>

*Last reviewed: 2026-06-17*
