# Examples

> Working code for the Hyperliquid WebSocket feed: a smoke test, a minimal client, several markets on one connection, keeping a book in sync from l2Diff, and reconnecting.

Every example below uses the same endpoint and the same header.

```
wss://toy.hl.supanode.xyz:48080/ws
x-token: YOUR_KEY
```

## Smoke test

One command, no code. It opens a connection, subscribes to the BTC book and prints frames until you stop it.

```bash
{ echo '{"method":"subscribe","subscription":{"type":"l2Book","coin":"BTC"}}'; sleep infinity; } \
  | websocat wss://toy.hl.supanode.xyz:48080/ws -H='x-token: YOUR_KEY'
```

If the handshake fails, the status code tells you why: `401` means the header did not arrive, `403` means the key is not valid for this endpoint.

## Minimal client

<Tabs>
  <Tab title="Python">
    ```python
    import asyncio, json, websockets

    URL = "wss://toy.hl.supanode.xyz:48080/ws"
    HEADERS = {"x-token": "YOUR_KEY"}

    async def main():
        async with websockets.connect(URL, additional_headers=HEADERS, ping_interval=20) as ws:
            await ws.send(json.dumps({
                "method": "subscribe",
                "subscription": {"type": "l2Book", "coin": "BTC"},
            }))
            async for raw in ws:
                msg = json.loads(raw)
                if msg.get("channel") != "l2Book":
                    continue
                bids, asks = msg["data"]["levels"]
                print(msg["data"]["time"], bids[0]["px"], asks[0]["px"], len(bids))

    asyncio.run(main())
    ```
  </Tab>
  <Tab title="Node.js">
    ```js
    import WebSocket from 'ws';

    const ws = new WebSocket('wss://toy.hl.supanode.xyz:48080/ws', {
      headers: { 'x-token': 'YOUR_KEY' },
    });

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

    ws.on('message', (raw) => {
      const msg = JSON.parse(raw);
      if (msg.channel !== 'l2Book') return;
      const [bids, asks] = msg.data.levels;
      console.log(msg.data.time, bids[0].px, asks[0].px, bids.length);
    });
    ```
  </Tab>
</Tabs>

<Warning>
**Run this on a server, not in a browser.** The key travels as a handshake header, and browser JavaScript cannot set one.
</Warning>

## Several markets on one connection

Send one subscribe message per market and read the `coin` field to tell the frames apart.

```python
for coin in ("BTC", "ETH", "SOL", "HYPE"):
    await ws.send(json.dumps({
        "method": "subscribe",
        "subscription": {"type": "l2Book", "coin": coin, "nLevels": 100},
    }))
```

Spot pairs use their index from `spotMeta` instead of a symbol:

```python
await ws.send(json.dumps({
    "method": "subscribe",
    "subscription": {"type": "l2Book", "coin": "@107"},
}))
```

## Deep book without the bandwidth: `l2Diff`

At 1000 levels a full snapshot on every block is a large frame. `l2Diff` sends the snapshot once and then only what changed. The client keeps the book and applies updates in order.

Two shapes to handle: a snapshot level is an object, an update level is a three-value array, and a removal is a bare price string.

```python
book = {"bids": {}, "asks": {}, "height": None}

def apply(msg):
    data = msg["data"]

    # A snapshot arrives first, and again if the feed restarts your view.
    if "Snapshot" in data:
        snap = data["Snapshot"]
        bids, asks = snap["levels"]                    # one list of two halves
        book["bids"] = {l["px"]: (l["sz"], l["n"]) for l in bids}
        book["asks"] = {l["px"]: (l["sz"], l["n"]) for l in asks}
        book["height"] = snap["height"]
        return True

    upd = data["Updates"]

    # Heights skip blocks that changed nothing, so compare - never count.
    if upd["prevHeight"] != book["height"]:
        book["height"] = None            # our copy is stale: wait for a snapshot
        return False

    for side in ("bids", "asks"):
        changes = upd.get(side, {})
        for px, sz, n in changes.get("upd", []):      # array, not an object
            book[side][px] = (sz, n)
        for px in changes.get("del", []):             # a plain price string
            book[side].pop(px, None)

    book["height"] = upd["height"]
    return True
```

Three rules make this safe:

1. **Parse the two level shapes separately.** `{"px": ..., "sz": ..., "n": ...}` in a snapshot, `[px, sz, n]` in an update, a bare price string in `del`. The two sides are also arranged differently: a snapshot carries one `levels` list of two halves, exactly like `l2Book`, while an update carries separate `bids` and `asks` objects.
2. **Never apply an update whose `prevHeight` does not match the height you last applied.** A mismatch means your copy has diverged. Drop it and rebuild from the next snapshot. Do not expect heights to increase by one — blocks that changed nothing inside your window are not sent.
3. **A snapshot can arrive at any time**, not only as the first message. Treat it as a fresh start rather than an error: drop the book you were keeping and rebuild from it.

## Unsubscribing

The unsubscribe object must repeat the subscription exactly, including `nLevels`.

```python
# Subscribed with nLevels: 100 - so unsubscribe with nLevels: 100.
await ws.send(json.dumps({
    "method": "unsubscribe",
    "subscription": {"type": "l2Book", "coin": "BTC", "nLevels": 100},
}))
```

## Reconnecting

Subscriptions are not restored for you. Keep the list you sent and replay it after every reconnect.

```python
SUBS = [
    {"type": "l2Book", "coin": "BTC", "nLevels": 100},
    {"type": "trades", "coin": "BTC"},
]

async def run_forever():
    while True:
        try:
            async with websockets.connect(URL, additional_headers=HEADERS, ping_interval=20) as ws:
                for sub in SUBS:
                    await ws.send(json.dumps({"method": "subscribe", "subscription": sub}))
                async for raw in ws:
                    handle(json.loads(raw))
        except Exception:
            await asyncio.sleep(1)       # then reconnect and replay SUBS
```

## Keep-alive

`ping_interval` in the examples above uses standard WebSocket ping frames, which the server answers. If your client library does not send them, use the application ping instead:

```json
{ "method": "ping" }
```

The answer is `{"channel":"pong"}`.

## See also

- [Streams](https://supanode.xyz/docs/hyperliquid/websocket/whats-available) — the shape of every frame.
- [Limits](https://supanode.xyz/docs/hyperliquid/websocket/limits) — depth ceiling and subscriptions per connection.
