Skip to main content

Connection

URL: wss://ws.limitless.exchange Namespace: /markets Transport: WebSocket only (no polling fallback) Authenticated channels (positions, order events) require an HMAC-signed handshake. Sign a fixed canonical message — {ISO-8601 timestamp}\nGET\n/socket.io/?EIO=4&transport=websocket\n — with the base64-decoded secret and pass three headers as extraHeaders:
import { createHmac } from 'crypto';
import { io } from 'socket.io-client';

function wsAuthHeaders(tokenId: string, secret: string) {
  const timestamp = new Date().toISOString();
  const message = `${timestamp}\nGET\n/socket.io/?EIO=4&transport=websocket\n`;
  const signature = createHmac('sha256', Buffer.from(secret, 'base64'))
    .update(message)
    .digest('base64');
  return { 'lmts-api-key': tokenId, 'lmts-timestamp': timestamp, 'lmts-signature': signature };
}

const socket = io('wss://ws.limitless.exchange/markets', {
  transports: ['websocket'],
  extraHeaders: wsAuthHeaders(TOKEN_ID, SECRET),
});
See Authentication for how to generate 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 emitting subscribe_market_prices:
// AMM price updates
socket.emit('subscribe_market_prices', {
  marketAddresses: ['0x1234...']
});

// CLOB orderbook updates
socket.emit('subscribe_market_prices', {
  marketSlugs: ['btc-100k-weekly']
});

// Both at once (recommended to avoid overwriting subscriptions)
socket.emit('subscribe_market_prices', {
  marketAddresses: ['0x1234...'],
  marketSlugs: ['btc-100k-weekly']
});
Subscriptions replace previous ones. If you want both AMM prices and CLOB orderbook, send both marketAddresses and marketSlugs together in a single call.

Subscribing to Position Updates

Subscribe to real-time position changes by emitting subscribe_positions. This requires an HMAC-signed handshake — see Authentication. subscribe_positions accepts the same payload as subscribe_market_prices:
FieldTypeDescription
marketAddressesstring[]Contract addresses for AMM markets
marketSlugsstring[]Slugs for CLOB markets
import { io } from 'socket.io-client';

const socket = io('wss://ws.limitless.exchange/markets', {
  transports: ['websocket'],
  extraHeaders: wsAuthHeaders(TOKEN_ID, SECRET),
});

socket.on('connect', () => {
  // Subscribe to position updates for specific markets
  socket.emit('subscribe_positions', {
    marketAddresses: ['0x1234...'],   // AMM markets
    marketSlugs: ['btc-100k-weekly'], // CLOB markets
  });
});

// Confirmation
socket.on('system', (data) => {
  console.log(data);
  // { message: 'Successfully subscribed to position updates', markets: { addresses: [...], slugs: [...] } }
});

// Position updates
socket.on('positions', (data) => {
  console.log('Position update:', data);
});
Like subscribe_market_prices, calling subscribe_positions again replaces the previous subscription. Include all markets in a single call.
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 emitting subscribe_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 provisional MATCHED the moment the engine fills your order (before the on-chain transaction), then a terminal MINED or FAILED.
import { io } from 'socket.io-client';

const socket = io('wss://ws.limitless.exchange/markets', {
  transports: ['websocket'],
  extraHeaders: wsAuthHeaders(TOKEN_ID, SECRET),
});

socket.on('connect', () => {
  socket.emit('subscribe_order_events');
});

socket.on('system', (msg) => {
  // { message: 'Successfully subscribed to order event updates' }
  console.log(msg);
});

socket.on('orderEvent', (data) => {
  if (data.source === 'OME' && data.type === 'EXECUTION') {
    console.log(`[EXECUTION ${data.status}] order=${data.orderId} remaining=${data.remainingSize}`);
  } else if (data.source === 'OME') {
    console.log(`[OME ${data.type}] order=${data.orderId} clientOrder=${data.clientOrderId ?? '(none)'}`);
  } else {
    console.log(
      `[SETTLEMENT ${data.type}] order=${data.orderId} clientOrder=${data.clientOrderId ?? '(none)'} tx=${data.txHash ?? '(none)'}`,
    );
  }
});
One subscription per connection. Sending subscribe_order_events a second time re-binds; the previous subscription is cancelled. There is no payload.
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.
Ordering is not guaranteed across sources. OME and SETTLEMENT events for the same order can arrive in either order within a few seconds — settlement can land before the corresponding UPDATE, or vice versa. Treat them as independent streams that both reference orderId / takerOrderId.
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.
Auth failures surface on the exception channel. If the HMAC auth headers are missing, invalid, or revoked, the guard emits a WsException to the client’s exception event and no orderEvent frames arrive. Subscribe to socket.on('exception', ...) if you want to detect auth failures rather than silently miss the stream.
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 — so 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 emitting subscribe_market_lifecycle. No authentication required.
socket.on('connect', () => {
  socket.emit('subscribe_market_lifecycle');
});

socket.on('marketCreated', (data) => {
  console.log('New market:', data.slug, data.title);
});

socket.on('marketResolved', (data) => {
  console.log('Market resolved:', data.slug, data.winningOutcome);
});
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.
To unsubscribe:
socket.emit('unsubscribe_market_lifecycle');

Event Reference

EventDirectionAuth RequiredDescription
connectServer → ClientNoConnection established
disconnectServer → ClientNoConnection lost
subscribe_market_pricesClient → ServerNoSubscribe to price/orderbook updates
subscribe_positionsClient → ServerYesSubscribe to position updates
subscribe_order_eventsClient → ServerYesSubscribe to OME and settlement events for your CLOB orders
subscribe_market_lifecycleClient → ServerNoSubscribe to market creation/resolution events
unsubscribe_market_lifecycleClient → ServerNoUnsubscribe from market lifecycle events
newPriceDataServer → ClientNoAMM market price update
orderbookUpdateServer → ClientNoCLOB orderbook update
marketCreatedServer → ClientNoNew market created and visible
marketResolvedServer → ClientNoMarket resolved with winning outcome
positionsServer → ClientYesPosition balance update
orderEventServer → ClientYesOME lifecycle, FAK/FOK execution, and settlement updates for your CLOB orders
systemServer → ClientNoSystem notifications
authenticatedServer → ClientYesAuthentication confirmation
exceptionServer → ClientNoError notifications

Event Payloads

newPriceData

{
  "marketAddress": "0x1234...",
  "updatedPrices": {
    "yes": "0.65",
    "no": "0.35"
  },
  "blockNumber": 12345678,
  "timestamp": "2024-01-01T00:00:00.000Z"
}

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.
{
  "marketSlug": "btc-100k-weekly",
  "orderbook": {
    "bids": [
      { "price": 0.53, "size": 100 },
      { "price": 0.52, "size": 250 }
    ],
    "asks": [
      { "price": 0.55, "size": 80 },
      { "price": 0.56, "size": 300 }
    ]
  },
  "timestamp": "2024-01-01T00:00:00.000Z"
}
FieldTypeDescription
marketSlugstringCLOB market slug
orderbook.bids{ price: number, size: number }[]Bid levels, highest price first
orderbook.asks{ price: number, size: number }[]Ask levels, lowest price first
timestampstringISO-8601 event timestamp
price and size are JSON numbers; coerce defensively to preserve decimal precision. For a one-shot snapshot, use GET /markets/{slug}/orderbook — the same shape.

positions

Position updates have different shapes depending on market type. AMM markets:
{
  "account": "0xabcd...",
  "marketAddress": "0x1234...",
  "positions": [
    {
      "tokenId": "123456",
      "balance": "1000000",
      "outcomeIndex": 0,
      "collateralOutOnSell": "950000"
    }
  ],
  "type": "AMM"
}
CLOB markets:
{
  "account": "0xabcd...",
  "marketSlug": "btc-100k-weekly",
  "positions": [
    {
      "tokenId": "19633204485790...",
      "ctfBalance": "10000000",
      "averageFillPrice": "0.65",
      "costBasis": "6500000",
      "marketValue": "7000000",
      "marketId": 7348
    }
  ],
  "tokenIds": ["19633204485790..."],
  "type": "CLOB"
}

marketCreated

Emitted when a new market is funded and visible. Hidden markets are excluded.
{
  "slug": "btc-above-110k-apr-2026",
  "title": "$BTC above $110,000 on Apr 5, 2026?",
  "type": "CLOB",
  "groupSlug": "btc-price-markets",
  "categoryIds": [1, 5],
  "createdAt": "2026-04-02T12:00:00.000Z"
}
FieldTypeDescription
slugstringMarket slug identifier
titlestringMarket title
type'AMM' | 'CLOB'Market type
groupSlugstring?Parent group slug (for NegRisk submarkets)
categoryIdsnumber[]?Category IDs the market belongs to
createdAtstringISO-8601 creation timestamp

marketResolved

Emitted when a market resolves. Sent to both market_lifecycle subscribers and existing market:{slug} room subscribers.
{
  "slug": "btc-above-110k-apr-2026",
  "type": "CLOB",
  "winningOutcome": "YES",
  "winningIndex": 0,
  "resolutionDate": "2026-04-05T14:00:00.000Z"
}
FieldTypeDescription
slugstringMarket slug identifier
type'AMM' | 'CLOB'Market type
winningOutcome'YES' | 'NO'The winning outcome
winningIndex0 | 1Index of the winning outcome (0 = YES, 1 = NO)
resolutionDatestringISO-8601 resolution timestamp

orderEvent

Emitted for both OME state changes and on-chain settlement results. Distinguish the two shapes by the source field.

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.
{
  "source": "OME",
  "type": "PLACEMENT",
  "eventId": 1234567,
  "orderId": "550e8400-e29b-41d4-a716-446655440000",
  "clientOrderId": "client-order-001",
  "userId": 42,
  "marketId": "17",
  "token": "87893014956437093847...",
  "side": "BUY",
  "price": 0.53,
  "remainingSize": 100,
  "timestamp": "2026-04-20T10:15:30.000Z"
}
FieldTypeDescription
source'OME'Discriminator for the OME shape
type'PLACEMENT' | 'UPDATE' | 'CANCELLATION'OME state transition
eventIdnumberMonotonic OME event id
orderIdstringUUID of the order
clientOrderIdstring?Client-supplied id from POST /orders, when the order was placed with one. Field is omitted (not null) when the originating order had no client id.
userIdnumberInternal user id of the order owner
marketIdstringNumeric market id (CLOB)
tokenstringCTF token id (decimal string)
side'BUY' | 'SELL'Order side
pricenumberLimit price (unquoted JSON number)
remainingSizenumberSize remaining on the book (unquoted JSON number)
reasonstring?Engine cancellation reason. STP_MAKER_CANCELLED on a CANCELLATION event when self-trade prevention cancelled your resting maker order. Omitted otherwise.
timestampstringISO-8601 event timestamp
type transitions:
  • PLACEMENT — order accepted by the OME.
  • UPDATE — remaining size changed (partial fill or amend).
  • CANCELLATION — removed from the book. Carries reason: "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.
{
  "source": "OME",
  "type": "EXECUTION",
  "status": "FILLED",
  "eventId": "terminal:550e8400-e29b-41d4-a716-446655440000",
  "orderId": "550e8400-e29b-41d4-a716-446655440000",
  "userId": 42,
  "marketId": "17",
  "token": "87893014956437093847...",
  "side": "BUY",
  "price": 0.53,
  "remainingSize": 0,
  "timestamp": "2026-04-20T10:15:40.000Z"
}
FieldTypeDescription
source'OME'Discriminator for the OME shape
type'EXECUTION'Terminal result of a FAK/FOK order
status'FILLED' | 'PARTIALLY_FILLED' | 'KILLED'Final outcome (see below)
eventIdstringterminal:<orderId> — string form, unlike the numeric lifecycle eventId
orderIdstringUUID of the order
userIdnumberInternal user id of the order owner
marketIdstringNumeric market id (CLOB)
tokenstringCTF token id (decimal string) — the raw token, not YES / NO
side'BUY' | 'SELL'Order side
pricenumberOrder price
remainingSizenumberUnfilled size at termination (0 on FILLED, the original size on KILLED)
timestampstringISO-8601 event timestamp
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. remainingSize is that cancelled remainder.
  • KILLED — a FAK matched nothing and was cancelled in full. remainingSize equals the original size. A taker rejected by self-trade prevention (stpPolicy: "cancel_taker" or "cancel_both") also surfaces here as status: "KILLED"; this frame carries no STP reason — the STP_TAKER_REJECTED reason is returned only on the synchronous POST /orders response.
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 — one for the taker order and one for each matched maker order, keyed by userId. Two types arrive in sequence:
  • MATCHEDprovisional. 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 no txHash yet, and the fill can still be rolled back by a later FAILED.
  • MINED / FAILEDterminal. Emitted after the settlement transaction resolves on-chain. MINED carries txHash and the taker’s realized fee; FAILED means the trade did not execute and no funds moved.
A 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.
{
  "source": "SETTLEMENT",
  "type": "MATCHED",
  "eventId": "matched:77985c10…:d45b884d…",
  "tradeEventId": "77985c10…",
  "orderId": "d45b884d…",
  "takerOrderId": "d45b884d…",
  "marketSlug": "will-abc-happen-by-2026",
  "tokenId": "27102822276156300166...",
  "token": "NO",
  "side": "BUY",
  "price": "0.53",
  "amountContracts": "25",
  "amountCollateral": "13.25",
  "configuredFeeRateBps": 30,
  "effectiveFeeBps": 27,
  "feeAmountContracts": "0.0675",
  "isEstimate": true,
  "timestamp": "2026-04-20T10:15:40.000Z"
}
FieldTypeDescription
source'SETTLEMENT'Discriminator for the settlement shape
type'MATCHED'Provisional, pre-chain match
eventIdstringmatched:<tradeEventId>:<orderId> — distinct namespace from the terminal settlement:… id
tradeEventIdstringTrade id shared with the terminal MINED / FAILED for this fill
orderIdstringUUID of the recipient’s own order in this fill
takerOrderIdstringUUID of the taker order in the trade
marketSlugstringCLOB market slug
tokenIdstringCTF token id of the recipient’s own side (decimal string)
token'YES' | 'NO'Outcome the recipient filled — its own side, not necessarily the taker’s
side'BUY' | 'SELL'Recipient order side
pricestringRecipient’s fill price (decimal string) — the weighted-average across the fill on the taker frame, the maker’s own resting price on a maker frame
amountContractsstringFilled contract amount for the recipient order
amountCollateralstringFilled collateral amount for the recipient order
configuredFeeRateBpsnumberFee rate configured for the order
effectiveFeeBpsnumberEffective fee rate used for the estimate
feeAmountContractsstring?Fee estimate in contracts — present on BUY fills
feeAmountCollateralstring?Fee estimate in collateral — present on SELL fills
isEstimatetrueAlways true on MATCHED: fee fields are estimates, not realized on-chain charges
timestampstringISO-8601 event timestamp
MATCHED is provisional — do not settle books on it. Use it as an early acknowledgement / UI signal only. Its fee fields are estimates (isEstimate: true), not realized charges, and the fill can still end in FAILED. Reconcile on the terminal MINED frame, which carries txHash and the taker’s realized fee.
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, but 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; 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.
{
  "source": "SETTLEMENT",
  "type": "MINED",
  "eventId": "settlement:1b3a…:550e8400-e29b-41d4-a716-446655440000",
  "tradeEventId": "1b3a…",
  "orderId": "550e8400-e29b-41d4-a716-446655440000",
  "clientOrderId": "client-order-001",
  "takerOrderId": "e4c3…",
  "takerAccount": "0xAbC…123",
  "makerMatches": [
    {
      "account": "0xDeF…456",
      "orderId": "cb12…",
      "matchedSize": "25",
      "price": "0.53"
    }
  ],
  "marketSlug": "will-abc-happen-by-2026",
  "tokenId": "87893014956437093847...",
  "side": "BUY",
  "price": "0.53",
  "amountContracts": "25",
  "amountCollateral": "13.25",
  "configuredFeeRateBps": 0,
  "effectiveFeeBps": 0,
  "feeAmountContracts": "0",
  "txHash": "0xabc…",
  "timestamp": "2026-04-20T10:15:40.000Z"
}
FieldTypeDescription
source'SETTLEMENT'Discriminator for the settlement shape
type'MINED' | 'FAILED'Settlement outcome
eventIdstringStable id in the form settlement:<tradeEventId>:<orderId>
tradeEventIdstringSettlement trade id shared by all participant events for the same match
orderIdstring?UUID of the recipient’s own order for this event
clientOrderIdstring?Client-supplied id of the recipient’s own order (orderId), when the order was placed with one. Field is omitted (not null) when the originating order had no client id. Counterparty (takerOrderId, makerMatches[].orderId) ids are never resolved to client ids.
takerOrderIdstring?UUID of the taker order in the trade
takerAccountstring?On-chain address of the taker (smart-wallet proxy or EOA)
makerMatchesarray?One entry per matched maker (account, orderId, matchedSize, price). A taker event can include multiple maker matches; each maker also receives a separate event for its own order.
marketSlugstring?CLOB market slug
tokenIdstring?CTF token id for the recipient side
side'BUY' | 'SELL'?Recipient order side
pricestring?Execution price as a decimal string
amountContractsstring?Filled contract amount for the recipient order
amountCollateralstring?Collateral amount for the recipient order
configuredFeeRateBpsnumber?Fee rate configured for the order
effectiveFeeBpsnumber?Effective fee rate applied to this trade
feeAmountContractsstring?Fee amount in contracts
txHashstring?Settlement transaction hash, when known
timestampstringISO-8601 event timestamp
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.