Skip to content
Get an API key

Connect to the production WebSocket endpoint:

wss://stream-production-75db.up.railway.app/v1/stream

The 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.

Non-browser clients should send the API key in the upgrade request:

Authorization: Bearer pk_live_YOUR_KEY

Browser 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.

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" }

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" }

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.

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 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.

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.

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.