Skip to main content

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.

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; for the connection model see the 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.

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}.
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);
    }
  }
});

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
// 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
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 Snapshots - 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
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;
}