Welcome to API Academy
Module 07 · Real-Time · ~22 min
Websockets.
By the end of this module, your bot reacts the instant a price moves on Limitless, instead of asking the exchange “what’s happening now?” on a timer and missing everything in between.
To get there, you’ll open a socket.io connection to wss://ws.limitless.exchange, subscribe to live price and orderbook feeds, and restore those subscriptions after every reconnect. Replace polling with a push-based stream, the foundation every real-time strategy in this tier builds on.
Real-Time tier · Reference cardHow do you connect to the Limitless websocket?
Open a socket.io connection to wss://ws.limitless.exchange on the /markets namespace; the SDK’s WebSocketClient wraps the handshake, and public feeds need no API key. Subscribe with subscribe_market_prices for live data: newPriceData events carry AMM price deltas (marketAddress, updatedPrices, blockNumber, timestamp) and orderbookUpdate events carry CLOB book deltas per market slug. The authenticated subscribe_positions channel requires your API key, which the SDK attaches as a connection header. Two rules keep the stream honest: subscribe_market_prices REPLACES your previous subscription, so send every address and slug in a single call; and the server forgets all subscriptions on every reconnect, so re-issue them from the connect handler (autoReconnect restores the socket, not the channels). Keep one connection per process; one socket covers as many markets as you care to stream.
WS transport verified 2026-06-09 against the backend gateway + SDK.
Section 01
Why streams beat polling.
Polling asks the server for state on a schedule; streaming lets the server push state as it changes. For anything where latency matters, executing on a signal, unwinding into a move, market-making on a shifting fair value, polling is structurally behind. The two cards below compare the tradeoffs honestly; WebSockets aren’t always the right choice. If your strategy is a cron job that snapshots the market once an hour, REST is fine. Everything else in this tier assumes you’ve moved to a stream.
REST polling
- Best case latency = your poll interval
- Burns rate limit on quiet markets
- Misses intra-interval moves entirely
- Scales linearly with market count
- Dead simple, fine for cron jobs
WS streams
- Push-based, deltas arrive in ms
- One connection covers many markets
- Lets you see every print, not a snapshot
- Heartbeats prove the pipe is alive
- Needs reconnect & resubscribe logic
Section 02
Connecting.
Open a secure WebSocket to wss://ws.limitless.exchange on the /markets namespace (socket.io-based). Pass your API key to the SDK client constructor, the SDK attaches it as a connection header. Public feeds (subscribe_market_prices) work without auth; subscribe_positions requires it. Keep the connection per process, don’t reopen it on every tick.
// Module 07, Connecting to the Limitless WebSocket.
// $ npm install @limitless-exchange/sdk
import { WebSocketClient } from '@limitless-exchange/sdk';
// The SDK wraps socket.io under the hood and targets the
// /markets namespace. Public feeds (prices, orderbook) need
// no auth; authenticated feeds (positions) require an API key.
const ws = new WebSocketClient({
url: 'wss://ws.limitless.exchange',
apiKey: process.env.LIMITLESS_API_KEY, // optional for public data
autoReconnect: true,
});
ws.on('connect', () => console.log('ws open'));
ws.on('disconnect', (why) => console.log('ws closed:', why));
ws.on('exception', (err) => console.error('stream error:', err));
// Fire the connect, subscriptions happen in Section 03.
ws.connect();
# Module 07, Connecting to the Limitless WebSocket.
# $ pip install limitless-sdk
import asyncio
import os
from limitless_sdk.websocket import WebSocketClient, WebSocketConfig
# WebSocketConfig targets wss://ws.limitless.exchange on the
# /markets namespace under the hood. auto_reconnect=True wires
# up exponential backoff for you (see Section 04).
config = WebSocketConfig(
url="wss://ws.limitless.exchange",
auto_reconnect=True,
reconnect_delay=5,
)
ws_client = WebSocketClient(config)
@ws_client.on("connect")
async def on_connect() -> None:
print("ws open")
@ws_client.on("disconnect")
async def on_disconnect() -> None:
print("ws closed")
@ws_client.on("exception")
async def on_exception(err) -> None:
print("stream error:", err)
async def main() -> None:
# API key is optional for public feeds.
await ws_client.connect(api_key=os.environ.get("LIMITLESS_API_KEY"))
await asyncio.Event().wait() # keep the task alive
if __name__ == "__main__":
asyncio.run(main())
// Module 07, Connecting to the Limitless WebSocket.
// $ go get github.com/limitless-labs-group/limitless-exchange-go-sdk@v1.0.5
package main
import (
"context"
"log"
"os"
limitless "github.com/limitless-labs-group/limitless-exchange-go-sdk/limitless"
)
func main() {
// WithWebSocketAPIKey is optional, public feeds work without it.
ws := limitless.NewWebSocketClient(
limitless.WithWebSocketAPIKey(os.Getenv("LIMITLESS_API_KEY")),
)
ctx := context.Background()
if err := ws.Connect(ctx); err != nil {
log.Fatalf("ws connect: %v", err)
}
defer ws.Disconnect()
log.Println("ws open, connected to wss://ws.limitless.exchange")
// Channel callbacks + Subscribe() calls happen in Section 03.
select {} // block forever
}
How to run this
- The client points at wss://ws.limitless.exchange. An API key is only required for authenticated channels like subscribe_positions, if you want to test those too, set LIMITLESS_API_KEY in your environment.
- Save the snippet above as ws-connect.ts, then run npx tsx ws-connect.ts.
- Save the snippet above as ws_connect.py, then run python ws_connect.py.
- Save the snippet above as main.go inside a Go module, then run go run main.go.
- You see ws open in the terminal and the process keeps running. The socket is alive but silent, no subscriptions yet, that’s Section 03.
Section 03
Subscribing.
After the auth handshake, send a subscribe message with the channel and the slugs you care about. The server replies with a one-shot snapshot, then incremental updates as state changes. Route each message by msg.type.
// Module 07, Subscribing to price + orderbook streams.
// GOTCHA: subscribe_market_prices REPLACES the previous
// subscription. If you want AMM prices AND CLOB orderbooks
// for multiple markets, send ALL addresses + slugs in a
// single call.
ws.on('newPriceData', (data) => {
// AMM price delta: { marketAddress, updatedPrices: {yes, no}, blockNumber, timestamp }
console.log('price', data.marketAddress, data.updatedPrices);
});
ws.on('orderbookUpdate', (data) => {
// CLOB delta: { marketSlug, orderbook, timestamp }
console.log('book', data.marketSlug);
});
ws.on('positions', (data) => {
// Authenticated snapshot, AMM or CLOB variant
console.log('positions', data.type, data);
});
// One call, all markets, both addresses and slugs allowed.
ws.subscribe('subscribe_market_prices', {
marketAddresses: ['0x76d3e2098Be66Aa7E15138F467390f0Eb7349B9b'],
marketSlugs: ['btc-100k-weekly', 'us-cpi-oct-above-3'],
});
// Authenticated, requires apiKey on the client.
ws.subscribe('subscribe_positions');
# Module 07, Subscribing to price + orderbook streams.
# GOTCHA: subscribe_market_prices REPLACES the previous
# subscription. Send every market in one call.
@ws_client.on("newPriceData")
async def on_price(data):
# AMM delta: {marketAddress, updatedPrices: {yes, no}, blockNumber, timestamp}
print("price", data["marketAddress"], data["updatedPrices"])
@ws_client.on("orderbookUpdate")
async def on_orderbook(data):
# CLOB delta: {marketSlug, orderbook, timestamp}
print("book", data["marketSlug"])
@ws_client.on("positions")
async def on_positions(data):
# Authenticated snapshot, AMM or CLOB variant
print("positions", data["type"])
# Subscribe from inside the connect handler so reconnects
# automatically restore the subscription (see Section 04).
@ws_client.on("connect")
async def on_connect():
await ws_client.subscribe(
"subscribe_market_prices",
{
"marketAddresses": ["0x76d3e2098Be66Aa7E15138F467390f0Eb7349B9b"],
"marketSlugs": ["btc-100k-weekly", "us-cpi-oct-above-3"],
},
)
await ws_client.subscribe("subscribe_positions")
// Module 07, Subscribing to price + orderbook streams.
// The Go SDK models each stream as a typed channel constant:
// ChannelSubscribeMarketPrices, ChannelSubscribePositions,
// ChannelSubscribeTransactions, ChannelSubscribeOrderEvents.
// CLOB orderbook updates and AMM price deltas both arrive on the
// market-prices subscription.
ws.OnNewPriceData(func(p limitless.NewPriceData) {
if len(p.UpdatedPrices) > 0 {
log.Printf("price %s yes=%.3f no=%.3f",
p.MarketAddress, p.UpdatedPrices[0].YesPrice, p.UpdatedPrices[0].NoPrice)
}
})
ws.OnOrderbookUpdate(func(u limitless.OrderbookUpdate) {
log.Printf("book %s", u.MarketSlug)
})
// Subscribe, channels are typed constants, not strings.
if err := ws.Subscribe(ctx, limitless.ChannelSubscribeMarketPrices, limitless.SubscriptionOptions{
MarketSlugs: []string{"btc-100k-weekly", "us-cpi-oct-above-3"},
}); err != nil {
log.Fatal(err)
}
How to run this
- Paste this snippet after the Section 02 connect code so the ws / ws_client client exists. subscribe_positions requires LIMITLESS_API_KEY, drop that line if you only want public price data. Swap the marketSlugs for markets that are currently active.
- Save the combined file as subscribe-markets.ts, then run npx tsx subscribe-markets.ts.
- Save the combined file as subscribe_markets.py, then run python subscribe_markets.py.
- Save the combined file as main.go inside a Go module, then run go run main.go.
- You see a stream of price lines for each AMM block and book lines whenever a CLOB level changes. Quiet markets may log one line per minute, that’s normal, not broken.
Section 04
Heartbeats & reconnect.
Connections die. Networks flap. The question is whether your bot notices and recovers. Send a ping every 15 seconds, expect a pong back, and reconnect with exponential backoff on any close or missing pong.
Pipe alive
ping 15s · backoff capped at 30s
// Module 07, Reconnects are automatic. YOU resubscribe.
//
// socket.io handles heartbeats internally, no manual ping/pong.
// autoReconnect: true gives you exponential backoff for free.
// What the SDK does NOT do is re-issue your subscriptions, so
// the server forgets them on every reconnect.
import { WebSocketClient } from '@limitless-exchange/sdk';
const ws = new WebSocketClient({
url: 'wss://ws.limitless.exchange',
apiKey: process.env.LIMITLESS_API_KEY,
autoReconnect: true,
});
const SLUGS = ['btc-100k-weekly', 'us-cpi-oct-above-3'];
function restoreSubscriptions() {
// Single call, subscribe_market_prices replaces the previous sub.
ws.subscribe('subscribe_market_prices', { marketSlugs: SLUGS });
ws.subscribe('subscribe_positions');
}
ws.on('connect', () => { console.log('connected'); restoreSubscriptions(); });
ws.on('reconnect', () => { console.log('reconnected'); restoreSubscriptions(); });
ws.on('orderbookUpdate', (d) => { /* apply delta */ });
ws.on('exception', (e) => console.error('ws:', e));
ws.connect();
# Module 07, Reconnects are automatic. YOU resubscribe.
#
# The SDK wraps socket.io, which handles heartbeats itself.
# auto_reconnect=True wires up exponential backoff. But the
# server does NOT persist subscriptions across reconnects,
# you re-issue them inside the "connect" handler, which fires
# on every (re)connection.
import asyncio
import os
from limitless_sdk.websocket import WebSocketClient, WebSocketConfig
config = WebSocketConfig(
url="wss://ws.limitless.exchange",
auto_reconnect=True,
reconnect_delay=5,
)
ws_client = WebSocketClient(config)
SLUGS = ["btc-100k-weekly", "us-cpi-oct-above-3"]
@ws_client.on("connect")
async def on_connect():
print("connected, (re)subscribing")
# Both events go in a single subscribe_market_prices call
# because subsequent subscribe_market_prices messages REPLACE
# the previous subscription.
await ws_client.subscribe(
"subscribe_market_prices",
{"marketSlugs": SLUGS},
)
await ws_client.subscribe("subscribe_positions")
@ws_client.on("orderbookUpdate")
async def on_orderbook(data):
pass # apply delta …
@ws_client.on("exception")
async def on_exception(err):
print("ws:", err)
async def main():
await ws_client.connect(api_key=os.environ["LIMITLESS_API_KEY"])
await asyncio.Event().wait()
if __name__ == "__main__":
asyncio.run(main())
// Module 07, Reconnects are automatic. YOU resubscribe.
//
// NewWebSocketClient uses socket.io under the hood, so
// heartbeats are managed for you and backoff is built in.
// Auto-reconnect is built in: the client records your subscriptions and
// replays them on every reconnect, so you subscribe once.
package main
import (
"context"
"log"
"os"
limitless "github.com/limitless-labs-group/limitless-exchange-go-sdk/limitless"
)
var slugs = []string{"btc-100k-weekly", "us-cpi-oct-above-3"}
func subscribeAll(ctx context.Context, ws *limitless.WebSocketClient) {
// CLOB orderbook + AMM price deltas both arrive on this subscription.
if err := ws.Subscribe(ctx, limitless.ChannelSubscribeMarketPrices, limitless.SubscriptionOptions{
MarketSlugs: slugs,
}); err != nil {
log.Println("subscribe:", err)
}
}
func main() {
ctx := context.Background()
ws := limitless.NewWebSocketClient(
limitless.WithWebSocketAPIKey(os.Getenv("LIMITLESS_API_KEY")),
limitless.WithAutoReconnect(true),
)
defer ws.Disconnect()
ws.OnOrderbookUpdate(func(u limitless.OrderbookUpdate) {
// apply delta …
})
if err := ws.Connect(ctx); err != nil {
log.Fatal(err)
}
// Subscribe once. The SDK records your subscriptions and replays them
// automatically on every reconnect, so you never re-subscribe by hand.
subscribeAll(ctx, ws)
select {} // block forever
}
How to run this
- Set LIMITLESS_API_KEY so subscribe_positions works. Keep the SLUGS array short (one or two active markets) while you test.
- Save the snippet above as reconnecting-client.ts, then run npx tsx reconnecting-client.ts.
- Save the snippet above as reconnecting_client.py, then run python reconnecting_client.py.
- Save the snippet above as main.go inside a Go module, then run go run main.go.
- You see connected / (re)connected logs followed by deltas. Drop your Wi-Fi for a few seconds and the client should re-emit a connect log and resume the stream, proof that restoreSubscriptions() ran.
Limitless websockets: what people ask
Each answer also ships invisibly as schema.org FAQ data for search engines and AI assistants. Tap a question to expand.
-
Do you need an API key for the Limitless websocket?
Only for authenticated channels. Public feeds,subscribe_market_priceswith itsnewPriceDataandorderbookUpdateevents, work with no auth at all.subscribe_positions(your own positions) requires an API key, which you pass to the SDK client constructor; the SDK attaches it as a connection header. SetLIMITLESS_API_KEYin the environment if you want both. -
What events does the Limitless websocket send?
Three you’ll handle constantly:newPriceData, the AMM price delta withmarketAddress,updatedPrices(yes/no),blockNumber, andtimestamp;orderbookUpdate, the CLOB delta withmarketSlug,orderbook, andtimestamp; andpositions, the authenticated snapshot of your own holdings. Route each message by type, and expect quiet markets to log one line a minute, that’s the market, not a broken stream. -
Why does a bot stop receiving prices after a websocket reconnect?
Because subscriptions are per-connection. When the socket drops, a wifi blip, a server restart, a missed heartbeat, the server forgets every channel you subscribed to, and reconnecting opens a fresh, empty session.autoReconnectrestores the socket, not the channels, so re-issue every subscribe from theconnecthandler, which fires on the first connect and every reconnect. Test it by killing your network for 30 seconds and confirming new prices arrive after. -
Why must all markets go in one subscribe_market_prices call?
Because eachsubscribe_market_pricesmessage REPLACES the previous subscription instead of adding to it. Subscribe to market A, then market B in a second call, and you silently stop receiving A. If you want AMM prices and CLOB orderbooks for multiple markets, send all themarketAddressesandmarketSlugsin a single call, and keep that list in one place so the reconnect handler resubscribes correctly. -
When is REST polling fine instead of a websocket?
When latency doesn’t matter: a cron job that snapshots the market once an hour is fine on REST. For anything where reaction time counts, executing on a signal, unwinding into a move, market-making on a shifting fair value, polling is structurally behind: its best-case latency is your poll interval, it burns rate limit on quiet markets, and it misses every move between polls.
Section 05
Module checklist.
Tick each item once you’ve actually done it. The Continue button unlocks at 5/5.
I can explain when polling is fine and when streams are mandatory
I opened a WS connection to the stream URL and authed with my API key
I subscribed via subscribe_market_prices and handled the newPriceData and orderbookUpdate events
My client sends heartbeats and I can see the pong roundtrip
My reconnect loop uses exponential backoff with jitter and resubscribes after reconnect
Module 07 complete
Streams flowing.
Your bot can hear the market in real time. When a price ticks or the book shifts, your code knows within milliseconds, fast enough to act on what other traders are seeing at the same moment, instead of after they’ve already moved.
Concretely, you can subscribe to live market updates and parse the message stream. Here’s what you walk away with:
A WebSocketClient pointed at wss://ws.limitless.exchange with autoReconnect on, one process, one connection, as many markets as you care to stream.
Handlers wired up for newPriceData (AMM touch), orderbookUpdate (CLOB depth), and subscribe_positions (your own fills), the three event streams every downstream module builds on.
A restoreSubscriptions() helper called on every connect event, so the server forgetting your subscriptions after a reconnect is no longer your problem.
Next up: turning the raw orderbookUpdate stream into a reliable local book, depth, touch, trades, and the REST reconciliation that catches drift before your bot trades on a stale view.
Complete the checklist above to unlock