Connection
URL:wss://ws.limitless.exchange
Namespace: /markets
Transport: WebSocket only (no polling fallback)
No client PING required. The server runs the Socket.IO heartbeat (server-initiated
ping / client pong) automatically. Every official Socket.IO client (JavaScript, Python python-socketio, Go, Rust) responds to it out of the box. Do not send your own PING frames. Keep the connection open and rely on the built-in heartbeat and the auto-reconnect flow in WebSocket Quickstart.{ISO-8601 timestamp}\nGET\n/socket.io/?EIO=4&transport=websocket\n — with the base64-decoded secret and pass three headers as extraHeaders:
TOKEN_ID and SECRET. If you use an official SDK, prefer its WebSocket client, which signs the handshake automatically from hmacCredentials (TypeScript) / hmac_credentials (Python) / WithHMACCredentials (Go).
Subscribing to market data
Subscribe to price and orderbook updates by emittingsubscribe_market_prices:
Subscribing to position updates
Subscribe to real-time position changes by emittingsubscribe_positions. This requires an HMAC-signed handshake. See Authentication.
subscribe_positions accepts the same payload as subscribe_market_prices:
Position updates are pushed automatically when your balances change (e.g. after a trade is mined). You do not need to poll.
Subscribing to order events
Subscribe to the full lifecycle of your CLOB orders by emittingsubscribe_order_events. Requires an HMAC-signed handshake. See Authentication. The subscription takes no payload. It is a per-user channel that delivers events for every order you are party to.
Several event shapes arrive on the same Socket.IO event name orderEvent. Distinguish first by source, then by type:
OME— off-chain matching engine updates: resting-order state changes (PLACEMENT,UPDATE,CANCELLATION) and the terminal result of an immediate-or-cancel order (EXECUTION).SETTLEMENT— settlement lifecycle for CLOB trades: a provisionalMATCHEDthe moment the engine fills your order (before the on-chain transaction), then a terminalMINEDorFAILED.
Channel is per-user, not per-market. You receive events for every order you are party to (as taker or maker). The subscription cannot be narrowed by market. Filter client-side on
marketId / marketSlug if needed.Resubscribe on reconnect. Subscriptions are not persisted server-side across disconnects. Your
connect handler must re-emit subscribe_order_events.Server-side deduplication. Repeated emissions within a 60-second sliding window are dropped, so retries and replays will not double-deliver. Client-side dedup is only required if you persist events across reconnects yourself.
Taker delay: order submission can be asynchronous. Some markets apply a short hold to marketable (taker) orders before the matching engine fills them. On such a market,
POST /orders returns right away with execution.settlementStatus: "DELAYED" and an eligibleAt timestamp instead of blocking until settlement. This stream is how you observe the fill. Wait for the provisional MATCHED frame, then the terminal MINED / FAILED, correlating by clientOrderId / tradeEventId. Maintenance mode can postpone delayed fills beyond eligibleAt; keep the order open in your integration until a terminal event arrives. postOnly (maker) orders are never delayed. A market’s current delay is readable as settings.takerDelayMs (milliseconds; 0 = none) on the market response.Subscribing to market lifecycle events
Subscribe to market creation and resolution events by emittingsubscribe_market_lifecycle. No authentication required.
marketResolved is also emitted to existing per-market room subscribers automatically (CLOB: market:{slug}, AMM: market:{address}). You don’t need a separate lifecycle subscription to receive resolution events for markets you’re already watching.Subscribing to unrealized PnL updates
Subscribe to invalidation hints for the live Unrealized PnL leaderboards by emittingsubscribe_unrealized_pnl. No authentication required. Two scopes are available: one leaderboard per open market, and a global biggest-open-positions list.
unsubscribe_unrealized_pnl with the same payload shape.
At most 50
MARKET scopes per connection. Additional subscribe_unrealized_pnl calls with new marketIds beyond the limit are rejected with an error frame carrying code: "UNREALIZED_PNL_SCOPE_LIMIT". The BIGGEST_POSITIONS scope does not count against the market limit.state on the hint tracks the projection. state: "READY" means the leaderboard you refetch will be current; BUILDING, STALE, or DEGRADED mean the REST route may return an empty or stale snapshot. Retry shortly.Event reference
Event payloads
newPriceData
orderbookUpdate
Emitted for CLOB markets when the book changes (new bids/asks, removals, or fills). orderbook carries the full updated book. Each side is a sorted array of price levels.
price and size are JSON numbers; coerce defensively to preserve decimal precision. For a one-shot snapshot, use GET /markets/{slug}/orderbook, which returns the same shape.positions
Position updates have different shapes depending on market type.
AMM markets:
marketCreated
Emitted when a new market is funded and visible. Hidden markets are excluded.
marketResolved
Emitted when a market resolves. Sent to both market_lifecycle subscribers and existing market:{slug} room subscribers.
unrealizedPnlProjectionChanged
Invalidation hint delivered to subscribe_unrealized_pnl subscribers when the underlying projection for their scope changes. It does not carry the leaderboard rows. Refetch the matching REST route to get the updated snapshot.
Market scope:
orderEvent
Emitted for both OME state changes and on-chain settlement results. Distinguish the two shapes by the source field.
Timestamp fields
EveryorderEvent frame carries a timestamp plus additive semantic timestamps. Read occurredAt, matchedAt, and publishedAt directly. Do not substitute one for another. All timestamps are ISO-8601 UTC strings, and any missing semantic timestamp arrives as null rather than a value copied from another field.
Prefer semantic timestamps. Use
occurredAt for lifecycle time, matchedAt for the persisted match time on settlement frames, and publishedAt for gateway queue time. Treat timestamp as a legacy field only.OME event (source: "OME")
The OME source carries two shapes: ongoing lifecycle state changes (PLACEMENT / UPDATE / CANCELLATION) and a one-shot terminal result for immediate-or-cancel orders (EXECUTION). Tell them apart by type.
Lifecycle (type: "PLACEMENT" | "UPDATE" | "CANCELLATION")
Emitted for every resting-order state change recorded by the matching engine. price and remainingSize are sent unquoted as JSON numbers. Coerce defensively to preserve decimal precision.
type transitions:
PLACEMENT— order accepted by the OME.UPDATE— remaining size changed (partial fill or amend).CANCELLATION— removed from the book. Carriesreason: "STP_MAKER_CANCELLED"when self-trade prevention cancelled your resting maker order against your own incoming order.
FAK/FOK terminal (type: "EXECUTION")
Emitted once when an immediate-or-cancel order reaches a recorded terminal state — a FAK (fill-and-kill), which always terminates, or a FOK (fill-or-kill) that filled. Delivered only to the order owner. Unlike the lifecycle frames, eventId is a string (terminal:<orderId>) and the frame carries a status label.
status outcomes:
FILLED— the order matched in full (a FAK that matched completely, or a FOK).PARTIALLY_FILLED— a FAK matched part of its size; the unfilled remainder was cancelled.remainingSizeis that cancelled remainder.KILLED— a FAK matched nothing and was cancelled in full.remainingSizeequals the original size. A taker rejected by self-trade prevention (stpPolicy: "cancel_taker"or"cancel_both") also surfaces here asstatus: "KILLED". This frame carries no STP reason. TheSTP_TAKER_REJECTEDreason is returned only on the synchronousPOST /ordersresponse.
price and remainingSize are JSON numbers (as on the lifecycle frames). Coerce defensively to preserve decimal precision.No fee or
clientOrderId on the terminal frame. It reports only the lifecycle outcome. For the realized fee, read the POST /orders response or the MINED settlement frame. token is the raw CTF token id, not the YES / NO outcome.A FOK is all-or-nothing. A fill-or-kill order either fills completely (
status: "FILLED") or is rejected with HTTP 400 and produces no event. It never emits PARTIALLY_FILLED or KILLED. Only a FAK reports a partial (PARTIALLY_FILLED) or zero (KILLED) fill.Settlement event (source: "SETTLEMENT")
The SETTLEMENT source carries the settlement lifecycle of a CLOB trade. Each participant receives its own events, keyed by userId: one for the taker order and one for each matched maker order. Two types arrive in sequence:
MATCHED— provisional. Emitted the instant the matching engine fills your order, before the on-chain settlement transaction. This is the early “your order will be matched for N” signal. Fee fields are estimates (isEstimate: true), there is notxHashyet, and the fill can still be rolled back by a laterFAILED.MINED/FAILED— terminal. Emitted after the settlement transaction resolves on-chain.MINEDcarriestxHashand the taker’s realized fee;FAILEDmeans the trade did not execute and no funds moved.
MATCHED and its terminal MINED / FAILED share the same tradeEventId and orderId but use different eventId namespaces (matched:… vs settlement:…), so they never dedup-collide. Correlate provisional → terminal by tradeEventId + orderId (MATCHED does not carry clientOrderId).
takerAccount and makerMatches[].account are on-chain addresses. For smart-wallet users this is the smart-wallet proxy; for EOA users it is the EOA.
Provisional match (type: "MATCHED")
Emitted pre-chain, the moment the engine fills the order. Per-profile: the taker and each maker receive their own frame carrying their own side, token, size, price, and fee estimate (in a cross-outcome match the taker and maker hold opposite tokens). The taker frame aggregates the whole fill; each maker frame describes only that maker’s leg.
Fee currency follows side. A BUY fill reports the fee estimate in
feeAmountContracts (feeAmountCollateral absent); a SELL fill reports it in feeAmountCollateral. The same convention applies on MINED.Makers are not charged a fee. Only the taker pays. On a maker’s frame, the fee estimate fields (
configuredFeeRateBps, effectiveFeeBps, feeAmountContracts / feeAmountCollateral) mirror the maker order’s configured rate and can be non-zero. A maker is not charged a fee on a matched CLOB trade; the realized maker fee is 0. Treat the taker-side estimate as a real pre-settlement charge to reconcile on MINED, and treat the maker-side estimate as informational only.Terminal settlement (type: "MINED" | "FAILED")
Emitted when a CLOB trade settles on-chain. Each participant receives a settlement event for their own order: one for the taker order and one for each matched maker order.
Reconciling WS events with
POST /orders. Submit an order with a clientOrderId and every orderEvent for that order — PLACEMENT, UPDATE, CANCELLATION, and the MINED / FAILED settlement frame — echoes the same clientOrderId. Match WS events to the originating request by clientOrderId rather than waiting for the POST /orders HTTP response, which only returns once settlement is MINED. If only one side supplied a clientOrderId, only that side’s event includes it.Maker fee on
MINED is informational too. Like the MATCHED estimate, a maker’s MINED fee fields are computed from the maker order’s configured rate, not a realized charge. Makers are not charged, so the realized maker fee is 0. Only the taker’s MINED fee is a realized charge.type: "MINED"— on-chain settlement confirmed.type: "FAILED"— settlement failed on-chain; the taker order did not execute and funds were not moved.