Welcome to API Academy
Module 08 · Real-Time · ~22 min
Order book.
By the end of this module, your bot keeps its own copy of the order book in memory, the same view a market maker watches all day, available to your code with no round-trip to the exchange.
To get there, you’ll maintain a live CLOB book in memory from the orderbookUpdate stream, reconcile against the REST snapshot when deltas go stale, and pull the best bid/ask plus trade prints out of the merged view.
Real-Time tier · Reference cardHow do you keep a live Limitless order book in memory?
Subscribe to the market via subscribe_market_prices on the websocket and consume orderbookUpdate deltas; the SDK maintains the merged book internally, so your code reads the best bid and ask straight from bids[0] and asks[0]. For reconciliation, pull a fresh snapshot any time from GET /markets/{slug}/orderbook (MarketFetcher.getOrderBook), which returns adjustedMidpoint, the asks and bids arrays, and lastTradePrice. Limitless does not document sequence numbers on the delta stream, so staleness detection is your safety net: the module wires a watchdog that reconciles if no delta arrives for 15 seconds, plus an unconditional reconcile every 60 seconds. Recent trade prints come from GET /markets/{slug}/events, the market’s matched-trade feed. The vocabulary pays off everywhere: asks sort ascending, bids descending, spread = asks[0].price − bids[0].price, and the mid is their average.
Order book transport verified 2026-06-09.
Section 01
Anatomy of the book.
A book is where traders declare what they’re willing to do before they do it. Every resting level is a promise: “I’ll buy this many shares at this price, no higher” or “I’ll sell at this price, no lower.” Your bot’s entire view of liquidity and short-term pressure comes from this structure, so getting fluent with the vocabulary pays off on every strategy that follows. The card on the left names the parts; the ladder on the right shows them arranged the way the SDK returns them, asks climbing, bids descending, spread in between.
A prediction market book is two price ladders for one outcome token. Resting asks sit above the mid, resting bids below. The best bid and best ask define the spread; the midpoint is your reference price.
- Asks (“offers”): sellers, sorted ascending
- Spread = asks[0].price − bids[0].price
- Bids: buyers, sorted descending
- Mid = (bids[0].price + asks[0].price) / 2
will-btc-above-100k · yes
Section 02
Subscribing to depth.
Subscribe via subscribe_market_prices with your market slug and the SDK streams orderbookUpdate delta events to you. The SDK maintains a merged local book internally, you read it via its accessors. For reconciliation, pull a fresh snapshot any time from GET /markets/{slug}/orderbook. Limitless does not document sequence numbers on the delta stream, so trust the SDK’s merged book + periodic REST reconciliation.
// Module 08, Subscribe to CLOB orderbook + pull a REST snapshot.
// $ npm install @limitless-exchange/sdk
import {
HttpClient,
WebSocketClient,
MarketFetcher,
} from '@limitless-exchange/sdk';
const http = new HttpClient({ baseURL: 'https://api.limitless.exchange' });
const mf = new MarketFetcher(http);
const ws = new WebSocketClient({
url: 'wss://ws.limitless.exchange',
autoReconnect: true,
});
const MARKET = 'btc-100k-weekly';
ws.on('connect', async () => {
// One-shot REST snapshot for reference + reconciliation.
// MarketFetcher.getOrderBook wraps GET /markets/{slug}/orderbook and
// returns { adjustedMidpoint, asks[], bids[], lastTradePrice, ... }.
const snapshot = await mf.getOrderBook(MARKET);
console.log(
`snapshot bids=${snapshot.bids.length} asks=${snapshot.asks.length}`,
);
// Subscribe to live deltas. The SDK maintains the book in memory
// for you, you only consume the merged orderbookUpdate events.
ws.subscribe('subscribe_market_prices', { marketSlugs: [MARKET] });
});
ws.on('orderbookUpdate', (data) => {
// Delta payload: { marketSlug, orderbook, timestamp }
console.log('book', data.marketSlug, data.timestamp);
});
ws.connect();
# Module 08, Subscribe to CLOB orderbook + pull a REST snapshot.
# $ pip install limitless-sdk
import asyncio
from limitless_sdk.api import HttpClient
from limitless_sdk.markets import MarketFetcher
from limitless_sdk.websocket import WebSocketClient, WebSocketConfig
http = HttpClient()
market_fetcher = MarketFetcher(http)
ws_client = WebSocketClient(WebSocketConfig(
url="wss://ws.limitless.exchange",
auto_reconnect=True,
))
MARKET = "btc-100k-weekly"
@ws_client.on("connect")
async def on_connect():
# REST snapshot for reference + reconciliation.
# get_orderbook wraps GET /markets/{slug}/orderbook and returns a
# dict with "bids" and "asks" arrays (each item has "price", "size").
snapshot = await market_fetcher.get_orderbook(MARKET)
print(f"snapshot bids={len(snapshot['bids'])} asks={len(snapshot['asks'])}")
# Subscribe to live deltas, the SDK maintains the local book.
await ws_client.subscribe(
"subscribe_market_prices",
{"marketSlugs": [MARKET]},
)
@ws_client.on("orderbookUpdate")
async def on_orderbook(data):
# Delta payload: {marketSlug, orderbook, timestamp}
print("book", data["marketSlug"], data["timestamp"])
async def main():
await ws_client.connect()
await asyncio.Event().wait()
if __name__ == "__main__":
asyncio.run(main())
// Module 08, Subscribe to CLOB orderbook + pull a REST snapshot.
// $ go get github.com/limitless-labs-group/limitless-exchange-go-sdk@v1.0.5
package main
import (
"context"
"log"
limitless "github.com/limitless-labs-group/limitless-exchange-go-sdk/limitless"
)
const market = "btc-100k-weekly"
func main() {
ctx := context.Background()
// HTTP client + MarketFetcher for typed REST calls.
client := limitless.NewHttpClient()
marketFetcher := limitless.NewMarketFetcher(client)
// GetOrderBook wraps GET /markets/{slug}/orderbook and returns
// a typed struct with Bids / Asks / AdjustedMidpoint / LastTradePrice.
snapshot, err := marketFetcher.GetOrderBook(ctx, market)
if err != nil {
log.Fatal(err)
}
log.Printf("snapshot bids=%d asks=%d", len(snapshot.Bids), len(snapshot.Asks))
// WebSocket for live deltas. The SDK applies deltas to its internal
// book, the callback gives you the merged view each update.
ws := limitless.NewWebSocketClient()
defer ws.Disconnect()
ws.OnOrderbookUpdate(func(u limitless.OrderbookUpdate) {
log.Printf("book %s", u.MarketSlug)
})
if err := ws.Connect(ctx); err != nil {
log.Fatal(err)
}
_ = ws.Subscribe(ctx, limitless.ChannelSubscribeMarketPrices, limitless.SubscriptionOptions{
MarketSlugs: []string{market},
})
select {}
}
How to run this
- Replace btc-100k-weekly with the slug of a currently-active CLOB market. Both the REST call and the WS feed are public, no API key required unless you want to layer authenticated channels on later.
- Save the snippet above as stream-orderbook.ts, then run npx tsx stream-orderbook.ts.
- Save the snippet above as stream_orderbook.py, then run python stream_orderbook.py.
- Save the snippet above as main.go inside a Go module, then run go run main.go.
- You see one snapshot bids=N asks=M line at startup, then a book line with a timestamp every time the book moves. Quiet markets may only tick once a minute, that’s the market, not the stream.
Section 03
Applying incremental updates.
Deltas come as { price, size } rows per side. Size 0 means “delete this level”, non-zero means “replace”. Check the sequence on every message; if you see prev + 2, you missed a delta and must resnapshot.
// Module 08, Delta updates + periodic REST reconciliation.
//
// NOTE: Limitless does not document sequence numbers on the
// orderbookUpdate stream. The SDK applies deltas to its internal
// book for you, so your job isn't "detect gaps", it's "notice
// staleness" and reconcile against the REST endpoint on a timer.
import { HttpClient, MarketFetcher, WebSocketClient } from '@limitless-exchange/sdk';
const http = new HttpClient({ baseURL: 'https://api.limitless.exchange' });
const mf = new MarketFetcher(http);
const ws = new WebSocketClient({ url: 'wss://ws.limitless.exchange', autoReconnect: true });
const MARKET = 'btc-100k-weekly';
let lastUpdate = 0; // ms since epoch of last delta
const STALE_MS = 15_000; // if no delta in 15s, reconcile
const RECON_MS = 60_000; // and always reconcile every minute
ws.on('orderbookUpdate', (data) => {
lastUpdate = Date.now();
// The SDK has already merged this delta into its internal book.
// Consume `data.orderbook` as your merged view for signal logic.
});
async function reconcile() {
// Pull a fresh snapshot via MarketFetcher.getOrderBook (wraps
// GET /markets/{slug}/orderbook) and diff against the SDK's view.
// Use this as your staleness safety net, the server doesn't
// persist subscriptions, and there's no sequence number to trust.
const snapshot = await mf.getOrderBook(MARKET);
console.log(`reconciled bids=${snapshot.bids.length} asks=${snapshot.asks.length}`);
}
setInterval(() => {
if (Date.now() - lastUpdate > STALE_MS) {
console.warn('book looks stale, reconciling');
reconcile().catch(console.error);
}
}, 5_000);
setInterval(reconcile, RECON_MS);
ws.connect();
# Module 08, Delta updates + periodic REST reconciliation.
#
# NOTE: Limitless does not document sequence numbers on the
# orderbookUpdate stream. The SDK maintains the local book for
# you, so your job isn't "detect gaps", it's "notice staleness"
# and reconcile against the REST endpoint on a timer.
import asyncio
import time
from limitless_sdk.api import HttpClient
from limitless_sdk.markets import MarketFetcher
from limitless_sdk.websocket import WebSocketClient, WebSocketConfig
http = HttpClient()
market_fetcher = MarketFetcher(http)
ws_client = WebSocketClient(WebSocketConfig(
url="wss://ws.limitless.exchange",
auto_reconnect=True,
))
MARKET = "btc-100k-weekly"
STALE_SEC = 15
RECON_SEC = 60
last_update = 0.0
@ws_client.on("orderbookUpdate")
async def on_orderbook(data):
global last_update
last_update = time.time()
# data["orderbook"] already reflects the merged book, consume it directly.
async def reconcile() -> None:
# MarketFetcher.get_orderbook wraps GET /markets/{slug}/orderbook
# and returns a dict with "bids" and "asks" arrays.
snapshot = await market_fetcher.get_orderbook(MARKET)
print(f"reconciled bids={len(snapshot['bids'])} asks={len(snapshot['asks'])}")
async def watchdog() -> None:
while True:
await asyncio.sleep(5)
if time.time() - last_update > STALE_SEC:
print("book looks stale, reconciling")
try:
await reconcile()
except Exception as e:
print("reconcile failed:", e)
async def heartbeat_reconcile() -> None:
while True:
await asyncio.sleep(RECON_SEC)
await reconcile()
async def main():
await ws_client.connect()
await asyncio.gather(watchdog(), heartbeat_reconcile())
if __name__ == "__main__":
asyncio.run(main())
// Module 08, Delta updates + periodic REST reconciliation.
//
// NOTE: Limitless does not document sequence numbers on the
// orderbookUpdate stream. The SDK keeps the local book merged,
// so your job isn't "detect gaps", it's "notice staleness"
// and reconcile against GET /markets/{slug}/orderbook.
package main
import (
"context"
"log"
"sync/atomic"
"time"
limitless "github.com/limitless-labs-group/limitless-exchange-go-sdk/limitless"
)
const market = "btc-100k-weekly"
var lastUpdate atomic.Int64 // UnixMilli
func reconcile(ctx context.Context, mf *limitless.MarketFetcher) {
// GetOrderBook wraps GET /markets/{slug}/orderbook and returns a
// typed struct with Bids / Asks / AdjustedMidpoint / LastTradePrice.
snap, err := mf.GetOrderBook(ctx, market)
if err != nil {
log.Println("reconcile failed:", err)
return
}
log.Printf("reconciled bids=%d asks=%d", len(snap.Bids), len(snap.Asks))
}
func main() {
ctx := context.Background()
client := limitless.NewHttpClient()
mf := limitless.NewMarketFetcher(client)
ws := limitless.NewWebSocketClient()
defer ws.Disconnect()
ws.OnOrderbookUpdate(func(u limitless.OrderbookUpdate) {
lastUpdate.Store(time.Now().UnixMilli())
// u.Orderbook is the merged view, consume directly.
})
if err := ws.Connect(ctx); err != nil {
log.Fatal(err)
}
_ = ws.Subscribe(ctx, limitless.ChannelSubscribeMarketPrices, limitless.SubscriptionOptions{
MarketSlugs: []string{market},
})
// Stale watchdog.
go func() {
t := time.NewTicker(5 * time.Second)
defer t.Stop()
for range t.C {
if time.Now().UnixMilli()-lastUpdate.Load() > 15_000 {
log.Println("book looks stale, reconciling")
reconcile(ctx, mf)
}
}
}()
// Heartbeat reconciliation.
go func() {
t := time.NewTicker(60 * time.Second)
defer t.Stop()
for range t.C {
reconcile(ctx, mf)
}
}()
select {}
}
How to run this
- No API key required, both the REST snapshot and the WS delta stream are public. Pick an active MARKET slug that actually trades so the watchdog has something to reconcile against.
- Save the snippet above as reconcile-book.ts, then run npx tsx reconcile-book.ts.
- Save the snippet above as reconcile_book.py, then run python reconcile_book.py.
- Save the snippet above as main.go inside a Go module, then run go run main.go.
- You see reconciled bids=N asks=M once a minute even when the stream is flowing. If the WS goes silent for 15 seconds the watchdog logs book looks stale and fires an extra reconcile, that’s your safety net working.
Trust the sequence number, not your clock.
Out-of-order UDP-like behaviour doesn’t exist on a WebSocket, but dropped messages do. The moment you see a gap, drop your local book and re-request a snapshot. Never try to “patch” around a missing delta.
Section 04
Best bid / ask & trades feed.
If you only care about the touch, read book.bids[0] and book.asks[0] from the merged book the SDK maintains for you. For AMM price moves, the lighter-weight newPriceData event fires with the updated YES/NO prices directly. There is no market-wide trade-print channel on the websocket; for the trade tape, poll the REST feed GET /markets/{slug}/events.
// Module 08, Touch prices + live trade prints.
//
// For the touch (top of book) on AMM markets, newPriceData is
// the lightest feed: a delta with {yes, no} after every block.
// For CLOB touch, read `orderbook.bids[0]` / `orderbook.asks[0]`
// from the orderbookUpdate payload. For individual prints the
// SDK exposes the `tx` transaction event.
import { WebSocketClient } from '@limitless-exchange/sdk';
const ws = new WebSocketClient({
url: 'wss://ws.limitless.exchange',
autoReconnect: true,
});
const MARKETS = ['btc-100k-weekly'];
ws.on('connect', () => {
ws.subscribe('subscribe_market_prices', { marketSlugs: MARKETS });
});
// AMM touch, fires on every block with the new yes/no prices.
ws.on('newPriceData', (d) => {
console.log(`[PRC] ${d.marketAddress} yes=${d.updatedPrices.yes} no=${d.updatedPrices.no}`);
});
// CLOB touch, best bid/ask comes from the merged orderbook delta.
ws.on('orderbookUpdate', (d) => {
const bid = d.orderbook.bids?.[0];
const ask = d.orderbook.asks?.[0];
if (bid && ask) {
console.log(`[BBO] ${d.marketSlug} bid=${bid.price}x${bid.size} ask=${ask.price}x${ask.size}`);
}
});
ws.connect();
# Module 08, Touch prices + live order book.
#
# AMM touch: newPriceData is the lightest feed, yes/no delta per block.
# CLOB touch: read orderbook.bids[0] / asks[0] from orderbookUpdate.
import asyncio
from limitless_sdk.websocket import WebSocketClient, WebSocketConfig
ws_client = WebSocketClient(WebSocketConfig(
url="wss://ws.limitless.exchange",
auto_reconnect=True,
))
MARKETS = ["btc-100k-weekly"]
@ws_client.on("connect")
async def on_connect():
await ws_client.subscribe(
"subscribe_market_prices",
{"marketSlugs": MARKETS},
)
@ws_client.on("newPriceData")
async def on_price(data):
prices = data["updatedPrices"]
print(f"[PRC] {data['marketAddress']} yes={prices['yes']} no={prices['no']}")
@ws_client.on("orderbookUpdate")
async def on_orderbook(data):
book = data["orderbook"]
bid = (book.get("bids") or [None])[0]
ask = (book.get("asks") or [None])[0]
if bid and ask:
print(f"[BBO] {data['marketSlug']} bid={bid['price']}x{bid['size']} "
f"ask={ask['price']}x{ask['size']}")
async def main():
await ws_client.connect()
await asyncio.Event().wait()
if __name__ == "__main__":
asyncio.run(main())
// Module 08, Touch prices + live order book.
//
// The Go SDK exposes typed callbacks instead of the raw event name:
// OnNewPriceData , AMM yes/no deltas
// OnOrderbookUpdate , CLOB merged book; first row = touch
// Both arrive on the single market-prices subscription. There is no
// market-wide trade-print channel; for the trade tape, poll the REST
// feed GET /markets/{slug}/events.
package main
import (
"context"
"log"
limitless "github.com/limitless-labs-group/limitless-exchange-go-sdk/limitless"
)
func main() {
ctx := context.Background()
ws := limitless.NewWebSocketClient()
defer ws.Disconnect()
ws.OnNewPriceData(func(p limitless.NewPriceData) {
if len(p.UpdatedPrices) > 0 {
log.Printf("[PRC] %s yes=%.3f no=%.3f",
p.MarketAddress, p.UpdatedPrices[0].YesPrice, p.UpdatedPrices[0].NoPrice)
}
})
ws.OnOrderbookUpdate(func(u limitless.OrderbookUpdate) {
if len(u.Orderbook.Bids) > 0 && len(u.Orderbook.Asks) > 0 {
bid, ask := u.Orderbook.Bids[0], u.Orderbook.Asks[0]
log.Printf("[BBO] %s bid=%.3f x%.2f ask=%.3f x%.2f",
u.MarketSlug, bid.Price, bid.Size, ask.Price, ask.Size)
}
})
if err := ws.Connect(ctx); err != nil {
log.Fatal(err)
}
// CLOB orderbook + AMM price updates both arrive on this one subscription.
if err := ws.Subscribe(ctx, limitless.ChannelSubscribeMarketPrices, limitless.SubscriptionOptions{
MarketSlugs: []string{"btc-100k-weekly"},
}); err != nil {
log.Fatal(err)
}
select {}
}
How to run this
- Pick a MARKETS slug that has both AMM liquidity (for newPriceData) and CLOB depth (for orderbookUpdate). All channels used here are public, no LIMITLESS_API_KEY needed.
- Save the snippet above as bbo-feed.ts, then run npx tsx bbo-feed.ts.
- Save the snippet above as bbo_feed.py, then run python bbo_feed.py.
- Save the snippet above as main.go inside a Go module, then run go run main.go.
- You see interleaved [PRC] (AMM yes/no), [BBO] (CLOB touch), and, in Go, [TRD] lines. Each type fires independently, so one going quiet doesn’t mean the others are broken.
Order book streaming: what people ask
Each answer also ships invisibly as schema.org FAQ data for search engines and AI assistants. Tap a question to expand.
-
How do you get the best bid and ask on a Limitless market?
For CLOB markets, readbids[0]andasks[0]from the merged book the SDK maintains fromorderbookUpdatedeltas; bids sort descending and asks ascending, so the first row of each side is the touch. For AMM price moves, the lighter-weightnewPriceDataevent fires with the updated YES/NO prices directly. The spread isasks[0].priceminusbids[0].price, and the mid is the average of the two. -
What does GET /markets/{slug}/orderbook return?
A one-shot snapshot of the full book:adjustedMidpoint, theasksandbidsarrays (each level a price and size), andlastTradePrice. The SDK wraps it asMarketFetcher.getOrderBook(TypeScript),get_orderbook(Python), orGetOrderBook(Go). It’s public, no API key required, and it’s the reference you reconcile your streamed book against. -
What does a size of 0 mean in an orderbook delta?
Delete that price level; a non-zero size replaces the level outright. Deltas arrive as{ price, size }rows per side. On Limitless you don’t apply them by hand, the SDK merges each delta into its internal book and hands you the merged view, but knowing the semantics matters when you log raw payloads or debug why a level vanished. -
How does a bot detect a stale order book?
Track the timestamp of the last delta and treat silence as suspicion. The module’s watchdog checks every 5 seconds and reconciles againstGET /markets/{slug}/orderbookif no delta arrived in 15 seconds, plus an unconditional reconcile every 60 seconds. Limitless does not document sequence numbers on the stream, so REST reconciliation is the safety net, not an optimisation. A quiet market can legitimately tick once a minute, so reconcile rather than panic. -
Do you have to apply order book deltas yourself?
No. The SDK appliesorderbookUpdatedeltas to its internal book and exposes the merged view on each event, so there’s no hand-rolled bids/asks dictionary to keep in sync. Your job is narrower: subscribe with the market slug, consume the merged view for signal logic, and pull a fresh REST snapshot to reconcile when the stream goes quiet or after a reconnect.
Section 05
Module checklist.
Tick each item once you’ve actually done it. The Continue button unlocks at 5/5.
I can sketch a YES book and point out best bid, best ask, spread, and mid
I subscribed via subscribe_market_prices and read the merged book the SDK maintains
I handle orderbookUpdate deltas through the SDK (no manual delta application)
On staleness or reconnect, I pull a fresh snapshot from GET /markets/{slug}/orderbook to reconcile
I know when to use newPriceData (AMM touch) vs orderbookUpdate (CLOB depth)
Module 08 complete
Book in memory.
Your bot has the full book in front of it. When it needs to know whether a fill is likely or where to rest a quote, the answer is local, no race condition with the exchange, no stale snapshot, no waiting on a REST call.
Concretely, your local book stays in sync with the exchange. Here’s what you walk away with:
A streaming client that consumes orderbookUpdate deltas and exposes the merged book the SDK maintains, no hand-rolled bids/asks dictionary to keep in sync.
A staleness watchdog plus a 60-second reconcile timer that pulls GET /markets/{slug}/orderbook and catches drift before your strategy trades on it.
A clear mental split between newPriceData for AMM touch and orderbookUpdate for CLOB depth, so you pick the right feed for each strategy in Tier 3.
Next up: keeping the pipe open under load, rate limits, token buckets, and the jittered backoff pattern every SDK retry helper uses.
Complete the checklist above to unlock