Skip to content

Orderbook Updates

Note: In other exchanges this is sometime called orderBook.

Receive live changes to market depth, price levels, and liquidity for specific trading pairs.

Endpoint

ws.send() wss://api.synthetix.io/v1/ws/subscriptions

Subscription Request

Subscribe to Orderbook Updates

Authenticate before subscribing:

{
  "jsonrpc": "2.0",
  "id": "auth-1",
  "method": "post",
  "params": {
    "action": "authenticate",
    "subAccountId": "1867542890123456789",
    "nonce": 1704067200000,
    "signature": {
      "v": 28,
      "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
      "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
    }
  }
}

EIP-712 Domain: { "name": "Synthetix", "version": "1", "chainId": 1, "verifyingContract": "0x0000000000000000000000000000000000000000" }

{
  "jsonrpc": "2.0",
  "id": "sub-1",
  "method": "subscribe",
  "params": {
    "type": "orderbook",
    "symbol": "BTC-USDT"
  }
}

Request Parameters

ParameterTypeRequiredDescription
idstringYesClient-generated unique request identifier
methodstringYesMust be "subscribe"
params.typestringYesMust be "orderbook"
params.symbolstringYesTrading pair symbol (e.g., "BTC-USDT")

Orderbook Update Messages

Initial Snapshot

Upon subscription, you'll receive a full orderbook snapshot:

{
  "method": "orderbook_depth_update",
  "data": {
    "symbol": "BTC-USDT",
    "timestamp": "2025-01-01T00:00:00.000Z",
    "bids": [
      {"price": "50000.00", "quantity": "1.5"},
      {"price": "49999.50", "quantity": "2.0"},
      {"price": "49999.00", "quantity": "0.8"}
    ],
    "asks": [
      {"price": "50001.00", "quantity": "1.2"},
      {"price": "50001.50", "quantity": "3.1"},
      {"price": "50002.00", "quantity": "0.9"}
    ]
  }
}

Incremental Updates

After the snapshot, you'll receive incremental updates with the same structure:

{
  "method": "orderbook_depth_update",
  "data": {
    "symbol": "BTC-USDT",
    "timestamp": "2025-01-01T00:00:01.000Z",
    "bids": [
      {"price": "50000.00", "quantity": "0.0"},  // Removed (quantity = 0)
      {"price": "49998.50", "quantity": "1.0"}   // Added/Updated
    ],
    "asks": [
      {"price": "50002.00", "quantity": "1.5"},  // Updated quantity
      {"price": "50003.00", "quantity": "0.5"}   // Added new level
    ]
  }
}

Price Level Format

Each price level is represented as an object with price and quantity fields:

{"price": "50000.50", "quantity": "1.25"}

Where:

  • price (string): Price level
  • quantity (string): Total quantity at this level

Special Case: When quantity is "0.0", the price level should be removed from the orderbook.

Managing Local Orderbook

Initial Setup

class OrderbookManager {
  constructor() {
    this.orderbooks = new Map();
  }
 
  processOrderbookUpdate(data) {
    const { symbol } = data;
 
    // If orderbook doesn't exist, initialize it
    if (!this.orderbooks.has(symbol)) {
      this.initializeOrderbook(symbol, data);
    } else {
      this.updateOrderbook(symbol, data);
    }
  }
 
  initializeOrderbook(symbol, data) {
    const orderbook = {
      symbol,
      bids: new Map(),
      asks: new Map(),
      timestamp: data.timestamp
    };
 
    // Load initial bids
    data.bids.forEach(({price, quantity}) => {
      orderbook.bids.set(price, quantity);
    });
 
    // Load initial asks
    data.asks.forEach(({price, quantity}) => {
      orderbook.asks.set(price, quantity);
    });
 
    this.orderbooks.set(symbol, orderbook);
    console.log(`Orderbook initialized for ${symbol}`);
  }
 
  updateOrderbook(symbol, data) {
    const orderbook = this.orderbooks.get(symbol);
    if (!orderbook) {
      console.error(`No orderbook found for ${symbol}`);
      return;
    }
 
    // Apply bid updates
    data.bids?.forEach(({price, quantity}) => {
      if (quantity === "0.0") {
        orderbook.bids.delete(price);
      } else {
        orderbook.bids.set(price, quantity);
      }
    });
 
    // Apply ask updates
    data.asks?.forEach(({price, quantity}) => {
      if (quantity === "0.0") {
        orderbook.asks.delete(price);
      } else {
        orderbook.asks.set(price, quantity);
      }
    });
 
    orderbook.timestamp = data.timestamp;
 
    this.notifyOrderbookChange(symbol, orderbook);
  }
 
  getOrderbook(symbol) {
    return this.orderbooks.get(symbol);
  }
 
  getBestBidAsk(symbol) {
    const orderbook = this.orderbooks.get(symbol);
    if (!orderbook) return null;
 
    const bestBid = Math.max(...Array.from(orderbook.bids.keys()).map(parseFloat));
    const bestAsk = Math.min(...Array.from(orderbook.asks.keys()).map(parseFloat));
 
    return {
      bestBid: bestBid.toString(),
      bestAsk: bestAsk.toString(),
      spread: (bestAsk - bestBid).toString(),
      spreadPercent: ((bestAsk - bestBid) / bestBid * 100).toFixed(4)
    };
  }
 
  getDepth(symbol, levels = 10) {
    const orderbook = this.orderbooks.get(symbol);
    if (!orderbook) return null;
 
    // Sort and limit bids (highest first)
    const bids = Array.from(orderbook.bids.entries())
      .sort(([a], [b]) => parseFloat(b) - parseFloat(a))
      .slice(0, levels);
 
    // Sort and limit asks (lowest first)
    const asks = Array.from(orderbook.asks.entries())
      .sort(([a], [b]) => parseFloat(a) - parseFloat(b))
      .slice(0, levels);
 
    return { bids, asks };
  }
 
  notifyOrderbookChange(symbol, orderbook) {
    // Emit events for UI updates
    this.emit('orderbookChange', {
      symbol,
      orderbook,
      bestBidAsk: this.getBestBidAsk(symbol),
      timestamp: Date.now()
    });
  }
}

Usage Example

const orderbookManager = new OrderbookManager();
 
// First authenticate with EIP-712 signature if required
const domain = {
  name: "Synthetix",
  version: "1",
  chainId: 1,
  verifyingContract: "0x0000000000000000000000000000000000000000"
};
 
// Note: Orderbook updates are public data and typically don't require authentication
// However, some implementations may require authentication for rate limiting purposes
 
// Subscribe to orderbook updates
const subscription = {
  id: "sub-1",
  method: "subscribe",
  params: {
    type: "orderbook",
    symbol: "BTC-USDT"
  }
};
 
ws.send(JSON.stringify(subscription));
 
// Handle incoming messages
ws.onmessage = (event) => {
  const message = JSON.parse(event.data);
 
  if (message.method === "orderbook_depth_update") {
    orderbookManager.processOrderbookUpdate(message.data);
  }
};
 
// Listen for orderbook changes
orderbookManager.on('orderbookChange', (event) => {
  const { symbol, bestBidAsk } = event;
  console.log(`${symbol} Best: ${bestBidAsk.bestBid}/${bestBidAsk.bestAsk}`);
 
  // Update UI
  updateOrderbookDisplay(symbol, orderbookManager.getDepth(symbol, 20));
});

Performance Optimization

Efficient Data Structures

// Use optimized data structure for large orderbooks
class OptimizedOrderbook {
  constructor() {
    this.bids = new Map(); // Could use sorted tree for very large books
    this.asks = new Map();
    this.bidsSorted = []; // Cache sorted arrays
    this.asksSorted = [];
    this.sortedDirty = true;
  }
 
  updateLevel(side, price, quantity) {
    const book = side === 'bid' ? this.bids : this.asks;
 
    if (quantity === "0.0") {
      book.delete(price);
    } else {
      book.set(price, quantity);
    }
 
    this.sortedDirty = true;
  }
 
  getSortedLevels(side, limit = 10) {
    if (this.sortedDirty) {
      this.updateSortedCache();
    }
 
    const sorted = side === 'bid' ? this.bidsSorted : this.asksSorted;
    return sorted.slice(0, limit);
  }
 
  updateSortedCache() {
    this.bidsSorted = Array.from(this.bids.entries())
      .sort(([a], [b]) => parseFloat(b) - parseFloat(a));
 
    this.asksSorted = Array.from(this.asks.entries())
      .sort(([a], [b]) => parseFloat(a) - parseFloat(b));
 
    this.sortedDirty = false;
  }
}

Rate Limiting Updates

class ThrottledOrderbookManager extends OrderbookManager {
  constructor(throttleMs = 50) {
    super();
    this.throttleMs = throttleMs;
    this.pendingUpdates = new Map();
    this.updateTimers = new Map();
  }
 
  updateOrderbook(symbol, data) {
    // Store latest update
    this.pendingUpdates.set(symbol, data);
 
    // Clear existing timer
    if (this.updateTimers.has(symbol)) {
      clearTimeout(this.updateTimers.get(symbol));
    }
 
    // Set new timer
    const timer = setTimeout(() => {
      const latestUpdate = this.pendingUpdates.get(symbol);
      if (latestUpdate) {
        super.updateOrderbook(symbol, latestUpdate);
        this.pendingUpdates.delete(symbol);
        this.updateTimers.delete(symbol);
      }
    }, this.throttleMs);
 
    this.updateTimers.set(symbol, timer);
  }
}

Authentication

While orderbook updates are public data and typically don't require authentication, some implementations may require EIP-712 authentication for rate limiting purposes.

EIP-712 Domain Separator

All authenticated requests use the following domain separator:

{
  "name": "Synthetix",
  "version": "1",
  "chainId": 1,
  "verifyingContract": "0x0000000000000000000000000000000000000000"
}

Error Handling

Connection Recovery

handleConnectionError(symbol) {
  const orderbook = this.orderbooks.get(symbol);
 
  if (!orderbook) {
    console.error(`No orderbook for ${symbol}, requesting snapshot`);
    this.resubscribe(symbol);
    return;
  }
 
  // Clear existing orderbook and resubscribe
  this.orderbooks.delete(symbol);
  this.resubscribe(symbol);
}
 
resubscribe(symbol) {
  // Re-subscribe to get fresh snapshot
  const subscription = {
    id: "resub-" + Date.now(),
    method: "subscribe",
    params: {
      type: "orderbook",
      symbol
    }
  };
  this.ws.send(JSON.stringify(subscription));
}

Common Issues

ErrorDescriptionSolution
Connection lostWebSocket disconnectionReconnect and resubscribe
Invalid symbolSymbol not availableCheck supported markets
Stale dataOrderbook out of syncClear and resubscribe

Use Cases

Market Making

orderbookManager.on('orderbookChange', (event) => {
  const { symbol, bestBidAsk } = event;
  const spread = parseFloat(bestBidAsk.spread);
 
  if (spread > 1.0) { // Wide spread opportunity
    console.log(`Wide spread detected on ${symbol}: ${spread}`);
    // Consider placing maker orders
  }
});

Liquidity Analysis

function analyzeLiquidity(symbol, priceRange = 0.01) {
  const orderbook = orderbookManager.getOrderbook(symbol);
  const bestBidAsk = orderbookManager.getBestBidAsk(symbol);
 
  const midPrice = (parseFloat(bestBidAsk.bestBid) + parseFloat(bestBidAsk.bestAsk)) / 2;
  const rangeMin = midPrice * (1 - priceRange);
  const rangeMax = midPrice * (1 + priceRange);
 
  let bidLiquidity = 0;
  let askLiquidity = 0;
 
  // Calculate liquidity within price range
  orderbook.bids.forEach((quantity, price) => {
    const priceNum = parseFloat(price);
    if (priceNum >= rangeMin) {
      bidLiquidity += parseFloat(quantity) * priceNum;
    }
  });
 
  orderbook.asks.forEach((quantity, price) => {
    const priceNum = parseFloat(price);
    if (priceNum <= rangeMax) {
      askLiquidity += parseFloat(quantity) * priceNum;
    }
  });
 
  return {
    bidLiquidity,
    askLiquidity,
    totalLiquidity: bidLiquidity + askLiquidity,
    liquidityImbalance: (bidLiquidity - askLiquidity) / (bidLiquidity + askLiquidity)
  };
}

Implementation Notes

  • Initial State: On subscription, you receive a full orderbook snapshot
  • Incremental Updates: All subsequent messages contain changes to apply
  • Price Level Management: Remove levels when quantity is "0.0"
  • Efficient Storage: Use appropriate data structures for your use case
  • Rate Limiting: Throttle UI updates for high-frequency markets
  • Error Recovery: Implement robust error handling and recovery

Related Endpoints