WebSocket API reference
Connect to the production WebSocket endpoint:
wss://stream-production-75db.up.railway.app/v1/streamThe service accepts JSON text frames. Venue names are normalized to lowercase; venue-native market ids and feed symbols keep their case. API keys are never accepted in the URL.
Authentication
Section titled “Authentication”Non-browser clients should send the API key in the upgrade request:
Authorization: Bearer pk_live_YOUR_KEYBrowser clients cannot set that header. Their first frame must arrive within 10 seconds and have this exact shape:
{ "op": "auth", "apiKey": "pk_live_YOUR_KEY" }Successful first-frame authentication returns this acknowledgement before queued subscription acknowledgements:
{ "type": "auth", "status": "ok" }Header-authenticated clients do not receive an auth acknowledgement. A missing, invalid, unknown,
revoked, or non-read-scoped key first receives an UNAUTHORIZED error frame, then the service
closes the socket with code 4001. A browser client that sends another operation before auth, or
does not authenticate before the deadline, is closed the same way.
Client subscription frames
Section titled “Client subscription frames”marketId is the venue-native upstream id. For example, a Polymarket order book uses the
outcome’s CLOB asset/token id, while Hyperliquid uses its coin symbol.
Per-market order books and trades:
{ "op": "subscribe", "channel": "orderbook", "venue": "polymarket", "marketId": "<asset_id>" }{ "op": "unsubscribe", "channel": "orderbook", "venue": "polymarket", "marketId": "<asset_id>" }{ "op": "subscribe", "channel": "trades", "venue": "hyperliquid", "marketId": "BTC" }{ "op": "unsubscribe", "channel": "trades", "venue": "hyperliquid", "marketId": "BTC" }Venue-wide order books are available only when the upstream implements a real firehose or multiplexed stream:
{ "op": "subscribeAll", "channel": "orderbook", "venue": "polymarket" }{ "op": "unsubscribeAll", "channel": "orderbook", "venue": "polymarket" }Reference-feed tickers use feed and symbol, not venue and marketId:
{ "op": "subscribeFeedTicker", "feed": "binance", "symbol": "BTC/USDT" }{ "op": "unsubscribeFeedTicker", "feed": "binance", "symbol": "BTC/USDT" }The venue option-price lane carries the on-chain market address separately. It is a price stream, not an order book:
{ "op": "subscribePrice", "venue": "rain", "marketId": "<market_id>", "marketAddress": "0x..." }{ "op": "unsubscribePrice", "venue": "rain", "marketId": "<market_id>", "marketAddress": "0x..." }The executable-arbitrage lane is cross-venue. Like the feed-ticker lane it carries neither venue
nor marketId — one shared surface spans every priced venue:
{ "op": "subscribeArbitrage" }{ "op": "unsubscribeArbitrage" }Acknowledgement frames
Section titled “Acknowledgement frames”A successful per-market subscription is acknowledged before any cached snapshot or live frame:
{ "type": "subscribed", "channel": "orderbook", "venue": "polymarket", "marketId": "<asset_id>" }{ "type": "unsubscribed", "channel": "orderbook", "venue": "polymarket", "marketId": "<asset_id>" }The other acknowledgement shapes are:
{ "type": "subscribed", "channel": "trades", "venue": "hyperliquid", "marketId": "BTC" }{ "type": "subscribed", "channel": "orderbook:all", "venue": "polymarket" }{ "type": "subscribed", "channel": "feedTicker", "feed": "binance", "symbol": "BTC/USDT" }{ "type": "subscribed", "channel": "price", "venue": "rain", "marketId": "<market_id>" }An unsubscribed acknowledgement uses the same fields. A Rain, XO, or PRED trade subscription
served from the configured chain-scanner tape also adds a disclosure object:
{ "type": "subscribed", "channel": "trades", "venue": "rain", "marketId": "<market_id>", "disclosure": { "provenance": "chain-scan", "latencyMs": 90000, "completeness": "<venue-specific omission disclosure>" }}The cross-venue arbitrage lane has nothing to echo, so its acknowledgements carry channel alone:
{ "type": "subscribed", "channel": "arbitrage" }{ "type": "unsubscribed", "channel": "arbitrage" }Order-book frames
Section titled “Order-book frames”For a per-market order-book subscription, the first book is a snapshot. Later venue ticks are
update frames. Both contain a complete book, never a delta. Prices are probabilities in [0, 1];
bids are best-first descending and asks are best-first ascending.
{ "type": "snapshot", "venue": "polymarket", "marketId": "<asset_id>", "data": { "bids": [{ "price": 0.4, "size": 10 }], "asks": [{ "price": 0.42, "size": 8 }], "timestamp": 1780000000000 }, "ts": 1780000000123}{ "type": "update", "venue": "polymarket", "marketId": "<asset_id>", "data": { "bids": [{ "price": 0.41, "size": 9 }], "asks": [{ "price": 0.43, "size": 7 }], "timestamp": 1780000000200 }, "ts": 1780000000210}When backpressure coalesces skipped book ticks, the latest complete book is sent as another
snapshot once the socket drains.
Trade frames
Section titled “Trade frames”Native and configured chain-scanner trade subscriptions share one frame shape:
{ "type": "trade", "venue": "hyperliquid", "marketId": "BTC", "data": { "id": "<trade_id>", "time": "2s ago", "timestamp": 1780000000000, "type": "Buy", "usd": 125.5, "outcome": "Yes", "outcomeIndex": 0, "shares": 10, "price": 0.55, "maker": "hyperliquid", "transactionHash": "<transaction_id>", "wallet": "0x...", "counterparty": "0x..." }, "ts": 1780000000123}outcomeIndex, wallet, and counterparty can be absent or null. usd and price can be
null on parimutuel venues where execution-time values do not exist. A chain-scanner frame adds
"provenance": "chain-scan" at the top level. Trade frames are dropped rather than buffered while
the client is backpressured.
Feed-ticker and option-price frames
Section titled “Feed-ticker and option-price frames”Feed tickers carry the normalized ticker under data. Only symbol, asOf, and provenance are
always present; price, volume, timestamp, datetime, and sourceMetadata fields are present only
when the upstream proves them.
{ "type": "feedTicker", "feed": "binance", "symbol": "BTC/USDT", "data": { "symbol": "BTC/USDT", "last": 61714.63, "asOf": "2026-08-13T12:00:00.000Z", "provenance": { "source": "binance-ws" }, "sourceMetadata": { "transport": "websocket" } }, "ts": 1780000000123}The option-price lane currently relays the normalized Rain frame:
{ "type": "price", "venue": "rain", "marketId": "<market_id>", "marketAddress": "0x...", "data": { "provider": "rain", "marketId": "<market_id>", "marketAddress": "0x...", "prices": [{ "choiceIndex": 0, "label": "Yes", "price01": 0.55, "rawPrice": "550000000000000000" }], "triggeredBy": { "eventName": "<event_name>", "transactionHash": "0x...", "blockNumber": "123", "logIndex": 4 }, "asOfISO": "2026-08-13T12:00:00.000Z" }, "ts": 1780000000123}choiceIndex, label, and rawPrice can be null. Every field inside triggeredBy can also be
null, and the whole object can be null. Feed-ticker and option-price frames are dropped rather
than buffered under backpressure.
Arbitrage frames
Section titled “Arbitrage frames”The arbitrage lane relays one shared server-side recompute of the cross-venue executable-arbitrage
surface — the streaming twin of GET /api/router/fetchArbitrage. Every frame is the whole current
surface; there is no incremental delta.
{ "type": "arbitrage", "data": { "exchange": "router", "computedAt": "2026-08-13T12:00:00.000Z", "publishedAt": "2026-08-13T12:00:00.004Z", "intervalMs": 3000, "heartbeatMs": 30000, "contracts": 100, "limit": 10, "rows": [ { "clusterId": "cluster:real", "question": "Will Team A win?", "similarity": 0.92, "contracts": 100, "legs": { "buyYes": { "venue": "polymarket", "canonicalMarketId": "polymarket:real", "side": "yes", "executable": true, "reasons": [], "vwap": 0.41, "cost": 41, "fee": 0, "filled": 100, "fullyFilled": true }, "buyNo": { "venue": "kalshi", "canonicalMarketId": "kalshi:real", "side": "no", "executable": true, "reasons": [], "vwap": 0.45, "cost": 45, "fee": 0.7, "filled": 100, "fullyFilled": true } }, "resolution": { "compatible": true, "reason": "", "auditReasons": [] }, "settlementFee": 0, "totalCost": 86.7, "payout": 100, "netEdge": 13.3, "roi": 0.1534, "resolutionEquivalence": "verified", "executable": true, "reasons": [], "label": "arbitrage", "asOf": "2026-08-13T11:59:58.000Z" } ] }, "ts": 1780000000123}A row is labeled arbitrage only when it has positive net edge, both legs are depth-executable at
the requested size against live asks, and resolution equivalence is verified. Every other row is
served as indicative price discrepancy with the per-leg reasons codes explaining why. Rows are
never filtered down to the winners — the indicative rows are part of the surface, with their
evidence.
intervalMs is the true recompute cadence (3000 ms by default). This is a shared server-side
recompute, not a tick-by-tick feed: a frame is published only when the priced surface actually
changed — a re-read of the same books carries a fresher asOf and is not treated as a change —
plus a heartbeat republish so a quiet market is distinguishable from a dead publisher. A heartbeat
frame is a genuine recompute against live books rather than a replay, so its computedAt and
publishedAt are truthful.
heartbeatMs is the bound the server computed for its own configuration, not a nominal target:
a republish can only happen on a recompute tick, so the advertised value is the first tick at or
after the 30000 ms target — 30000 ms at the default 3000 ms interval, and 40000 ms at a 20000 ms
one. Take it from the frame rather than hard-coding it. A healthy publisher republishes at least
that often, so sustained silence beyond the advertised heartbeatMs means a publisher outage or
an entitlement teardown, not a quiet market.
The three timestamps let you measure the lane instead of trusting it. publishedAt − computedAt is
the time the server spent turning a finished computation into a published frame, and ts − publishedAt is the relay and fan-out to your socket; both are sub-millisecond in practice, because
nothing on that path buffers, batches, or waits for a timer. The recompute interval is the only
deliberate delay in the lane, and it exists to bound upstream venue API cost rather than as a design
preference — expect a change to surface within one interval, and on average within half of one.
A new subscriber receives the current surface immediately after its subscribed acknowledgement,
when one is available. If nothing has been published since the service last had a subscriber, no
snapshot is sent and the first frame arrives on the next recompute.
Per-leg vwap, cost, and fee are null whenever the leg cannot honestly be priced as the
claimed trade; note and feeBasis are present only where the venue’s fee model needs them.
settlementFee, totalCost, netEdge, and roi are null when the pair cannot be priced, and
asOf is null when either book is unavailable. Arbitrage frames are dropped rather than coalesced
or buffered under backpressure — because each frame is the whole surface, the next frame supersedes
anything skipped.
Errors and unsupported capabilities
Section titled “Errors and unsupported capabilities”Protocol errors are JSON frames. Depending on the failed operation they echo venue, marketId,
channel, feed, or symbol:
{ "type": "error", "code": "NOT_SUPPORTED", "message": "venue 'smarkets' has no native live trade stream", "venue": "smarkets", "marketId": "<market_id>", "channel": "trades"}If a venue has neither a native live trade stream nor a configured chain-scanner tape, a trades
subscription returns this honest NOT_SUPPORTED frame. The socket stays open, no subscription is
created, and the service never fabricates polling or trade data. Unsupported order-book,
venue-wide, feed-ticker, and option-price subscriptions follow the same non-fatal pattern.
subscribeArbitrage answers the same NOT_SUPPORTED code — echoing channel alone — on
deployments where the arbitrage publisher is not enabled.
The arbitrage channel is additionally gated on the same arbitrage plan feature as the REST verb
GET /api/router/fetchArbitrage. A key whose plan does not include it receives a non-fatal
PLAN_UPGRADE_REQUIRED frame instead of a subscription:
{ "type": "error", "code": "PLAN_UPGRADE_REQUIRED", "message": "the \"arbitrage\" feature requires the Builder plan or higher (current plan: \"free\")", "channel": "arbitrage"}The Free plan does not include it. The socket stays open and no subscription is created.
The same frame is also sent mid-stream. Entitlements are re-checked on the connection’s per-minute
metering tick against uncached key state, so a plan that stops entitling the feature loses the
arbitrage subscription within about a minute: the service unsubscribes it, releases its
subscription slot, and sends PLAN_UPGRADE_REQUIRED. If instead the API key itself has stopped
verifying — revoked, deleted, or rotated — the channel is torn down the same way but the frame
carries UNAUTHORIZED, because that caller needs to re-authenticate rather than upgrade.
In both cases the socket is never closed and every other subscription on it continues. A verification attempt that FAILS to complete changes nothing: only a fresh, conclusive answer tears the channel down, so an unreachable key store never interrupts a paying customer.
Other non-fatal operation codes are BAD_MESSAGE, NOT_SUBSCRIBED, MARKET_NOT_FOUND,
SUBSCRIPTION_LIMIT, and post-auth RATE_LIMITED. Authentication, credit, platform, connection,
and server-lifecycle failures can close the connection after their error frame or close reason.