Market data

Markets, order books, trades, candles and prices over REST and WebSocket.

All market data is public – no keys or auth needed.

Discover markets

curl "https://mainnet.zklighter.elliot.ai/api/v1/orderBookDetails"

orderBookDetails returns order_book_details (perps) and spot_order_book_details (spot). The fields you'll use most:

FieldMeaning
market_idIndex used everywhere else
size_decimals, price_decimalsInteger scaling for orders (see Core concepts)
min_base_amount, min_quote_amountOrder minimums (maker orders)
statusOnly trade active markets

Cache this at startup and refresh periodically – new markets are listed regularly. Never hard-code market IDs other than for tests.

Stream over WebSocket

const ws = new WebSocket("wss://mainnet.zklighter.elliot.ai/stream");

ws.onopen = () => {
  ws.send(JSON.stringify({ type: "subscribe", channel: "order_book/0" }));
  ws.send(JSON.stringify({ type: "subscribe", channel: "trade/0" }));
  ws.send(JSON.stringify({ type: "subscribe", channel: "market_stats/all" }));
};

// Keepalive: send something at least every 2 minutes
setInterval(() => ws.send(JSON.stringify({ type: "ping" })), 60_000);

ws.onmessage = (e) => {
  const msg = JSON.parse(e.data);
  switch (msg.type) {
    case "subscribed/order_book": book.init(msg.order_book);  break; // full snapshot
    case "update/order_book":     book.apply(msg.order_book); break; // deltas
    case "update/trade":          /* new trades */ break;
    case "update/market_stats":   /* mark, index, funding, 24h stats */ break;
  }
};

Every channel and its payload is documented in the WebSocket reference. Always reconnect and resubscribe automatically: deployments can drop connections.

Keeping an order book in sync

The order_book channel sends a full snapshot on subscribe, then only changed levels. A level's size is its new total, and "0" means remove it. Each update's begin_nonce must equal the previous message's nonce; if it doesn't, you missed an update and must resubscribe. Use nonce for continuity, not offset (see Order Book).

A minimal implementation:

type Level = { price: string; size: string };
type BookMsg = { asks: Level[]; bids: Level[]; nonce: number; begin_nonce?: number };

const book = {
  asks: new Map<string, string>(), // price -> size
  bids: new Map<string, string>(),
  nonce: -1,

  init(ob: BookMsg) {
    this.asks.clear(); this.bids.clear();
    this.merge(ob);
    this.nonce = ob.nonce;
  },

  apply(ob: BookMsg) {
    if (this.nonce < 0) return;                 // no snapshot yet
    if (ob.begin_nonce !== this.nonce) {        // gap: resync
      this.nonce = -1;
      ws.send(JSON.stringify({ type: "unsubscribe", channel: "order_book/0" }));
      ws.send(JSON.stringify({ type: "subscribe",   channel: "order_book/0" }));
      return;
    }
    this.merge(ob);
    this.nonce = ob.nonce;
  },

  merge(ob: BookMsg) {
    for (const [side, levels] of [[this.asks, ob.asks], [this.bids, ob.bids]] as const) {
      for (const { price, size } of levels) {
        if (Number(size) === 0) side.delete(price); else side.set(price, size);
      }
    }
  },
};

Sort the maps by price when you need best bid/ask or a depth view.

REST alternatives

For polling or backfill, use orderBookOrders, recentTrades, candles, fundings and exchangeStats. Mind the rate limits: Standard accounts get only 60 requests/minute – prefer WebSocket.

Using lighter-ts

elliottech/lighter-ts open-sources Lighter's own client-side state store and WebSocket pipeline (see SDK). If you use it, note that markets have a multiplier, so its "display" values differ from wire values:

  • display_size = real_size × multiplier
  • display_price = real_price ÷ multiplier

Keep conversion in one place – mixing real and display numbers is the most common pricing bug.


Did this page help you?