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:
| Field | Meaning |
|---|---|
market_id | Index used everywhere else |
size_decimals, price_decimals | Integer scaling for orders (see Core concepts) |
min_base_amount, min_quote_amount | Order minimums (maker orders) |
status | Only 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 × multiplierdisplay_price = real_price ÷ multiplier
Keep conversion in one place – mixing real and display numbers is the most common pricing bug.
Updated 2 days ago
