Overview

This guide shows how to maintain a real-time local orderbook by combining a REST snapshot with WebSocket updates. This is the standard approach used by trading bots and UIs.

Step 1: Connect to WebSocket

const API_KEY = "ps_live_abc123def456";

const ws = new WebSocket(
  `wss://api.perps.studio/ws/v1/perps?apiKey=${API_KEY}`
);

Step 2: Fetch Initial Snapshot

Before subscribing to updates, fetch the full orderbook via REST:
const BASE = "https://api.perps.studio/v1";

const snapshot = await fetch(
  `${BASE}/perps/hyperliquid/markets/BTC/orderbook?depth=50`,
  { headers: { "X-API-Key": API_KEY } },
).then((r) => r.json());

// Initialize local book
const localBook = {
  bids: new Map<string, string>(), // price -> size
  asks: new Map<string, string>(),
};

for (const level of snapshot.bids) {
  localBook.bids.set(level.price, level.size);
}
for (const level of snapshot.asks) {
  localBook.asks.set(level.price, level.size);
}

Step 3: Subscribe to Updates

ws.onopen = () => {
  ws.send(JSON.stringify({
    event: "subscribe",
    data: {
      provider: "hyperliquid",
      channel: "orderbook",
      params: { symbol: "BTC" },
    },
  }));
};

Step 4: Apply Updates

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);

  if (msg.error) {
    console.error("WS Error:", msg.error);
    return;
  }

  if (msg.channel !== "orderbook") return;

  const update = msg.data;

  // Apply bid updates
  for (const [price, size] of update.bids) {
    if (size === "0") {
      localBook.bids.delete(price);
    } else {
      localBook.bids.set(price, size);
    }
  }

  // Apply ask updates
  for (const [price, size] of update.asks) {
    if (size === "0") {
      localBook.asks.delete(price);
    } else {
      localBook.asks.set(price, size);
    }
  }

  // Get sorted top of book
  const topBids = [...localBook.bids.entries()]
    .sort((a, b) => parseFloat(b[0]) - parseFloat(a[0]))
    .slice(0, 10);

  const topAsks = [...localBook.asks.entries()]
    .sort((a, b) => parseFloat(a[0]) - parseFloat(b[0]))
    .slice(0, 10);

  const bestBid = topBids[0]?.[0] ?? "N/A";
  const bestAsk = topAsks[0]?.[0] ?? "N/A";
  const spread = bestBid !== "N/A" && bestAsk !== "N/A"
    ? (parseFloat(bestAsk) - parseFloat(bestBid)).toFixed(2)
    : "N/A";

  console.log(`BTC | Bid: ${bestBid} | Ask: ${bestAsk} | Spread: ${spread}`);
};

Step 5: Handle Reconnection

function createConnection() {
  const ws = new WebSocket(
    `wss://api.perps.studio/ws/v1/perps?apiKey=${API_KEY}`
  );

  ws.onclose = () => {
    console.log("Disconnected. Reconnecting in 2s...");
    setTimeout(() => {
      // Re-fetch snapshot on reconnect to avoid stale state
      createConnection();
    }, 2000);
  };

  ws.onopen = async () => {
    // Re-fetch full snapshot
    const snapshot = await fetch(
      `${BASE}/perps/hyperliquid/markets/BTC/orderbook?depth=50`,
      { headers: { "X-API-Key": API_KEY } },
    ).then((r) => r.json());

    // Reset local book from snapshot
    localBook.bids.clear();
    localBook.asks.clear();
    for (const level of snapshot.bids) {
      localBook.bids.set(level.price, level.size);
    }
    for (const level of snapshot.asks) {
      localBook.asks.set(level.price, level.size);
    }

    // Re-subscribe
    ws.send(JSON.stringify({
      event: "subscribe",
      data: {
        provider: "hyperliquid",
        channel: "orderbook",
        params: { symbol: "BTC" },
      },
    }));
  };

  return ws;
}

Multi-Provider Comparison

You can subscribe to the same market on different providers to compare orderbooks:
// Subscribe to BTC on both Hyperliquid and Aster
for (const provider of ["hyperliquid", "aster"]) {
  ws.send(JSON.stringify({
    event: "subscribe",
    data: {
      provider,
      channel: "orderbook",
      params: { symbol: "BTC" },
    },
  }));
}

// Messages include the provider field, so you can differentiate
ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  console.log(`[${msg.provider}] Best bid: ${msg.data.bids[0]?.[0]}`);
};
This pattern is the foundation for cross-provider arbitrage. See the Cross-Provider Arb recipe for a complete implementation.