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.