# API Keys Source: https://docs.sx.bet/api-reference/api-key Generating and using an API key An API key authenticates every request that is not public market data — orders, cancels, balances, trades, fills, positions, and funding. It is also required to fetch a realtime connection token and to register or cancel a heartbeat. Public market-data routes work without one. See [Authentication by route](/api-reference/auth-matrix) for the full list of authenticated vs unauthenticated routes ## Generating API Key 1. Visit [sx.bet](https://sx.bet) and register or log in to your account. You can connect your MetaMask wallet, or log in using your Fortmatic email address. 2. If using MetaMask, `sign` the Signature Request. 3. Click the `Account` tab on the top navigation bar. 4. Click the `Overview` tab on the account navigation bar. 5. You will see an `API Credentials` card. Click `GENERATE API KEY NOW`. An API Key will be displayed. 6. The API Key generated will not be displayed again, so please **copy and save this key for future use**. If you lose your key, you can generate a new one by following the same steps. Any previous keys used will be unauthorized if you generate a new key. Only one key can be active at a time. For Toronto testnet, generate the key on [toronto.sx.bet](https://toronto.sx.bet) — keys are per-environment. See [Environments](/developers/environments). ## Usage ```bash theme={null} curl --location --request GET 'https://api.sx.bet/user/realtime-token-v3/api-key' \ --header 'x-sx-api-key: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' ``` Pass your API key in the `x-sx-api-key` header. The example above fetches a realtime token used to authenticate WebSocket connections. See [Initialization](/developers/realtime-initialization#connect). Once your API key is generated, add it as an HTTP header named `x-sx-api-key` on every authenticated request. **`x-api-key` is a different, legacy credential and the wrong header here.** # Authentication by route Source: https://docs.sx.bet/api-reference/auth-matrix Which SX Bet API routes require authentication. ## Public routes | Route | What it gives you | Link | | ---------------------------- | -------------------------------- | ----------------------------------------------- | | `GET /metadata/obv3` | Exchange metadata | [Docs →](/api-reference/get-metadata-obv3) | | `GET /orderbook-v3/snapshot` | The whole book for one market | [Docs →](/api-reference/get-orderbook-snapshot) | | `GET /trades-v3/public` | The anonymised recent-trade tape | [Docs →](/api-reference/get-trades-v3-public) | ## Authenticated routes Send `x-sx-api-key` as a header. Every response is scoped to the address that owns the key. | Route | What it does | Link | | ------------------------------------- | ------------------------------------------- | ----------------------------------------------------------- | | `GET /orders-v3/odds/best` | Best available odds per market | [Docs →](/api-reference/get-best-odds-v3) | | `GET /orderbook-v3/snapshot/event` | The book for markets on one event | [Docs →](/api-reference/get-event-orderbook-snapshot) | | `GET /orders-v3` | Your active orders | [Docs →](/api-reference/get-orders-v3) | | `GET /orders-v3/{orderId}` | One of your orders by id (any status) | [Docs →](/api-reference/get-order-v3) | | `POST /orders-v3` | Places one signed order | [Docs →](/api-reference/post-orders-v3) | | `DELETE /orders-v3` | Cancels named orders by id | [Docs →](/api-reference/delete-orders-v3) | | `DELETE /orders-v3/all` | Cancels every open order you have | [Docs →](/api-reference/delete-orders-v3-all) | | `DELETE /orders-v3/event` | Cancels your orders on one event | [Docs →](/api-reference/delete-orders-v3-event) | | `GET /trades-v3` | Your bet history | [Docs →](/api-reference/get-trades-v3) | | `GET /trades-v3/{tradeId}` | One of your bets by tradeId | [Docs →](/api-reference/get-trade-v3) | | `GET /fills-v3` | Your fill history | [Docs →](/api-reference/get-fills-v3) | | `GET /positions-v3` | Your positions | [Docs →](/api-reference/get-positions-v3) | | `GET /user/balance-v3` | Your current balance | [Docs →](/api-reference/get-user-balance-v3) | | `GET /ledger-v3` | Your balance ledger | [Docs →](/api-reference/get-ledger-v3) | | `GET /user/fees-v3` | The fee rates set on your account | [Docs →](/api-reference/get-user-fees-v3) | | `GET /user/pending-deploy-proxy` | Whether a proxy deploy is still pending | [Docs →](/api-reference/get-user-pending-deploy-proxy) | | `POST /user/deploy-proxy` | Deploys your proxy wallet | [Docs →](/api-reference/post-user-deploy-proxy) | | `GET /user/proxy` | Your proxy wallet address and deploy status | [Docs →](/api-reference/get-user-proxy) | | `POST /user/transfer-to-proxy` | Moves tokens into your proxy | [Docs →](/api-reference/post-user-transfer-to-proxy) | | `GET /user/transfer-to-proxy/pending` | Open deposit `sessionIds` (newest first) | [Docs →](/api-reference/get-user-transfer-to-proxy-pending) | | `GET /user/transfer-to-proxy/status` | Outcome of one deposit by `sessionId` | [Docs →](/api-reference/get-user-transfer-to-proxy-status) | # Best Odds Source: https://docs.sx.bet/api-reference/channel-best-odds-v3 Subscribe to real-time best odds updates Subscribe to best odds changes across all order books. A publication goes out whenever a market's top of book changes on either side — a new best price, a size change at the best price, or the best level disappearing. This channel has no history, so a subscriber receives nothing until the next change anywhere in the system. Seed from [`GET /orders-v3/odds/best`](/api-reference/get-best-odds-v3) on every connect and treat this channel as updates on top of that snapshot. **CHANNEL NAME** `best_odds_v3:global` **MESSAGE PAYLOAD FORMAT** One market per publication, as a single object. | Name | Type | Description | | ------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | marketHash | string | The market whose top of book changed | | outcomeOne | object or `null` | Best maker level on outcome one. `null` when that side is empty | | outcomeTwo | object or `null` | Best maker level on outcome two. `null` when that side is empty | | outcomeOne.percentageOdds | string | The odds the `maker` receives in the sx.bet protocol format. To convert to implied odds divide by 10^20. To get taker implied odds: `takerOdds = 1 - percentageOdds / 10^20`. See [unit conversions](/developers/unit-conversions). | | outcomeOne.size | string | Aggregate maker size at that level, in base units. See [unit conversions](/developers/unit-conversions). | This is exactly one element of the `bestOdds` array that [`GET /orders-v3/odds/best`](/api-reference/get-best-odds-v3) returns. *** ```javascript JavaScript theme={null} // To subscribe const sub = client.newSubscription("best_odds_v3:global"); sub.on("publication", (ctx) => { const data = ctx.data; // message handler logic }); sub.subscribe(); ``` ```python Python theme={null} import asyncio from centrifuge import Client, PublicationContext, SubscriptionEventHandler async def on_publication(ctx: PublicationContext) -> None: print(ctx.data) async def main(): client = Client( "wss://realtime.sx.bet/connection/websocket", token="YOUR_TOKEN", # from /user/realtime-token-v3/api-key ) await client.connect() handler = SubscriptionEventHandler(on_publication=on_publication) sub = client.new_subscription("best_odds_v3:global", handler) await sub.subscribe() await asyncio.Future() # keep running asyncio.run(main()) ``` The above returns JSON structured like this: ```json theme={null} { "marketHash": "0xbf06c6379c922d8118612d1d7493b20f6df6929437fbf6022904327f9516a2eb", "outcomeOne": { "percentageOdds": "40000000000000000000", "size": "1000000" }, "outcomeTwo": null } ``` # Fill Updates Source: https://docs.sx.bet/api-reference/channel-fills-v3 Subscribe to real-time updates for your own fills Subscribe to your own matches at fill grain — one row per match between your order and a counterparty's. A bet filled by three makers produces three messages here and one on [`account:trades_v3_#{address}`](/api-reference/channel-trades-v3). **The first message for a new fill is `MATCHED`, which comes before the on-chain lock**. A second message follows once the funds are escrowed (`LOCKED`) or the lock fails (`FAILED`). Act on `MATCHED` for speed, or wait for `LOCKED` if a revised bet would cost you — see [which status to act on](/developers/bet-lifecycle#which-status-to-act-on). **CHANNEL NAME FORMAT** `account:fills_v3_#{address}` | Name | Type | Description | | ------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | address | string | Your checksummed address. It must equal your realtime token's `sub` — see [subscribing to your own channels](/developers/realtime-overview#subscribing-to-your-own-channels) | **MESSAGE PAYLOAD FORMAT** One fill per message, wrapped in `fill`. Quarter-line legs each get their own message, distinguished by `quarterlineMarketHash`. | Name | Type | Description | | --------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | id | string | The row's key, `{matchId}:{orderId}:{marketHash}`. | | matchId | string | The engine's match. Unique per fill | | orderId | string | The order you signed that produced this fill, not the counterparty's | | tradeId | string | The bet all of this fill's siblings belong to | | marketHash | string | The market. For a parlay or quarter-line bet, the parent market | | quarterlineMarketHash | string or `null` | The specific half-line leg, when the bet was on a quarter line | | isParlay | boolean | `true` when `marketHash` is a parlay parent | | userAddress | string | Your user address, checksummed | | wallet | string | Your proxy address | | fillAmount | string | Your stake in this fill, in base units. See [unit conversions](/developers/unit-conversions). | | fillOdds | string | The odds this fill got, in the sx.bet protocol format. To convert to implied odds divide by 10^20. See [unit conversions](/developers/unit-conversions). | | returnAmount | string | Gross return for this fill if it wins, stake included | | netReturnAmount | string | `returnAmount` less the projected settlement payout fee. | | isBettingOutcomeOne | boolean | The side you backed | | status | string | `"MATCHED"`, `"LOCKED"`, `"FAILED"` or `"SETTLED"`. See [bet lifecycle](/developers/bet-lifecycle) | | isMaker | boolean | Which side of the match this fill was on. `true` on the maker side — your order was the resting one; `false` on the taker side | | txHash | string or `null` | The on-chain lock transaction. `null` until it lands | | ceRefundAmount | string | Capital-efficiency refund on this fill. See [capital efficiency](/developers/capital-efficiency) | | ceRefundFeeAmount | string | The fee taken on that refund | | usedBetCredits | boolean | Whether bet credits paid for it | | clientOrderId | string | Your own tag from the order that produced this fill, present only when set — the maker's tag on maker rows, the taker's on taker rows. See [client order id](/developers/client-order-id) | | externalUserId | string | Optional partner tag from the order that produced this fill, present only when set — the maker's tag on maker rows, the taker's on taker rows. See [External user id](/developers/external-user-id) | | createdAt | string | ISO timestamp for when the row was created | | updatedAt | string | ISO timestamp for the last modification of this row | | settlement | object or `null` | `null` until the fill is `"SETTLED"`. A different shape from the bet-grain block — it carries `settleTxHash`, has no `settleDate`, and its `settleFeeAmount` is nullable. Do not share a parser between the two | *** ```javascript JavaScript theme={null} // To subscribe const address = "0xbcc6D643e4159A75ED1dB4e13330230B82F2AEe5"; // checksummed const sub = client.newSubscription(`account:fills_v3_#${address}`, { recoverable: true }); sub.on("publication", (ctx) => { const data = ctx.data; // message handler logic }); sub.subscribe(); ``` ```python Python theme={null} import asyncio from centrifuge import Client, PublicationContext, SubscriptionEventHandler async def on_publication(ctx: PublicationContext) -> None: print(ctx.data) async def main(): address = "0xbcc6D643e4159A75ED1dB4e13330230B82F2AEe5" # checksummed client = Client( "wss://realtime.sx.bet/connection/websocket", token="YOUR_TOKEN", # from /user/realtime-token-v3/api-key ) await client.connect() handler = SubscriptionEventHandler(on_publication=on_publication) sub = client.new_subscription(f"account:fills_v3_#{address}", handler) await sub.subscribe() await asyncio.Future() # keep running asyncio.run(main()) ``` The above returns JSON structured like this: ```json theme={null} { "fill": { "id": "0xec8eaaa2…5793:0x02d22c2a…c55ea:0x1fec4ed9…3266", "matchId": "0xec8eaaa21ef52bf5f2a3fee5f139258a63f65cf2218dfee40362f2fc90e55793", "orderId": "0x02d22c2ac3f2aa27ca96e156aa2fa4affb15064a169e02b55881bdd6867c55ea", "tradeId": "0xac6bb30bba6ea82d8717219a4fa49d15b58fd638fba479f294b4d402376d14d6", "marketHash": "0x1fec4ed9b71c2f812db900af7d12446158a0eda56041106f99551c496efd3266", "quarterlineMarketHash": null, "isParlay": false, "userAddress": "0xbcc6D643e4159A75ED1dB4e13330230B82F2AEe5", "wallet": "0x4361123dbdc1D812fdf7D27045aF358C9C8AA70A", "fillAmount": "333334", "fillOdds": "40000000000000000000", "returnAmount": "833335", "netReturnAmount": "829335", "isBettingOutcomeOne": false, "isMaker": false, "status": "LOCKED", "txHash": "0x64307e2f35d0a81b0f44e9043e99cf01def3b2f079fb68cf2c5a942b88d95e9a", "ceRefundAmount": "0", "ceRefundFeeAmount": "0", "usedBetCredits": false, "createdAt": "2026-07-31T18:47:09.577Z", "updatedAt": "2026-07-31T18:47:12.837Z", "settlement": null } } ``` # Live Score & Fixture Updates Source: https://docs.sx.bet/api-reference/channel-fixtures Subscribe to real-time live scores and fixture updates Two channels carry fixture-related data. Subscribe to one or both depending on your use case. *** ## Live Scores Subscribe to live score updates across all active events. **CHANNEL NAME** `fixtures:live_scores` **MESSAGE PAYLOAD FORMAT** | Name | Type | Description | | ------------- | ---------- | -------------------------------------------------------------------------- | | teamOneScore | number | The current score for the home team (`teamOneName` in the `Market` object) | | teamTwoScore | number | The current score for the away team (`teamTwoName` in the `Market` object) | | sportXeventId | string | The event ID for this update | | currentPeriod | string | An identifier for the current period | | periodTime | string | The current time for the period. `"-1"` if not applicable (e.g. tennis) | | sportId | number | The sport ID for this market | | leagueId | number | The league ID for this market | | periods | `Period[]` | Individual period information | | extra | string | JSON-encoded extra data for this live score update | Where a `Period` object looks like: | Name | Type | Description | | ------------ | ------- | ---------------------------- | | label | string | The period name | | isFinished | boolean | `true` if the period is over | | teamOneScore | string | The score of team one | | teamTwoScore | string | The score of team two | ```javascript JavaScript theme={null} // To subscribe const sub = client.newSubscription("fixtures:live_scores"); sub.on("publication", (ctx) => { const data = ctx.data; // message handler logic }); sub.subscribe(); ``` ```python Python theme={null} import asyncio from centrifuge import Client, PublicationContext, SubscriptionEventHandler async def on_publication(ctx: PublicationContext) -> None: print(ctx.data) async def main(): client = Client( "wss://realtime.sx.bet/connection/websocket", token="YOUR_TOKEN", # from /user/realtime-token-v3/api-key ) await client.connect() handler = SubscriptionEventHandler(on_publication=on_publication) sub = client.new_subscription("fixtures:live_scores", handler) await sub.subscribe() await asyncio.Future() # keep running asyncio.run(main()) ``` The above returns JSON structured like this: ```json theme={null} { "teamOneScore": 2, "teamTwoScore": 1, "sportXeventId": "L7178624", "currentPeriod": "4th Set", "periodTime": "-1", "sportId": 6, "leagueId": 1263, "periods": [ { "label": "1st Set", "isFinished": true, "teamOneScore": "4", "teamTwoScore": "6" }, { "label": "4th Set", "isFinished": false, "teamOneScore": "1", "teamTwoScore": "2" } ], "extra": "..." } ``` *** ## Fixture Updates Subscribe to fixture state changes across all events. **CHANNEL NAME** `fixtures:global` **MESSAGE PAYLOAD FORMAT** | Name | Type | Description | | ------------- | ------ | --------------------------------------------------------------------------- | | sportXeventId | string | The event ID for this update | | status | number | The fixture status. See [fixture statuses](/api-reference/fixture-statuses) | If a status change means you need the affected markets, look them up by event ID. ```javascript JavaScript theme={null} // To subscribe const sub = client.newSubscription("fixtures:global"); sub.on("publication", (ctx) => { const data = ctx.data; // message handler logic }); sub.subscribe(); ``` ```python Python theme={null} import asyncio from centrifuge import Client, PublicationContext, SubscriptionEventHandler async def on_publication(ctx: PublicationContext) -> None: print(ctx.data) async def main(): client = Client( "wss://realtime.sx.bet/connection/websocket", token="YOUR_TOKEN", # from /user/realtime-token-v3/api-key ) await client.connect() handler = SubscriptionEventHandler(on_publication=on_publication) sub = client.new_subscription("fixtures:global", handler) await sub.subscribe() await asyncio.Future() # keep running asyncio.run(main()) ``` The above returns JSON structured like this: ```json theme={null} { "sportXeventId": "L12003787", "status": 2 } ``` # Line Changes Source: https://docs.sx.bet/api-reference/channel-line-changes Subscribe to real-time line changes Subscribe to all line changes. Messages are sent for particular combinations of event IDs and market types. Note that only market types with lines will have updates sent. See [the market types section](/api-reference/market-types) for details on which types have lines. **CHANNEL NAME** `main_line:global` **MESSAGE PAYLOAD FORMAT** | Name | Type | Description | | ------------- | ------ | ------------------------------------------------------------------- | | marketHash | string | The market which is now the main line for this event ID | | marketType | number | The type of market this update refers to | | sportXeventId | string | The event ID for this update | | updateTime | string | Millisecond epoch of the change, stringified. Compare as an integer | To get the actual line, fetch the market using the `marketHash`. *** ```javascript JavaScript theme={null} // To subscribe const sub = client.newSubscription("main_line:global"); sub.on("publication", (ctx) => { const data = ctx.data; // message handler logic }); sub.subscribe(); ``` ```python Python theme={null} import asyncio from centrifuge import Client, PublicationContext, SubscriptionEventHandler async def on_publication(ctx: PublicationContext) -> None: print(ctx.data) async def main(): client = Client( "wss://realtime.sx.bet/connection/websocket", token="YOUR_TOKEN", # from /user/realtime-token-v3/api-key ) await client.connect() handler = SubscriptionEventHandler(on_publication=on_publication) sub = client.new_subscription("main_line:global", handler) await sub.subscribe() await asyncio.Future() # keep running asyncio.run(main()) ``` The above returns JSON structured like this: ```json theme={null} [ { "sportXeventId": "L12003787", "marketType": 3, "marketHash": "0x38cceead7bda65c18574a34994ebd8af154725d08aa735dcbf26247a7dcc67bd", "updateTime": "1785765520299" } ] ``` # Market Updates Source: https://docs.sx.bet/api-reference/channel-markets Subscribe to real-time market changes Subscribe to all changes in markets on sx.bet. You will get updates when: * A new market is added * A market is removed (set to `INACTIVE`) * A market's fields have changed (for example, game time has changed or the market has settled) **CHANNEL NAME** `markets:global` **MESSAGE PAYLOAD FORMAT** See [the markets section](/api-reference/get-markets-active) for the format of the message. *** ```javascript JavaScript theme={null} // To subscribe const sub = client.newSubscription("markets:global"); sub.on("publication", (ctx) => { const data = ctx.data; // message handler logic }); sub.subscribe(); ``` ```python Python theme={null} import asyncio from centrifuge import Client, PublicationContext, SubscriptionEventHandler async def on_publication(ctx: PublicationContext) -> None: print(ctx.data) async def main(): client = Client( "wss://realtime.sx.bet/connection/websocket", token="YOUR_TOKEN", # from /user/realtime-token-v3/api-key ) await client.connect() handler = SubscriptionEventHandler(on_publication=on_publication) sub = client.new_subscription("markets:global", handler) await sub.subscribe() await asyncio.Future() # keep running asyncio.run(main()) ``` The above returns JSON structured like this: ```json theme={null} [ { "status": "ACTIVE", "marketHash": "0xbf06c6379c922d8118612d1d7493b20f6df6929437fbf6022904327f9516a2eb", "outcomeOneName": "Cf Montreal", "outcomeTwoName": "The Field", "outcomeVoidName": "NO_CONTEST", "teamOneName": "Cf Montreal", "teamTwoName": "The Field", "participantOneId": 53730576, "participantTwoId": null, "type": 274, "gameTime": 2208988800, "line": null, "reportedDate": null, "outcome": null, "teamOneScore": null, "teamTwoScore": null, "sportXeventId": "L12003787", "liveEnabled": false, "sportLabel": "Soccer", "sportId": 5, "leagueId": 1115, "leagueLabel": "Major League Soccer", "group1": "Major League Soccer" } ] ``` # Order Book Updates Source: https://docs.sx.bet/api-reference/channel-orderbook-v3 Subscribe to real-time changes in a market's order book Subscribe to changes in a particular order book. You will receive updates when orders are posted, cancelled, expire, or are filled. Every publication is the complete resting book for that market, not a delta — replace your state for that market rather than patching it. Each payload carries a `version` (see below). Apply a publication only when its `version` is strictly greater than the one you hold for that market — see [book versioning](/developers/book-versioning). Watching many markets on the same events? Subscribe to [`orderbook_v3_event:{eventId}`](/api-reference/channel-orderbook-v3-event) instead. **CHANNEL NAME FORMAT** `orderbook_v3:{marketHash}` | Name | Type | Description | | ---------- | ------ | -------------------------- | | marketHash | string | The market to subscribe to | **MESSAGE PAYLOAD FORMAT** | Name | Type | Description | | ---------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | marketHash | string | The market this book belongs to | | eventId | string | The event this market belongs to | | version | string | A single string that increases as the book changes. Apply an update only when it is strictly greater than the one you hold — see [book versioning](/developers/book-versioning) | | outcomeOne | `Level[]` | Resting levels on outcome one, sorted best first | | outcomeTwo | `Level[]` | Resting levels on outcome two, sorted best first | Where a `Level` object looks like: | Name | Type | Description | | -------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | percentageOdds | string | The odds the `maker` receives in the sx.bet protocol format. To convert to implied odds divide by 10^20. To get taker implied odds: `takerOdds = 1 - percentageOdds / 10^20`. See [unit conversions](/developers/unit-conversions). | | size | string | Aggregate remaining maker size at that level, in base units. See [unit conversions](/developers/unit-conversions). | There is no `showTakerPerspective` parameter on this channel — levels are always the maker frame, unlike [`GET /orderbook-v3/snapshot`](/api-reference/get-orderbook-snapshot), which accepts it. *** ```javascript JavaScript theme={null} // To subscribe const marketHash = "0xbf06c6379c922d8118612d1d7493b20f6df6929437fbf6022904327f9516a2eb"; const sub = client.newSubscription(`orderbook_v3:${marketHash}`, { recoverable: true }); sub.on("publication", (ctx) => { const data = ctx.data; // message handler logic }); sub.subscribe(); ``` ```python Python theme={null} import asyncio from centrifuge import Client, PublicationContext, SubscriptionEventHandler async def on_publication(ctx: PublicationContext) -> None: print(ctx.data) async def main(): market_hash = "0xbf06c6379c922d8118612d1d7493b20f6df6929437fbf6022904327f9516a2eb" client = Client( "wss://realtime.sx.bet/connection/websocket", token="YOUR_TOKEN", # from /user/realtime-token-v3/api-key ) await client.connect() handler = SubscriptionEventHandler(on_publication=on_publication) sub = client.new_subscription(f"orderbook_v3:{market_hash}", handler) await sub.subscribe() await asyncio.Future() # keep running asyncio.run(main()) ``` The above returns JSON structured like this: ```json theme={null} { "marketHash": "0xbf06c6379c922d8118612d1d7493b20f6df6929437fbf6022904327f9516a2eb", "eventId": "L13058397", "version": "00100000000000010015000", "outcomeOne": [{ "percentageOdds": "40000000000000000000", "size": "1000000" }], "outcomeTwo": [] } ``` # Event Order Book Updates Source: https://docs.sx.bet/api-reference/channel-orderbook-v3-event Subscribe to real-time order book updates for every market on an event Subscribe to order book changes for every market on a single event. The payloads are identical to [`orderbook_v3:{marketHash}`](/api-reference/channel-orderbook-v3), including `marketHash` — this channel exists so that watching a whole event costs one channel instead of one per market. This namespace keeps the last 100 publications for 5 minutes, so pass `recoverable: true` to recover missed messages on reconnect. If a gap comes back `recovered: false`, re-seed from [`GET /orderbook-v3/snapshot/event`](/api-reference/get-event-orderbook-snapshot) — follow `nextKey` until it is omitted. **CHANNEL NAME FORMAT** `orderbook_v3_event:{eventId}` | Name | Type | Description | | ------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------- | | eventId | string | The event to subscribe to. Receives updates for all markets under this event. The same identifier markets report as `sportXeventId` | **MESSAGE PAYLOAD FORMAT** Identical to [`orderbook_v3:{marketHash}`](/api-reference/channel-orderbook-v3), including the `version` field. `version` is per market, so key your state by `marketHash` and apply a publication only when its `version` is strictly greater than the one you hold for that market — see [book versioning](/developers/book-versioning). *** ```javascript JavaScript theme={null} // To subscribe const eventId = "L12003787"; const sub = client.newSubscription(`orderbook_v3_event:${eventId}`, { recoverable: true }); sub.on("publication", (ctx) => { const data = ctx.data; // message handler logic — key state by data.marketHash }); sub.subscribe(); ``` ```python Python theme={null} import asyncio from centrifuge import Client, PublicationContext, SubscriptionEventHandler async def on_publication(ctx: PublicationContext) -> None: print(ctx.data) async def main(): event_id = "L12003787" client = Client( "wss://realtime.sx.bet/connection/websocket", token="YOUR_TOKEN", # from /user/realtime-token-v3/api-key ) await client.connect() handler = SubscriptionEventHandler(on_publication=on_publication) sub = client.new_subscription(f"orderbook_v3_event:{event_id}", handler) await sub.subscribe() await asyncio.Future() # keep running asyncio.run(main()) ``` The above returns JSON structured like this: ```json theme={null} { "marketHash": "0xbf06c6379c922d8118612d1d7493b20f6df6929437fbf6022904327f9516a2eb", "eventId": "L13058397", "version": "00100000000000010015000", "outcomeOne": [{ "percentageOdds": "40000000000000000000", "size": "1000000" }], "outcomeTwo": [] } ``` # Active Order Updates Source: https://docs.sx.bet/api-reference/channel-orders-v3 Subscribe to real-time changes in a user's orders Subscribe to changes in your own orders. You will receive updates when orders rest, are filled, are cancelled, or expire. **CHANNEL NAME FORMAT** `account:orders_v3_#{address}` | Name | Type | Description | | ------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | address | string | Your checksummed address. It must equal your realtime token's `sub` — see [subscribing to your own channels](/developers/realtime-overview#subscribing-to-your-own-channels) | **MESSAGE PAYLOAD FORMAT** One order per message, wrapped in `order`, even when one engine command changed several of your orders. Those arrive as separate publications. The inner object is the same one [`GET /orders-v3`](/api-reference/get-orders-v3) returns in its `data.orders` array. | Name | Type | Description | | ------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | id | string | A unique identifier for this order. | | marketHash | string | The market for this order | | userAddress | string | Your user address, checksummed — the account that signed | | wallet | string | Your proxy address, which is what holds the funds | | isBettingOutcomeOne | boolean | The outcome this order backs. | | percentageOdds | string | The odds the order's owner receives in the sx.bet protocol format. To convert to implied odds divide by 10^20. To get taker implied odds: `takerOdds = 1 - percentageOdds / 10^20`. See [unit conversions](/developers/unit-conversions). | | totalBetSize | string | Total size of this order in base units. See [unit conversions](/developers/unit-conversions). | | remainingSize | string | The unfilled portion of the original order size. The filled amount is `totalBetSize - remainingSize`. Use the `status` field to determine whether the remaining amount is still active. | | expiry | string | ISO 8601 timestamp after which this order is no longer valid. Note the asymmetry: you **send** `expiry` to [`POST /orders-v3`](/api-reference/post-orders-v3) as a unix epoch timestamp in seconds, and **read** it back here as an ISO 8601 string. | | status | string | `"ACTIVE"` if resting, `"INACTIVE"` if no longer resting. | | inactiveReason | string or `null` | `null` while active; one of nine values when terminal. See [Order Lifecycle](/developers/order-lifecycle) | | eventId | string | The event related to this order | | clientOrderId | string | Your own tag, present only when you set one | | externalUserId | string | Optional partner tag, present only when you set one on [`POST /orders-v3`](/api-reference/post-orders-v3). See [External user id](/developers/external-user-id) | | createdAt | string | ISO timestamp for when the order first rested | | updatedAt | string | ISO timestamp for when the change was committed. | *** ```javascript JavaScript theme={null} // To subscribe const address = "0xbcc6D643e4159A75ED1dB4e13330230B82F2AEe5"; // checksummed const sub = client.newSubscription(`account:orders_v3_#${address}`, { recoverable: true }); sub.on("publication", (ctx) => { const data = ctx.data; // message handler logic }); sub.subscribe(); ``` ```python Python theme={null} import asyncio from centrifuge import Client, PublicationContext, SubscriptionEventHandler async def on_publication(ctx: PublicationContext) -> None: print(ctx.data) async def main(): address = "0xbcc6D643e4159A75ED1dB4e13330230B82F2AEe5" # checksummed client = Client( "wss://realtime.sx.bet/connection/websocket", token="YOUR_TOKEN", # from /user/realtime-token-v3/api-key ) await client.connect() handler = SubscriptionEventHandler(on_publication=on_publication) sub = client.new_subscription(f"account:orders_v3_#{address}", handler) await sub.subscribe() await asyncio.Future() # keep running asyncio.run(main()) ``` The above returns JSON structured like this: ```json theme={null} { "order": { "id": "0xbc9c298f6584e484dc23a8b4ac95755faabbb5f966b37b869b3a74acf0790fe6", "marketHash": "0xbf06c6379c922d8118612d1d7493b20f6df6929437fbf6022904327f9516a2eb", "userAddress": "0xbcc6D643e4159A75ED1dB4e13330230B82F2AEe5", "wallet": "0x4361123dbdc1D812fdf7D27045aF358C9C8AA70A", "isBettingOutcomeOne": true, "percentageOdds": "40000000000000000000", "totalBetSize": "1000000", "remainingSize": "1000000", "expiry": "2026-08-03T14:58:31.000Z", "status": "ACTIVE", "inactiveReason": null, "eventId": "L12003787", "createdAt": "2026-08-03T13:58:31.947Z", "updatedAt": "2026-08-03T13:58:32.123Z" } } ``` # Parlay Market Requests Source: https://docs.sx.bet/api-reference/channel-parlay-requests Subscribe to real-time parlay market requests When a bettor requests a Parlay Market, a message is sent via the `parlay_markets:global` channel. In order to offer orders on Parlay Markets, you will need to subscribe to this channel. The payload will contain the `marketHash` associated with the Parlay Market. You can post orders to this market as you would for any other market using this `marketHash`. The payload also contains the token and size the bettor is requesting. The `legs` contain the underlying markets that make up the parlay — query each leg's `marketHash` to get current orders for that individual market. **CHANNEL NAME** `parlay_markets:global` *** **`ParlayMarket` PAYLOAD FORMAT** | Name | Type | Description | | ------------ | ------------------ | -------------------------------------------------------------------------------------------------------------------------- | | channelName | string | Legacy field — always `"markets:parlay"`. | | marketHash | string | The parlay market associated with this request | | requestorId | string | An anonymized stable ID for the bettor who requested the parlay market | | baseToken | string | The token this request is denominated in | | requestSize | string | The size in baseTokens that the bettor is requesting. See [unit conversions](/developers/unit-conversions). May be absent. | | chainVersion | string | An internal classification copied from the legs. May be absent. | | legs | ParlayMarketLeg\[] | An array of legs that make up the parlay | **`ParlayMarketLeg` PAYLOAD FORMAT** | Name | Type | Description | | ----------------- | ------- | -------------------------------------------------- | | marketHash | string | The market for an individual leg within the parlay | | bettingOutcomeOne | boolean | The side the bettor is betting for this leg | The `requestSize` only indicates what the user is requesting and does not limit how much you can offer. You are allowed to offer any size. *** ```javascript JavaScript theme={null} // To subscribe const sub = client.newSubscription("parlay_markets:global"); sub.on("publication", (ctx) => { const data = ctx.data; // message handler logic }); sub.subscribe(); ``` ```python Python theme={null} import asyncio from centrifuge import Client, PublicationContext, SubscriptionEventHandler async def on_publication(ctx: PublicationContext) -> None: print(ctx.data) async def main(): client = Client( "wss://realtime.sx.bet/connection/websocket", token="YOUR_TOKEN", # from /user/realtime-token-v3/api-key ) await client.connect() handler = SubscriptionEventHandler(on_publication=on_publication) sub = client.new_subscription("parlay_markets:global", handler) await sub.subscribe() await asyncio.Future() # keep running asyncio.run(main()) ``` The above returns JSON structured like this: ```json theme={null} { "channelName": "markets:parlay", "marketHash": "0x3f8893a68554eca5aaee57896505ea345e757b8809e8301b8ad8873a98b1c73b", "requestorId": "08e489e45313812cf236b43ab68c6236e3e8d63375a07d6bafdec1ecbed85c1b", "baseToken": "0x1BC6326EA6aF2aB8E4b6Bc83418044B1923b2956", "requestSize": "50000000", "chainVersion": "SXR", "legs": [ { "marketHash": "0x22ed4cf508418f44e787f9c8e79f76eb31587efa20fe700b3582e09f01775944", "bettingOutcomeOne": false }, { "marketHash": "0x186685cf65e1a22952ad42982e1ec75b6a457fa3bdec59fa6f891258a718ddfe", "bettingOutcomeOne": true } ] } ``` # Public Trade Updates Source: https://docs.sx.bet/api-reference/channel-recent-trades-v3 Subscribe to real-time public trade updates Subscribe to all public trade updates on the exchange. Only the taker's side of a match is published, so the tape is one-sided. **CHANNEL NAME** `recent_trades_v3:global` **MESSAGE PAYLOAD FORMAT** The body is `{"trade": {…}}` — note the wrapper. The inner object is the same one [`GET /trades-v3/public`](/api-reference/get-trades-v3-public) returns. | Name | Type | Description | | ------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | tradeId | string | Groups the fills that made up this bet. | | marketHash | string | The market. For a parlay or quarter-line bet, the parent market | | isBettingOutcomeOne | boolean | The side the taker backed | | isParlay | boolean | `true` for a parlay bet | | totalStake | string | The stake in base units. See [unit conversions](/developers/unit-conversions). | | totalReturn | string | Total payout if the bet wins, stake included | | weightedAverageOdds | string | The odds in the sx.bet protocol format, blended across the bet's fills, from the bettor's perspective. To convert to implied odds divide by 10^20. See [unit conversions](/developers/unit-conversions). | | betTime | string | ISO timestamp for when the bet was placed | | gameTime | string | ISO timestamp for kickoff | | outcomeLabel | string (optional) | Label for the side that was backed — outcome one when `isBettingOutcomeOne` is true, otherwise outcome two. Absent when market metadata cannot be resolved (e.g. some parlay parents) | | teamOneName | string (optional) | Team one display name. Absent when market metadata cannot be resolved | | teamTwoName | string (optional) | Team two display name. Absent when market metadata cannot be resolved | | type | number (optional) | Market type id. See [Market types](/api-reference/market-types). Absent when market metadata cannot be resolved | | leagueLabel | string (optional) | League display name. Absent when market metadata cannot be resolved | | sportId | number (optional) | Sport id. Absent when market metadata cannot be resolved | | eventId | string (optional) | Prefixed event id (e.g. `L12003787`). Absent when market metadata cannot be resolved | ```javascript JavaScript theme={null} // To subscribe const sub = client.newSubscription("recent_trades_v3:global", { recoverable: true }); sub.on("publication", (ctx) => { const data = ctx.data; // message handler logic }); sub.subscribe(); ``` ```python Python theme={null} import asyncio from centrifuge import Client, PublicationContext, SubscriptionEventHandler async def on_publication(ctx: PublicationContext) -> None: print(ctx.data) async def main(): client = Client( "wss://realtime.sx.bet/connection/websocket", token="YOUR_TOKEN", # from /user/realtime-token-v3/api-key ) await client.connect() handler = SubscriptionEventHandler(on_publication=on_publication) sub = client.new_subscription("recent_trades_v3:global", handler) await sub.subscribe() await asyncio.Future() # keep running asyncio.run(main()) ``` The above returns JSON structured like this: ```json theme={null} { "trade": { "tradeId": "0xac6bb30bba6ea82d8717219a4fa49d15b58fd638fba479f294b4d402376d14d6", "marketHash": "0x1fec4ed9b71c2f812db900af7d12446158a0eda56041106f99551c496efd3266", "isBettingOutcomeOne": true, "isParlay": false, "totalStake": "1000000", "totalReturn": "2500000", "weightedAverageOdds": "40000000000000000000", "betTime": "2026-07-31T18:22:41.000Z", "gameTime": "2026-08-01T23:00:00.000Z", "outcomeLabel": "Raptors", "teamOneName": "Toronto Raptors", "teamTwoName": "Charlotte Hornets", "type": 1, "leagueLabel": "NBA", "sportId": 1, "eventId": "L12003787" } } ``` # Your Trade Updates Source: https://docs.sx.bet/api-reference/channel-trades-v3 Subscribe to real-time updates for your own trades Subscribe to your own bets at bet grain — one row per bet you placed, whatever number of makers filled it. For one message per match instead, use [`account:fills_v3_#{address}`](/api-reference/channel-fills-v3). You will receive updates when a bet is matched, locks, fails, or settles. **The first message for a new bet is `MATCHED`, which comes before the on-chain lock.** A second message follows once the funds are escrowed (`LOCKED`) or the escrow fails (`FAILED`). Act on `MATCHED` for speed, or wait for `LOCKED` in certain situations — see [which status to act on](/developers/bet-lifecycle#which-status-to-act-on). **CHANNEL NAME FORMAT** `account:trades_v3_#{address}` | Name | Type | Description | | ------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | address | string | Your checksummed address. It must equal your realtime token's `sub` — see [subscribing to your own channels](/developers/realtime-overview#subscribing-to-your-own-channels) | **MESSAGE PAYLOAD FORMAT** One bet per message, wrapped in `trade`. Every message carries the whole current row, not a delta. | Name | Type | Description | | ------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | tradeId | string | The bet. Every fill for this bet carries the same value | | userAddress | string | Your user address, checksummed | | wallet | string | Your proxy address — where the funds live | | marketHash | string | The market. For a parlay or quarter-line bet, the parent market | | isBettingOutcomeOne | boolean | The side you backed | | isMaker | boolean | Which side of the book you were on. `true` when you were the maker — your order rested and was filled; `false` when you were the taker | | isParlay | boolean | `true` when `marketHash` is a parlay parent | | totalStake | string | The amount risked, in base units. See [unit conversions](/developers/unit-conversions). | | totalReturn | string | Gross payout if it wins, stake included | | netTotalReturn | string | `totalReturn` less the projected settlement payout fee — what you would actually be paid. | | weightedAverageOdds | string | The odds in the sx.bet protocol format, blended across the bet's fills. To convert to implied odds divide by 10^20. See [unit conversions](/developers/unit-conversions). | | ceRefundAmount | string | Capital-efficiency refund on this bet. See [capital efficiency](/developers/capital-efficiency) | | ceRefundFeeAmount | string | The fee taken on that refund | | usedBetCredits | boolean | Whether bet credits paid for it | | fillCount | number | How many fills make up this bet. A number, while every amount is a string | | status | string | `"MATCHED"`, `"LOCKED"`, `"FAILED"` or `"SETTLED"`. See [bet lifecycle](/developers/bet-lifecycle) | | ownOrderIds | string\[] | Order ids of your own orders behind this bet — the same values as `id` on order rows and `orderId` from [`POST /orders-v3`](/api-reference/post-orders-v3). These are not `clientOrderId`s. | | externalUserId | string | Optional partner tag from the order that produced this bet, present only when set. See [External user id](/developers/external-user-id) | | betTime | string | ISO timestamp for when the bet was placed | | gameTime | string | ISO timestamp for kickoff | | createdAt | string | ISO timestamp for when the row was created | | updatedAt | string | ISO timestamp for the last modification of this row | | settlement | object or `null` | `null` on every message until `status` is `"SETTLED"` | Where a `settlement` object looks like: | Name | Type | Description | | -------------------- | ------ | ----------------------------------------------- | | outcome | number | The graded outcome | | settleReturnAmount | string | What was returned on settlement | | settleFeeAmount | string | The fee taken on settlement | | settleCeRefundAmount | string | Capital-efficiency refund applied at settlement | | settleDate | string | ISO timestamp for when the bet settled | *** ```javascript JavaScript theme={null} // To subscribe const address = "0xbcc6D643e4159A75ED1dB4e13330230B82F2AEe5"; // checksummed const sub = client.newSubscription(`account:trades_v3_#${address}`, { recoverable: true }); sub.on("publication", (ctx) => { const data = ctx.data; // message handler logic }); sub.subscribe(); ``` ```python Python theme={null} import asyncio from centrifuge import Client, PublicationContext, SubscriptionEventHandler async def on_publication(ctx: PublicationContext) -> None: print(ctx.data) async def main(): address = "0xbcc6D643e4159A75ED1dB4e13330230B82F2AEe5" # checksummed client = Client( "wss://realtime.sx.bet/connection/websocket", token="YOUR_TOKEN", # from /user/realtime-token-v3/api-key ) await client.connect() handler = SubscriptionEventHandler(on_publication=on_publication) sub = client.new_subscription(f"account:trades_v3_#{address}", handler) await sub.subscribe() await asyncio.Future() # keep running asyncio.run(main()) ``` The above returns JSON structured like this: ```json theme={null} { "trade": { "tradeId": "0xac6bb30bba6ea82d8717219a4fa49d15b58fd638fba479f294b4d402376d14d6", "userAddress": "0xbcc6D643e4159A75ED1dB4e13330230B82F2AEe5", "wallet": "0x4361123dbdc1D812fdf7D27045aF358C9C8AA70A", "marketHash": "0x1fec4ed9b71c2f812db900af7d12446158a0eda56041106f99551c496efd3266", "isBettingOutcomeOne": false, "isMaker": false, "isParlay": false, "totalStake": "1000000", "totalReturn": "2500000", "netTotalReturn": "2488000", "weightedAverageOdds": "40000000000000000000", "ceRefundAmount": "0", "ceRefundFeeAmount": "0", "usedBetCredits": false, "fillCount": 2, "ownOrderIds": ["0x02d22c2ac3f2aa27ca96e156aa2fa4affb15064a169e02b55881bdd6867c55ea"], "status": "LOCKED", "betTime": "2026-07-31T18:47:09.577Z", "gameTime": "2040-01-01T00:00:00.000Z", "createdAt": "2026-07-31T18:47:09.575Z", "updatedAt": "2026-07-31T18:47:12.837Z", "settlement": null } } ``` # Cancel orders by id Source: https://docs.sx.bet/api-reference/delete-orders-v3 DELETE /orders-v3 Cancel specific open orders by order id on the SX Bet exchange. `DELETE /orders-v3` cancels orders you name by id, and is the only cancel route that reports a **per-order outcome**. This is a synchronous endpoint. **The HTTP status is not the cancel outcome** This route returns `200` whether or not anything was cancelled. An id you do not own, an id that no longer exists, and an id you already cancelled all arrive as `200` with the order in `notCancelled`. **There is a 5-second confirmation timeout.** Orders that report in time land in `cancelled` or `notCancelled`; any that do not report before the timeout come back in the `unconfirmed` bucket. `unconfirmed` does **not** mean the cancel failed — the command was still submitted and usually takes effect shortly after. Re-read [`GET /orders-v3`](/api-reference/get-orders-v3) to confirm, rather than assuming either outcome. **At most `limits.maxCancelOrders` ids per request (100 today).** Read the cap from [`GET /metadata/obv3`](/api-reference/get-metadata-obv3); exceeding it is a `400`. To cancel every order at once, use [`DELETE /orders-v3/all`](/api-reference/delete-orders-v3-all) instead of paging ids through this route. # Cancel all orders Source: https://docs.sx.bet/api-reference/delete-orders-v3-all DELETE /orders-v3/all Cancel all your open orders across all markets on SX Bet. `DELETE /orders-v3/all` submits a cancel command for every `PENDING` or `ACTIVE` order belonging to the authenticated address, across every market and event. **This is an asynchronous endpoint — a `200` means submitted, not cancelled.** The route publishes cancel commands and returns without waiting for a response, so your orders are still on the book when the response arrives. Confirm with [`GET /orders-v3?eventId=…`](/api-reference/get-orders-v3) or watch [`account:orders_v3_#{address}`](/api-reference/channel-orders-v3) before acting on the result. # Cancel orders by event Source: https://docs.sx.bet/api-reference/delete-orders-v3-event DELETE /orders-v3/event Cancel all your open orders for a specific event on SX Bet. `DELETE /orders-v3/event` submits a cancel command for every `PENDING` or `ACTIVE` order you hold on a single event, across all of that event's markets and both outcomes. For anything finer than an event — one market, one side, one price level — name the ids on [`DELETE /orders-v3`](/api-reference/delete-orders-v3); there is no cancel-by-market route. **This is an asynchronous endpoint — a `200` means submitted, not cancelled.** The route publishes cancel commands and returns without waiting for a response, so your orders are still on the book when the response arrives. Confirm with [`GET /orders-v3?eventId=…`](/api-reference/get-orders-v3) or watch [`account:orders_v3_#{address}`](/api-reference/channel-orders-v3) before acting on the result. # Order signing Source: https://docs.sx.bet/api-reference/eip712-order-signing How to sign an order with your private key or an injected wallet. `POST /orders-v3` requires an EIP-712 signature over the eight-field `Order` struct. Read the domain from [`GET /metadata/obv3`](/api-reference/get-metadata-obv3). See [Posting orders](/developers/posting-orders). ## Private key signing ```js JavaScript theme={null} import { Wallet, randomBytes, hexlify } from "ethers"; const wallet = new Wallet(process.env.SX_PRIVATE_KEY); const { data: meta } = await fetch("https://api.sx.bet/metadata/obv3").then((r) => r.json() ); const domain = meta.domain; // { name, version, chainId, verifyingContract } const types = { Order: [ { name: "marketHash", type: "bytes32" }, { name: "baseToken", type: "address" }, { name: "totalBetSize", type: "uint256" }, { name: "percentageOdds", type: "uint256" }, { name: "salt", type: "uint256" }, { name: "expiry", type: "uint256" }, { name: "maker", type: "address" }, { name: "isMakerBettingOutcomeOne", type: "bool" }, ], }; const value = { marketHash: "0x…", // 32-byte market hash baseToken: meta.activeAsset.baseToken, totalBetSize: "1000000", // 1 USDC, base units percentageOdds: "50000000000000000000", // 50% on the 1e20 scale salt: hexlify(randomBytes(32)), expiry: Math.floor(Date.now() / 1000) + 3600, // unix seconds; here, 1 hour out maker: wallet.address, isMakerBettingOutcomeOne: true, }; const orderSignature = await wallet.signTypedData(domain, types, value); ``` ```python Python theme={null} import os import secrets import time import requests from eth_account import Account from eth_account.messages import encode_typed_data account = Account.from_key(os.environ["SX_PRIVATE_KEY"]) meta = requests.get("https://api.sx.bet/metadata/obv3").json()["data"] structured_data = { "types": { "EIP712Domain": [ {"name": "name", "type": "string"}, {"name": "version", "type": "string"}, {"name": "chainId", "type": "uint256"}, {"name": "verifyingContract", "type": "address"}, ], "Order": [ {"name": "marketHash", "type": "bytes32"}, {"name": "baseToken", "type": "address"}, {"name": "totalBetSize", "type": "uint256"}, {"name": "percentageOdds", "type": "uint256"}, {"name": "salt", "type": "uint256"}, {"name": "expiry", "type": "uint256"}, {"name": "maker", "type": "address"}, {"name": "isMakerBettingOutcomeOne", "type": "bool"}, ], }, "primaryType": "Order", "domain": meta["domain"], "message": { "marketHash": "0x…", "baseToken": meta["activeAsset"]["baseToken"], "totalBetSize": 1000000, "percentageOdds": 50000000000000000000, "salt": int("0x" + secrets.token_hex(32), 16), "expiry": int(time.time()) + 3600, "maker": account.address, "isMakerBettingOutcomeOne": True, }, } signed = Account.sign_message( encode_typed_data(full_message=structured_data), private_key=os.environ["SX_PRIVATE_KEY"], ) order_signature = signed.signature.to_0x_hex() ``` Using a private key is the most straightforward approach for bots and scripts. *** ## Injected provider signing ```js theme={null} import { BrowserProvider, randomBytes, hexlify } from "ethers"; const provider = new BrowserProvider(window.ethereum); const signer = await provider.getSigner(); const { data: meta } = await fetch("https://api.sx.bet/metadata/obv3").then((r) => r.json() ); const domain = meta.domain; const types = { Order: [ { name: "marketHash", type: "bytes32" }, { name: "baseToken", type: "address" }, { name: "totalBetSize", type: "uint256" }, { name: "percentageOdds", type: "uint256" }, { name: "salt", type: "uint256" }, { name: "expiry", type: "uint256" }, { name: "maker", type: "address" }, { name: "isMakerBettingOutcomeOne", type: "bool" }, ], }; const value = { marketHash: "0x…", baseToken: meta.activeAsset.baseToken, totalBetSize: "1000000", percentageOdds: "50000000000000000000", salt: hexlify(randomBytes(32)), expiry: Math.floor(Date.now() / 1000) + 3600, maker: await signer.getAddress(), isMakerBettingOutcomeOne: true, }; const orderSignature = await signer.signTypedData(domain, types, value); ``` Any injected provider that exposes `eth_signTypedData_v4` (e.g. MetaMask) works. > This signing scheme uses the [EIP-712 typed data standard](https://eips.ethereum.org/EIPS/eip-712). # Fixture statuses Source: https://docs.sx.bet/api-reference/fixture-statuses Reference of fixture statuses on SX Bet. A fixture's `status` is a number, returned by [`GET /fixture/active`](/api-reference/get-fixture-active) and [`GET /fixture/status`](/api-reference/get-fixture-status), and published on [`fixtures:global`](/api-reference/channel-fixtures). ## The nine statuses | ID | Name | Description | | -- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 1 | Not started yet | The event has not started yet | | 2 | In progress | The event is live | | 3 | Finished | The event is finished | | 4 | Cancelled | The event has been cancelled | | 5 | Postponed | Postponed and will be played later. If no new start time arrives within 48 hours the event is cancelled; if one does, it returns to **Not started yet** with the new time | | 6 | Interrupted | Temporarily interrupted — a rain delay, for example. Coverage resumes under the same event id | | 7 | Abandoned | A final status. The event will not resume | | 8 | Coverage lost | Coverage for this event has been lost | | 9 | About to start | Not started, but starting soon. Shown up to 30 minutes before the start | ## Which statuses sweep your resting orders If a fixture transitions into an ineligible trading state, all orders are cancelled. | Transition into | Sweeps your orders | `inactiveReason` you receive | | ------------------- | ------------------ | ---------------------------- | | **2 — In progress** | **Yes** | `EVENT_LIFECYCLE` | | **3 — Finished** | **Yes** | `EVENT_LIFECYCLE` | | **4 — Cancelled** | **Yes** | `EVENT_LIFECYCLE` | | 1, 5, 6, 7, 8, 9 | No | — | **The sweep is not a substitute for your own risk controls.** # Best odds Source: https://docs.sx.bet/api-reference/get-best-odds-v3 GET /orders-v3/odds/best Get the best available odds for a set of markets on the SX Bet exchange. `GET /orders-v3/odds/best` returns the **best level on each side** for a set of markets. It requires an [API key](/api-reference/api-key). For every level on a single market, use [`GET /orderbook-v3/snapshot`](/api-reference/get-orderbook-snapshot) — both read the top of the same book. Prefer the realtime [`best_odds_v3:global`](/api-reference/channel-best-odds-v3) channel over polling this endpoint. Use this route once to seed state, then apply stream publications. `marketHashes` is required — comma-separated, maximum 100. # Get event orderbook snapshot Source: https://docs.sx.bet/api-reference/get-event-orderbook-snapshot GET /orderbook-v3/snapshot/event Get a snapshot of the order book for markets on an event. `GET /orderbook-v3/snapshot/event` returns the **aggregated** book for markets on a single event. It requires an [API key](/api-reference/api-key). Use this to re-seed state for the realtime [`orderbook_v3_event:{eventId}`](/api-reference/channel-orderbook-v3-event) channel after a `recovered: false` publication — page until `nextKey` is omitted, instead of hitting the single-market [snapshot](/api-reference/get-orderbook-snapshot) once per market. Use `showTakerPerspective` for display only. `percentageOdds` is from the perspective of the maker. [Read the order book](/developers/order-book#convert-a-level-into-what-you-can-bet) walks the conversion through. # Get your fills Source: https://docs.sx.bet/api-reference/get-fills-v3 GET /fills-v3 Get your fill history from the SX Bet API. `GET /fills-v3` returns **one row per match**, each with its own amount, price and settlement. Where [`GET /trades-v3`](/api-reference/get-trades-v3) gives one row for a whole bet, this gives the fills that made it up. A trade will have multiple fills that compose it. Narrow with `tradeId` (one bet) or `orderId` (the signed order id — same value as `id` on order rows and `orderId` from [`POST /orders-v3`](/api-reference/post-orders-v3)). # Fixtures Source: https://docs.sx.bet/api-reference/get-fixture-active GET /fixture/active Retrieve active fixtures (games and events) for a league on SX Bet. This endpoint only returns fixtures that have a status of either 1, 2, 6, 7, 8, or 9. See the [fixture statuses](/api-reference/get-fixture-status) page for more details. # Fixture status Source: https://docs.sx.bet/api-reference/get-fixture-status GET /fixture/status Check the current status of specific fixtures by event ID on SX Bet. # Leagues Source: https://docs.sx.bet/api-reference/get-leagues GET /leagues List all leagues supported by the SX Bet exchange. # Active leagues Source: https://docs.sx.bet/api-reference/get-leagues-active GET /leagues/active Get leagues that currently have active markets on the SX Bet exchange. # Get your ledger Source: https://docs.sx.bet/api-reference/get-ledger-v3 GET /ledger-v3 Get your ledger from the SX Bet API. `GET /ledger-v3` is an audit trail for your account. It lists all bets, inflows, and outflows. | Event Type | Meaning | | ------------ | --------------------------------------------------------------------------------------- | | `DEPOSIT` | Money in | | `WITHDRAWAL` | Money out | | `SETTLEMENT` | A bet settled | | `PAYOUT` | Winnings paid | | `REFUND` | Money returned — including [capital-efficiency](/developers/capital-efficiency) refunds | | `ADJUSTMENT` | A manual or corrective movement | **`eventStatus`** — `PENDING`, `SUCCESS`, `FAILED`. **`depositType`** — `BRIDGE`, `FROM_EOA`, `PROXY_TO_PROXY`. **`withdrawalType`** — `BRIDGE`, `PROXY_TO_PROXY`. # Live scores Source: https://docs.sx.bet/api-reference/get-live-scores GET /live-scores Get real-time live scores for active events on SX Bet. # Active markets Source: https://docs.sx.bet/api-reference/get-markets-active GET /markets/active Retrieve all active betting markets on the SX Bet exchange, with filters for sport, league, and event. To retrieve odds for a particular market, you must query the orderbook separately — [`GET /orderbook-v3/snapshot`](/api-reference/get-orderbook-snapshot) Only one of `type` and `betGroup` can be present. Not both. # Find markets Source: https://docs.sx.bet/api-reference/get-markets-find GET /markets/find Look up specific markets by their market hash, including settlement details for resolved markets. There are a few additional fields if you are querying a market that has been settled/reported: `reportedDate`, `outcome`, `teamOneScore`, `teamTwoScore`. # Popular markets Source: https://docs.sx.bet/api-reference/get-markets-popular GET /markets/popular Get the most popular betting markets by volume on the SX Bet exchange. # Exchange metadata Source: https://docs.sx.bet/api-reference/get-metadata-obv3 GET /metadata/obv3 Retrieve exchange metadata including contract addresses, supported tokens, and limits This is a sample response. Visit [https://api.sx.bet/metadata/obv3](https://api.sx.bet/metadata/obv3) (or [https://api.toronto.sx.bet/metadata/obv3](https://api.toronto.sx.bet/metadata/obv3)) for up-to-date values. # Get an order by ID Source: https://docs.sx.bet/api-reference/get-order-v3 GET /orders-v3/{orderId} Look up one of your orders by its signed order ID — any status. `GET /orders-v3/{orderId}` returns **one order** belonging to the authenticated account. Unlike [`GET /orders-v3`](/api-reference/get-orders-v3), this is **not** limited to orders resting on the book — filled, cancelled, and expired orders are returned when they still exist. Pass the signed order ID: the same value as `id` on order rows and `orderId` from [`POST /orders-v3`](/api-reference/post-orders-v3). A missing id, or an id that belongs to another account, returns **404**. The response does not distinguish those cases. # Get an order by client ID Source: https://docs.sx.bet/api-reference/get-order-v3-by-client-id GET /orders-v3/client/{clientOrderId} Look up one of your orders by the clientOrderId you set on submit — any status. `GET /orders-v3/client/{clientOrderId}` returns **one order** belonging to the authenticated account. Unlike [`GET /orders-v3`](/api-reference/get-orders-v3), this is **not** limited to orders resting on the book — filled, cancelled, and expired orders are returned when they still exist. Pass the `clientOrderId` you set on [`POST /orders-v3`](/api-reference/post-orders-v3). The tag is [unique per address](/developers/client-order-id#uniqueness), so this returns at most one row. A missing tag, a tag that was never set, or a tag that belongs to another account, returns **404**. The response does not distinguish those cases. # Get orderbook snapshot Source: https://docs.sx.bet/api-reference/get-orderbook-snapshot GET /orderbook-v3/snapshot Get a snapshot of the order book for a market on SX Bet. `GET /orderbook-v3/snapshot` returns the **aggregated** book for a **single** market. Prefer the realtime [`orderbook_v3:{marketHash}`](/api-reference/channel-orderbook-v3) channel over polling this endpoint. Use the snapshot once to bootstrap state, then apply stream publications. Use `showTakerPerspective` for display only. `percentageOdds` is from the perspective of the maker. [Read the order book](/developers/order-book#convert-a-level-into-what-you-can-bet) walks the conversion through. # Get your orders Source: https://docs.sx.bet/api-reference/get-orders-v3 GET /orders-v3 Query your active orders on the SX Bet orderbook. `GET /orders-v3` returns the orders the authenticated account has **resting on the book right now**, oldest first. The scope is your own account. # Get your positions Source: https://docs.sx.bet/api-reference/get-positions-v3 GET /positions-v3 Get your positions from the SX Bet API. `GET /positions-v3` rolls your bets up **per market**. Ten bets on one market are one row here, with the exposure already netted: what you win if outcome one lands, what you lose if it does not, and the blended price you got on each side. [Tracking positions](/developers/which-grain#positions) covers what the roll-up does and does not net. # Sports Source: https://docs.sx.bet/api-reference/get-sports GET /sports List all sports available for betting on the SX Bet exchange. # Teams Source: https://docs.sx.bet/api-reference/get-teams GET /teams Get all teams within a specific league on SX Bet. # Get a trade by ID Source: https://docs.sx.bet/api-reference/get-trade-v3 GET /trades-v3/{tradeId} Look up one of your bets by its opaque tradeId. `GET /trades-v3/{tradeId}` returns **one bet** (BET grain) belonging to the authenticated account — the same row shape as [`GET /trades-v3`](/api-reference/get-trades-v3). Pass the opaque `tradeId`. Use it to join fills via [`GET /fills-v3?tradeId=…`](/api-reference/get-fills-v3). A missing id, or an id that belongs to another account, returns **404**. The response does not distinguish those cases. # Get your bets Source: https://docs.sx.bet/api-reference/get-trades-v3 GET /trades-v3 Query your bet history from the SX Bet API. `GET /trades-v3` returns **one row per bet**: everything that resulted from a single order you submitted. An order that matched against four resting orders is one row here with `fillCount: 4`. The individual matches are on [`GET /fills-v3`](/api-reference/get-fills-v3). `tradeId` identifies the bet — filter fills by that value, and look up one bet with [`GET /trades-v3/{tradeId}`](/api-reference/get-trade-v3). `ownOrderIds` joins the other way: the `orderId`s of your orders, not `clientOrderId`. # Get public trades Source: https://docs.sx.bet/api-reference/get-trades-v3-public GET /trades-v3/public Query recent public trades on the SX Bet exchange. `GET /trades-v3/public` is the public tape: bets other people placed, stripped of identity. # Get your balance Source: https://docs.sx.bet/api-reference/get-user-balance-v3 GET /user/balance-v3 Get your wallet balance from the SX Bet API. One row per token and escrow pair. | Field | Meaning | | ------------------------ | ----------------------------------------------------------------- | | `availableAmount` | Settled funds not locked in bets | | `escrowedAmount` | Collateral locked behind open bets | | `pendingAvailableAmount` | Signed delta on available (in-flight bet, deposit, or withdrawal) | | `pendingEscrowAmount` | Signed delta on escrow (collateral entering or leaving) | Spendable balance for orders: `availableAmount`. # Get your fees Source: https://docs.sx.bet/api-reference/get-user-fees-v3 GET /user/fees-v3 Get the fee rates that apply to your account from the SX Bet API. `GET /user/fees-v3` returns the five fee rates that apply to your account. Rates are set per account. [Fees](/developers/fees) covers what each rate is charged on, and when. Every rate is a decimal fraction string: `"0.05"` is 5%, `"0.005"` is 0.5%. # Get your proxy wallet Source: https://docs.sx.bet/api-reference/get-user-proxy GET /user/proxy Get the proxy wallet address for the authenticated account. `GET /user/proxy` returns the proxy wallet for the authenticated account. **`deployed` is the only field that reports existence.** The proxy address is a deterministic derivation from your user address, so it is returned whether or not the contract exists. `deployed` indicates if it has been successfully deployed or not. # Pending deposit Source: https://docs.sx.bet/api-reference/get-user-transfer-to-proxy-pending GET /user/transfer-to-proxy/pending List open deposits to your proxy wallet by session id. `GET /user/transfer-to-proxy/pending` returns the `sessionIds` of every deposit started by [`POST /user/transfer-to-proxy`](/api-reference/post-user-transfer-to-proxy) that is still open, newest first. For the outcome of one deposit, poll [`GET /user/transfer-to-proxy/status`](/api-reference/get-user-transfer-to-proxy-status) with that id. **An empty `sessionIds` list does not mean the deposit succeeded.** It clears on success and on failure alike. Use status for `SUCCESS` / `FAILED`, then confirm the amount with [`GET /user/balance-v3`](/api-reference/get-user-balance-v3) if you need the balance. # Deposit status Source: https://docs.sx.bet/api-reference/get-user-transfer-to-proxy-status GET /user/transfer-to-proxy/status Get the outcome of a proxy deposit by session id. `GET /user/transfer-to-proxy/status` returns `PENDING`, `SUCCESS`, or `FAILED` for one deposit started by [`POST /user/transfer-to-proxy`](/api-reference/post-user-transfer-to-proxy). Pass the `sessionId` from that submit response. # SX Bet API Reference Source: https://docs.sx.bet/api-reference/introduction REST endpoints and real-time WebSocket data for markets, orders, trades, and positions on the SX Bet exchange.
SX Bet # API Reference

Fetch markets, read the book, post orders, and subscribe to real-time channels.

## Explore the API The one call every integration starts with: chain id, escrow, decimals, limits, odds ladder. Submit a signed order. `timeInForce` decides whether it rests or executes. Every channel, which namespace it lands in, and what it recovers. The whole book for one market, and the REST seed a realtime subscription starts from.
# Market types Source: https://docs.sx.bet/api-reference/market-types Reference of market types on SX Bet. ## The table | ID | Name | Has lines | Description | Bet group | Quarter-line eligible | | ---- | --------------------------------- | --------- | -------------------------------------------------------------------- | --------------------- | --------------------- | | 1 | 1X2 | false | Who will win the game (1X2) | `1X2` | false | | 2 | Under/Over | true | Will the score be under/over a specific line | `game-lines` | true | | 3 | Asian Handicap | true | Who will win the game with handicap (no draw) | `game-lines` | true | | 28 | Under/Over Including Overtime | true | Will the score including overtime be over/under a specific line | `game-lines` | true | | 29 | Under/Over Rounds | true | Will the number of rounds in the match be under/over a specific line | `game-lines` | true | | 52 | 12 | false | Who will win the game | `game-lines` | false | | 166 | Under/Over Games | true | Number of games will be under/over a specific line | `game-lines` | true | | 201 | Asian Handicap Games | true | Who will win more games with handicap (no draw) | `game-lines` | true | | 226 | 12 Including Overtime | false | Who will win the game including overtime (no draw) | `game-lines` | false | | 342 | Asian Handicap Including Overtime | true | Who will win the game with handicap (no draw) including Overtime | `game-lines` | true | | 835 | Asian Under/Over | true | Will the score be under/over specific asian line | `game-lines` | true | | 1536 | Under/Over Maps | true | Will the number of maps be under/over a specific line | `game-lines` | true | | 88 | To Qualify | false | Which team will qualify | `to-qualify` | false | | 274 | Outright Winner | false | Winner of a tournament, not a single match | `outright-winner` | false | | 165 | Set Total | true | Number of sets will be under/over a specific line | `set-betting` | false | | 866 | Set Spread | true | Which team/player will win more sets with handicap | `set-betting` | false | | 53 | Asian Handicap Halftime | true | Who will win the 1st half with handicap (no draw) | `first-half-lines` | false | | 63 | 12 Halftime | false | Who will win the 1st half (no draw) | `first-half-lines` | false | | 77 | Under/Over Halftime | true | Will the score in the 1st half be under/over a specific line | `first-half-lines` | false | | 21 | Under/Over First Period | true | Will the score in the 1st period be under/over a specific line | `first-period-lines` | false | | 64 | Asian Handicap First Period | true | Who will win the 1st period with handicap (no draw) | `first-period-lines` | false | | 202 | First Period Winner Home/Away | false | Who will win the 1st Period Home/Away | `first-period-lines` | false | | 45 | Under/Over Second Period | true | Will the score in the 2nd period be under/over a specific line | `second-period-lines` | false | | 65 | Asian Handicap Second Period | true | Who will win the 2nd period with handicap (no draw) | `second-period-lines` | false | | 203 | Second Period Winner Home/Away | false | Who will win the 2nd Period Home/Away | `second-period-lines` | false | | 46 | Under/Over Third Period | true | Will the score in the 3rd period be under/over a specific line | `third-period-lines` | false | | 66 | Asian Handicap Third Period | true | Who will win the 3rd period with handicap (no draw) | `third-period-lines` | false | | 204 | Third Period Winner Home/Away | false | Who will win the 3rd Period Home/Away | `third-period-lines` | false | | 205 | Fourth Period Winner Home/Away | false | Who will win the 4th Period Home/Away | `fourth-period-lines` | false | | 236 | 1st 5 Innings Under/Over | true | Will the score in the 1st five innings be under/over a specific line | `first-five-innings` | false | | 281 | 1st 5 Innings Asian Handicap | true | Who will win the 1st five innings with handicap (no draw) | `first-five-innings` | false | | 1618 | 1st 5 Innings Winner-12 | false | Who will win in the 1st five innings | `first-five-innings` | false | | 17 | Both Teams To Score | false | Will both teams score | — | false | | 41 | 1st Period Winner | false | Who will win the 1st period | — | false | | 42 | 2nd Period Winner | false | Who will win the 2nd period | — | false | **Quarter-line eligible** marks the types that can be split into quarter lines: 2, 3, 28. See [Quarter-line markets](/developers/quarter-line-markets). ## Bet groups A **bet group** is a set of market types presented together. It is the scope that matters for two things: the `betGroup` filter on [`GET /markets/active`](/api-reference/get-markets-active), and the market grouping used by [capital efficiency](/developers/capital-efficiency) — positions offset within a group, not across groups. | Bet group | Label | Market types | | --------------------- | ------------------- | ----------------------------------------------- | | `game-lines` | Game Lines | 2, 3, 28, 29, 52, 166, 201, 226, 342, 835, 1536 | | `outright-winner` | Outright Winner | 274 | | `set-betting` | Set Betting | 165, 866 | | `first-half-lines` | First Half Lines | 53, 63, 77 | | `first-period-lines` | First Period Lines | 21, 64, 202 | | `second-period-lines` | Second Period Lines | 45, 65, 203 | | `third-period-lines` | Third Period Lines | 46, 66, 204 | | `fourth-period-lines` | Fourth Period Lines | 205 | | `first-five-innings` | First Five Innings | 236, 281, 1618 | | `to-qualify` | To Qualify | 88 | | `1X2` | 1X2 | 1 | # Parlay markets Source: https://docs.sx.bet/api-reference/parlay-markets How parlay (multi-leg) markets work on SX Bet Bettors can request a custom parlay on [SX.Bet](https://sx.bet) by selecting multiple markets. When they submit a parlay request, a message is sent via Websocket (See [this link](/api-reference/channel-parlay-requests) for more details on the request). Once this message is sent, makers will be able to submit orders to this market for upto three seconds. Market makers can use the payload data from the Parlay Request to submit an [order](/api-reference/post-orders-v3). Market makers have a three second window to post orders. After this point, bettors will be shown all available orders at the same time and no other orders will be viewable by the bettor. Bettors can choose which order to take, and take it by posting a `FOK` order like any other non-parlay order. An `IOC` order on a parlay market is rejected with `PARLAY_ORDER_MUST_NOT_BE_IOC`. Market makers can [cancel](/api-reference/delete-orders-v3) orders like any other non-parlay order. The parlay order-book that is displayed to a bettor will automatically close after one minute. Your orders may still be active even though the order-book window has closed (the user will not be able to view your order after the window closes). Parlay Orders will expire just as normal orders do, so please set the `expiry` on your orders accordingly. It must not outlive the parlay's own expiry, or the order is rejected with `PARLAY_ORDER_EXPIRY_BEYOND_PARLAY_EXPIRY`. Parlay Markets act as regular markets, but with additional fields to indicate the underlying legs that make up the Parlay. # Set heartbeat Source: https://docs.sx.bet/api-reference/post-heartbeat-v3 POST /heartbeat/v3 Register or refresh a heartbeat timer for your proxy wallet. `POST /heartbeat/v3` registers or refreshes a heartbeat timer for account. | | | | ------- | ------------------------------------------- | | Auth | **API key only** (`x-sx-api-key`) | | Scope | The proxy wallet owned by the key's address | | Refresh | Call again to push `expiresAt` forward | ## Request | Field | Type | Rule | | ---------------- | ------- | ----------------------------------------------- | | `timeoutSeconds` | integer | Required. `0`–`3600`. `0` clears an armed timer | ## Response `200 OK` returns the armed heartbeat: | Field | Meaning | | ---------------- | ---------------------------------------------- | | `proxyAddress` | The deployed proxy the heartbeat is armed for | | `timeoutSeconds` | The timeout that was registered | | `expiresAt` | When the timer lapses (`now + timeoutSeconds`) | ## Errors | Status | When | | ------ | -------------------------------------------------------------------------------------------- | | `400` | `INVALID_PROXY` — no deployed proxy for your address; or `timeoutSeconds` outside `0`–`3600` | | `401` | Missing or invalid API key (`BAD_AUTH`) | | `503` | OBv3 is unavailable on this environment | **You need a deployed proxy first.** If your proxy is not yet deployed the call returns `400 INVALID_PROXY`. Deploy it with [`POST /user/deploy-proxy`](/api-reference/post-user-deploy-proxy). # Create orders Source: https://docs.sx.bet/api-reference/post-orders-v3 POST /orders-v3 Submit a new order to the SX Bet orderbook. Making and taking are the same request with a different [`timeInForce`](/developers/time-in-force) — there is no separate fill endpoint. [EIP-712 order signing](/api-reference/eip712-order-signing) covers the eight signed fields and the domain. **A deployed proxy wallet is required before any order is accepted** **This endpoint is asynchronous by default.** A success response means the order was *submitted* to the matching engine, not that it rested or matched. Each order comes back as `SUBMITTED` — the terminal outcome (rested as `ACTIVE`, matched, or gone) arrives later on [`account:orders_v3_#{address}`](/api-reference/channel-orders-v3). Subscribe to that channel before you submit and treat it as the source of truth. **Optional synchronous mode.** Set `waitForOutcome: true` to have the call wait up to `maxWaitTime` and return each accepted order's terminal matching `outcome` inline. `outcome` is the *matching* result, not on-chain finality. The funds are escrowed a moment later, when it reaches `LOCKED` on [`account:trades_v3_#{address}`](/api-reference/channel-trades-v3). See [which status to act on](/developers/bet-lifecycle#which-status-to-act-on). `expiry` is required and must be a unix epoch timestamp in **seconds** (not milliseconds) that is in the future. Also an `expiry` that falls inside the market's betting delay plus two seconds is rejected. **Posting an order does not lock capital**, regardless of `timeInForce`. Funds are escrowed only when the order matches. See [Risk limits](/developers/risk-limits). # Deploy a proxy wallet Source: https://docs.sx.bet/api-reference/post-user-deploy-proxy POST /user/deploy-proxy Deploy a proxy wallet for your account on SX Bet. Required in order to trade. This call is asynchronous and will complete in a few seconds. To check whether the deployment has completed, poll [`GET /user/proxy`](/api-reference/get-user-proxy) and wait for `deployed` to be `true`. Until then, [`POST /orders-v3`](/api-reference/post-orders-v3) and [`POST /user/transfer-to-proxy`](/developers/funding) reject with `PROXY_NOT_DEPLOYED`. # Fund a proxy wallet Source: https://docs.sx.bet/api-reference/post-user-transfer-to-proxy POST /user/transfer-to-proxy Transfer tokens from your wallet to your proxy wallet on SX Bet. `POST /user/transfer-to-proxy` moves tokens from your EOA into your proxy wallet. You authorise it with an **EIP-2612 permit** — a signature over `(owner, spender, value, nonce, deadline)`. [Funding](/developers/funding) walks the whole deposit end to end. A `200` means recorded, not completed. This is an asynchronous endpoint. Poll [`GET /user/transfer-to-proxy/status`](/api-reference/get-user-transfer-to-proxy-status) with the returned `sessionId` until `status` is `SUCCESS` or `FAILED`. # References Source: https://docs.sx.bet/api-reference/references Endpoints, chain IDs, contract addresses, and network details for mainnet and testnet. ## Toronto (testnet) | Reference | Value | | ------------------ | ---------------------------------------------------- | | API base URL | `https://api.toronto.sx.bet` | | App | `https://toronto.sx.bet` | | Realtime WebSocket | `wss://realtime.toronto.sx.bet/connection/websocket` | | Chain id | `79479957` | | USDC | `0x1BC6326EA6aF2aB8E4b6Bc83418044B1923b2956` | | Escrow Contract | `0x007D30a86366EdA2a410a176329f991565d8CfA4` | ## Production (mainnet) | Reference | Value | | ------------------ | -------------------------------------------- | | API base URL | `https://api.sx.bet` | | App | `https://sx.bet` | | Realtime WebSocket | `wss://realtime.sx.bet/connection/websocket` | | Chain id | `4162` | | USDC | `0x6629Ce1Cf35Cc1329ebB4F63202F3f197b3F050B` | | Escrow Contract | `0xF946f2AE410bCeF6cFe53FB27D4F178A79B7863D` | ## B. Runtime values `GET /metadata/obv3` has additional environment configuration | Value | Where it is used | | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------- | | `domain` (`name`, `version`, `chainId`, `verifyingContract`) | the EIP-712 domain — pass `meta.domain` to `signTypedData` | | `activeAsset.baseToken` | the `baseToken` field of every order | | `activeAsset.escrowAddress` | equals `domain.verifyingContract` | | `activeAsset.decimals` | converting whole tokens to base units | | `transferToProxyExecutorAddress` | EIP-2612 permit `spender` for [transfer-to-proxy](/api-reference/post-user-transfer-to-proxy) | | `oddsLadderStepSize` | rounding `percentageOdds` onto the ladder | | `limits.orderSizeMinimumBaseUnits` | the floor for `totalBetSize` | | `limits.minRestingOrderSizeBaseUnits` | dust floor — a partial-fill remainder below this is discarded and the order closes `FILLED` | | `limits.maxCreateOrders`, `limits.maxCancelOrders` | batch sizes for create and cancel | | `bettingDelay` | resolve the per-order delay — field-by-field on [Betting delays](/developers/betting-delays) | Field-by-field detail, the mixed string/number types, and the values that differ by environment are on [Exchange metadata](/api-reference/get-metadata-obv3). ## Versions of dependencies These are the latest versions of js dependencies that are referenced in documentation: * `ethers`: 6.16.0 # Search fixtures Source: https://docs.sx.bet/api-reference/search GET /search Find active fixtures by team name on SX Bet. # Testnet Source: https://docs.sx.bet/api-reference/testnet Using the SX Bet testnet for development and testing We have deployed a testnet chain and application ([https://toronto.sx.bet](https://toronto.sx.bet)). See configuration for testnet [here](/api-reference/references). To receive testnet funds, [open a support chat](https://sx.bet) on SX Bet. # Unit conversion Source: https://docs.sx.bet/api-reference/unit-conversion Converting token amounts and odds formats. ## Tokens Token amounts in the API use an integer representation with a `decimals` value to avoid rounding issues. For example, 100 USDC is stored as `100 * 10^6 = 100000000`. | Token | Address | Decimals | | ----- | -------------------------------------------------------------------------------------- | -------- | | USDC | See [`GET /metadata/obv3`](/api-reference/get-metadata-obv3) (`activeAsset.baseToken`) | 6 | To convert from a nominal amount (such as 100 USDC) to the integer amount used in the API: `apiAmount = nominalAmount * 10^decimals` To convert back: `nominalAmount = apiAmount / 10^decimals` where `decimals` comes from `activeAsset.decimals` on metadata (currently `6` for USDC). *** ## Odds Odds are specified in an implied-odds format like `839135214364235000`. To convert to a readable implied probability, divide by `10^20`. That example is `0.0839`, or `8.39%`. To convert from implied probability to decimal odds, take the inverse. For example, `0.0839` in decimal format is `1 / 0.0839 ≈ 11.917`. *** ## Bookmaker odds `percentageOdds` is always from the **market maker's** perspective. Odds shown on sx.bet in the order book are what the **taker** receives. See [Order book](/developers/order-book). Suppose a maker is betting outcome one (`isMakerBettingOutcomeOne = true`) and receiving implied odds of `704552840724436400000 / 10^20 = 0.704552841`. The taker is betting *outcome two* and receiving implied odds of `1 - 0.704552841 = 0.295447159`. That displays under the second order book as about `29.5%` implied, or `1 / 0.295447159 ≈ 3.3847` decimal. For BigInt-safe helpers, time encodings, and more worked examples, see [Unit conversions](/developers/unit-conversions). # Account models for external sites Source: https://docs.sx.bet/developers/account-models The two supported ways to represent your site's users on SX.bet. If you are integrating SX.bet markets into your own site or app, there are two supported account models for representing your users. Pick the one that matches how you want to manage accounts and funds. This page is about representing **your site's users**. If you are trading your own account, see [Posting orders](/developers/posting-orders) and [Market making](/developers/market-making) instead. ## Single account The simplest path, and the recommended one for most integrations. You create one SX.bet account, generate one [API key](/api-reference/api-key), and make every API call from your backend with that key. Your single account represents all of your users. Tell your users' activity apart with [`externalUserId`](/developers/external-user-id): set it to your own id for the person on each order, and the same value comes back on their orders, fills, and bets so you can attribute activity per user. You choose how funds are held: custody all user funds yourself, or delegate custody to SX. See [External user id](/developers/external-user-id) for how per-user attribution works. ## Whitelabel / skin Each user on your app is their own SX.bet user, and your site is effectively a skin of [sx.bet](https://sx.bet). You implement authentication directly from your own domain. More information on this model is coming shortly. Reach out through [Work with us](/developers/work-with-us) if you are planning a whitelabel integration. ## Related Attribute orders, fills, and bets to people on your platform. Generate and use the key your backend calls with. Building at scale or exploring a partnership. # Accounts Source: https://docs.sx.bet/developers/accounts Understand accounts and proxy wallets on SX Bet. An account is two addresses. You **sign** with one and you are **funded** at the other. | | | | --------------------- | ---------------------------------------------------------------------- | | **Your EOA** | The wallet you control with a private key. Your identity to the API | | **Your proxy wallet** | A contract your EOA owns. Where your money lives and where bets settle | ## Creating an account Sign up at [sx.bet](https://sx.bet) and choose a username. Two ways in: * **Email or Google** — a non-custodial [Magic](https://magic.link/) wallet is created for you, tied to your email. Easiest with no prior crypto experience. * **Connect a wallet** — bring an existing wallet such as [MetaMask](https://metamask.io) or [Rabby](https://rabby.io). You end up with a public address and a private key. A proxy wallet is deployed for you when you log in to the site, or when you call [`POST /user/deploy-proxy`](/api-reference/post-user-deploy-proxy). ## Non-custodial USDC sits in a proxy contract your account. **Trading is proxy-only.** Funds must be in your proxy wallet to trade. Withdrawals and transfers out of your proxy wallet require your signature. ## Your address and private key * Your **address** is your public identity — something like `0x52adf738AAD93c31f798a30b2C74D658e1E9a562`. It is how the API identifies you and it is safe to share. * Your **private key** proves you own the wallet. It signs every order. Never expose this. Anyone with your private key has full control of your wallet and funds. Never share it, never hardcode it, never commit it — use environment variables. If you signed up with email or Google, you can retrieve your private key from the [assets page](https://sx.bet/wallet/assets): ## Looking up your proxy [`GET /user/proxy`](/api-reference/get-user-proxy) returns your proxy address. To see deposits, withdrawals, and settlements on your account, call [`GET /ledger-v3`](/api-reference/get-ledger-v3). ## The Safe underneath Every deployed proxy has a Safe smart account automatically attached, reported as `multisigSafeAddress`. The Safe authorises the sensitive actions — withdrawal, internal transfers, signer management. Safe and Safe\{Core} are trademarks of the Safe Ecosystem Foundation, used here descriptively. SX Bet is not affiliated with or endorsed by Safe Global. ## Related The request, what a repeat call does, and the `200`. The predicted address, the `deployed` flag, and the Safe address. Every deposit, withdrawal, and settlement on your account. Moving USDC in with a permit, without paying gas. What you can actually spend, and why. # Authentication Source: https://docs.sx.bet/developers/authentication Different operations on SX Bet require different levels of authentication. Here's what you need and why. ## Authentication by scope | Operation | Auth required | | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------- | | Fetch markets, the order book, the anonymized public tape | None | | Fetch best odds; read your own orders, positions, trades, fills, or balances; subscribe to WebSocket channels; register a heartbeat | API key | | Place an order and account actions | API key + signature | Send the key as the `x-sx-api-key` header. Generate one from the API section of your account page on [sx.bet](https://sx.bet). See [API keys](/api-reference/api-key). ## Auth by route | Method | Route | Auth | Signature | | -------- | --------------------------------- | ------- | --------------- | | `GET` | `/metadata/obv3` | None | — | | `GET` | `/sports` | None | — | | `GET` | `/leagues` | None | — | | `GET` | `/leagues/active` | None | — | | `GET` | `/teams` | None | — | | `GET` | `/fixture/active` | None | — | | `GET` | `/fixture/status` | None | — | | `GET` | `/live-scores` | None | — | | `GET` | `/search` | None | — | | `GET` | `/markets/active` | None | — | | `GET` | `/markets/find` | None | — | | `GET` | `/markets/popular` | None | — | | `GET` | `/orderbook-v3/snapshot` | None | — | | `GET` | `/trades-v3/public` | None | — | | `GET` | `/orders-v3/odds/best` | API key | — | | `GET` | `/orderbook-v3/snapshot/event` | API key | — | | `GET` | `/orders-v3` | API key | — | | `POST` | `/orders-v3` | API key | EIP-712 order | | `DELETE` | `/orders-v3` | API key | — | | `DELETE` | `/orders-v3/all` | API key | — | | `DELETE` | `/orders-v3/event` | API key | — | | `POST` | `/heartbeat/v3` | API key | — | | `GET` | `/trades-v3` | API key | — | | `GET` | `/fills-v3` | API key | — | | `GET` | `/positions-v3` | API key | — | | `POST` | `/user/deploy-proxy` | API key | — | | `GET` | `/user/proxy` | API key | — | | `POST` | `/user/transfer-to-proxy` | API key | EIP-2612 permit | | `GET` | `/user/transfer-to-proxy/pending` | API key | — | | `GET` | `/user/transfer-to-proxy/status` | API key | — | | `GET` | `/user/pending-deploy-proxy` | API key | — | | `GET` | `/user/balance-v3` | API key | — | | `GET` | `/user/fees-v3` | API key | — | | `GET` | `/user/realtime-token-v3` | API key | — | # Reading balances Source: https://docs.sx.bet/developers/balances-and-ledger Understand your balances and ledger on SX Bet. [`GET /user/balance-v3`](/api-reference/get-user-balance-v3) describes the balances of your account. The four amounts, and what each is for: | Field | Meaning | | ------------------------ | --------------------------------------------- | | `availableAmount` | Available to bet | | `pendingAvailableAmount` | In-flight change to available. **Signed** | | `escrowedAmount` | Locked behind your open bets — your "at risk" | | `pendingEscrowAmount` | In-flight change to escrow. **Signed** | Every amount is a string in base units. USDC has 6 decimals, so `"1028000014"` is **1028.000014 USDC**. ## Reading a balance ```js theme={null} const { data: meta } = await get("/metadata/obv3"); const { data } = await get("/user/balance-v3"); const row = data.balances.find(b => b.tokenAddress === meta.activeAsset.baseToken); const spendable = row ? BigInt(row.availableAmount) + BigInt(row.pendingAvailableAmount) : 0n; ``` ## Related Every field, and what an empty array means. Getting money in, and confirming it arrived. # Bet lifecycle Source: https://docs.sx.bet/developers/bet-lifecycle How a bet's status changes from placement to settlement on SX Bet. ## The states ```mermaid theme={null} flowchart LR M["MATCHED"] L["LOCKED"] S["SETTLED"] F["FAILED"] M --> L --> S M --> F ``` | Status | Means | | --------- | ------------------------------------------------------------------------ | | `MATCHED` | The matching engine matched you. The on-chain lock is not confirmed yet. | | `LOCKED` | Funds are escrowed on chain. **The bet is real and irreversible.** | | `SETTLED` | The fixture is graded and the outcome is recorded in `settlement`. | | `FAILED` | The on-chain lock did not succeed. No funds moved. | ## Which status to act on `MATCHED` is the matching engine's word that you are matched. `LOCKED` means the funds are escrowed on chain, and nothing revises the bet after that. V3 has driven on-chain lock failures to effectively zero, and a matched bet going on to `LOCKED` is overwhelmingly the norm. However, expect some hiccups in the early days and we cannot provide 100% guarantees at the moment. So choose by what a revision would cost you: * **I can deal with the occasional revised bet** — act on `MATCHED`. It arrives well ahead of the lock (couple seconds), and it is what happens in almost every case. * **I cannot deal with any revised bet** — hedging off-venue, or paying a customer against it — wait for `LOCKED`. It is the only irreversible status. A bet that does not lock ends at `FAILED`, and no funds moved. ## Reading `outcome` | Value | Means | | ----- | ---------------- | | `1` | Outcome one won. | | `2` | Outcome two won. | | `0` | **Void.** | A **quarter-line** bet needs more care. The parent's `outcome` carries direction only, so `outcome` alone does not tell you whether the bet half-won or fully won. See [Quarter-line markets](/developers/quarter-line-markets). ## Related The order state machine. Bet, leg, or position — which endpoint answers your question. # Betting delays Source: https://docs.sx.bet/developers/betting-delays Betting delays added to guard against toxic flow. Read `data.bettingDelay` from [`GET /metadata/obv3`](/api-reference/get-metadata-obv3). Every value is milliseconds. A delay applies only to an order that takes resting liquidity. An order that only adds liquidity is accepted immediately and goes `ACTIVE` without waiting. A delayed order matches against the book as it stands when the delay elapses, not as it stood when you submitted it, so it can fill less than the depth you saw, or nothing at all. ## Live Resolve in this order: `leagueIdToMs[leagueId]`, then `sportIdToMs[sportId]`, then `liveMsDefault`. Live means the market's `gameTime` has passed, not that the fixture is in play. A late-kickoff market still counts as live once `gameTime` has elapsed. ## Pregame Use `pregameMsDefault`. There is no league or sport override. # Book versioning Source: https://docs.sx.bet/developers/book-versioning How to determine whether an order book update is newer than the one you have. Every order book payload — the REST snapshots and both `orderbook_v3` channels — carries a `version`: a single string that increases as the book changes. ```json theme={null} "version": "00100000000000000000042" ``` It answers one question: **is this payload newer than the book I already hold?** `version` applies only to the orderbook channels — [`orderbook_v3`](/api-reference/channel-orderbook-v3) and [`orderbook_v3_event`](/api-reference/channel-orderbook-v3-event) — and the REST snapshot that seeds them. ## The apply rule Apply a payload only when its version is **strictly greater** than the one you hold: ``` apply if incoming.version > current.version ``` Compare as strings. A plain string comparison gives the correct ordering — no need to parse it into a number or `BigInt`. ## Keep state per market, and seed it from REST `version` is per market, so key your state by `marketHash` before you compare. Seed from the REST snapshot at startup, then apply only live publications that have a version greater than the seed or previous message. ## Gaps are normal `version` is not a counter but it is increasing. Consecutive changes on a market jump by arbitrary amounts, not by 1. ## Related Recovery, ordering and deduplication across reconnects. The REST seed that carries the same version. Seeding and subscribing to the book and best odds. # Cancelling orders Source: https://docs.sx.bet/developers/cancelling-orders How to cancel open orders on the SX Bet exchange. There are three cancel routes. All three return `200` even when nothing was cancelled, so a `200` alone never confirms an order is off the book — you have to check the response body, and what the body can tell you differs by route: | Route | Scope | Does the response body confirm the order is off the book? | | ------------------------------------------------------------------ | ----------------------------- | ---------------------------------------------------------- | | [`DELETE /orders-v3`](/api-reference/delete-orders-v3) | ids you name, up to 100 | **Yes** — waits for the matching engine, reports per order | | [`DELETE /orders-v3/event`](/api-reference/delete-orders-v3-event) | every open order on one event | **No** — confirms only that a cancel command was accepted | | [`DELETE /orders-v3/all`](/api-reference/delete-orders-v3-all) | every open order you have | **No** — confirms only that a cancel command was accepted | ## Cancelling asynchronously **Cancel by id is synchronous.** The route waits up to 5 seconds for the matching engine to act before responding, and reports `cancelled` or `notCancelled` per order — plus a third bucket, `unconfirmed`, for when the matching engine's answer doesn't arrive within that window. **Cancel-all and cancel-by-event are asynchronous.** Both routes return as soon as the matching engine accepts the cancel commands, before your orders are actually off the book. The response's `cancelledSubmitted` list names which commands were accepted; nothing in the response confirms the orders are gone. ## Choosing a route **Use cancel by id** when you know what you want gone: repricing a level, pulling one side, reacting to a fill. **Use cancel by event** for reacting to event-level changes **Use cancel-all** as a circuit breaker in case everything needs to be immediately cancelled. ## Watching cancels land For the asynchronous endpoints, the direct signal is the `account:orders_v3_#{address}` channel: when the matching engine removes your order, you get an update with `status: "INACTIVE"` and `inactiveReason: "USER_REQUESTED"`. See [Real-time data](/developers/realtime-overview) and [Tracking your orders](/developers/my-orders). ## Cancels from elsewhere Orders sometime leave the book without you cancelling them, and `inactiveReason` says why. See [the full table](/developers/order-lifecycle#the-inactivereason-values) ## Related Arming the switch that cancels for you when your process stops. # Capital efficiency Source: https://docs.sx.bet/developers/capital-efficiency How SX Bet's capital efficiency system reduces locked escrow by recognizing hedged positions. SX Bet locks tokens in escrow when you place a bet and releases them at settlement. Capital efficiency (CE) short-circuits that: when your positions within a market offset each other, the exchange calculates your true worst-case loss and returns the excess escrow to your wallet immediately — at fill time, without waiting for the market to settle. It is automatic: **there is nothing to opt into and no endpoint to call.** ## How it works Every market pool has a worst-case loss that escrow must cover. On each fill, the exchange recomputes that worst case across all of your open positions in the pool, and refunds anything held above it. The recalculation runs for every party to the trade — makers and takers alike, not just the taker who triggered it. If your worst case has dropped, escrow adjusts down to match and releases the excess to your wallet in the same on-chain batch that locks the fill. ## Example You bet 100 USDC on outcome one at even odds (50% implied) — a win returns 200, so your worst case is the 100 you staked, and 100 stays locked. Later you bet 100 USDC on outcome two in the same market, also at even odds. Now one side always pays the other, so your worst-case net loss is 0. The exchange recalculates and refunds the full 200 USDC on the second fill. ## Tracking CE refunds Refunds are reported as fields on rows you already read, not through a dedicated endpoint or channel: * **Per bet** — `ceRefundAmount` on [`GET /trades-v3`](/api-reference/get-trades-v3). * **Per fill** — `ceRefundAmount` on [`GET /fills-v3`](/api-reference/get-fills-v3). * **Realtime** — the affected rows are republished on the [trades](/api-reference/channel-trades-v3) and [fills](/api-reference/channel-fills-v3) channels with new `ceRefundAmount` values and a fresh `updatedAt`. There is no separate "refund happened" event. `ceRefundAmount` is **post-fee** — the amount you actually received. `ceRefundFeeAmount` is the fee taken on that refund. ## Reclaim at settlement Refunds issued during a market's life are reclaimed when the market settles. Within each market, the total refunded to you is subtracted from the returns on your winning bets — or, if the market is voided, from your returned stakes — taken oldest bet first. The reclaim follows the order your bets were placed rather than any internal identifier, so the same bets in the same sequence always settle to the same amounts, including when a settlement is retried. The amount reclaimed is reported as `settleCeRefundAmount`. ## P\&L accounting CE refunds return to your wallet before settlement, so they are not in `settleReturnAmount`. When reconciling P\&L for a market that issued refunds, add them back in: ``` actual_return = settleReturnAmount + ceRefundAmount − totalStake ``` `settleCeRefundAmount` on the settlement object is a different number — the refund *consumed against your gross payout* at settlement, not what you received. P\&L uses the received figure. **Do not add refunds on top of a figure that already includes them.** The `pnl` field on [`GET /positions-v3?status=SETTLED`](/api-reference/get-positions-v3) is `settleReturnAmount + ceRefundAmount − totalStake`, with the refund already in it. ## Related Where to find the refund on each leg. Netted exposure — the same idea, read-only. The other meaning of `ceRefundAmount`. Why one bet produces two refund calculations. # Client order id Source: https://docs.sx.bet/developers/client-order-id Attach your own id to an order so you can reconcile it across HTTP and realtime. `clientOrderId` is an optional tag you attach to an order on [`POST /orders-v3`](/api-reference/post-orders-v3). It is your own value, echoed back to you on the create response, the order row, the orders channel, and in the fills grain. `clientOrderId` has no effect on funds, fees, or matching. ## Why it is useful You do not know an order's id until [`POST /orders-v3`](/api-reference/post-orders-v3) responds. That makes correlation awkward when the pieces of one order's lifecycle arrive on different paths: the HTTP response, and then separate publications on [`account:orders_v3_#{address}`](/api-reference/channel-orders-v3). A `clientOrderId` is a value you choose *before* you submit, so you can stitch those messages together against your own records the moment each one lands, rather than waiting on the id. This is the standard pattern on async trading platforms: attach your own reference at submit time, then match it back as updates flow in. ## Format | Rule | Value | | ------------------ | ---------------------------------------------- | | Optional | Omit it and the field is absent from responses | | Max length | 64 characters | | Allowed characters | `^[A-Za-z0-9_-]+$` — letters, digits, `_`, `-` | ## Uniqueness A `clientOrderId` must be **unique per address** — not just within one request, but across every order you currently have in the system. The safe rule is to treat each `clientOrderId` as single-use. Generate a fresh value per order. ## Where it shows up `clientOrderId` is echoed on the **order** grain: * [`POST /orders-v3`](/api-reference/post-orders-v3) — the `200` body's `orders` array echoes your `clientOrderId` on each result when you set one, alongside `orderId`, `status`, and `commandId`. * [`GET /orders-v3`](/api-reference/get-orders-v3) — each order row carries `clientOrderId` when set. * [`GET /orders-v3/client/{clientOrderId}`](/api-reference/get-order-v3-by-client-id) — look up one of your orders (any status) by the tag you set. * [`account:orders_v3_#{address}`](/api-reference/channel-orders-v3) — each publication carries it too when set. It is also echoed on the **fill** grain, so you can trace a fill straight back to the order that produced it: * [`GET /fills-v3`](/api-reference/get-fills-v3) and [`account:fills_v3_#{address}`](/api-reference/channel-fills-v3) — each [fill](/developers/which-grain#fills) carries the `clientOrderId` of the order on that side (the maker's tag on maker rows, the taker's on taker rows) when that order carried a tag; otherwise the field is omitted. ## Recommended pattern ```javascript JavaScript theme={null} // Mint a fresh, single-use id and record what you need before you submit. const clientOrderId = `mm-${crypto.randomUUID()}`; myOrders.set(clientOrderId, { timeInForce: "GTC", // write-only on the API — record it yourself marketHash, submittedAt: Date.now(), }); await fetch(`${API}/orders-v3`, authed({ method: "POST", body: JSON.stringify({ orders: [{ ...order, clientOrderId }] }), })); // Later, on account:orders_v3_#{address}, look each update up by its clientOrderId. // Or poll GET /orders-v3/client/{clientOrderId} when you need the current row. ``` ## Related The submit flow this field rides on. The other write-only field worth recording against your tag. Keep a local store keyed by an id you control. Look up one of your orders by the tag you set. Why the order id is a digest, not a server-assigned value. A per-person tag when one SX account places orders for many people. # The exchange model Source: https://docs.sx.bet/developers/exchange-model How SX Bet works as a peer-to-peer prediction market exchange. ## Overview SX Bet is modelled on a financial exchange, not a sportsbook. No house takes the other side of your bet — every bet is matched peer-to-peer between two users. ## Makers and Takers Every matched bet involves two roles: Rests on the orderbook. Specifies an outcome, stake, and desired odds, and sits open until it's matched. Matches an existing resting order immediately, in full or in part. ## Order lifecycle ``` Maker or taker signs an order and submits it ↓ If the order is matched, the amount is locked in the Escrow contract ↓ Fixture plays out ↓ Result is graded and settlement is recorded on-chain ↓ Escrow pays the winning side automatically ``` ## Next # External user id Source: https://docs.sx.bet/developers/external-user-id Tag orders with your own user id. `externalUserId` is an optional tag you attach to an order on [`POST /orders-v3`](/api-reference/post-orders-v3). The tag is a value you choose so that, when one SX wallet places orders for many people on your platform, you can tell the resulting orders, fills, and trades apart. `externalUserId` has no effect on funds, fees, or matching. It is the mechanism behind the [single-account model](/developers/account-models#single-account). ## Example ```javascript JavaScript theme={null} await fetch(`${API}/orders-v3`, authed({ method: "POST", body: JSON.stringify({ orders: [{ ...order, externalUserId: platformUserId }], }), })); const { data } = await fetch(`${API}/trades-v3?perPage=50`, authed()).then((r) => r.json()); const forUser = data.trades.filter((t) => t.externalUserId === platformUserId); ``` ## Related The submit flow this field rides on. A per-order tag. Unique, charset-restricted, and absent from the bet grain. Bets, fills, and positions — which row to read for a per-user history. Every field, every validation rule. The two ways to represent your site's users on SX.bet. Building at scale or exploring a partnership. # Fees Source: https://docs.sx.bet/developers/fees The five per-account fee rates on SX Bet v3, how each is charged, and how to read your own rates. Fees on v3 are set **per account**, not globally, so your rates may differ from another account's. There are exactly five, all returned by [`GET /user/fees-v3`](/api-reference/get-user-fees-v3). Each is a decimal-fraction string — `"0.05"` is 5%, `"0.005"` is 0.5% — and `null` means the rate is unset and nothing is charged. ## The five rates | Rate | Applies to | | ---------------------- | --------------------------------------------------- | | `makerPayoutFee` | Profit on a winning single where you were the maker | | `takerPayoutFee` | Profit on a winning single where you were the taker | | `makerParlayPayoutFee` | Profit on a winning parlay where you were the maker | | `takerParlayPayoutFee` | Profit on a winning parlay where you were the taker | | `refundFee` | A capital-efficiency refund, charged at fill time | ## How payout fees are charged The four payout fees are charged on **profit** (`totalReturn − totalStake`), at **settlement**, and only on a bet that **won**. A loss and a void are never charged. Which of each maker/taker pair applies comes from your side of the fill: the resting order is the maker, the incoming order that filled it is the taker. What you were actually charged lands in `settlement.settleFeeAmount` on the bet — see [`GET /trades-v3`](/api-reference/get-trades-v3). ## The refund fee `refundFee` is charged at **fill time** instead of settlement, and applies to the whole capital-efficiency refund rather than to profit. It has no maker or taker variant. The refund you receive, `ceRefundAmount`, is already net of it, and the fee itself is `ceRefundFeeAmount`. See [Capital efficiency](/developers/capital-efficiency). ## Finding your rates ```js theme={null} const { data: fees } = await get("/user/fees-v3"); // Each rate is a decimal-fraction string, or null when unset. const takerRate = fees.takerPayoutFee ? Number(fees.takerPayoutFee) : 0; ``` A `null` rate means nothing is charged for that category — treat it as `0`. ## Related The endpoint, every field, and what `null` means. Where the refund fee applies, and how refunds are tracked. What you can spend, and what settlement pays out. # Fetching markets Source: https://docs.sx.bet/developers/fetch-markets Query active sports markets from the SX Bet API. All active markets are available from [`GET /markets/active`](/api-reference/get-markets-active). You can filter by sport, league, event, market type, and more. To look up a specific market by hash, use [`GET /markets/find`](/api-reference/get-markets-find). *** ## Fetch markets by sport Pass one or more `sportIds` to scope results to a particular sport. ```bash theme={null} # NFL and NBA together curl "https://api.sx.bet/markets/active?sportIds=1,4" ``` Use [`GET /sports`](/api-reference/get-sports) to get a full list of sport IDs and their labels. *** ## Fetch markets by league Pass a `leagueId` to get all active markets in a specific league. ```bash theme={null} # English Premier League (leagueId=29) curl "https://api.sx.bet/markets/active?leagueId=29" ``` Use [`GET /leagues/active`](/api-reference/get-leagues-active) to get a full list of active league IDs. *** ## Fetch markets by event If you already have a `sportXeventId` (returned on any market object), you can fetch all markets for that specific fixture. ```bash theme={null} curl "https://api.sx.bet/markets/active?eventId=L16068923" ``` This returns every market type available for that single game — moneylines, spreads, totals, and any period or prop markets. *** ## Find fixtures by team name If you do not already have an `eventId`, use [`GET /search`](/api-reference/search) to look up active fixtures by team name. Pass a `query` between 3 and 100 characters. The match is case-insensitive, and it supports partial names. Results are limited to 8 active fixtures, ordered by `gameTime`. ```bash theme={null} curl "https://api.sx.bet/search?query=mia" ``` Each result includes the `eventId`, scheduled `gameTime`, both team names, and a `type` array of available market type IDs for that fixture. Pass the `eventId` to [`GET /markets/active`](/api-reference/get-markets-active) to fetch markets for that fixture. *** ## Fetch a specific market by marketHash If you have a `marketHash`, use [`GET /markets/find`](/api-reference/get-markets-find) to retrieve it directly. You can pass up to 30 hashes in a single request. ```bash theme={null} curl "https://api.sx.bet/markets/find?marketHashes=0x024902...dd7667b,0x1c8f12...bcbc83" ``` [`GET /markets/find`](/api-reference/get-markets-find) also returns settled markets, so it is useful for looking up historical results. *** ## Filters ### Main lines only For spread and total markets, multiple lines are usually available at once (for example over 1.5, 2.5, 3.5 goals). Set `onlyMainLine=true` to return only the primary line for each market type — the line where both sides are closest to 50/50. ```bash theme={null} curl "https://api.sx.bet/markets/active?leagueId=29&onlyMainLine=true" ``` ### Live markets only Set `liveOnly=true` to return only markets currently available for in-play betting. ```bash theme={null} curl "https://api.sx.bet/markets/active?liveOnly=true" ``` ### Filter by market type Pass one or more market type IDs using the `type` parameter to narrow results to a specific bet type. ```bash theme={null} # Moneyline and Asian Handicap curl "https://api.sx.bet/markets/active?type=52,3" ``` Common market types: | `type` | Name | | ------ | ------------------------- | | `1` | 1X2 (win / draw / no win) | | `52` | 12 (moneyline, no draw) | | `226` | 12 Including Overtime | | `3` | Asian Handicap (spread) | | `2` | Under/Over (totals) | See the full list on the [Market Types](/api-reference/market-types) page. *** ## Pagination [`GET /markets/active`](/api-reference/get-markets-active) uses cursor-based pagination. Each response includes a `nextKey`. Pass it as `paginationKey` in your next request. The maximum `pageSize` is 100, which is also the default. **Stop when `nextKey` is absent, never when a page comes back short.** A short page can still carry a `nextKey`, with the next page empty. The page-size parameter is `pageSize` on this route, not `perPage`. A wrong name is silently ignored and you get a default-sized page, never an error. *** ## Real-time market updates Rather than poll [`GET /markets/active`](/api-reference/get-markets-active), subscribe to a channel and receive market changes as they happen. Three channels are relevant to markets: | Channel | What you receive | | --------------------------------------------------------- | -------------------------------------------------------------------------------- | | [`markets:global`](/api-reference/channel-markets) | A market's metadata changing — created, or moved between `ACTIVE` and `INACTIVE` | | [`main_line:global`](/api-reference/channel-line-changes) | Main line shifts on markets whose type carries a line | | [`fixtures:live_scores`](/api-reference/channel-fixtures) | Live scores for **every** event — there is no per-event filter | **None of the three keeps history**, so a change you miss while disconnected is gone. Seed from REST on connect. Also, `markets:global` publishes an **array**, while `fixtures` publishes a bare object. See [Real-time data](/developers/realtime-overview). # Funding Source: https://docs.sx.bet/developers/funding How to fund your proxy wallet with USDC on SX Bet. This page covers funding your proxy from **USDC you already hold in your EOA**. You can also skip the API and use the **deposit modal** on [sx.bet](https://sx.bet). [`GET /metadata/obv3`](/api-reference/get-metadata-obv3) gives you the token to fund with and the permit `spender`. An EIP-2612 signature over `(owner, spender, value, nonce, deadline)`. [`POST /user/transfer-to-proxy`](/api-reference/post-user-transfer-to-proxy). Returns `200` with a `sessionId`. Poll [`GET /user/transfer-to-proxy/status`](/api-reference/get-user-transfer-to-proxy-status) with the returned `sessionId` until `status` is `SUCCESS` or `FAILED`, then confirm the money in [`/user/balance-v3`](/api-reference/get-user-balance-v3). ## Working code ```js theme={null} const BASE = "https://api.sx.bet"; // Mainnet — use https://api.toronto.sx.bet for testnet const API_KEY = process.env.SX_API_KEY; const headers = { "x-sx-api-key": API_KEY }; const get = (path) => fetch(`${BASE}${path}`, { headers }).then((r) => r.json()); const post = (path, body) => fetch(`${BASE}${path}`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify(body), }).then((r) => r.json()); // 1. What are we funding with? const { data: meta } = await get("/metadata/obv3"); const token = meta.activeAsset.baseToken; // 2. Sign the permit. Give it a generous deadline — the floor is one hour. const deadline = String(Math.floor(Date.now() / 1000) + 6 * 3600); const signature = await signPermit({ owner: myEoa, spender: meta.transferToProxyExecutorAddress, // not your proxy tokenAddress: token, value: "500000000", // 500 USDC, base units deadline, }); // 3. Submit. const { data } = await post("/user/transfer-to-proxy", { owner: myEoa, spender: meta.transferToProxyExecutorAddress, tokenAddress: token, value: "500000000", deadline, signature, }); console.log("queued:", data.sessionId, "→", data.proxyAddress); // 4. Wait for this deposit's outcome, then confirm. await waitUntil(async () => { const { data: st } = await get( `/user/transfer-to-proxy/status?sessionId=${data.sessionId}` ); return st.status === "SUCCESS" || st.status === "FAILED"; }); const { data: bal } = await get("/user/balance-v3"); ``` ## Other ways to fund The permit path is `FROM_EOA` as appears on your ledger endpoint. Two other deposit types appear in the ledger: | `depositType` | Source | | ---------------- | ------------------------------------------------------------------- | | `FROM_EOA` | This flow — a permit-authorised transfer from your own wallet | | `BRIDGE` | Funds arriving from another chain, using the deposit flow on sx.bet | | `PROXY_TO_PROXY` | An internal transfer from another proxy | ## Related The request, the two warnings, and the `200`. Reading what actually arrived. Deploy the proxy first, and poll until it is live. # Heartbeat Source: https://docs.sx.bet/developers/heartbeat Arm a dead-man switch that cancels your open orders if your process dies. [`POST /heartbeat/v3`](/api-reference/post-heartbeat-v3) registers a timer that is meant to cancel your resting orders if you do not send a heartbeat in time by calling the same endpoint ## Arming and refreshing Arming and refreshing are the same request. The body is one field, `timeoutSeconds`, from `0` to `3600`. ```js JavaScript theme={null} const beat = async () => { const res = await fetch(`${API}/heartbeat/v3`, { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ timeoutSeconds: 60 }), }); if (!res.ok) throw new Error(`heartbeat failed: HTTP ${res.status}`); return (await res.json()).data.expiresAt; // 2026-08-06T14:31:07.482Z }; await beat(); setInterval(() => beat().catch(console.error), 20_000); ``` ```python Python theme={null} def beat(): res = requests.post(f"{API}/heartbeat/v3", headers=auth, json={"timeoutSeconds": 60}) res.raise_for_status() return res.json()["data"]["expiresAt"] # 2026-08-06T14:31:07.482Z ``` Refresh inside the timeout window ## When it lapses Every open order is cancelled for your account. It has the same effect as [`DELETE /orders-v3/all`](/api-reference/delete-orders-v3-all). The cancels are asynchronous, so orders will be cancelled shortly after with `inactiveReason: "HEARTBEAT_TIMEOUT"`. ## Stop the heartbeat To clear the heartbeat, call [`POST /heartbeat/v3`](/api-reference/post-heartbeat-v3) again with `timeoutSeconds: 0`. That deactivates the timer. ## Related The `POST /heartbeat/v3` endpoint reference. The shutdown path that confirms each cancel. Every way an order leaves the book, and how to distinguish them. # SX Bet Developer Hub Source: https://docs.sx.bet/developers/introduction Build trading bots, custom frontends, and analytics tools on the SX Bet peer-to-peer sports prediction market. Explore quickstarts, API references, and real-time data guides.
SX Bet # Developer Hub

Build trading bots, custom frontends, and analytics tools on the only open, peer-to-peer sports prediction market.

## Build on SX with AI Connect your AI coding assistant to the SX Bet docs by installing the MCP server. The docs MCP allows your agent to search these docs while you build. ```bash Claude Code theme={null} claude mcp add --transport http sx-bet https://docs.sx.bet/mcp ``` ```json Cursor theme={null} // .cursor/mcp.json { "mcpServers": { "sx-bet": { "url": "https://docs.sx.bet/mcp" } } } ``` ```json VS Code theme={null} // .vscode/mcp.json { "servers": { "sx-bet": { "type": "http", "url": "https://docs.sx.bet/mcp" } } } ``` ```json Windsurf theme={null} // Windsurf Settings → MCP Configuration { "mcpServers": { "sx-bet": { "url": "https://docs.sx.bet/mcp" } } } ``` ## Try the API ```javascript JavaScript theme={null} const res = await fetch("https://api.sx.bet/markets/active?sportIds=5&pageSize=5"); const { data } = await res.json(); console.log(data.markets); // array of active markets; data.nextKey pages on ``` ```python Python theme={null} import requests res = requests.get("https://api.sx.bet/markets/active", params={"sportIds": 5, "pageSize": 5}) markets = res.json()["data"]["markets"] # data["nextKey"] pages on print(markets) ``` ## Start building Sign in, deploy and fund a proxy, read the book, place your first order. Full endpoint reference with request and response schemas. Subscribe to books, your own orders, and the public trade feed over one WebSocket. Trading bots, custom frontends, analytics tools, and more. ## Explore the platform Signing in with a wallet, and signing orders. Everyone posts an order. The engine decides the aggressor afterwards. How markets nest, and the identifiers you query by. Which side a resting level is liquidity for, and who set the price. Post quotes, manage them, and cancel by event. Take a resting price with an IOC or FOK order.
# Maintenance Source: https://docs.sx.bet/developers/maintenance What happens to your resting orders during a maintenance window or matching engine restart. During maintenance or matching engine restart, **every resting order** is cancelled and reposting is your job. ## What you receive The cancels arrive on [`account:orders_v3_#{address}`](/api-reference/channel-orders-v3), one message per order, with `status: "INACTIVE"` and `inactiveReason: "SYSTEM"`. ```json theme={null} { "order": { "id": "0xbc9c298f6584e484dc23a8b4ac95755faabbb5f966b37b869b3a74acf0790fe6", "status": "INACTIVE", "inactiveReason": "SYSTEM", "remainingSize": "1000000" } } ``` # Market making Source: https://docs.sx.bet/developers/market-making An overview of market making on SX Bet — how the maker role works, how exposure is budgeted, and where the book's design constrains a quoting loop. Market making means resting `GTC` orders on the book and managing what happens to them — earning the spread between the price you post and what a taker receives. The mechanics of posting are on [Posting orders](/developers/posting-orders). ## Prerequisites Your proxy must be [deployed and funded](/developers/posting-orders#prerequisites) before `POST /orders-v3` accepts anything. ## Minimal example 1. **`GET /metadata/obv3`** — once, at startup. 2. **`GET /markets/active`** — discover the markets you cover. 3. **`GET /orderbook-v3/snapshot`** — read the aggregated levels and the book's version. 4. **Price** — your model's output, snapped to the ladder. 5. **`POST /orders-v3`** — up to 10 orders per request. 6. **Watch** — `account:orders_v3_#{address}` for order state, `account:fills_v3_#{address}` for fills (joined on `orderId`), `orderbook_v3:{marketHash}` for the book moving. 7. **Reprice** — `DELETE /orders-v3` by id, then `POST` the replacement. ## Related Fields, signing, and the validation rules on create. Picking a cancel route, and confirming before you exit. The order row in full, and why a missing order has more than one explanation. Rounding a model price onto the odds ladder. Quoting parlay requests, where `FOK` is required. # Market making parlays Source: https://docs.sx.bet/developers/market-making-parlays How to listen for parlay requests, price them, and submit orders as a market maker. ## Overview This guide covers the full flow for market making parlays on SX Bet: 1. Connect to the WebSocket. 2. Receive parlay requests. 3. Analyse the legs. 4. Calculate odds. 5. Post orders. All of it has to happen within the 3-second RFQ window. ## Step 1: Connect to the parlay channel Subscribe to the [`parlay_markets:global` channel](/api-reference/channel-parlay-requests) to receive parlay requests in real time. You need a token from the [`GET /user/realtime-token-v3/api-key`](/developers/realtime-initialization#connect) endpoint. ```javascript JavaScript theme={null} import { Centrifuge } from "centrifuge"; const API_URL = "https://api.sx.bet"; // Mainnet — use https://api.toronto.sx.bet for testnet const WS_URL = "wss://realtime.sx.bet/connection/websocket"; async function fetchToken() { const res = await fetch(`${API_URL}/user/realtime-token-v3/api-key`, { headers: { "x-sx-api-key": process.env.SX_API_KEY }, }); if (!res.ok) throw new Error(`Token endpoint returned ${res.status}`); const { token } = await res.json(); return token; } const client = new Centrifuge(WS_URL, { getToken: fetchToken, }); const sub = client.newSubscription("parlay_markets:global"); sub.on("publication", (ctx) => { const parlayRequest = ctx.data; handleParlayRequest(parlayRequest); }); sub.subscribe(); client.connect(); ``` ```python Python theme={null} import asyncio import os import aiohttp from centrifuge import Client, PublicationContext, SubscriptionEventHandler API_URL = "https://api.sx.bet" # Mainnet — use https://api.toronto.sx.bet for testnet WS_URL = "wss://realtime.sx.bet/connection/websocket" async def fetch_token(ctx=None): async with aiohttp.ClientSession() as session: async with session.get( f"{API_URL}/user/realtime-token-v3/api-key", headers={"x-sx-api-key": os.environ["SX_API_KEY"]}, ) as resp: data = await resp.json() return data["token"] class ParlayHandler(SubscriptionEventHandler): async def on_publication(self, ctx: PublicationContext) -> None: handle_parlay_request(ctx.data) async def main(): client = Client(WS_URL, get_token=fetch_token) sub = client.new_subscription("parlay_markets:global", ParlayHandler()) await client.connect() await sub.subscribe() await asyncio.Future() # run forever asyncio.run(main()) ``` ## Step 2: Parse the parlay request Each incoming message contains the parlay's `marketHash`, the requested token and size, and the legs: ```json theme={null} { "marketHash": "0x38cceead7bda65c18574a34994ebd8af154725d08aa735dcbf26247a7dcc67bd", "requestorId": "08e489e45313812cf236b43ab68c6236e3e8d63375a07d6bafdec1ecbed85c1b", "requestSize": "100000000", "legs": [ { "marketHash": "0x0d64c52e8781acdada86920a2d1e5acd6f29dcfe285cf9cae367b671dff05f7d", "bettingOutcomeOne": true }, { "marketHash": "0xe609a49d083cd41214a0db276c1ba323c4a947eefd2e4260386fec7b5d258188", "bettingOutcomeOne": false } ] } ``` | Field | Description | | ------------- | ----------------------------------------------------------------------------------- | | `marketHash` | The parlay market to post your order against — the **parent** | | `requestorId` | An **HMAC of the requesting address**, not the address itself | | `requestSize` | Requested size in base units (see [unit conversions](/developers/unit-conversions)) | | `legs` | Array of individual markets and the outcomes the bettor selected | ## Step 3: Look up each leg Query each leg's market via [`GET /markets/find`](/api-reference/get-markets-find) to see what the bettor selected. ```javascript JavaScript theme={null} async function handleParlayRequest(parlayRequest) { const { marketHash: parlayMarketHash, requestSize, legs } = parlayRequest; // Fetch market data for each leg const legData = await Promise.all( legs.map(async (leg) => { const response = await fetch( `${BASE_URL}/markets/find?${new URLSearchParams({ marketHash: leg.marketHash })}` ).then((r) => r.json()); const market = response.data; // Fetch best odds for this leg's market const ordersResponse = await fetch( `${BASE_URL}/orders-v3/odds/best?${new URLSearchParams({ marketHashes: leg.marketHash })}`, { headers: { "x-sx-api-key": process.env.SX_API_KEY } } ).then((r) => r.json()); // bestOdds is an ARRAY, one row per requested hash, in request order. // Each row is {marketHash, outcomeOne, outcomeTwo}; a side with no // liquidity is a bare null, NOT an object with null fields. const row = ordersResponse.data.bestOdds[0]; const side = leg.bettingOutcomeOne ? row?.outcomeOne : row?.outcomeTwo; return { ...leg, market, // null when that side of the leg is empty — handle it, do not assume a price reference: side ? { odds: side.percentageOdds, size: side.size } : null, }; }) ); console.log("Parlay request received:"); legData.forEach((leg, i) => { const m = leg.market; const side = leg.bettingOutcomeOne ? m.outcomeOneName : m.outcomeTwoName; console.log(` Leg ${i + 1}: ${m.teamOneName} vs ${m.teamTwoName} — ${side}`); }); // Calculate your price and post an order const odds = calculateParlayOdds(legData); if (odds) { await postParlayOrder(parlayMarketHash, odds); } } ``` ```python Python theme={null} def handle_parlay_request(parlay_request): parlay_market_hash = parlay_request["marketHash"] request_size = parlay_request["requestSize"] legs = parlay_request["legs"] # Fetch market data for each leg leg_data = [] for leg in legs: market_resp = requests.get( f"{BASE_URL}/markets/find", params={"marketHash": leg["marketHash"]} ) market = market_resp.json()["data"] odds_resp = requests.get( f"{BASE_URL}/orders-v3/odds/best", params={"marketHashes": leg["marketHash"]}, headers={"x-sx-api-key": os.environ["SX_API_KEY"]}, ) row = odds_resp.json()["data"]["bestOdds"][0] # A side with no liquidity is a bare None, not a dict of Nones. side = row.get("outcomeOne") if leg["bettingOutcomeOne"] else row.get("outcomeTwo") reference = {"odds": side["percentageOdds"], "size": side["size"]} if side else None leg_data.append({**leg, "market": market, "reference": reference}) for i, leg in enumerate(leg_data): m = leg["market"] side = m["outcomeOneName"] if leg["bettingOutcomeOne"] else m["outcomeTwoName"] print(f" Leg {i + 1}: {m['teamOneName']} vs {m['teamTwoName']} — {side}") # Calculate your price and post an order odds = calculate_parlay_odds(leg_data) if odds: post_parlay_order(parlay_market_hash, odds) ``` ## Step 4: Calculate your odds The simplest approach is to multiply the implied probabilities of each leg. Your pricing strategy is up to you. This is a basic example: ```javascript JavaScript theme={null} const ODDS_PRECISION = 10n ** 20n; function calculateParlayOdds(legData) { // Simple approach: multiply implied probabilities of each leg let combinedProbability = 1.0; for (const leg of legData) { // Use your own fair odds estimate per leg // This example uses a fixed estimate — replace with your model const legProbability = estimateLegProbability(leg); combinedProbability *= legProbability; } // Add your margin const margin = 0.03; // 3% edge const makerProbability = combinedProbability + margin; // Convert to SX protocol format (maker's percentageOdds) // percentageOdds = maker's implied probability * 10^20 const percentageOdds = BigInt(Math.round(makerProbability * 1e20)); return percentageOdds.toString(); } function estimateLegProbability(leg) { // Replace with your actual pricing model // This is just a placeholder return 0.5; } ``` ```python Python theme={null} ODDS_PRECISION = 10 ** 20 def calculate_parlay_odds(leg_data): # Simple approach: multiply implied probabilities of each leg combined_probability = 1.0 for leg in leg_data: leg_probability = estimate_leg_probability(leg) combined_probability *= leg_probability # Add your margin margin = 0.03 # 3% edge maker_probability = combined_probability + margin # Convert to SX protocol format percentage_odds = str(int(maker_probability * ODDS_PRECISION)) return percentage_odds def estimate_leg_probability(leg): # Replace with your actual pricing model return 0.5 ``` `percentageOdds` is **your own** implied probability, scaled by 10^20 — the price *you* are paying, for the side named by `isMakerBettingOutcomeOne`. ## Step 5: Post your order Posting a parlay order is posting an order. There is nothing parlay-specific about the payload or the signature. **The order's expiry must be no later than the parlay market's expiry.** The signing code lives on the shared pages: The struct, the domain, and signing the digest. Keep your order's `expiry` — a unix epoch timestamp in seconds — inside the parlay's own expiry. ## Step 6: Manage your orders Cancelling a parlay order is cancelling an order. Use [`DELETE /orders-v3`](/api-reference/delete-orders-v3) with the ids you want cancelled. Picking a route, confirming before you exit, and why a `200` is not enough. ## Related The RFQ flow. Posting new orders The eight-field struct every order shares. The message you subscribe to. # Markets Source: https://docs.sx.bet/developers/markets-and-events What a market is on SX Bet, and every field on a market row. ## What is a market? A market on SX Bet represents a single, binary question about the outcome of a sporting event. For example: * **Will the Lakers or the Celtics win?** *(Moneyline)* * **Will the total goals scored be over or under 2.5?** *(Total)* * **Will Manchester City win by more than 1.5 goals?** *(Spread)* Every market has exactly two sides you can bet on. When you place a bet, you're taking a position on one of those two outcomes. If neither outcome is valid (e.g. the game is cancelled), the market resolves as void and all bets are returned. Many markets exist for any single fixture. A Premier League match might have a moneyline market, multiple spread markets at different lines, multiple over/under totals, and more. ## The market hash Every market on SX Bet is identified by a unique `marketHash`. ``` "marketHash": "0x024902746edaed3ffd447aca28f695362264e045be71b3d2ba53e2097dd7667b" ``` The `marketHash` is the primary key used across the entire API. You'll use it to: * Read the book on a market * Post and cancel orders * Query your bet history * Subscribe to orderbook updates via WebSocket ## Outcomes Every market has three outcome fields: | Field | Description | | ----------------- | ---------------------------------------------- | | `outcomeOneName` | The name of the first bettable outcome | | `outcomeTwoName` | The name of the second bettable outcome | | `outcomeVoidName` | The condition under which the market is voided | The meaning of these fields depends on the market type: | Market type | `outcomeOneName` | `outcomeTwoName` | | ----------- | -------------------------- | -------------------------- | | Moneyline | Team/player one | Team/player two | | Spread | Team A covering the spread | Team B covering the spread | | Total | Over | Under | | 1X2 | Team A wins | Team A does not win | ## Market types In addition to outcome names, each market has a `type` field. This numeric identifier tells you what kind of market it is. SX Bet supports over 30 market types — see the [full list on the Market Types page](/api-reference/market-types). A few common examples: | `type` | Name | Description | | ------ | --------------------- | ------------------------------------------------------------------------------------------- | | `52` | 12 | Who will win the game (no draw) — e.g. `"Lakers"` vs `"Celtics"` | | `1` | 1X2 | Who will win the game, including draw — e.g. `"Man City wins"` vs `"Man City does not win"` | | `226` | 12 Including Overtime | Who will win the game including overtime | | `3` | Asian Handicap | Who will win with a points handicap — e.g. `"Lakers -3.5"` vs `"Celtics +3.5"` | | `2` | Under/Over | Will the total score be over or under a line — e.g. `"Over 2.5"` vs `"Under 2.5"` | For spread and total markets, the `line` field contains the relevant value: ```json theme={null} { "type": 2, "outcomeOneName": "Over 2.5", "outcomeTwoName": "Under 2.5", "line": 2.5 } ``` ## Main lines and alternate lines For spread and total markets, multiple lines are often available for the same fixture. For example, a soccer match might have totals at 1.5, 2.5, and 3.5 goals. The `mainLine` field indicates whether a market is currently the primary line for its type: ```json theme={null} { "type": 2, "line": 1.5, "mainLine": false } // alternate line { "type": 2, "line": 2.5, "mainLine": true } // main line { "type": 2, "line": 3.5, "mainLine": false } // alternate line ``` The main line is the primary, most balanced line where both outcomes are closest to having an equal probability (50/50) — it shifts as the market moves. A market type either carries a line or it does not — see [Market types](/api-reference/market-types), the **Has lines** column. | | Line-bearing type (e.g. 28, Under/Over) | Line-free type (e.g. 274, Outright Winner) | | ----------------- | ------------------------------------------- | ------------------------------------------ | | Markets per event | one per line value | one | | `line` | present, a number like `48.5` | **absent entirely** | | `mainLine` | present on the ones that have been computed | **absent entirely** | ## `isQuarterLineMarket` `isQuarterLineMarket` is present on **every** market row, as a boolean. ```json theme={null} "isQuarterLineMarket": false ``` A quarter-line market settles as a half-win or half-loss. See [Quarter-line markets](/developers/quarter-line-markets). ## Market status The `status` field tells you whether a market is currently open for trading. | Status | Description | | ---------- | ----------------------------------------------------- | | `ACTIVE` | The market is open — orders can be posted and matched | | `INACTIVE` | The market is closed for trading | ## Live markets Many markets support in-play (live) betting. The `liveEnabled` field indicates whether a market is available for live betting: ```json theme={null} { "liveEnabled": true } ``` Live markets remain active while the game is in progress. Odds and liquidity can move quickly on live markets, so be mindful of [taking liquidity](/developers/taking-liquidity) and [betting delays](/developers/betting-delays) on live markets. ## Full market object Here's a complete market object for reference: ```json theme={null} { "status": "ACTIVE", "marketHash": "0x1c8f12c7e05760295e95ea83666e0e199c9ba07b571631d695f8a91325bcbc83", "outcomeOneName": "Paris Saint Germain -2", "outcomeTwoName": "Chelsea +2", "outcomeVoidName": "NO_GAME_OR_EVEN", "teamOneName": "Paris Saint Germain", "teamTwoName": "Chelsea", "type": 3, "gameTime": 1773259200, "line": -2, "sportXeventId": "L18148217", "liveEnabled": true, "sportLabel": "Soccer", "sportId": 5, "leagueId": 30, "leagueLabel": "Champions League_UEFA", "group1": "Champions League", "group2": "UEFA", "chainVersion": "SXR", "participantOneId": 839, "participantTwoId": 4, "mainLine": false, "isQuarterLineMarket": false, "__type": "Market" } ``` | Field | Description | | --------------------- | ----------------------------------------------------------------------------------------------- | | `status` | `ACTIVE` or `INACTIVE` | | `marketHash` | The unique identifier for the market | | `outcomeOneName` | Outcome one for this market | | `outcomeTwoName` | Outcome two for this market | | `outcomeVoidName` | Outcome void for this market | | `teamOneName` | The name of the scheduled home team/player | | `teamTwoName` | The name of the scheduled away team/player | | `type` | The type of the market | | `gameTime` | The UNIX timestamp of the game | | `line` | The line of the market. Only applicable to markets with a line | | `sportXeventId` | The unique event ID for this market | | `liveEnabled` | Whether or not this match is available for live betting | | `sportLabel` | The name of the sport for this market | | `sportId` | The ID of the sport for this market | | `leagueId` | The league ID for this market | | `leagueLabel` | The name of the league for this market | | `mainLine` | Whether this market is currently the main line. Not present on markets without multiple lines | | `isQuarterLineMarket` | Whether the exchange produced this market by splitting a line-bearing market into quarter lines | | `group1` | Indicator to the client of how to display this market | | `group2` | Indicator to the client of how to display this market | A settled market additionally carries `reportedDate`, `outcome`, `teamOneScore` and `teamTwoScore` — see [Find markets](/api-reference/get-markets-find). A parlay market carries `legs` — see [Parlays](/developers/parlays). ## Related The sport → league → event → market hierarchy, and how to traverse it. Every `type` id, its bet group, and whether it carries a line. Filters, pagination, and querying by sport, league, event or hash. What `isQuarterLineMarket` changes about settlement. # Overview Source: https://docs.sx.bet/developers/markets-overview All market data on SX Bet is available through the API. No API key or account is required to fetch market data. ## Data hierarchy Market data on SX Bet is organized in a hierarchy. Understanding this structure will help you navigate the API efficiently. | Level | Endpoint | Description | Filter by | | --------- | ---------------------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------- | | Sports | `GET /sports` | All sports available on SX Bet | — | | Leagues | `GET /leagues/active` | Active leagues for a given sport | `sportId` | | Fixtures | `GET /fixture/active` | Active games/events for a given league | `leagueId` | | Markets | `GET /markets/active` | Bettable binary outcomes for a given fixture | `sportIds`, `eventId`, `leagueId`, `type`, `betGroup`, `onlyMainLine`, `liveOnly`, `gameTime` | | Orderbook | `GET /orderbook-v3/snapshot` | The full resting book for **one** market | `marketHash` | | Best odds | `GET /orders-v3/odds/best` | Best level per side for many markets | `marketHashes` (≤100) | A market row carries no odds — prices live at the book level, not on the market. The two pricing routes are not interchangeable. The snapshot gives full depth for one market. Best odds gives the top of book for up to a hundred. See [Active markets](/api-reference/get-markets-active). ## Try it yourself Run the script below in your terminal. It walks you through the full hierarchy interactively. Select a sport, then a league, then a fixture. It then displays all available main-line markets for that fixture. ```python Python theme={null} import requests BASE_URL = "https://api.sx.bet" # Mainnet — use https://api.toronto.sx.bet for testnet # Step 1: Fetch and display all sports sports = requests.get(f"{BASE_URL}/sports").json()["data"] print("=== Sports ===") for s in sports: print(f" [{s['sportId']}] {s['label']}") sport_id = int(input("\nEnter a sport ID: ")) # Step 2: Fetch active leagues for the selected sport leagues = requests.get( f"{BASE_URL}/leagues/active", params={"sportId": sport_id} ).json()["data"] print("\n=== Active Leagues ===") for l in leagues: print(f" [{l['leagueId']}] {l['label']}") league_id = int(input("\nEnter a league ID: ")) # Step 3: Fetch active fixtures for the selected league fixtures = requests.get( f"{BASE_URL}/fixture/active", params={"leagueId": league_id} ).json()["data"] print("\n=== Active Fixtures ===") for f in fixtures: home = f.get("participantOneName", "N/A") away = f.get("participantTwoName", "N/A") print(f" [{f['eventId']}] {home} vs {away} — {f['startDate']}") event_id = input("\nEnter an event ID: ") # Step 4: Fetch main-line markets for the selected fixture markets = requests.get( f"{BASE_URL}/markets/active", params={"eventId": event_id, "onlyMainLine": True} ).json()["data"]["markets"] print(f"\n=== Markets ({len(markets)} found) ===") for m in markets: print(f" {m['outcomeOneName']} vs {m['outcomeTwoName']}") print(f" marketHash: {m['marketHash']}") ``` ```javascript JavaScript theme={null} import * as readline from "readline"; const BASE_URL = "https://api.sx.bet"; // Mainnet — use https://api.toronto.sx.bet for testnet const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const ask = (q) => new Promise((res) => rl.question(q, res)); // Step 1: Fetch and display all sports const sportsData = await fetch(`${BASE_URL}/sports`).then((r) => r.json()); console.log("=== Sports ==="); for (const s of sportsData.data) { console.log(` [${s.sportId}] ${s.label}`); } const sportId = await ask("\nEnter a sport ID: "); // Step 2: Fetch active leagues for the selected sport const leaguesData = await fetch( `${BASE_URL}/leagues/active?${new URLSearchParams({ sportId })}` ).then((r) => r.json()); console.log("\n=== Active Leagues ==="); for (const l of leaguesData.data) { console.log(` [${l.leagueId}] ${l.label}`); } const leagueId = await ask("\nEnter a league ID: "); // Step 3: Fetch active fixtures for the selected league const fixturesData = await fetch( `${BASE_URL}/fixture/active?${new URLSearchParams({ leagueId })}` ).then((r) => r.json()); console.log("\n=== Active Fixtures ==="); for (const f of fixturesData.data) { const home = f.participantOneName ?? "N/A"; const away = f.participantTwoName ?? "N/A"; console.log(` [${f.eventId}] ${home} vs ${away} — ${f.startDate}`); } const eventId = await ask("\nEnter an event ID: "); // Step 4: Fetch main-line markets for the selected fixture const marketsData = await fetch( `${BASE_URL}/markets/active?${new URLSearchParams({ eventId, onlyMainLine: true })}` ).then((r) => r.json()); const markets = marketsData.data.markets; console.log(`\n=== Markets (${markets.length} found) ===`); for (const m of markets) { console.log(` ${m.outcomeOneName} vs ${m.outcomeTwoName}`); console.log(` marketHash: ${m.marketHash}`); } rl.close(); ``` *** ## Sports SX Bet covers a wide range of sports. Each sport is identified by a numeric `sportId`. Pass a `sportId` to other endpoints (such as `/leagues/active`) to filter results by sport. | `sportId` | Sport | | --------- | ------------------ | | `1` | Basketball | | `2` | Hockey | | `3` | Baseball | | `4` | Golf | | `5` | Soccer | | `6` | Tennis | | `7` | Mixed Martial Arts | | `8` | Football | | `9` | E Sports | | `10` | Novelty Markets | | `11` | Rugby Union | | `12` | Racing | | `13` | Boxing | | `14` | Crypto | | `15` | Cricket | | `16` | Economics | | `17` | Politics | | `18` | Entertainment | | `20` | Rugby League | | `24` | Horse Racing | | `26` | AFL | This list may not be exhaustive. Query `GET /sports` for the full, up-to-date list. *** ## Leagues SX Bet supports many leagues across all sports. Each league is identified by a `leagueId`. There are two ways to fetch leagues: 1. **`GET /leagues/active`** — Returns only leagues that currently have active fixtures. Pass a `sportId` to filter by sport. 2. **`GET /leagues`** — Returns all leagues supported by SX Bet, including those without any active fixtures. Also accepts an optional `sportId` filter. *** ## Fixtures A fixture on SX Bet represents an individual game or event — for example, Toronto Raptors vs. Detroit Pistons on March 10th, 2026. Many markets exist for any given fixture. To fetch active fixtures, query `GET /fixture/active` with a `leagueId`. Each fixture is identified by a unique `eventId`. **Example fixture response:** ```json theme={null} { "participantOneName": "William Jewell", "participantTwoName": "Indianapolis", "startDate": "2020-11-28T03:45:00.000Z", "status": 1, "leagueId": 2, "leagueLabel": "NCAA", "sportId": 1, "eventId": "L6217784" } ``` The `startDate` field contains the scheduled start time of the game in UTC. The `status` field indicates the current state of the fixture: | Status ID | Name | Description | | --------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `1` | Not started | The event has not started yet | | `2` | In progress | The event is live | | `3` | Finished | The event has finished | | `4` | Cancelled | The event has been cancelled | | `5` | Postponed | The event is postponed. If a new start time is confirmed within 48 hours, it will revert to "Not started". Otherwise, it will be cancelled. | | `6` | Interrupted | The event has been interrupted (e.g. a rain delay). Coverage will resume under the same event ID. | | `7` | Abandoned | The event has been abandoned and will not resume (e.g. a player injury in tennis). | | `8` | Coverage lost | Coverage for this event has been lost | | `9` | About to start | The event is about to start (shown up to 30 minutes before tip-off) | **The wrong spelling of the event id returns `200`, not an error.** Validation strips the unknown query field, so `/markets/active?sportXeventId=…` returns **every** market with your filter ignored. One identifier has three spellings, and the casing matters: * You **read** `sportXeventId` — lowercase `e` — from market, fixture and search rows. * You **pass** `eventId` to every route and channel. * `GET /live-scores` and `GET /fixture/status` take a third form: **`sportXEventIds`**, capital `E` and plural. Lowercase there is a `400 ["sportXEventIds must be a string"]`. *** ## Markets A market represents a single binary outcome you can bet on — for example, "Over 2.5 Total Goals" vs. "Under 2.5 Total Goals". Many markets exist for any given fixture. Every market on SX Bet is identified by a unique `marketHash`, which is used throughout the API to fetch orderbooks, post orders, and query trades. Querying active markets from the API. # Migrate to V3 in 30 minutes Source: https://docs.sx.bet/developers/migrate-to-v3 How to migrate an existing V2 integration to V3. This guide is for developers with a **working V2 client**. For the reasoning behind the changes, see [Improvements](/developers/new-in-v3). For a new integration, start at [Quickstart](/developers/quickstart). ## What's fundamentally different Five changes underlie the rest of this guide: 1. **Capital moves to a proxy wallet.** You deploy and fund a secure proxy. A new prerequisite with no V2 equivalent. 2. **One endpoint to make and take.** Maker and taker both POST to `/orders-v3`. `timeInForce` (`GTC` rests; `IOC`/`FOK` execute immediately) determines which you are. 3. **API keys are required** on every authenticated request — and the header is renamed: V2 `X-Api-Key` → V3 `x-sx-api-key`. Search-and-replace it everywhere. 4. **Refund and capital efficiency events are in-lined.** No longer separate events or endpoints 5. **Order signing process has changed** 6. **`pendingFills`** fields have been removed. ## What has not changed * `percentageOdds` is still implied probability × 10^20; `totalBetSize` is still your stake in base-token units (USDC = 6 decimals). See [Unit conversions](/developers/unit-conversions). * The odds ladder is still enforced (default step 0.125%). Read `oddsLadderStepSize` from `GET /metadata/obv3`. See [Odds rounding](/developers/odds-rounding). * Resting orders are still quoted in the maker frame; taker implied probability = 1 − maker probability. (`showTakerPerspective=true` on the orderbook snapshot does the inversion for you.) * You still sign with your EOA private key, and maker is still your EOA address. * Market discovery is unchanged: `GET /markets/active`, `GET /markets/find`, and the sports / leagues / fixtures endpoints and shapes. * The `{ "status": ..., "data": ... }` response envelope is unchanged. * The realtime endpoint and protocol are unchanged (`wss://realtime.sx.bet/connection/websocket`, Centrifugo), and the `fixtures:*`, `markets:global`, `main_line:global` and `parlay_markets:global` channels are unchanged. * Testnet hosts are unchanged: `https://api.toronto.sx.bet` and `wss://realtime.toronto.sx.bet/connection/websocket`. ## Step-by-step guide Deploy a proxy your account owns and move USDC into it — see [Accounts](/developers/accounts). Easiest way is just login to [sx.bet](https://sx.bet) and go through the wizard. Prefer to stay in code? The whole flow is API-first too (all with the `x-sx-api-key` header): ```text theme={null} POST /user/deploy-proxy # deploy GET /user/proxy # poll until deployed; returns the proxy address GET /user/balance-v3 # confirm funds arrived ``` Funding the proxy replaces V2's "enable betting" step. There is no longer an ERC-20 approval of the `TokenTransferProxy` (`POST /orders/approve` is gone) Already live on V2? Your trading capital currently sits in your EOA under V2's model; to trade on V3 it must be inside the proxy. Login to your SX Bet account and follow the pop-up wizard to migrate. All V2 orders at cutoff will be auto-cancelled. **V2 clients will fail to run after V3 goes live at 10AM EST on August 26.** Create a **new** API key — your existing V2 key will not work. Log in at [sx.bet](https://sx.bet), open **Account → Overview**, generate a key, and send it in the `x-sx-api-key` header on every authenticated request. See [API Keys](/api-reference/api-key). This key is more sensitive than before, as it can now cancel all your orders. * There is no longer a separate endpoint for fills. The endpoint is [`POST /orders-v3`](/api-reference/post-orders-v3) `timeInForce` is new and required and the signature has changed. `timeInForce` implies if your order will rest or not. * If you're a market maker, use `GTC`. If you're a taker, use `IOC`. See [Time in force](/developers/time-in-force). * The signing of the order has also changed. See the example below. * The old `apiExpiry` field was eliminated and merged into just a single `expiry` field. In V3 `expiry` is a real unix epoch timestamp in seconds (not milliseconds) inside the signature, and it is mandatory — it must be in the future, and `0` is rejected. There is no never-expires order. Because it is signed, changing an order’s TTL means re-signing. V2’s constant `2209006800` is gone — do not send it. * The V2 slippage vocabulary is also gone — there are no `desiredOdds` or `oddsSlippage` fields, and no `ODDS_STALE` error. `percentageOdds` is the worst price you will accept; the engine matches at that price or better. See [Odds](/developers/odds). * **This endpoint, by default, is now asynchronous, meaning that the outcome of your order (filled / partially filled / cancelled) is not known immediately**. Use `waitForOutcome` to keep the old synchronous behaviour. This applies to both orders that will match, clear, or rest. Field-by-field, what changed on the order body from V2 (`POST /orders/new` and `POST /orders/fill/v2`) to V3 (`POST /orders-v3`): | Field | V2 | V3 | | ------------------------------ | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | `expiry` | Dummy constant `2209006800` | Real TTL as a unix epoch timestamp in seconds, and part of the signature. Required, and must be in the future — `0` is rejected. | | `apiExpiry` | The real order TTL, sent alongside `expiry` | **Removed** — merged into `expiry` | | `executor` | Required signed address | **Removed** | | `timeInForce` | — (resting was implied; takers called `POST /orders/fill/v2`) | **New, required** — `GTC`, `IOC`, or `FOK` | | `signature` → `orderSignature` | `signature`, over a 9-field struct ordered `…, expiry, salt, maker, executor, …` | `orderSignature`, over the 8-field struct ordered `…, salt, expiry, maker, …` (no `executor`) | | `desiredOdds` / `oddsSlippage` | Taker slippage fields on the fill endpoint | **Removed** — `percentageOdds` is the worst price you'll accept | | `waitForOutcome` | — (fills were synchronous) | **New, optional** — the endpoint is async by default; set it to keep sync behaviour | `marketHash`, `maker` (your EOA, never the proxy), `baseToken`, `totalBetSize`, `percentageOdds`, `salt`, and `isMakerBettingOutcomeOne` are unchanged. The EIP-712 domain `version` is now `"1"`, not `"1.0"`. ```javascript JavaScript theme={null} import { Wallet, hexlify, randomBytes } from "ethers"; const API = "https://api.sx.bet"; // testnet: https://api.toronto.sx.bet const wallet = new Wallet(process.env.SX_PRIVATE_KEY); const marketHash = process.env.SX_MARKET_HASH; const { data: meta } = await fetch(`${API}/metadata/obv3`).then((r) => r.json()); const order = { marketHash, maker: wallet.address, baseToken: meta.activeAsset.baseToken, totalBetSize: "100000000", percentageOdds: "52500000000000000000", salt: hexlify(randomBytes(32)), expiry: Math.floor(Date.now() / 1000) + 3600, // ONE expiry, signed. Real unix seconds, required isMakerBettingOutcomeOne: true, timeInForce: "GTC", // NEW, required. }; // EIP-712 typed data. 8 fields, salt BEFORE expiry. No executor. const domain = meta.domain; // complete EIP-712 domain; version is "1", not "1.0" const types = { Order: [ { name: "marketHash", type: "bytes32" }, { name: "baseToken", type: "address" }, { name: "totalBetSize", type: "uint256" }, { name: "percentageOdds", type: "uint256" }, { name: "salt", type: "uint256" }, { name: "expiry", type: "uint256" }, { name: "maker", type: "address" }, { name: "isMakerBettingOutcomeOne", type: "bool" }, ] }; const orderSignature = await wallet.signTypedData(domain, types, order); const res = await fetch(`${API}/orders-v3`, { method: "POST", headers: { "Content-Type": "application/json", "x-sx-api-key": process.env.SX_API_KEY }, // waitForOutcome makes the call block for the matching result (the old sync behaviour). body: JSON.stringify({ orders: [{ ...order, orderSignature }], waitForOutcome: true }), }); console.log(JSON.stringify(await res.json(), null, 2)); ``` ```python Python theme={null} import os, secrets, time, requests from eth_account import Account from eth_account.messages import encode_typed_data API = "https://api.sx.bet" # testnet: https://api.toronto.sx.bet account = Account.from_key(os.environ["SX_PRIVATE_KEY"]) market_hash = os.environ["SX_MARKET_HASH"] meta = requests.get(f"{API}/metadata/obv3").json()["data"] order = { "marketHash": market_hash, "maker": account.address, "baseToken": meta["activeAsset"]["baseToken"], "totalBetSize": "100000000", "percentageOdds": "52500000000000000000", "salt": "0x" + secrets.token_hex(32), "expiry": int(time.time()) + 3600, # ONE expiry, signed. Real unix seconds, required "isMakerBettingOutcomeOne": True, "timeInForce": "GTC", # NEW, required. GTC rests } # EIP-712 typed data. 8 fields, salt BEFORE expiry. No executor. domain = meta["domain"] # complete EIP-712 domain; version is "1", not "1.0" types = {"Order": [ {"name": "marketHash", "type": "bytes32"}, {"name": "baseToken", "type": "address"}, {"name": "totalBetSize", "type": "uint256"}, {"name": "percentageOdds", "type": "uint256"}, {"name": "salt", "type": "uint256"}, {"name": "expiry", "type": "uint256"}, {"name": "maker", "type": "address"}, {"name": "isMakerBettingOutcomeOne", "type": "bool"}]} message = {**order, "totalBetSize": int(order["totalBetSize"]), "percentageOdds": int(order["percentageOdds"]), "salt": int(order["salt"], 16)} signed = account.sign_message(encode_typed_data(domain, types, message)) # waitForOutcome makes the call block for the matching result (the old sync behaviour). res = requests.post(f"{API}/orders-v3", headers={"x-sx-api-key": os.environ["SX_API_KEY"]}, json={"orders": [{**order, "orderSignature": signed.signature.to_0x_hex()}], "waitForOutcome": True}) print(res.json()) ``` ```json Response theme={null} { "status": "success", "data": { "orders": [ { "orderId": "0x7f611d85216fa11810d5357ffaaeafbbefb88e69b9fd26bfae7b13a67cceb9dc", "status": "SUBMITTED", "commandId": "550e8400-e29b-41d4-a716-446655440000", "outcome": { "state": "FULLY_FILLED", "remainingAmount": "0", "fillAmount": "2000000", "blendedOdds": "50000000000000000000", "matchIds": ["0x9a1c2f3e4b5d6a7f8c9b0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a"], "tradeId": "0xtrade1" } } ] } } ``` The top-level `status` (`SUBMITTED`/`FAILED`) is the accept/reject of your submission and is independent of `outcome.state`. `outcome` is present only because `waitForOutcome` was set; without it the call returns as soon as the order is accepted and you track the result on the `account:orders_v3` channel instead. Worked example — the two frames: you post GTC with `percentageOdds = "52500000000000000000"` (52.5% — your implied probability on your own outcome). A taker who wants the other outcome sees your level as 47.5% under `showTakerPerspective=true` and submits their own order with `percentageOdds = "47500000000000000000"` as their worst acceptable price. Full new signing guide: [EIP-712 order signing](/api-reference/eip712-order-signing). As mentioned above, **the fill endpoint is gone.** Taking is just submitting an order with `timeInForce: "IOC" / "FOK"`. `percentageOdds` is the worst price you will accept. ```javascript JavaScript theme={null} import { Wallet, hexlify, randomBytes } from "ethers"; const API = "https://api.sx.bet"; // testnet: https://api.toronto.sx.bet const wallet = new Wallet(process.env.SX_PRIVATE_KEY); // `domain` and `types` are identical to the posting step — reuse them. // No inversion. percentageOdds is YOUR price for the outcome YOU want, read from // GET /orderbook-v3/snapshot?...&showTakerPerspective=true. No slippage field exists. const order = { marketHash, maker: wallet.address, baseToken: meta.activeAsset.baseToken, totalBetSize: "50000000", percentageOdds: takerPrice, // your bound; matches at this or better salt: hexlify(randomBytes(32)), expiry: Math.floor(Date.now() / 1000) + 3600, isMakerBettingOutcomeOne: true, // the outcome YOU want timeInForce: "IOC", // or "FOK". This is the only taker-specific field }; const orderSignature = await wallet.signTypedData(domain, types, order); // SAME domain + types as posting const res = await fetch(`${API}/orders-v3`, { method: "POST", headers: { "Content-Type": "application/json", "x-sx-api-key": process.env.SX_API_KEY }, // waitForOutcome blocks for the fill result — takers usually want this. body: JSON.stringify({ orders: [{ ...order, orderSignature }], waitForOutcome: true }), }); console.log(JSON.stringify(await res.json(), null, 2)); ``` ```python Python theme={null} import os, secrets, time, requests from eth_account import Account from eth_account.messages import encode_typed_data # `domain` and `types` are identical to the posting step — reuse them. API = "https://api.sx.bet" # testnet: https://api.toronto.sx.bet account = Account.from_key(os.environ["SX_PRIVATE_KEY"]) # No inversion. percentage_odds is YOUR price for the outcome YOU want, read from # GET /orderbook-v3/snapshot?...&showTakerPerspective=true. No slippage field exists. order = { "marketHash": market_hash, "maker": account.address, "baseToken": meta["activeAsset"]["baseToken"], "totalBetSize": "50000000", "percentageOdds": taker_price, # your bound; matches at this or better "salt": "0x" + secrets.token_hex(32), "expiry": int(time.time()) + 3600, "isMakerBettingOutcomeOne": True, # the outcome YOU want "timeInForce": "IOC", # or "FOK". This is the only taker-specific field } message = {**order, "totalBetSize": int(order["totalBetSize"]), "percentageOdds": int(order["percentageOdds"]), "salt": int(order["salt"], 16)} signed = account.sign_message(encode_typed_data(domain, types, message)) # SAME domain + types as posting # waitForOutcome blocks for the fill result — takers usually want this. res = requests.post(f"{API}/orders-v3", headers={"x-sx-api-key": os.environ["SX_API_KEY"]}, json={"orders": [{**order, "orderSignature": signed.signature.to_0x_hex()}], "waitForOutcome": True}) print(res.json()) ``` ```json Response theme={null} { "status": "success", "data": { "orders": [ { "orderId": "0x0e9e6910f10e0aced2059f2736011c05b9a9cf5c587d150f3b413159560c7a76", "status": "SUBMITTED", "commandId": "74a01fe8-6555-4492-8d28-879c151aef8b", "outcome": { "state": "PARTIAL_FILL_DONE", "remainingAmount": "20000000", "fillAmount": "30000000", "blendedOdds": "47500000000000000000", "matchIds": ["0x6c2d2a49c159720d26fa89a13ac49711aba2adac5dad2c12255440ebdf7a62c4"], "tradeId": "0xac6bb30bba6ea82d8717219a4fa49d15b58fd638fba479f294b4d402376d14d6" } } ] } } ``` An `IOC` order fills what it can and cancels the rest, so a partial fill returns `outcome.state: "PARTIAL_FILL_DONE"` with the unmatched `remainingAmount` cancelled (a `FOK` order instead fills completely or cancels whole). Drop `waitForOutcome` and the call returns as soon as the order is accepted (`status: "SUBMITTED"`, no `outcome`), and you track fills on the `account:fills_v3` channel instead. Cancels no longer have an extra signature. In V3 all three are just `DELETE`s authenticated by your API key. `CancelOrderV2SportX` / `CancelOrderEventsSportX` / `CancelAllOrdersSportX` EIP-712 domains are now gone. ```javascript JavaScript theme={null} const API = "https://api.sx.bet"; // testnet: https://api.toronto.sx.bet // V3 — no signature. Just the key. const auth = { "x-sx-api-key": process.env.SX_API_KEY, "Content-Type": "application/json" }; // By id: body is { orders: [{ orderId }] }, up to limits.maxCancelOrders (100). const res = await fetch(`${API}/orders-v3`, { method: "DELETE", headers: auth, body: JSON.stringify({ orders: orderIds.map((orderId) => ({ orderId })) }), }); console.log(JSON.stringify(await res.json(), null, 2)); // By event: eventId is a QUERY param, and there is no body. await fetch(`${API}/orders-v3/event?eventId=${eventId}`, { method: "DELETE", headers: auth }); await fetch(`${API}/orders-v3/all`, { method: "DELETE", headers: auth }); ``` ```python Python theme={null} import os, requests API = "https://api.sx.bet" # testnet: https://api.toronto.sx.bet # V3 — no signature. Just the key. auth = {"x-sx-api-key": os.environ["SX_API_KEY"]} # By id: body is {"orders": [{"orderId": ...}]}, up to limits.maxCancelOrders (100). res = requests.delete(f"{API}/orders-v3", headers=auth, json={"orders": [{"orderId": o} for o in order_ids]}) print(res.json()) # By event: eventId is a QUERY param, and there is no body. requests.delete(f"{API}/orders-v3/event", headers=auth, params={"eventId": event_id}) requests.delete(f"{API}/orders-v3/all", headers=auth) ``` ```json Response (cancel by id) theme={null} { "status": "success", "data": { "cancelled": [ { "orderId": "0x0e9e6910f10e0aced2059f2736011c05b9a9cf5c587d150f3b413159560c7a76", "commandId": "74a01fe8-6555-4492-8d28-879c151aef8b" } ], "notCancelled": [ { "orderId": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", "reason": "NOT_FOUND" } ], "unconfirmed": [] } } ``` Cancel-by-id waits for the engine and reports per order; cancel-by-event and cancel-all are asynchronous and only confirm the job was published. See [Cancelling orders](/developers/cancelling-orders). Endpoint renamed: V2 `POST /heartbeat` → V3 [`POST /heartbeat/v3`](/api-reference/post-heartbeat-v3). V2's `requestor` field is gone (the timer is keyed to your API key's account): ```http theme={null} POST /heartbeat/v3 x-sx-api-key: $SX_API_KEY { "timeoutSeconds": 60 } # range 0–3600; response returns expiresAt ``` Semantics are unchanged: miss the window and every open order is cancelled, arriving on the `account:orders_v3` channel as `inactiveReason: "HEARTBEAT_TIMEOUT"`. Refresh well inside the window (e.g. every 20s on a 60s timeout). To disarm, call the same endpoint with `timeoutSeconds: 0` — that clears an armed timer. There is no separate cancel route; V2's `POST /heartbeat/cancel` has no V3 successor. See [Heartbeat](/developers/heartbeat). The token route: point it at `GET /user/realtime-token-v3/api-key` (V2 used `GET /user/realtime-token/api-key`). Adjust channel names and a few payload keys. The addresses in the channel keys are your EOA / user address **checksummed** and **NOT** your proxy wallet address. | Purpose | V2 | V3 | Page | | ------------------------- | --------------------------------------------------- | ----------------------------------- | ---------------------------------------------- | | Market depth (per market) | `order_book:market_{marketHash}` | `orderbook_v3:{marketHash}` | [→](/api-reference/channel-orderbook-v3) | | Market depth (per event) | `order_book:event_{sportXeventId}` | `orderbook_v3_event:{eventId}` | [→](/api-reference/channel-orderbook-v3-event) | | Best odds | `best_odds:global` | `best_odds_v3:global` | [→](/api-reference/channel-best-odds-v3) | | My open orders | `active_orders:{address}` | `account:orders_v3_#{address}` | [→](/api-reference/channel-orders-v3) | | My bets | `account:#{address}`, `type: "consolidated_trades"` | `account:trades_v3_#{address}` | [→](/api-reference/channel-trades-v3) | | My fills | - | `account:fills_v3_#{address}` | [→](/api-reference/channel-fills-v3) | | Public tape | `recent_trades:global` | `recent_trades_v3:global` | [→](/api-reference/channel-recent-trades-v3) | | Parlay RFQs | `parlay_markets:global` | `parlay_markets:global` (unchanged) | [→](/api-reference/channel-parlay-requests) | `fixtures:*`, `markets:global` and `main_line:global` are unchanged. The order book channels are more than a rename — they no longer stream individual orders. See the next step, **Re-model the order book feed**, for the new anonymous, aggregated shape. Delivery is at-least-once, so a message can be replayed. De-duplicate on the client using the `messageId` in `ctx.tags` — drop any publication whose `messageId` you have already seen. See [Realtime reliability](/developers/realtime-reliability). The orderbook channels ([`orderbook_v3`](/api-reference/channel-orderbook-v3) and [`orderbook_v3_event`](/api-reference/channel-orderbook-v3-event)) are the exception: use their `version` field to order and de-duplicate updates within a market — apply a publication only when its `version` is strictly greater than the one you hold. See [Book versioning](/developers/book-versioning). Full reference: [Real-time data](/developers/realtime-overview). In V2, [`order_book:market_{marketHash}`](/api-reference/channel-orderbook-v3) published a stream of **individual order rows/deltas** you merged into a local book. In V3, [`orderbook_v3:{marketHash}`](/api-reference/channel-orderbook-v3) (and [`orderbook_v3_event:{eventId}`](/api-reference/channel-orderbook-v3-event)) publish the **entire resting book on every update, aggregated by price into anonymous levels**. * **No per-order identity.** Levels carry only `percentageOdds` and aggregate `size`. Your own orders still arrive individually on [`account:orders_v3_#{address}`](/api-reference/channel-orders-v3). * **Replace, don't merge.** V2 required upsert/remove by `orderHash`; every V3 publication is the complete book for that market. * **Version-gate instead of `updateTime`.** Each publication carries a `version` (a single monotonic string). Apply a message only when its `version` is strictly greater than the one you hold for that market. See [Book versioning](/developers/book-versioning). * **Sides are explicit.** Levels are split into `outcomeOne` / `outcomeTwo` (always the maker frame), replacing V2's per-row `isMakerBettingOutcomeOne`. There is no `showTakerPerspective` on this channel (unlike [`GET /orderbook-v3/snapshot`](/api-reference/get-orderbook-snapshot)); invert yourself with `takerOdds = 1 − percentageOdds / 10^20`. Before/after payloads: ```json V2 (array of orders) theme={null} [ { "orderHash": "0x…", "status": "ACTIVE", "maker": "0x…", "totalBetSize": "1000000", "percentageOdds": "40000000000000000000", "isMakerBettingOutcomeOne": true, "expiry": 2209006800, "apiExpiry": 1234567890, "salt": "0x…", "signature": "0x…", "updateTime": "…", "marketHash": "0x…" } ] ``` ```json V3 (full aggregated book) theme={null} { "marketHash": "0xbf06c6379c922d8118612d1d7493b20f6df6929437fbf6022904327f9516a2eb", "version": "00100000000000010015000", "outcomeOne": [{ "percentageOdds": "40000000000000000000", "size": "1000000" }], "outcomeTwo": [] } ``` Subscribe first, then seed from REST inside the `subscribed` handler, and run both the snapshot and every publication through the same version rule so ordering never depends on timing. When the channel replays the book from history (`recovered: true`), skip the seed entirely. Full pattern: [Seeding and subscribing to the order book](/developers/odds#seeding-and-subscribing-realtime-to-the-order-book). ```javascript JavaScript theme={null} const API = "https://api.sx.bet"; // testnet: https://api.toronto.sx.bet const marketHash = "0xbf06c6379c922d8118612d1d7493b20f6df6929437fbf6022904327f9516a2eb"; const books = new Map(); // marketHash -> { version, outcomeOne, outcomeTwo } // Both the REST seed and live messages go through this. Strictly-greater version wins. function applyBook(book) { const held = books.get(book.marketHash); if (held && book.version <= held.version) return; // stale — ignore books.set(book.marketHash, book); // replace — never merge by orderHash } const sub = client.newSubscription(`orderbook_v3:${marketHash}`, { recoverable: true }); // Every publication runs through the version rule; stale ones are discarded. sub.on("publication", (ctx) => applyBook(ctx.data)); sub.on("subscribed", async (ctx) => { // recovered: true means the book was replayed from history — no REST seed needed. if (ctx.recovered) return; const { data: seed } = await fetch( `${API}/orderbook-v3/snapshot?marketHash=${marketHash}`, ).then((r) => r.json()); // Same version rule, so a newer live update that arrived during the fetch is not clobbered. applyBook(seed); }); sub.subscribe(); ``` ```python Python theme={null} API = "https://api.sx.bet" # testnet: https://api.toronto.sx.bet market_hash = "0xbf06c6379c922d8118612d1d7493b20f6df6929437fbf6022904327f9516a2eb" books = {} # market_hash -> {version, outcomeOne, outcomeTwo} # Both the REST seed and live messages go through this. Strictly-greater version wins. def apply_book(book) -> None: held = books.get(book["marketHash"]) if held and book["version"] <= held["version"]: # stale — ignore return books[book["marketHash"]] = book # replace — never merge by orderHash async def on_publication(ctx) -> None: apply_book(ctx.data) async def on_subscribed(ctx) -> None: # recovered=True means the book was replayed from history — no REST seed needed. if ctx.recovered: return seed = requests.get(f"{API}/orderbook-v3/snapshot", params={"marketHash": market_hash}).json()["data"] apply_book(seed) # same version rule, so a newer live update isn't clobbered sub = client.new_subscription( f"orderbook_v3:{market_hash}", SubscriptionEventHandler(on_publication=on_publication, on_subscribed=on_subscribed), ) await sub.subscribe() ``` Full reference: [Order book updates](/api-reference/channel-orderbook-v3) and [Event order book updates](/api-reference/channel-orderbook-v3-event). Nothing changes about how you hear requests: subscribe to `parlay_markets:global` exactly as in V2. What changes is how you respond. Price the request, then post your quote to [`POST /orders-v3`](/api-reference/post-orders-v3) with `timeInForce: "GTC"` against the parlay `marketHash`, signed the new way (see the posting step above). Every order needs a real `expiry` (a future unix epoch timestamp in seconds); for a parlay it must be no later than the parlay market's own expiry. Full walkthrough: [Market making parlays](/developers/market-making-parlays). We have renamed the data types to more intuitive names, and adjusted the endpoints. These endpoints all require an API-key and you can only see your own activity. | You want | V3 endpoint | Grain | Old | | ----------------------- | ------------------------------------------------------ | ------------------ | ------------------------------ | | One row per bet | [`GET /trades-v3`](/api-reference/get-trades-v3) | bet | consolidated\_trades | | One row per matched leg | [`GET /fills-v3`](/api-reference/get-fills-v3) | fill | trades | | Net exposure per market | [`GET /positions-v3`](/api-reference/get-positions-v3) | position | consolidated\_trades (grouped) | | Your resting orders | [`GET /orders-v3`](/api-reference/get-orders-v3) | active orders only | - | See [Grain types](/developers/which-grain). Three changes apply across all four: * Rsults are bound to your API key, so V2's `bettor=...` and `maker=true` query params are gone. * Pagination is now uniform: `perPage` + `nextKey` cursor everywhere. * V2's `settled=true/false` becomes the `status` filter (`MATCHED` / `LOCKED` / `SETTLED` / `FAILED`). Field-level diffs are on each reference page. **Identifier changes.** If your V2 client keys off `orderHash` or `fillHash`, remap it. | Concept | V2 | V3 | | ------------ | ------------------ | -------------------- | | Order | `orderHash` | `orderId` | | Bet | consolidated trade | `tradeId` | | Fill / match | `fillHash` | `id` on fills object | There is no refund endpoint or refund channel in V3. Capital-efficiency refunds are republished as fields **inline** on the trade and fill rows you already read — not as a separate resource or event. (If you never consumed CE refunds in V2, there is nothing to do here.) | V2 | V3 | | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `GET /trades/portfolio/refunds` | `ceRefundAmount` / `ceRefundFeeAmount` on [`GET /fills-v3`](/api-reference/get-fills-v3) and [`GET /trades-v3`](/api-reference/get-trades-v3) | | `ce_refunds:{address}` channel | Read the fields off the `account:fills_v3_#{address}` / `account:trades_v3_#{address}` publications | | `marketHasRefunds` flag | No successor — test `ceRefundAmount > 0` | **Order status** — `PENDING → ACTIVE → INACTIVE` (IOC/FOK orders skip `ACTIVE` and go straight to `INACTIVE`): | Status | Meaning | | ---------- | --------------------------------------------------------------------------- | | `PENDING` | Accepted and published to the matching engine. Not live, not yet matchable. | | `ACTIVE` | Resting on the book, in whole or in part. | | `INACTIVE` | No longer resting. `inactiveReason` says why. | `FILLED` is an `inactiveReason`, not a status: a fully filled order is `status: "INACTIVE"` with `inactiveReason: "FILLED"`. See [Order lifecycle](/developers/order-lifecycle). **Bet status** — `MATCHED → LOCKED → SETTLED`, or `MATCHED → FAILED`: | Status | Meaning | | --------- | ------------------------------------------------------------------------ | | `MATCHED` | The matching engine matched you. The on-chain lock is not confirmed yet. | | `LOCKED` | Funds are escrowed on chain. The bet is real and irreversible. | | `SETTLED` | The fixture is graded and the outcome is recorded in `settlement`. | | `FAILED` | The on-chain lock did not succeed. No funds moved. | See [Bet lifecycle](/developers/bet-lifecycle). *** ## Endpoint changes | Action | V2 | V3 | Page | | --------------------------- | -------------------------------------- | --------------------------------------------------- | ------------------------------------------ | | Exchange metadata | `GET /metadata` | `GET /metadata/obv3` | [→](/api-reference/get-metadata-obv3) | | Deploy proxy wallet | — | `POST /user/deploy-proxy` | [→](/api-reference/post-user-deploy-proxy) | | Look up proxy address | — | `GET /user/proxy` | [→](/api-reference/get-user-proxy) | | Fund proxy | — | `POST /user/transfer-to-proxy` | [→](/developers/funding) | | Balances | — | `GET /user/balance-v3` | [→](/api-reference/get-user-balance-v3) | | Fees | `GET /metadata` (`oracleFees`, global) | `GET /user/fees-v3` (per account) | [→](/api-reference/get-user-fees-v3) | | Withdraw | — | `POST /orders-v3/multisig/withdraw` | — | | Create order | `POST /orders/new` | `POST /orders-v3` | [→](/api-reference/post-orders-v3) | | Take liquidity | `POST /orders/fill/v2` | `POST /orders-v3` with `IOC` / `FOK` | [→](/developers/taking-liquidity) | | Enable betting | `POST /orders/approve` | — (fund the proxy) | [→](/developers/funding) | | List my open orders | `GET /orders?maker=…` | `GET /orders-v3` | [→](/api-reference/get-orders-v3) | | Get one order by id | `GET /orders?orderHashes=…` | `GET /orders-v3/{orderId}` | [→](/api-reference/get-order-v3) | | Market order book | `GET /orders?marketHashes=…` | `GET /orderbook-v3/snapshot?marketHash=…` | [→](/api-reference/get-orderbook-snapshot) | | Best odds | `GET /orders/odds/best` | `GET /orders-v3/odds/best` | [→](/api-reference/get-best-odds-v3) | | Cancel by id | `POST /orders/cancel/v2` | `DELETE /orders-v3` | [→](/api-reference/delete-orders-v3) | | Cancel by event | `POST /orders/cancel/event` | `DELETE /orders-v3/event` | [→](/api-reference/delete-orders-v3-event) | | Cancel all | `POST /orders/cancel/all` | `DELETE /orders-v3/all` | [→](/api-reference/delete-orders-v3-all) | | Register / cancel heartbeat | `POST /heartbeat` | `POST /heartbeat/v3` (cancel = `timeoutSeconds: 0`) | [→](/developers/heartbeat) | | Fill-grain history | `GET /trades` | `GET /fills-v3` | [→](/api-reference/get-fills-v3) | | Bet-grain history | `GET /trades/consolidated` | `GET /trades-v3` | [→](/api-reference/get-trades-v3) | | Get one bet by id | — | `GET /trades-v3/{tradeId}` | [→](/api-reference/get-trade-v3) | ## Rate limits have changed V2 limited endpoint *groups* (all `POST /orders/*` shared 5,500/min; all `GET /trades/*` shared 200/min). V3 limits are **per-endpoint**. Relevant changes: | V3 endpoint | Limit | Watch out | | ------------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------- | | `POST /orders-v3` | 5,500 / 60s | Same as v2 | | `GET /orderbook-v3/snapshot` | 27,500 / 300s | Public, per IP. Seed once, then use the `orderbook_v3` channels. Excessive polling will result in restrictions. | | `GET /orders-v3` | 1,200 / 60s | Far below V2's 5,500/min GET orders group — move book-polling to the `orderbook_v3` channels. | | `DELETE /orders-v3` | 10,000 / 60s | | | `DELETE /orders-v3/event` | 120 / 60s | | | `DELETE /orders-v3/all` | 60 / 60s | | | `POST /heartbeat/v3` | 600 / 60s | | | `GET /fills-v3` · `GET /positions-v3` | 300 / 60s each | | Full list, response headers, and the global bucket: [Rate limits](/developers/rate-limits). ## Migration checklist Run it on testnet ([api.toronto.sx.bet](https://api.toronto.sx.bet)) first. * [ ] Create an API key. Apply the key on every privileged endpoint. * [ ] Deploy and fund the proxy. * [ ] Fetch `GET /metadata/obv3`; wire `domain`, `activeAsset.baseToken`, `oddsLadderStepSize` and `limits.*` into config; delete the `executorAddress` lookup. * [ ] Re-implement order signing. * [ ] Post a minimum-size `GTC` with `waitForOutcome: true`; then take it with an `IOC` from a second account. * [ ] Port cancels to the three `DELETE`s; delete the V2 cancel-signing code; test "by id", "by event", and "all". * [ ] Move realtime: new token route, channel renames per the "Realtime changes" step, `messageId` dedup, and `version`-gating on the orderbook channels. * [ ] Point accounting at `/trades-v3` / `/fills-v3` / `/positions-v3`; remap `orderHash` / `fillHash` ids; switch pagination to `perPage` + `nextKey`. * [ ] Arm `POST /heartbeat/v3` and let it lapse once on testnet to watch `HEARTBEAT_TIMEOUT` cancel your orders. What changed in V3. The full V3 flow, metadata to placed bet. # Tracking your orders Source: https://docs.sx.bet/developers/my-orders How to track your open orders ## Keeping an up-to-date view of your orders **Seed your view from REST, keep it live off the account channel, and de-duplicate with `messageId`.** The snapshot gives you what is resting right now; the channel gives you every change after. 1. **Subscribe to [`account:orders_v3_#{address}`](/api-reference/channel-orders-v3) first** — before you seed and before you post anything. Subscribing first means no transition slips through the gap between your snapshot and your feed. 2. **Seed from [`GET /orders-v3`](/api-reference/get-orders-v3).** Load the currently-resting orders into a store keyed by `order.id` — an id you can compute locally before you submit. Optionally tag each entry with a `clientOrderId` when you post; the API echoes it back, so it is a cheap way to recognize your own order later. 3. **Key your store by `order.id`, and de-duplicate the channel by `messageId`.** Drop any publication whose `messageId` (in `ctx.tags`) you have already applied; otherwise write the row into your store under its `order.id`. ## Looking up one order [`GET /orders-v3/{orderId}`](/api-reference/get-order-v3) returns a single order you own in **any** status. If you tagged the order on submit, [`GET /orders-v3/client/{clientOrderId}`](/api-reference/get-order-v3-by-client-id) looks it up by that tag instead. ## Related Parameters, the fifteen-field row, and the available filters. One order, any status. One order, looked up by the tag you set. Where terminal state actually comes from. The three statuses, all nine terminal reasons, and the correct way to test for a fill. Bets, fills and positions — picking between them. # V3 Improvements Source: https://docs.sx.bet/developers/new-in-v3 An overview of the V3 exchange model and how it's structured. ## V3 Improvements V3 rebuilds the exchange around the proxy-wallet model below. What you get for it: * **Reliable settlement** — the fill failures of V2 are eliminated. * **Elimination of trade ordering issues** - race conditions between cancels and fills are eliminated. * **Faster orders API** — placing an order via [`POST /orders-v3`](/api-reference/post-orders-v3) is roughly 50% lower latency than V2. * **Faster confirmation** — the time from placing a bet to on-chain settlement drops 2–3×. * **Automatic order bashing** — an order whose price crosses the book fills immediately against resting liquidity instead of resting. * **Withdrawal controls** — you can add multiple signers to your wallet to approve withdrawals. See [Accounts](/developers/accounts). * **Privacy** — your orders, positions and trades are not attributable to your address. ## Capital lives in a proxy wallet Every bet is placed, collateralised and paid out through a proxy contract you own, not your EOA / user address. [Accounts](/developers/accounts) · [Funding](/developers/funding) · [Reading balances](/developers/balances-and-ledger) ## One signed order does everything Every order — a resting quote or an immediate bet — is a call submitted to [`POST /orders-v3`](/api-reference/post-orders-v3). `timeInForce` sets its behaviour: `GTC` rests on the book ("maker" orders), `IOC` and `FOK` execute against it immediately ("taker" or "market" orders). [Posting orders](/developers/posting-orders) · [Taking liquidity](/developers/taking-liquidity) · [Time in force](/developers/time-in-force) · [EIP-712 order signing](/api-reference/eip712-order-signing) · ## The order book is aggregated and anonymous The public book is price levels, not individual orders. Each snapshot carries a `version`, so you can reconcile a REST seed against the realtime stream and always keep the newer state. [Read the order book](/developers/order-book) · [Book versioning](/developers/book-versioning) · [GET /orderbook-v3/snapshot](/api-reference/get-orderbook-snapshot) ## Your activity is private The public book shows aggregated price levels, not individual orders, so your resting quotes are not attributable to you. ## Where everything lives | To… | Read | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Set up and fund an account | [Accounts](/developers/accounts), [Funding](/developers/funding), [Reading balances](/developers/balances-and-ledger) | | Understand the exchange | [Exchange model](/developers/exchange-model), [Time in force](/developers/time-in-force), [Unit conversions](/developers/unit-conversions) | | Read market data | [Markets overview](/developers/markets-overview), [Fetch markets](/developers/fetch-markets), [Read the order book](/developers/order-book), [Odds](/developers/odds), [Real-time](/developers/realtime-overview) | | Post and manage quotes | [Posting orders](/developers/posting-orders), [Cancelling orders](/developers/cancelling-orders), [Tracking your orders](/developers/my-orders), [Market making](/developers/market-making) | | Take liquidity | [Taking liquidity](/developers/taking-liquidity), [Betting delays](/developers/betting-delays) | | Build parlays | [Parlays](/developers/parlays), [Market-making parlays](/developers/market-making-parlays) | | Track your activity | [Which grain?](/developers/which-grain), [Tracking your orders](/developers/my-orders), [Reading your bets](/developers/which-grain#bets), [Reconciling fills](/developers/which-grain#fills), [Tracking positions](/developers/which-grain#positions) | | Handle lifecycles | [Order lifecycle](/developers/order-lifecycle), [Bet lifecycle](/developers/bet-lifecycle) | Deploy a proxy, read the book, and place a bet end to end. Port an existing client, subsystem by subsystem. # Fetching odds Source: https://docs.sx.bet/developers/odds Read the order book and best odds over REST, then stream both in realtime. | | [`GET /orderbook-v3/snapshot`](/api-reference/get-orderbook-snapshot) | [`GET /orderbook-v3/snapshot/event`](/api-reference/get-event-orderbook-snapshot) | [`GET /orders-v3/odds/best`](/api-reference/get-best-odds-v3) | | ---------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------- | | Use it to | get the book for one event | get the book for every market in an event | scan a watchlist or a board | | Markets per call | **1** | **all** on one event | **100** hashes, or **5** leagues | | Levels per side | **all** | **all** | **1** — top of book | | Realtime | [`orderbook_v3:{marketHash}`](/api-reference/channel-orderbook-v3) | [`orderbook_v3_event:{eventId}`](/api-reference/channel-orderbook-v3-event) | [`best_odds_v3:global`](/api-reference/channel-best-odds-v3) | ## Fetching the order book ```bash theme={null} curl "https://api.sx.bet/orderbook-v3/snapshot?marketHash=0x5be2a55e4b7dec1cec1b75af93b3fdef178ec1473866da95b3c68c1d4c99d4fd" ``` ```json theme={null} { "status": "success", "data": { "marketHash": "0x5be2a55e…d4fd", "outcomeOne": [{ "percentageOdds": "52000000000000000000", "size": "2000000" }], "outcomeTwo": [{ "percentageOdds": "46750000000000000000", "size": "1000000" }], "version": "00100000000000002210000" } } ``` `size` is the resting maker's stake, not what you can bet against it. To turn a level into taker capacity and price a bet across levels, see [The order book](/developers/order-book). ## Fetching best odds Top of book for select markets. A side is `null` when it has no liquidity. ```bash theme={null} curl "https://api.sx.bet/orders-v3/odds/best?marketHashes=0x5be2a55e…d4fd,0xbf06c637…a2eb" \ --header "x-sx-api-key: YOUR_API_KEY" ``` ```javascript seed_best_odds.mjs theme={null} const API = "https://api.sx.bet"; // Mainnet — use https://api.toronto.sx.bet for testnet const marketHashes = [ "0x5be2a55e4b7dec1cec1b75af93b3fdef178ec1473866da95b3c68c1d4c99d4fd", "0xbf06c6379c922d8118612d1d7493b20f6df6929437fbf6022904327f9516a2eb", ]; // One request per <=100 hashes. Comma-separated — never repeated params. const url = `${API}/orders-v3/odds/best?marketHashes=${marketHashes.join(",")}`; const { status, data } = await fetch(url, { headers: { "x-sx-api-key": process.env.SX_API_KEY }, }).then((r) => r.json()); if (status !== "success") throw new Error(`best-odds returned status=${status}`); for (const row of data.bestOdds) { // row is { marketHash, outcomeOne, outcomeTwo }; each side is a level or null. console.log(row.marketHash, row.outcomeOne, row.outcomeTwo); } ``` ## Seeding and then subscribing to realtime best odds Seed from `GET /orders-v3/odds/best` and treat publications as updates on top of that seed. Note that the channel is a global channel ```javascript theme={null} const sub = client.newSubscription("best_odds_v3:global"); sub.on("publication", (ctx) => { if (watchlist.has(ctx.data.marketHash)) applyBestOdds(ctx.data); }); sub.on("subscribed", async () => { // Unconditional — this channel has no recovery, so seed on every connect. const { status, data } = await fetch( `${API}/orders-v3/odds/best?marketHashes=${marketHashes.join(",")}`, { headers: { "x-sx-api-key": process.env.SX_API_KEY } } ).then((r) => r.json()); if (status !== "success") return; // keep prior state; retry next tick for (const row of data.bestOdds) applyBestOdds(row); }); sub.subscribe(); ``` A publication goes out only when a market's top of book **changes**. ## Seeding and subscribing realtime to the order book `orderbook_v3:{marketHash}` carries the same shape the snapshot serves, and each publication is a full replacement, not a delta. Subscribe first, then seed from REST, and run both the snapshot and every publication through the version [apply rule](/developers/book-versioning#the-apply-rule). Watching many markets on the same events? Prefer [`orderbook_v3_event:{eventId}`](/api-reference/channel-orderbook-v3-event) and seed from [`GET /orderbook-v3/snapshot/event`](/api-reference/get-event-orderbook-snapshot). ```javascript theme={null} const sub = client.newSubscription(`orderbook_v3:${marketHash}`); // Every publication runs through the version rule; stale ones are discarded. sub.on("publication", (ctx) => applyUpdate(ctx.data)); sub.on("subscribed", async (ctx) => { // recovered: true means the book was replayed — no REST seed needed. if (ctx.recovered) return; const { data } = await fetch( `${API}/orderbook-v3/snapshot?marketHash=${marketHash}` ).then((r) => r.json()); // The snapshot goes through the same version rule, so a newer live update // that arrived during the fetch is not clobbered. `data` is flat — the level // arrays sit directly beside `marketHash` and `version`. applySnapshot(marketHash, data.version, data); }); sub.subscribe(); ``` ## Related Depth, sizing, and pricing a bet across levels. Applying live book updates in the right order. Tokens, snapshot + subscribe, and reconnect handling. Parameters, caps, and validation rules. # Odds rounding Source: https://docs.sx.bet/developers/odds-rounding How to validate and round odds to the SX Bet odds ladder. `percentageOdds` must land exactly on a step of the **odds ladder**, or the order is rejected. The ladder exists to prevent diming — undercutting a resting quote by a meaninglessly small increment. Read the step size from `oddsLadderStepSize` on [`GET /metadata/obv3`](/api-reference/get-metadata-obv3). ## Units | | | | -------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | `percentageOdds` | An implied probability scaled by 1020, sent as a **string**. `100%` is `10^20`; `1%` is `10^18`; `0.001%` is `10^15`. | | `oddsLadderStepSize` | A **number** in units of 1015. `125` means the step is `125 × 10^15 = 1.25 × 10^17` = **0.125%**. | So with a step size of 125, the valid values are `0.000%, 0.125%, 0.250%, …`. `40.125%` is on the ladder, and `40.100%` is not. `oddsLadderStepSize` is returned as a number from 0 to 1000, which reads as a percentage like this: | Metadata value | Step size | | -------------- | --------- | | 10 | 0.010% | | 25 | 0.025% | | 125 | 0.125% | ## The rule An order's `percentageOdds` is accepted when all of the following hold: 1. `0 < percentageOdds < 10^20` — strict on both ends. 2. `percentageOdds % (oddsLadderStepSize × 10^15) == 0`, **or** the value is one of two hardcoded exceptions. 3. The two exceptions are exactly **0.1%** (`10^17`) and exactly **99.9%** (`999 × 10^17`). They are always legal regardless of step size, so the extreme ends of the book stay quotable. One `POST /orders-v3` per row: | `percentageOdds` | Implied | Result | | ----------------------- | -------- | ------------------------------ | | `40000000000000000000` | 40.000% | `200` accepted | | `40125000000000000000` | 40.125% | `200` accepted | | `40100000000000000000` | 40.100% | `400` `ODDS_NOT_ON_LADDER` | | `100000000000000000` | 0.100% | `200` accepted — exception | | `99900000000000000000` | 99.900% | `200` accepted — exception | | `0` | 0.000% | `400` — bounds, not the ladder | | `100000000000000000000` | 100.000% | `400` — bounds, not the ladder | ## Checking and rounding Read the step size once at startup, then keep the arithmetic in integers. ```javascript JavaScript theme={null} const ODDS_PRECISION = 10n ** 20n; const stepSize = 125n; // GET /metadata/obv3 → oddsLadderStepSize const STEP = stepSize * 10n ** 15n; // 1.25e17 = 0.125% const EXTRA = new Set([10n ** 17n, 999n * 10n ** 17n]); // 0.1% and 99.9% const onLadder = (odds) => { const o = BigInt(odds); if (o <= 0n || o >= ODDS_PRECISION) return false; if (EXTRA.has(o)) return true; return o % STEP === 0n; }; const roundDown = (odds) => (BigInt(odds) / STEP) * STEP; const roundUp = (odds) => { const o = BigInt(odds); return o % STEP === 0n ? o : (o / STEP + 1n) * STEP; }; ``` ```python Python theme={null} ODDS_PRECISION = 10 ** 20 step_size = 125 # GET /metadata/obv3 -> oddsLadderStepSize STEP = step_size * 10 ** 15 # 1.25e17 = 0.125% EXTRA = {10 ** 17, 999 * 10 ** 17} # 0.1% and 99.9% def on_ladder(odds: int) -> bool: if odds <= 0 or odds >= ODDS_PRECISION: return False if odds in EXTRA: return True return odds % STEP == 0 def round_down(odds: int) -> int: return (odds // STEP) * STEP def round_up(odds: int) -> int: return odds if odds % STEP == 0 else ((odds // STEP) + 1) * STEP ``` Real output from both, run against the values in the table above: ``` 40000000000000000000 40.000% on_ladder=True 40125000000000000000 40.125% on_ladder=True 40100000000000000000 40.100% on_ladder=False 100000000000000000 0.100% on_ladder=True 99900000000000000000 99.900% on_ladder=True 0 0.000% on_ladder=False 100000000000000000000 100.000% on_ladder=False rounding 40.100%: down -> 40000000000000000000 (40.000%) up -> 40125000000000000000 (40.125%) ``` ## Example of turning a probability into a valid price This is the full path from a model output to a submittable field. Multiply into integer space **first**, apply your margin in integer space, then snap. ```javascript JavaScript theme={null} // Your model says 40.3%, and you want to rest on it. const modelProbability = 0.403; // 1. Into 1e20 space, via an integer basis — never Number * 1e20 directly. const basisPoints = BigInt(Math.round(modelProbability * 10_000)); // 4030 const raw = (basisPoints * ODDS_PRECISION) / 10_000n; // 40300000000000000000n // 2. Snap. Resting order → round down. const percentageOdds = roundDown(raw); // 40250000000000000000n // 3. Submit as a string, and re-check before you do. if (!onLadder(percentageOdds)) throw new Error("off ladder"); const field = percentageOdds.toString(); // "40250000000000000000" ``` ```python Python theme={null} # Your model says 40.3%, and you want to rest on it. model_probability = 0.403 # 1. Into 1e20 space via an integer basis. basis_points = round(model_probability * 10_000) # 4030 raw = basis_points * ODDS_PRECISION // 10_000 # 40300000000000000000 # 2. Snap. Resting order -> round down. percentage_odds = round_down(raw) # 40250000000000000000 # 3. Submit as a string, and re-check before you do. assert on_ladder(percentage_odds) field = str(percentage_odds) # "40250000000000000000" ``` Both produce `40250000000000000000` — 40.250%, one step below the model's 40.300%. ## Do not hardcode the step size `oddsLadderStepSize` is **125** today and can change in the future. ```bash theme={null} curl -s https://api.sx.bet/metadata/obv3 | jq '.data.oddsLadderStepSize' # 125 ``` Read it at startup alongside the rest of the bootstrap document, and derive `STEP` from it. See [Exchange metadata](/api-reference/get-metadata-obv3) for the other values. ## Related Where `oddsLadderStepSize` comes from. Implied/decimal/American odds, the 1e20 scale, base units, and time formats in one place. # The order book Source: https://docs.sx.bet/developers/order-book How the order book is structured and working code to fetch it and update it. A market has one order book with two sides, each a list of **price levels** — an implied price and the total stake resting at it: ```json theme={null} { "outcomeOne": [ { "percentageOdds": "52000000000000000000", "size": "2000000" }, { "percentageOdds": "51500000000000000000", "size": "1000000" }, { "percentageOdds": "50000000000000000000", "size": "3000000" } ], "outcomeTwo": [ { "percentageOdds": "46750000000000000000", "size": "1000000" }, { "percentageOdds": "46000000000000000000", "size": "3000000" }, { "percentageOdds": "42000000000000000000", "size": "1000000" } ] } ``` * **`percentageOdds` is implied probability scaled by 1020** — `"52000000000000000000"` is 52.000%. * **`size` is stake in base units** — USDC has 6 decimals, so `"2000000"` is 2 USDC. It is **the resting party's** stake, not the amount you can bet against it. Note that the side here is the side the resting order is betting. So if you were to match, you would be betting the opposite. **To bet an outcome, you consume the orders resting on the *other* outcome.** Someone resting on outcome one wants outcome one. They are not offering it to you. They offer outcome two instead, funded by their stake. Orders at the same odds collapse into one level. Each level sums the unfilled part of every order at that price. Matching is **price-time priority**. A taker consumes the best opposing price first. When several orders sit at that price, the earliest resting order fills first. ## Odds must sit on the ladder `percentageOdds` on a new order must be on the ladder or the order will be rejected. [Odds rounding](/developers/odds-rounding) has the full rules ## Fetching the book | | [`GET /orderbook-v3/snapshot`](/api-reference/get-orderbook-snapshot) | [`GET /orders-v3/odds/best`](/api-reference/get-best-odds-v3) | | ----------------- | --------------------------------------------------------------------- | ------------------------------------------------------------- | | Markets per call | **1** | up to **100** hashes, or **5** leagues | | Levels per side | **all** | **1** (top of book) | | Carries a version | **yes** | no | Both read the same book. Use best odds to simply get the best odds available on each side and the snapshot to size and price a bet. ## Convert a level into what you can bet A level's `size` is the resting party's stake. The stake **you** can put up against it is: ``` takerCapacity = size × (10^20 − percentageOdds) / percentageOdds ``` and the price you pay is `10^20 − percentageOdds`. Do both calculations using BigInt. Examples: ```javascript read_book.mjs theme={null} const API = "https://api.sx.bet"; // Mainnet — use https://api.toronto.sx.bet for testnet const marketHash = "0x5be2a55e4b7dec1cec1b75af93b3fdef178ec1473866da95b3c68c1d4c99d4fd"; const ODDS = 10n ** 20n; // 1e20 == 100% implied // A level's `size` is the resting party's stake. This is what you can bet. const takerCapacity = ({ size, percentageOdds }) => (BigInt(size) * (ODDS - BigInt(percentageOdds))) / BigInt(percentageOdds); const pct = (odds) => (Number(BigInt(odds) / 10n ** 14n) / 1e4).toFixed(3); const usdc = (base) => (Number(base) / 1e6).toFixed(2); const res = await fetch( `${API}/orderbook-v3/snapshot?marketHash=${marketHash}` ); if (!res.ok) throw new Error(`HTTP ${res.status}`); const { data } = await res.json(); console.log(`version ${data.version}`); for (const side of ["outcomeOne", "outcomeTwo"]) { const levels = data[side]; console.log(`\n${side}: ${levels.length} level(s)`); levels.forEach((level, i) => { console.log( ` [${i}] maker ${pct(level.percentageOdds)}% ` + `size ${usdc(level.size)} USDC ` + `-> taker ${pct(ODDS - BigInt(level.percentageOdds))}% ` + `for up to ${usdc(takerCapacity(level))} USDC` ); }); } ``` ```python read_book.py theme={null} import requests API = "https://api.sx.bet" # Mainnet — use https://api.toronto.sx.bet for testnet MARKET_HASH = "0x5be2a55e4b7dec1cec1b75af93b3fdef178ec1473866da95b3c68c1d4c99d4fd" ODDS = 10 ** 20 # 1e20 == 100% implied def taker_capacity(level: dict) -> int: """A level's `size` is the resting party's stake. This is what you can bet.""" size = int(level["size"]) maker_odds = int(level["percentageOdds"]) return size * (ODDS - maker_odds) // maker_odds res = requests.get(f"{API}/orderbook-v3/snapshot", params={"marketHash": MARKET_HASH}) res.raise_for_status() data = res.json()["data"] print(f"version {data['version']}") for side in ("outcomeOne", "outcomeTwo"): levels = data[side] print(f"\n{side}: {len(levels)} level(s)") for i, level in enumerate(levels): maker_odds = int(level["percentageOdds"]) print( f" [{i}] maker {maker_odds / ODDS:.3%} " f"size {int(level['size']) / 1e6:.2f} USDC " f"-> taker {(ODDS - maker_odds) / ODDS:.3%} " f"for up to {taker_capacity(level) / 1e6:.2f} USDC" ) ``` Both produce: ``` version 00100000000000002210000 outcomeOne: 3 level(s) [0] maker 52.000% size 2.00 USDC -> taker 48.000% for up to 1.85 USDC [1] maker 51.500% size 1.00 USDC -> taker 48.500% for up to 0.94 USDC [2] maker 50.000% size 3.00 USDC -> taker 50.000% for up to 3.00 USDC outcomeTwo: 3 level(s) [0] maker 46.750% size 1.00 USDC -> taker 53.250% for up to 1.14 USDC [1] maker 46.000% size 3.00 USDC -> taker 54.000% for up to 3.52 USDC [2] maker 42.000% size 1.00 USDC -> taker 58.000% for up to 1.38 USDC ``` ## Aggregate depth across a side Levels are already aggregated by exact price, so a depth ladder is a running total. ```javascript theme={null} const ODDS = 10n ** 20n; /** Cumulative capacity available for betting `bettingOutcomeOne`. */ function depth(book, bettingOutcomeOne) { const levels = bettingOutcomeOne ? book.outcomeTwo : book.outcomeOne; let cumulative = 0n; return levels.map((level) => { const makerOdds = BigInt(level.percentageOdds); cumulative += (BigInt(level.size) * (ODDS - makerOdds)) / makerOdds; return { price: (ODDS - makerOdds).toString(), cumulative: cumulative.toString() }; }); } ``` Against the same book: ``` bet outcomeTwo: [{"price":"48000000000000000000","cumulative":"1846153"}, {"price":"48500000000000000000","cumulative":"2787900"}, {"price":"50000000000000000000","cumulative":"5787900"}] bet outcomeOne: [{"price":"53250000000000000000","cumulative":"1139037"}, {"price":"54000000000000000000","cumulative":"4660776"}, {"price":"58000000000000000000","cumulative":"6041728"}] ``` ## Staying up to date Polling the snapshot works, but the book changes on every match, cancel and expiry. The channel [`orderbook_v3:{marketHash}`](/api-reference/channel-orderbook-v3) sends immediate real-time updates. It carries the **same book and the same version** the snapshot route serves, and the exchange publishes one message per book change. ```json theme={null} { "marketHash": "0x5be2a55e4b7dec1cec1b75af93b3fdef178ec1473866da95b3c68c1d4c99d4fd", "version": "00100000000000002209000", "outcomeOne": [ { "percentageOdds": "52500000000000000000", "size": "1000000" }, { "percentageOdds": "52000000000000000000", "size": "2000000" }, { "percentageOdds": "51500000000000000000", "size": "1000000" }, { "percentageOdds": "50000000000000000000", "size": "3000000" } ], "outcomeTwo": [ { "percentageOdds": "46750000000000000000", "size": "1000000" }, { "percentageOdds": "46000000000000000000", "size": "3000000" }, { "percentageOdds": "42000000000000000000", "size": "1000000" } ] } ``` Each publication is a **complete replacement** for that market's book, not a delta. To apply one, overwrite both arrays. The channel is always from the **maker's perspective** The full seed-and-subscribe order is on [Real-time](/developers/realtime-overview). The version comparison is on [Book versioning](/developers/book-versioning#the-apply-rule). ## Related Sign and submit an order onto the book. Scanning many markets, and getting a live client its first snapshot. Applying live updates in the right order. GTC, IOC, FOK — and what happens when there is not enough depth. # Order lifecycle Source: https://docs.sx.bet/developers/order-lifecycle How order statuses changes during an order's lifetime. An order has three statuses. ```mermaid theme={null} flowchart LR P["PENDING"] A["ACTIVE"] I["INACTIVE"] P --> A --> I P -->|may skip ACTIVE entirely| I ``` | Status | Means | | ---------- | ------------------------------------------------------------------------------- | | `PENDING` | Accepted and published to the matching engine. **Not live, not yet matchable.** | | `ACTIVE` | Resting on the book, in whole or in part. | | `INACTIVE` | No longer resting. `inactiveReason` says why. | **`FILLED` is an `inactiveReason`, not a status.** A fully filled order is `status: "INACTIVE"` with `inactiveReason: "FILLED"`. A partial fill whose remainder falls below the minimum resting size (`limits.minRestingOrderSizeBaseUnits`) is also reported as `FILLED` — the engine discards the dust remainder rather than resting it. IOC or FOK orders ("market orders") **skip `ACTIVE` entirely**. Both go from `PENDING` straight to `INACTIVE`. The order never rests, so it is never `ACTIVE`. ## The `inactiveReason` values An order becomes `INACTIVE` if it is filled or cancelled. `inactiveReason` tells you which condition applied. | `inactiveReason` | Fires when | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `FILLED` | Fully matched, or a partial fill left a remainder below the minimum resting size (`limits.minRestingOrderSizeBaseUnits`), which is discarded. | | `USER_REQUESTED` | You cancelled it. | | `EXPIRED` | The order's `expiry` passed. | | `NO_LIQUIDITY` | An IOC found no compatible liquidity, or an FOK could not fill completely. | | `INSUFFICIENT_BALANCE` | A balance check failed at match time — on **either** the maker or the taker side. | | `EVENT_LIFECYCLE` | The fixture moved into a [state that sweeps resting orders](/api-reference/fixture-statuses#which-statuses-sweep-your-resting-orders) — in progress, finished, or cancelled. **Includes kickoff.** | | `MARKET_HALTED` | An operator explicitly halted the market. | | `HEARTBEAT_TIMEOUT` | Your [heartbeat](/developers/heartbeat) lapsed. Sweeps **every** open order on your account. | | `SYSTEM` | The exchange cancelled the order. Covers several distinct internal causes. | ## Related What happens after a fill. Order types # Parlays Source: https://docs.sx.bet/developers/parlays How SX Bet's request-for-quote parlay system works. A parlay is a multi-leg bet. There is no standing parlay orderbook. A bettor picks their legs, and the request is broadcast. Market makers price it inside a **3-second window**. The bettor then has a minute to take one of the resulting quotes. ## Parlay markets are real markets Once it exists, a parlay market is an ordinary market. It has its own `marketHash` and behaves like every other market on the exchange. You post an ordinary signed order to it, you choose outcome one or outcome two, and you cancel it the same way. The difference is that a parlay market has **legs** — an array of underlying single markets and the outcomes the bettor selected for each. ## The flow ``` Bettor selects multiple legs ↓ Parlay request broadcast on `parlay_markets:global` ↓ Market makers have 3 seconds to post orders ↓ All submitted orders shown to the bettor at once ↓ Bettor has 1 minute to take one before the book closes ↓ The fill executes like any other bet ``` ## The request Makers receive this on the [`parlay_markets:global`](/api-reference/channel-parlay-requests) channel: ```json theme={null} { "marketHash": "0x38cceead7bda65c18574a34994ebd8af154725d08aa735dcbf26247a7dcc67bd", "requestSize": "100000000", "legs": [ { "marketHash": "0x0d64c52e8781acdada86920a2d1e5acd6f29dcfe285cf9cae367b671dff05f7d", "bettingOutcomeOne": true }, { "marketHash": "0xe609a49d083cd41214a0db276c1ba323c4a947eefd2e4260386fec7b5d258188", "bettingOutcomeOne": false } ] } ``` The top-level `marketHash` is the **parlay parent** — the market you post orders against. Each entry in `legs` names an underlying market and which side of it the parlay needs. The full field list (including the `requestorId` pseudonym) and looking up each leg via `GET /markets/find` are on [Market making parlays](/developers/market-making-parlays). ## Betting a parlay You post an ordinary order on the parlay parent hash — nothing about signing is parlay-specific. See [Market making parlays](/developers/market-making-parlays) for the field-by-field walkthrough and working code. **A parlay taker uses `FOK` orders only** ## How a parlay settles early **One VOID leg voids the whole parlay.** If any leg is reported void, the parlay is void and everyone gets their stake back. **A non-VOID leg going against the parlay decides it early.** When one leg is reported as a loss, the parlay cannot win. ## Parlay early refunds Parlay bets are eligible for early [capital-efficiency](/developers/capital-efficiency) refunds — you can get the stake on a decided position back before the whole parlay has finished settling. A parlay can end one of a few ways: it wins, it loses, or it is voided if any leg comes back void. The early refund kicks in the moment a reported leg takes loss off the table for one side. Once that side can only end up winning or getting voided, it can no longer lose its stake, so the stake is returned right away without waiting for the remaining legs to report. Which side that is depends on what you backed. If you bet against the parlay and one leg goes against it, the parlay can no longer win, so your stake comes back early — and the same in reverse, where whoever is now guaranteed not to lose is refunded as soon as the outcome is locked in. This only returns the stake on the position that's already decided. Any remaining legs keep reporting, and the parlay finishes settling in the usual way. ## Parlay vs single bets | | Single Bet | Parlay | | ------------------- | ---------------------------------- | ----------------------------------- | | **Orderbook** | Persistent, always available | Created on-demand per request | | **Discovery** | Browse existing orders | Receive RFQ via WebSocket | | **Timing** | Post anytime | 3-second maker window | | **Market creation** | Markets exist for scheduled events | Market created when bettor requests | | **Legs** | Single market | Multiple underlying markets | | **Fees** | Single-bet rates | Separate parlay rates | ## Fees Parlays use separate rates — `makerParlayPayoutFee` and `takerParlayPayoutFee` — at [`GET /user/fees-v3`](/api-reference/get-user-fees-v3). ## Related Listening, pricing and quoting inside the 3-second window. The request payload, and the window it opens. The fields and behaviour of parlay markets. `GTC`, `IOC` and `FOK`, and which of them a parlay accepts. # Posting orders Source: https://docs.sx.bet/developers/posting-orders How to construct, sign, and submit orders to the SX Bet orderbook. Every order — resting quote or immediate bet — goes through [`POST /orders-v3`](/api-reference/post-orders-v3). The field-by-field reference lives on the endpoint page. ## Prerequisites **A deployed, funded proxy is required before any OBv3 order** — `POST /orders-v3` rejects with `400 PROXY_NOT_DEPLOYED` until then. See [Deploy a proxy wallet](/api-reference/post-user-deploy-proxy). You also need your API key, sent as `x-sx-api-key`. See [Authentication](/developers/authentication) and [API key](/api-reference/api-key). ## Steps 1. [**`GET /metadata/obv3`**](/api-reference/get-metadata-obv3) — chain id, Escrow address, active token, ladder step, size limits. 2. **Round your odds down onto the ladder** — a multiple of `oddsLadderStepSize × 10^15`. See [Odds rounding](/developers/odds-rounding). 3. **Choose a `timeInForce`** — `GTC` to rest, `IOC`/`FOK` to execute now. See [Time in force](/developers/time-in-force). 4. Sign the eight-field [EIP-712 `Order` struct](/api-reference/eip712-order-signing). 5. [**`POST /orders-v3`**](/api-reference/post-orders-v3) — expect **`200`** and a per-order result carrying `SUBMITTED`, the server-assigned `commandId`, and your `clientOrderId` / `externalUserId` if you set them. 6. Watch your [`account:orders_v3`](/api-reference/channel-orders-v3) stream for the transition instead of polling. Or set [`waitForOutcome: true`](/api-reference/post-orders-v3) to have the call wait and return each order's terminal `outcome` inline. **Posting an order does not lock capital**, regardless of `timeInForce`. Funds are only escrowed when the order **matches** — see [Risk limits](/developers/risk-limits). ## Posting a GTC order end to end ```js post_gtc_order.mjs theme={null} import { Wallet, TypedDataEncoder, getAddress, zeroPadValue, randomBytes, hexlify } from "ethers"; const API = "https://api.sx.bet"; // Mainnet — use https://api.toronto.sx.bet for testnet const wallet = new Wallet(process.env.SX_PRIVATE_KEY); const authed = (init = {}) => ({ ...init, headers: { "Content-Type": "application/json", "x-sx-api-key": process.env.SX_API_KEY, ...init.headers, }, }); // 1. Fetch metadata and build the signing domain. const { data: meta } = await fetch(`${API}/metadata/obv3`).then((r) => r.json()); const domain = meta.domain; // complete EIP-712 domain // 2. Round the odds DOWN onto the ladder, before signing. const step = BigInt(meta.oddsLadderStepSize) * 10n ** 15n; const wanted = 40000000000000000000n; // 40.000% implied const percentageOdds = ((wanted / step) * step).toString(); // 3. Build the order. `expiry` is a NUMBER; the three amounts are STRINGS. const order = { marketHash: process.env.SX_MARKET_HASH, maker: wallet.address, totalBetSize: "1000000", // 1 USDC, 6 dp percentageOdds, salt: hexlify(randomBytes(32)), expiry: Math.floor(Date.now() / 1000) + 3600, // unix seconds; here, 1 hour out baseToken: meta.activeAsset.baseToken, isMakerBettingOutcomeOne: true, // the outcome YOU want to bet timeInForce: "GTC", }; // 4. Sign the eight-field struct. Order matters: salt precedes expiry. const TYPES = { Order: [ { name: "marketHash", type: "bytes32" }, { name: "baseToken", type: "address" }, { name: "totalBetSize", type: "uint256" }, { name: "percentageOdds", type: "uint256" }, { name: "salt", type: "uint256" }, { name: "expiry", type: "uint256" }, { name: "maker", type: "address" }, { name: "isMakerBettingOutcomeOne", type: "bool" }, ], }; const message = { marketHash: zeroPadValue(order.marketHash, 32), baseToken: getAddress(order.baseToken), totalBetSize: BigInt(order.totalBetSize), percentageOdds: BigInt(order.percentageOdds), salt: BigInt(order.salt), expiry: BigInt(order.expiry), maker: getAddress(order.maker), isMakerBettingOutcomeOne: order.isMakerBettingOutcomeOne, }; const orderSignature = await wallet.signTypedData(domain, TYPES, message); // The order id IS the EIP-712 digest, lowercased. Compute it before submitting. const orderId = TypedDataEncoder.hash(domain, TYPES, message).toLowerCase(); // 5. Submit. const res = await fetch(`${API}/orders-v3`, authed({ method: "POST", body: JSON.stringify({ orders: [{ ...order, orderSignature }] }), })); const body = await res.json(); console.log(`HTTP ${res.status}`); console.log(JSON.stringify(body, null, 2)); // 6. PENDING is not live. Poll until it appears, or watch the account channel. for (let i = 0; i < 10; i++) { await new Promise((r) => setTimeout(r, 1000)); const { data } = await fetch(`${API}/orders-v3?marketHash=${order.marketHash}`, authed()) .then((r) => r.json()); const row = data.orders.find((o) => o.id === orderId); if (row) { console.log(`ACTIVE after ${i + 1}s:`); console.log(JSON.stringify(row, null, 2)); break; } } ``` ```python post_gtc_order.py theme={null} import json, os, secrets, time, requests from eth_account import Account from eth_account.messages import encode_typed_data from eth_utils import keccak, to_checksum_address API = "https://api.sx.bet" # Mainnet — use https://api.toronto.sx.bet for testnet account = Account.from_key(os.environ["SX_PRIVATE_KEY"]) headers = {"Content-Type": "application/json", "x-sx-api-key": os.environ["SX_API_KEY"]} # 1. Fetch metadata and build the signing domain. meta = requests.get(f"{API}/metadata/obv3").json()["data"] domain = { "name": meta["domain"]["name"], # "OBv3 Escrow" "version": meta["domain"]["version"], # "1" "chainId": meta["chainId"], # from metadata, never a constant "verifyingContract": to_checksum_address(meta["activeAsset"]["escrowAddress"]), } # 2. Round the odds DOWN onto the ladder, before signing. step = int(meta["oddsLadderStepSize"]) * 10**15 wanted = 40_000_000_000_000_000_000 # 40.000% implied percentage_odds = (wanted // step) * step # 3. Build the order. `expiry` is a NUMBER; the three amounts are STRINGS. order = { "marketHash": os.environ["SX_MARKET_HASH"], "maker": account.address, "totalBetSize": "1000000", # 1 USDC, 6 dp "percentageOdds": str(percentage_odds), "salt": str(int.from_bytes(secrets.token_bytes(32), "big")), "expiry": int(time.time()) + 3600, # unix seconds; here, 1 hour out "baseToken": meta["activeAsset"]["baseToken"], "isMakerBettingOutcomeOne": True, # the outcome YOU want to bet "timeInForce": "GTC", } # 4. Sign the eight-field struct. Order matters: salt precedes expiry. TYPES = {"Order": [ {"name": "marketHash", "type": "bytes32"}, {"name": "baseToken", "type": "address"}, {"name": "totalBetSize", "type": "uint256"}, {"name": "percentageOdds", "type": "uint256"}, {"name": "salt", "type": "uint256"}, {"name": "expiry", "type": "uint256"}, {"name": "maker", "type": "address"}, {"name": "isMakerBettingOutcomeOne", "type": "bool"}, ]} message = { **order, "baseToken": to_checksum_address(order["baseToken"]), "totalBetSize": int(order["totalBetSize"]), "percentageOdds": int(order["percentageOdds"]), "salt": int(order["salt"]), "maker": to_checksum_address(order["maker"]), } del message["timeInForce"] # not part of the signed struct signable = encode_typed_data(domain_data=domain, message_types=TYPES, message_data=message) order_signature = account.sign_message(signable).signature.to_0x_hex() # The order id IS the EIP-712 digest, lowercased. Compute it before submitting. order_id = "0x" + keccak(b"\x19" + signable.version + signable.header + signable.body).hex() # 5. Submit. res = requests.post(f"{API}/orders-v3", headers=headers, json={"orders": [{**order, "orderSignature": order_signature}]}) body = res.json() print(f"HTTP {res.status_code}") print(json.dumps(body, indent=2)) returned = body["data"]["orders"][0]["orderId"] print(f"local digest : {order_id}") print(f"returned id : {returned}") print(f"match : {returned.lower() == order_id}") # 6. PENDING is not live. Poll until it appears, or watch the account channel. for i in range(10): time.sleep(1) data = requests.get(f"{API}/orders-v3", headers=headers, params={"marketHash": order["marketHash"]}).json()["data"] row = next((o for o in data["orders"] if o["id"].lower() == order_id), None) if row: print(f"ACTIVE after {i + 1}s:") print(json.dumps(row, indent=2)) break ``` Output: ``` HTTP 200 { "status": "success", "data": { "orders": [ { "orderId": "0xb4ade904cd8fcdd2c68cf11dbf90a63a9c5d5c762c5a619d74b21b1a62899065", "status": "SUBMITTED", "commandId": "550e8400-e29b-41d4-a716-446655440000" } ] } } ACTIVE after 1s: { "id": "0xb4ade904cd8fcdd2c68cf11dbf90a63a9c5d5c762c5a619d74b21b1a62899065", "marketHash": "0x81cfc23d0a02403f32d29b5a7c5686acd5eaedcdaa461cf253a1488e4cac0fcf", "userAddress": "0xbcc6D643e4159A75ED1dB4e13330230B82F2AEe5", "wallet": "0x4361123dbdc1D812fdf7D27045aF358C9C8AA70A", "isBettingOutcomeOne": true, "percentageOdds": "40000000000000000000", "totalBetSize": "1000000", "remainingSize": "1000000", "expiry": "2026-07-31T19:52:31.000Z", "status": "ACTIVE", "inactiveReason": null, "eventId": "L12952568", "createdAt": "2026-07-31T18:52:31.482Z", "updatedAt": "2026-07-31T18:52:31.497Z" } ``` `expiry` is required and must be a unix epoch timestamp in **seconds** (not milliseconds) that is in the future. An `expiry` that falls inside the market's betting delay plus two seconds is rejected. ## Batching `POST /orders-v3` takes an array of up to `limits.maxCreateOrders` orders (currently **10**), one shared `maker` per batch — the full field rules are on [Create orders](/api-reference/post-orders-v3). * **One maker per request.** Every order in the batch must carry the same `maker`. Mixed makers are a `400`. * **Batches are not atomic.** A partial outcome is normal. Always read the status of every entry in the array. ## Dust remainders A partial fill that would leave a remainder below `limits.minRestingOrderSizeBaseUnits` does not rest that remainder. The order then goes terminal: `status: "INACTIVE"` with `inactiveReason: "FILLED"`. ## Re-quoting There is no amend. To move a price: cancel, then post a new order. ## Related Every field, every validation rule. The same signing and submission path, from the taker's side. Choosing between GTC, IOC and FOK. Reading your own orders back, and what the API will not tell you. Quoting, exposure and re-quote loops. Tag orders when one SX account places bets for many people. # Public trades Source: https://docs.sx.bet/developers/public-trades Query the public trade feed from the SX Bet API. The public tape is bets other people placed, anonymized. Two ways to fetch: | | | | -------------------------------------------------------------------- | ------------------------------------- | | [`GET /trades-v3/public`](/api-reference/get-trades-v3-public) | Paginated history, filtered by market | | [`recent_trades_v3:global`](/api-reference/channel-recent-trades-v3) | Realtime | ## Read the tape for a market ```js theme={null} const qs = new URLSearchParams({ marketHash, perPage: "50" }); const { data } = await (await fetch( `https://api.sx.bet/trades-v3/public?${qs}` )).json(); for (const t of data.trades) { const stake = Number(t.totalStake) / 1e6; const odds = Number(t.weightedAverageOdds) / 1e20; const side = t.outcomeLabel ?? (t.isBettingOutcomeOne ? "one" : "two"); console.log(`${t.betTime} ${stake.toFixed(2)} USDC on ${side} @ ${odds.toFixed(3)}`); } ``` ## Prefer the channel for anything live ```js theme={null} centrifuge.newSubscription("recent_trades_v3:global") .on("publication", ({ data }) => onTrade(data.trade)) .subscribe(); ``` Seed from REST on connect, then apply channel messages. ## Related Core trade fields plus optional market metadata, and what the API holds back. The same data, in realtime. What people will trade at, versus what they already traded at. The authenticated view — full bet grain, including identity and settlement. # Quarter-line markets Source: https://docs.sx.bet/developers/quarter-line-markets How quarter-line markets work on SX Bet. Soccer markets on SX Bet support quarter lines — Asian handicap and Asian totals in `.25` increments (for example `-1.25`, `+0.75`, `Over 2.75`). ```json theme={null} { "status": "success", "data": [ { "status": "ACTIVE", "marketHash": "0x59b0e45f79d4b6eedaed199011b8bf72a8b4342e3b410457cc42abcf05d3592d", "outcomeOneName": "Brighton and Hove Albion -1.25", "outcomeTwoName": "Wolverhampton Wanderers +1.25", "outcomeVoidName": "NO_GAME_OR_EVEN", "teamOneName": "Brighton and Hove Albion", "teamTwoName": "Wolverhampton Wanderers", "type": 3, "gameTime": 1778335200, "line": -1.25, "sportXeventId": "L18724593", "liveEnabled": false, "sportLabel": "Soccer", "sportId": 5, "leagueId": 29, "leagueLabel": "English Premier League", "group1": "English Premier League", "chainVersion": "SXR", "participantOneId": 1002, "participantTwoId": 998, "mainLine": false, "sxTeamOneId": 100, "sxTeamTwoId": 97, "__type": "Market" } ] } ``` ## How quarter lines work The exchange splits a bet on a quarter-line market into two trades, on the surrounding whole and half lines. Each trade takes half the stake, at the same odds. A 50 USDC bet on `Brighton -1.25 @ 1.9` is recorded as: * 25 USDC on `Brighton -1.0 @ 1.9` * 25 USDC on `Brighton -1.5 @ 1.9` The two child trades settle independently against the same final score. That reproduces the quarter-line outcomes a single binary market cannot express: | Result | `Brighton -1.0` | `Brighton -1.5` | Net payout on 50 USDC | | ----------------------- | --------------- | --------------- | --------------------- | | Brighton wins by 2+ | Win | Win | 47.50 USDC (full win) | | Brighton wins by 1 | Push | Loss | 25 USDC (half loss) | | Brighton draws or loses | Loss | Loss | 0 USDC (full loss) | Orderbooks, posting orders, and taking liquidity on whole and half lines work as usual. The split happens only when you place a bet against a quarter-line `marketHash`. ## How quarter-line bets appear in the API One bet on a quarter-line market produces **one bet row** and **two fill rows** (one per leg). The hashes point at different markets: | Grain | `marketHash` | Extra field | | ------------------------------------- | ------------------------------------------------------------- | ------------------------------------ | | [Bet](/developers/which-grain#bets) | The **parent** — the `.25` / `.75` market you ordered against | none | | [Fill](/developers/which-grain#fills) | A **leg** — one of the surrounding whole/half lines | `quarterlineMarketHash` = the parent | ### Settlement Each leg settles independently against the same final score, and the parent's `outcome` is derived from the pair: | Leg one | Leg two | Parent `outcome` | | ------- | ----------------------- | ---------------- | | Won | Won | `1` | | Lost | Lost | `2` | | Void | Void | `0` | | Won | Void *(or void + won)* | **`1`** | | Lost | Void *(or void + lost)* | **`2`** | # Quickstart Source: https://docs.sx.bet/developers/quickstart Go from zero to your first bet on SX Bet. This quickstart walks you through the essentials of programmatic betting on SX Bet. By the end, you'll have deployed a wallet, read a live orderbook, and placed a real bet. ## What you'll need * An account on SX Bet (for signing orders) * An API key (for authenticated requests) * USDC (to place a bet) Market data and orderbook reads are public — no authentication needed. You only need an account, a key and funds to place orders. ## Base URL All API requests go to: ``` https://api.sx.bet ``` Use `https://api.toronto.sx.bet` for testnet. See [Testnet & Mainnet](/developers/testnet-and-mainnet). ## Install dependencies ```bash Python theme={null} pip install requests eth-account python-dotenv centrifuge-python aiohttp ``` ```bash JavaScript theme={null} npm install ethers dotenv centrifuge ``` Sign up at [sx.bet](https://sx.bet) with email or Google. Complete registration by choosing a username. Fetch your private key from the [assets page](https://sx.bet/wallet/assets) — you'll use it to sign orders. Then generate an API key from the **API section of your account page**; it is shown once. Store both in a `.env` file: ```bash theme={null} SX_PRIVATE_KEY="0xyour_private_key_here" SX_API_KEY="your_api_key_here" ``` Bets are placed from a **proxy wallet** your account owns. You can either deploy this proxy wallet programmatically, or simply login to [sx.bet](https://sx.bet) and one will be deployed for you automatically. ```bash cURL theme={null} # Deploy curl -X POST "https://api.sx.bet/user/deploy-proxy" \ -H "x-sx-api-key: $SX_API_KEY" # Poll until deployed curl "https://api.sx.bet/user/proxy" -H "x-sx-api-key: $SX_API_KEY" ``` Once the proxy is deployed, fund it with USDC. Easiest path: deposit in the [sx.bet](https://sx.bet) UI. Programmatically, transfer from your EOA (`FROM_EOA`) with an EIP-2612 permit via [`POST /user/transfer-to-proxy`](/api-reference/post-user-transfer-to-proxy) — full flow in [Funding](/developers/funding). Confirm a spendable balance: ```bash cURL theme={null} curl "https://api.sx.bet/user/balance-v3" -H "x-sx-api-key: $SX_API_KEY" ``` Fetch active markets with [`GET /markets/active`](/api-reference/get-markets-active) to find one you want to bet on. Copy the `marketHash` — you'll need it in the next steps. ```bash cURL theme={null} curl "https://api.sx.bet/markets/active?onlyMainLine=true" ``` ```python Python theme={null} import requests BASE_URL = "https://api.sx.bet" # Mainnet — use https://api.toronto.sx.bet for testnet markets = requests.get( f"{BASE_URL}/markets/active", params={"onlyMainLine": True} ).json()["data"]["markets"] first = markets[0] print("Market hash:", first["marketHash"]) print("Event:", first["teamOneName"], "vs", first["teamTwoName"]) print(f"Outcomes: [1 = {first['outcomeOneName']}, 2 = {first['outcomeTwoName']}]") print("Start time:", first["gameTime"]) ``` ```javascript JavaScript theme={null} const BASE_URL = "https://api.sx.bet"; // Mainnet — use https://api.toronto.sx.bet for testnet const data = await fetch( `${BASE_URL}/markets/active?onlyMainLine=true` ).then((r) => r.json()); const markets = data.data.markets; const first = markets[0]; console.log("Market hash:", first.marketHash); console.log("Event:", first.teamOneName, "vs", first.teamTwoName); console.log(`Outcomes: [1 = ${first.outcomeOneName}, 2 = ${first.outcomeTwoName}]`); console.log("Start time:", first.gameTime); ``` Read the orderbook with `showTakerPerspective=true`, which quotes each side as the price *you* would pay to bet that outcome. Index `0` is the best available. Copy the `percentageOdds` for the outcome you want — you submit it as-is in the next step. ```bash cURL theme={null} curl "https://api.sx.bet/orderbook-v3/snapshot?marketHash=YOUR_MARKET_HASH&showTakerPerspective=true" ``` ```python Python theme={null} market_hash = "YOUR_MARKET_HASH" # from step 3 book = requests.get( f"{BASE_URL}/orderbook-v3/snapshot", params={"marketHash": market_hash, "showTakerPerspective": True} ).json()["data"] # Best price to bet each outcome. The list is empty if nobody is quoting that side. for label, levels in (("outcome 1", book["outcomeOne"]), ("outcome 2", book["outcomeTwo"])): if not levels: print(f"Best price on {label}: no liquidity") continue best = levels[0] print(f"Best price on {label}: {int(best['percentageOdds']) / 10**20:.2%}" f" ({best['percentageOdds']}), {int(best['size']) / 1e6:.2f} USDC resting") ``` ```javascript JavaScript theme={null} const marketHash = "YOUR_MARKET_HASH"; // from step 3 const { data } = await fetch( `${BASE_URL}/orderbook-v3/snapshot?${new URLSearchParams({ marketHash, showTakerPerspective: true, })}` ).then((r) => r.json()); const book = data; // Best price to bet each outcome. The list is empty if nobody is quoting that side. for (const [label, levels] of [["outcome 1", book.outcomeOne], ["outcome 2", book.outcomeTwo]]) { const best = levels[0]; if (!best) { console.log(`Best price on ${label}: no liquidity`); continue; } console.log( `Best price on ${label}: ${(Number(best.percentageOdds) / 1e18).toFixed(2)}%` + ` (${best.percentageOdds}), ${(Number(best.size) / 1e6).toFixed(2)} USDC resting` ); } ``` Sign an order for the market from step 3 at the price from step 4, and submit it with [`timeInForce: "FOK"`](/developers/time-in-force) — fill completely and immediately, or nothing happens. Contract addresses and the chain id come from `GET /metadata/obv3`. The order id *is* the EIP-712 digest of what you signed, see [EIP-712 order signing](/api-reference/eip712-order-signing). ```python Python theme={null} import os import secrets import time import requests from eth_account import Account from eth_account.messages import encode_typed_data from eth_utils import keccak BASE_URL = "https://api.sx.bet" # Mainnet — use https://api.toronto.sx.bet for testnet account = Account.from_key(os.environ["SX_PRIVATE_KEY"]) market_hash = "YOUR_MARKET_HASH" # from step 3 percentage_odds = "YOUR_BEST_ODDS" # from step 4 is_betting_outcome_one = True # True = bet outcome 1, False = bet outcome 2 stake = "1000000" # 1 USDC (6 decimals) meta = requests.get(f"{BASE_URL}/metadata/obv3").json()["data"] order = { "marketHash": market_hash, "baseToken": meta["activeAsset"]["baseToken"], "totalBetSize": stake, "percentageOdds": percentage_odds, "salt": "0x" + secrets.token_hex(32), "expiry": int(time.time()) + 3600, # unix seconds; here, 1 hour out "maker": account.address, "isMakerBettingOutcomeOne": is_betting_outcome_one, } # --- Sign the order --- DOMAIN = meta["domain"] # complete EIP-712 domain ORDER_TYPES = { "Order": [ {"name": "marketHash", "type": "bytes32"}, {"name": "baseToken", "type": "address"}, {"name": "totalBetSize", "type": "uint256"}, {"name": "percentageOdds", "type": "uint256"}, {"name": "salt", "type": "uint256"}, {"name": "expiry", "type": "uint256"}, {"name": "maker", "type": "address"}, {"name": "isMakerBettingOutcomeOne", "type": "bool"}, ], } # The typed data wants integers where the request body wants strings. message = { **order, "totalBetSize": int(order["totalBetSize"]), "percentageOdds": int(order["percentageOdds"]), "salt": int(order["salt"], 16), } signable = encode_typed_data(DOMAIN, ORDER_TYPES, message) signed = account.sign_message(signable) # The order id IS the EIP-712 digest, lowercased — compute it and check the server agrees. order_id = "0x" + keccak(b"\x19" + signable.version + signable.header + signable.body).hex() # --- Submit --- response = requests.post( f"{BASE_URL}/orders-v3", headers={"x-sx-api-key": os.environ["SX_API_KEY"]}, json={"orders": [{ **order, "timeInForce": "FOK", "orderSignature": signed.signature.to_0x_hex(), }]}, ) result = response.json() print("Order result:", result) print("digest matches:", result["data"]["orders"][0]["orderId"].lower() == order_id) ``` ```javascript JavaScript theme={null} import "dotenv/config"; import { Wallet, TypedDataEncoder, randomBytes, hexlify } from "ethers"; const BASE_URL = "https://api.sx.bet"; // Mainnet — use https://api.toronto.sx.bet for testnet const wallet = new Wallet(process.env.SX_PRIVATE_KEY); const marketHash = "YOUR_MARKET_HASH"; // from step 3 const percentageOdds = "YOUR_BEST_ODDS"; // from step 4 const isBettingOutcomeOne = true; // true = bet outcome 1, false = bet outcome 2 const stake = "1000000"; // 1 USDC (6 decimals) const { data: meta } = await fetch(`${BASE_URL}/metadata/obv3`).then((r) => r.json()); const order = { marketHash, baseToken: meta.activeAsset.baseToken, totalBetSize: stake, percentageOdds, salt: hexlify(randomBytes(32)), expiry: Math.floor(Date.now() / 1000) + 3600, // unix seconds; here, 1 hour out maker: wallet.address, isMakerBettingOutcomeOne: isBettingOutcomeOne, }; // --- Sign the order --- const domain = meta.domain; // complete EIP-712 domain const types = { Order: [ { name: "marketHash", type: "bytes32" }, { name: "baseToken", type: "address" }, { name: "totalBetSize", type: "uint256" }, { name: "percentageOdds", type: "uint256" }, { name: "salt", type: "uint256" }, { name: "expiry", type: "uint256" }, { name: "maker", type: "address" }, { name: "isMakerBettingOutcomeOne", type: "bool" }, ], }; const orderSignature = await wallet.signTypedData(domain, types, order); // The order id IS the EIP-712 digest, lowercased — compute it and check the server agrees. const orderId = TypedDataEncoder.hash(domain, types, order).toLowerCase(); // --- Submit --- const response = await fetch(`${BASE_URL}/orders-v3`, { method: "POST", body: JSON.stringify({ orders: [{ ...order, timeInForce: "FOK", orderSignature }], }), headers: { "Content-Type": "application/json", "x-sx-api-key": process.env.SX_API_KEY, }, }); const result = await response.json(); console.log("Order result:", result); console.log("digest matches:", result.data.orders[0].orderId === orderId); ``` Watch your order and the market in real time over a single WebSocket connection. Subscribe to two channels: `orderbook_v3:{marketHash}` is public and streams the complete resting book on every change, while `account:orders_v3_#{address}` is authenticated and pushes each of your own orders as it rests, fills, or goes inactive. Fetch a realtime JWT from [`GET /user/realtime-token-v3/api-key`](/developers/realtime-initialization#connect). The account channel suffix must equal your token's `sub`, which is your **user address** — the account that signs orders (the `maker` from step 5), **not** the proxy wallet that holds the funds. Using the proxy address here silently returns nothing. See [Real-time data](/developers/realtime-overview), [Order Book Updates](/api-reference/channel-orderbook-v3), and [Active Order Updates](/api-reference/channel-orders-v3). ```python Python theme={null} import asyncio import os import aiohttp from centrifuge import Client, PublicationContext, SubscriptionEventHandler BASE_URL = "https://api.sx.bet" # Mainnet — use https://api.toronto.sx.bet for testnet WS_URL = "wss://realtime.sx.bet/connection/websocket" # Testnet: wss://realtime.toronto.sx.bet/connection/websocket market_hash = "YOUR_MARKET_HASH" # from step 3 address = "YOUR_CHECKSUMMED_ADDRESS" # your USER address (the maker from step 5), checksummed — NOT the proxy wallet # Async so the client can refresh the token before its 24h expiry. Passing a # static string instead would kill the connection once the token lapses. async def get_token(ctx=None): async with aiohttp.ClientSession() as session: async with session.get( f"{BASE_URL}/user/realtime-token-v3/api-key", headers={"x-sx-api-key": os.environ["SX_API_KEY"]}, ) as resp: resp.raise_for_status() return (await resp.json())["token"] async def on_book(ctx: PublicationContext) -> None: book = ctx.data # Every message is the full resting book — replace your state, don't patch it. print("book update:", book["marketHash"], book["outcomeOne"], book["outcomeTwo"]) async def on_order(ctx: PublicationContext) -> None: order = ctx.data["order"] print("order update:", order["id"], order["status"], order["remainingSize"]) async def main(): client = Client(WS_URL, get_token=get_token) await client.connect() book_sub = client.new_subscription( f"orderbook_v3:{market_hash}", SubscriptionEventHandler(on_publication=on_book), ) await book_sub.subscribe() order_sub = client.new_subscription( f"account:orders_v3_#{address}", SubscriptionEventHandler(on_publication=on_order), ) await order_sub.subscribe() await asyncio.Future() # keep running asyncio.run(main()) ``` ```javascript JavaScript theme={null} import "dotenv/config"; import { Centrifuge, UnauthorizedError } from "centrifuge"; const BASE_URL = "https://api.sx.bet"; // Mainnet — use https://api.toronto.sx.bet for testnet const WS_URL = "wss://realtime.sx.bet/connection/websocket"; // Testnet: wss://realtime.toronto.sx.bet/connection/websocket const marketHash = "YOUR_MARKET_HASH"; // from step 3 const address = "YOUR_CHECKSUMMED_ADDRESS"; // your USER address (the maker from step 5), checksummed — NOT the proxy wallet // Pass the FUNCTION, not a string, so Centrifugo can refresh the token before it expires. async function fetchToken() { const res = await fetch(`${BASE_URL}/user/realtime-token-v3/api-key`, { headers: { "x-sx-api-key": process.env.SX_API_KEY }, }); if (res.status === 401 || res.status === 403) throw new UnauthorizedError(); if (!res.ok) throw new Error(`token endpoint returned ${res.status}`); return (await res.json()).token; } const client = new Centrifuge(WS_URL, { getToken: fetchToken }); client.connect(); // Public: every message is the full resting book — replace your state, don't patch it. client .newSubscription(`orderbook_v3:${marketHash}`) .on("publication", ({ data }) => { console.log("book update:", data.marketHash, data.outcomeOne, data.outcomeTwo); }) .subscribe(); // Authenticated: the suffix must match your token's `sub` (your user address, not the proxy wallet). client .newSubscription(`account:orders_v3_#${address}`, { recoverable: true }) .on("publication", ({ data }) => { const order = data.order; console.log("order update:", order.id, order.status, order.remainingSize); }) .subscribe(); ``` Order book levels are always the maker frame. Convert to the price you'd pay with `takerOdds = 1 - percentageOdds / 10^20`. See [unit conversions](/developers/unit-conversions). How the exchange and orderbook work, in depth. Signing and submitting orders that execute immediately. # Rate limits Source: https://docs.sx.bet/developers/rate-limits Request limits for the SX Bet REST API. ## Authenticated limits Each row is the per-API-key budget for that route. ### Orders | Endpoint | Max requests | Window | | ----------------------------------------- | ------------ | ------ | | `GET` `/orders-v3` | 1,200 | 60s | | `GET` `/orders-v3/{orderId}` | 1,200 | 60s | | `GET` `/orders-v3/client/{clientOrderId}` | 1,200 | 60s | | `POST` `/orders-v3` | 5,500 | 60s | | `DELETE` `/orders-v3` | 10,000 | 60s | | `DELETE` `/orders-v3/event` | 120 | 60s | | `DELETE` `/orders-v3/all` | 60 | 60s | | `POST` `/heartbeat/v3` | 600 | 60s | ### Positions & trades | Endpoint | Max requests | Window | | --------------------------------- | ------------ | ------ | | `GET` `/fills-v3` | 300 | 60s | | `GET` `/positions-v3` | 300 | 60s | | `GET` `/trades-v3` | 300 | 60s | | `GET` `/trades-v3/{tradeId}` | 300 | 60s | | `GET` `/positions-v3/settled/pnl` | 120 | 60s | ### Account | Endpoint | Max requests | Window | | --------------------------------------- | ------------ | ------ | | `GET` `/user/balance-v3` | 300 | 60s | | `GET` `/user/fees-v3` | 120 | 60s | | `GET` `/user/realtime-token-v3/api-key` | 30 | 60s | ### Proxy wallet | Endpoint | Max requests | Window | | --------------------------------------- | ------------ | ------ | | `GET` `/user/proxy` | 120 | 60s | | `GET` `/user/pending-deploy-proxy` | 120 | 60s | | `GET` `/user/transfer-to-proxy/pending` | 120 | 60s | | `GET` `/user/transfer-to-proxy/status` | 120 | 60s | | `POST` `/user/deploy-proxy` | 20 | 60s | | `POST` `/user/transfer-to-proxy` | 20 | 60s | ### Multisig | Endpoint | Max requests | Window | | ------------------ | ------------ | ------ | | `/user/multisig/*` | 60 | 60s | ## Public limits Public read endpoints are not metered against your API-key budget. They are limited per client IP at the edge. ### Order book | Endpoint | Max requests | Window | | ------------------------------ | ------------ | ------ | | `GET` `/orderbook-v3/snapshot` | 27,500 | 300s | | `GET` `/orders-v3/odds/best` | 5,500 | 60s | ### Summary & metadata | Endpoint | Max requests | Window | | ---------------------- | ------------ | ------ | | `GET` `/metadata/obv3` | 500 | 60s | | `GET` `/summary-v3/*` | 500 | 60s | ### Trades | Endpoint | Max requests | Window | | ------------------------------- | ------------ | ------ | | `GET` `/trades-v3/public` | 200 | 60s | | `GET` `/trades-v3/public/count` | 200 | 60s | ### Markets & sports data These endpoint families share a single combined budget of **500 requests** per **60s** per client IP. A burst on any one of them draws down the budget for all of them. | Endpoint | | --------------------------------- | | `GET` `/markets/*` | | `GET` `/fixture/*` | | `GET` `/leagues` and `/leagues/*` | | `GET` `/sports` | | `GET` `/teams` | | `GET` `/live-scores` | | `GET` `/search` | ## Response headers Every authenticated response carries the state of your budget for that route, so read the limit in force from the headers rather than hardcoding a value. | Header | On | Meaning | | ----------------------- | ---------------------- | -------------------------------------------------- | | `X-RateLimit-Limit` | every metered response | Max requests allowed in the window for this route. | | `X-RateLimit-Remaining` | every metered response | Requests left in the current window. | | `Retry-After` | `429` only | Seconds to wait before retrying. | ## Global limit Beyond the per-key budgets above, all unauthenticated traffic shares a single global bucket at the edge of **35,000 requests** per **10 min** window. ## When limits are exceeded A request over the limit returns HTTP `429 Too Many Requests`. On a `429`, `X-RateLimit-Remaining` is `0` and the `Retry-After` header gives the seconds to wait before retrying — back off until then rather than retrying immediately. ## Use realtime instead of polling Subscriptions do not use your request budget. | Instead of polling | Subscribe to | | ----------------------------------------- | ------------------------------ | | `GET /orderbook-v3/snapshot` in a loop | `orderbook_v3:{marketHash}` | | the snapshot across a whole event | `orderbook_v3_event:{eventId}` | | `GET /orders-v3/odds/best` in a loop | `best_odds_v3:global` | | `GET /orders-v3` to track your own orders | `account:orders_v3_#{address}` | | `GET /trades-v3` to see your bets land | `account:trades_v3_#{address}` | | `GET /fills-v3` for fill detail | `account:fills_v3_#{address}` | ## Related Setting up a channel, and what it sends. # Initialization Source: https://docs.sx.bet/developers/realtime-initialization Connect to the SX Bet real-time WebSocket API using Centrifuge. ## Install ```bash npm theme={null} npm install centrifuge ``` ```bash pip theme={null} pip install centrifuge-python aiohttp ``` Centrifugo provides official client SDKs for most platforms: | SDK | Language / Platform | | --------------------------------------------------------------------- | ------------------------------------------- | | [centrifuge-js](https://github.com/centrifugal/centrifuge-js) | JavaScript — browser, Node.js, React Native | | [centrifuge-python](https://github.com/centrifugal/centrifuge-python) | Python (asyncio) | | [centrifuge-go](https://github.com/centrifugal/centrifuge-go) | Go | | [centrifuge-dart](https://github.com/centrifugal/centrifuge-dart) | Dart / Flutter | | [centrifuge-swift](https://github.com/centrifugal/centrifuge-swift) | Swift (iOS) | | [centrifuge-java](https://github.com/centrifugal/centrifuge-java) | Java / Android | | [centrifuge-csharp](https://github.com/centrifugal/centrifuge-csharp) | C# (.NET, MAUI, Unity) | For the full list including community SDKs, see the [Centrifugo client SDK docs](https://centrifugal.dev/docs/transports/client_sdk). ## Connect Fetch a token using your API key, then instantiate and connect the Centrifuge client. You only need one client instance — all channel subscriptions are multiplexed over the single connection. If you need more than 512 subscriptions, create additional client instances (each connection supports up to 512 channels). See [Limits](/developers/realtime-reference#global-limits) for details. ```javascript JavaScript theme={null} import { Centrifuge } from "centrifuge"; const RELAYER_URL = "https://api.sx.bet"; // Mainnet — use https://api.toronto.sx.bet for testnet const WS_URL = "wss://realtime.sx.bet/connection/websocket"; // Mainnet — use wss://realtime.toronto.sx.bet/connection/websocket for testnet async function fetchToken(apiKey) { const res = await fetch(`${RELAYER_URL}/user/realtime-token-v3/api-key`, { headers: { "x-sx-api-key": apiKey }, }); if (!res.ok) { const body = await res.text(); throw new Error(`Token endpoint returned ${res.status}: ${body}`); } const { token } = await res.json(); return token; } const client = new Centrifuge(WS_URL, { getToken: () => fetchToken(YOUR_API_KEY), }); client.connect(); ``` ```python Python theme={null} import asyncio import os import aiohttp from centrifuge import Client RELAYER_URL = "https://api.sx.bet" # Mainnet — use https://api.toronto.sx.bet for testnet WS_URL = "wss://realtime.sx.bet/connection/websocket" # Mainnet — use wss://realtime.toronto.sx.bet/connection/websocket for testnet # Must be `async def`, and use an async HTTP client. A blocking call here stalls the # event loop, and with it every subscription on the connection. async def fetch_token(ctx=None): async with aiohttp.ClientSession() as session: async with session.get( f"{RELAYER_URL}/user/realtime-token-v3/api-key", headers={"x-sx-api-key": os.environ["SX_API_KEY"]}, ) as resp: if not resp.ok: body = await resp.text() raise Exception(f"Token endpoint returned {resp.status}: {body}") data = await resp.json() return data["token"] async def main(): client = Client(WS_URL, get_token=fetch_token) await client.connect() await asyncio.Future() # keep running asyncio.run(main()) ``` ## Subscribe to a channel Once connected, create a subscription for the channel you want. All channel pages in this section use this same pattern — replace `"channel:name"` with the channel name format documented on each page. ```javascript JavaScript theme={null} const sub = client.newSubscription("channel:name", { recoverable: true }); sub.on("publication", (ctx) => { const data = ctx.data; // handle incoming message }); sub.subscribe(); ``` ```python Python theme={null} import asyncio from centrifuge import Client, PublicationContext, SubscriptionEventHandler, SubscriptionOptions async def on_publication(ctx: PublicationContext) -> None: print(ctx.data) async def main(): client = Client(WS_URL, get_token=fetch_token) await client.connect() handler = SubscriptionEventHandler(on_publication=on_publication) options = SubscriptionOptions(recoverable=True) sub = client.new_subscription("channel:name", handler, options) await sub.subscribe() await asyncio.Future() # keep running asyncio.run(main()) ``` Pass `recoverable: true` on channels whose namespace has history to get at-least-once delivery across reconnects. You do not need `positioned: true` — requesting recovery grants a stream position. See [Namespace history capabilities](/developers/realtime-reference#history-and-recovery-per-namespace) for which channels support this, and [Recovery & reliability](/developers/realtime-reliability) for how to handle recovery outcomes. Each publication event exposes a `ctx` object with the following structure: ```json theme={null} { "channel": "parlay_markets:global", "data": { }, "tags": { "publishedAt": "1773869618503", "messageId": "5db68f78-8442-4530-8476-62495333e9ee" } } ``` `ctx.data` is the channel payload documented on each channel's reference page. `ctx.tags.messageId` is a UUID present on every publication — use it to deduplicate messages (see [Recovery & reliability → Dedup](/developers/realtime-reliability#dedup)). ## Snapshot + subscribe pattern Don't rely on the live feed alone to build initial state. Subscribe first, then seed from REST inside the `subscribed` handler. This closes the gap between "when you fetched the snapshot" and "when your first publication arrives": 1. Create the subscription with `recoverable: true` (which also grants a stream position — you do not need `positioned: true`). 2. In the `subscribed` handler, fetch current state from REST. 3. Apply live updates as publications arrive. On reconnects, the same `subscribed` handler fires — check `wasRecovering` and `recovered` to decide whether you need to re-seed (see [Recovery & reliability](/developers/realtime-reliability)). ```javascript JavaScript theme={null} const sub = client.newSubscription(`orderbook_v3:${marketHash}`); // Every publication runs through the version rule; stale ones are discarded. sub.on("publication", (ctx) => applyUpdate(ctx.data)); sub.on("subscribed", async (ctx) => { // recovered: true means the book was replayed — no REST seed needed. if (ctx.recovered) return; const { data } = await fetch( `https://api.sx.bet/orderbook-v3/snapshot?marketHash=${marketHash}` ).then((r) => r.json()); // The snapshot goes through the same version rule, so a newer live update // that arrived during the fetch is not clobbered. `data` is flat — the same // shape a publication carries, with the level arrays beside `version`. applySnapshot(marketHash, data.version, data); }); sub.subscribe(); client.connect(); ``` ```python Python theme={null} import aiohttp from centrifuge import ( PublicationContext, SubscribedContext, SubscriptionEventHandler, ) class BookHandler(SubscriptionEventHandler): def __init__(self, market_hash: str) -> None: self.market_hash = market_hash async def on_subscribed(self, ctx: SubscribedContext) -> None: # recovered: True means the book was replayed — no REST seed needed. if ctx.recovered: return async with aiohttp.ClientSession() as session: async with session.get( f"https://api.sx.bet/orderbook-v3/snapshot?marketHash={self.market_hash}" ) as res: body = await res.json() data = body["data"] # The snapshot runs through the same version rule as live updates, and # carries the same flat shape a publication does. apply_snapshot(self.market_hash, data["version"], data) async def on_publication(self, ctx: PublicationContext) -> None: apply_update(ctx.data) async def subscribe_book(client, market_hash: str) -> None: sub = client.new_subscription( f"orderbook_v3:{market_hash}", BookHandler(market_hash) ) await sub.subscribe() ``` ## Cleanup When you no longer need a subscription, clean it up to free resources: ```javascript JavaScript theme={null} sub.unsubscribe(); sub.removeAllListeners(); client.removeSubscription(sub); ``` ```python Python theme={null} await sub.unsubscribe() client.remove_subscription(sub) ``` # Realtime overview Source: https://docs.sx.bet/developers/realtime-overview Subscribe to live updates on markets, orders, trades, odds, and scores using the SX Bet WebSocket API. ## Overview SX Bet's WebSocket API delivers real-time updates on orderbook changes, trade executions, market status, and live scores. All channels are powered by [Centrifugo](https://centrifugal.dev/) over a single WebSocket connection and require a short-lived token. Centrifugo provides [official client SDKs](https://centrifugal.dev/docs/transports/client_sdk) for JavaScript, Python, Go, Dart, Swift, Java, and C#; the examples here use the JavaScript and Python ones. Rather than polling REST endpoints, subscribe to the channels relevant to your workflow. The recommended pattern for most use cases is: [fetch current state via REST, then subscribe to stay updated](/developers/realtime-initialization#snapshot-+-subscribe-pattern) — this avoids gaps between your initial snapshot and the live feed. Pass your API key via the `getToken` callback. Token refresh is handled automatically. Create a `Centrifuge` client pointed at the WebSocket URL. Create a subscription for each channel you need and attach a publication handler. *** ## Getting started Install a client SDK, fetch a token, connect, and subscribe — see [Initialization](/developers/realtime-initialization). *** ## Channels | Channel | What you receive | Payload reference | | ------------------------------ | -------------------------------------------------------- | ------------------------------------------------------------------- | | `orderbook_v3:{marketHash}` | The full aggregated book for one market, on every change | [orderbook\_v3 →](/api-reference/channel-orderbook-v3) | | `orderbook_v3_event:{eventId}` | The same book bodies for every market on an event | [orderbook\_v3\_event →](/api-reference/channel-orderbook-v3-event) | | `best_odds_v3:global` | Top-of-book odds changes across all markets | [best\_odds\_v3 →](/api-reference/channel-best-odds-v3) | | `account:orders_v3_#{address}` | Your order state changes, and why an order ended | [orders\_v3 →](/api-reference/channel-orders-v3) | | `account:trades_v3_#{address}` | Your bets, at bet grain | [trades\_v3 →](/api-reference/channel-trades-v3) | | `account:fills_v3_#{address}` | Your individual fills | [fills\_v3 →](/api-reference/channel-fills-v3) | | `recent_trades_v3:global` | The anonymized public tape, one message per taker bet | [recent\_trades\_v3 →](/api-reference/channel-recent-trades-v3) | | `markets:global` | Market create, update, and settlement | [markets →](/api-reference/channel-markets) | | `main_line:global` | Which market is now an event's main line | [line changes →](/api-reference/channel-line-changes) | | `fixtures:global` | Fixture metadata updates | [fixtures →](/api-reference/channel-fixtures) | | `fixtures:live_scores` | Live match scores | [fixtures →](/api-reference/channel-fixtures) | | `parlay_markets:global` | Incoming parlay quote requests | [parlay requests →](/api-reference/channel-parlay-requests) | Account channels use Centrifugo's user-limited form — a `#` followed by the checksummed address the channel belongs to, which must byte-match your token's `sub`. See [Subscribing to your own channels](#subscribing-to-your-own-channels). ### Subscribing to your own channels Account channels use Centrifugo's **user-limited channel** form: a `#` followed by the user the channel belongs to. ```javascript JavaScript theme={null} const address = "0xbcc6D643e4159A75ED1dB4e13330230B82F2AEe5"; // checksummed client.newSubscription(`account:orders_v3_#${address}`, { recoverable: true }) .on("publication", ({ data }) => onOrder(data.order)) .subscribe(); ``` ```python Python theme={null} import asyncio from centrifuge import Client, PublicationContext, SubscriptionEventHandler ADDRESS = "0xbcc6D643e4159A75ED1dB4e13330230B82F2AEe5" # checksummed async def on_publication(ctx: PublicationContext) -> None: on_order(ctx.data["order"]) async def main(): client = Client(WS_URL, get_token=fetch_token) await client.connect() handler = SubscriptionEventHandler(on_publication=on_publication) sub = client.new_subscription(f"account:orders_v3_#{ADDRESS}", handler) await sub.subscribe() await asyncio.Future() # keep running asyncio.run(main()) ``` Ensure you checksum the address passed in and do not use your proxy wallet address, use your **account address** *** ## Recovery & reliability Recovery, history, deduplication, and the snapshot-plus-subscribe seed pattern live on the [Recovery & reliability](/developers/realtime-reliability) reference. In short: pass `recoverable: true` on channels whose namespace has history, then check `recovered` in the `subscribed` handler to decide whether to re-seed from REST. *** ## Connection & Subscription Lifecycle The client connection and each subscription have separate lifecycles. The key rule is: * `connecting` and `subscribing` are non-terminal states. They fire on the initial connect or subscribe and also on automatic retry paths. * `disconnected` and `unsubscribed` are terminal states for automatic retry. ### Client lifecycle The client connection moves through these states: * `disconnected -> connecting -> connected`: initial connect * `connected -> connecting -> connected`: retryable disconnect, then successful reconnect * `connecting/connected -> disconnected`: terminal disconnect Use the client events to understand what happened: * `connecting`: fired on the initial `connect()` and on retryable reconnects. The event includes a `code` and `reason`. * `connected`: fired when the transport is established and the client is ready. * `disconnected`: fired only when the client reaches terminal `disconnected` state. After this, the SDK will not reconnect automatically. * `error`: fired for internal errors that do not necessarily cause a state transition, such as transport errors during initial connect or reconnect, or connection token refresh errors. ```javascript theme={null} client.on("connecting", (ctx) => console.log("connecting", ctx.code, ctx.reason)); client.on("connected", () => console.log("connected")); client.on("disconnected", (ctx) => console.log("disconnected", ctx.code, ctx.reason)); client.on("error", (ctx) => console.error("client error", ctx)); ``` To reconnect after a terminal disconnect, call `client.connect()` explicitly. ### Subscription lifecycle Each client-side subscription moves through its own state machine: * `unsubscribed -> subscribing -> subscribed`: initial subscribe * `subscribed -> subscribing -> subscribed`: retryable interruption, reconnect, or resubscribe * `subscribing/subscribed -> unsubscribed`: terminal subscription stop Use subscription events to understand what happened: * `subscribing`: fired on the initial `subscribe()` and on retryable resubscribe paths. * `subscribed`: fired when the subscription becomes active. * `unsubscribed`: fired only when the subscription reaches terminal `unsubscribed` state. After this, the SDK will not resubscribe automatically. * `publication`: fired whenever a new message arrives on the subscription while it is active. * `error`: fired for internal subscription errors that do not necessarily cause a state transition, such as temporary subscribe errors or subscription token related errors. ```javascript theme={null} sub.on("subscribing", (ctx) => console.log("subscribing", ctx.code, ctx.reason)); sub.on("subscribed", (ctx) => console.log("subscribed", ctx.wasRecovering, ctx.recovered)); sub.on("unsubscribed", (ctx) => console.log("unsubscribed", ctx.code, ctx.reason)); sub.on("publication", (ctx) => console.log("publication", ctx.data)); sub.on("error", (ctx) => console.error("subscription error", ctx.error?.code, ctx.error?.message)); ``` To start a terminally unsubscribed subscription again, call `sub.subscribe()` explicitly. Handle `unsubscribed` on every account channel. *** ## Examples ### Consume a global feed ```javascript JavaScript theme={null} import { Centrifuge } from "centrifuge"; const client = new Centrifuge("wss://realtime.sx.bet/connection/websocket", { getToken: () => fetchToken(YOUR_API_KEY), // see Initialization }); const sub = client.newSubscription("markets:global"); sub.on("publication", (ctx) => { for (const market of ctx.data) { console.log(`${market.marketHash}: status=${market.status}`); } }); sub.subscribe(); client.connect(); ``` ```python Python theme={null} import asyncio from centrifuge import Client, PublicationContext, SubscriptionEventHandler async def on_publication(ctx: PublicationContext) -> None: for market in ctx.data: print(f"{market['marketHash']}: status={market['status']}") async def main(): client = Client( "wss://realtime.sx.bet/connection/websocket", get_token=fetch_token, ) await client.connect() handler = SubscriptionEventHandler(on_publication=on_publication) sub = client.new_subscription("markets:global", handler) await sub.subscribe() await asyncio.Future() # keep running asyncio.run(main()) ``` ### Maintain a recoverable order book Subscribe to `orderbook_v3:{marketHash}` with `recoverable: true`. Apply only newer versions, and seed from the REST snapshot on subscribe so you never miss updates between your snapshot and the live feed: ```javascript JavaScript theme={null} let book = null; function isNewer(incoming, current) { if (!current) return true; return incoming > current; // version strings order lexicographically } async function watchMarket(client, marketHash) { const sub = client.newSubscription(`orderbook_v3:${marketHash}`, { recoverable: true, }); sub.on("publication", ({ data }) => { if (!isNewer(data.version, book?.version)) return; book = data; }); sub.on("subscribed", async () => { const { data } = await fetch( `https://api.sx.bet/orderbook-v3/snapshot?marketHash=${marketHash}` ).then((r) => r.json()); if (isNewer(data.version, book?.version)) { book = data; // REST data is flat — same shape a publication carries } }); sub.subscribe(); } ``` ```python Python theme={null} from centrifuge import ( PublicationContext, SubscribedContext, SubscriptionEventHandler, SubscriptionOptions, ) book: dict | None = None def is_newer(incoming: str, current: str | None) -> bool: # version strings order lexicographically. return current is None or incoming > current class BookHandler(SubscriptionEventHandler): def __init__(self, market_hash: str) -> None: self.market_hash = market_hash async def on_publication(self, ctx: PublicationContext) -> None: global book d = ctx.data if not is_newer(d["version"], book["version"] if book else None): return book = d async def on_subscribed(self, ctx: SubscribedContext) -> None: global book snapshot = await fetch_json( f"https://api.sx.bet/orderbook-v3/snapshot?marketHash={self.market_hash}" ) d = snapshot["data"] # REST data is flat — same shape a publication carries if is_newer(d["version"], book["version"] if book else None): book = d async def watch_market(client, market_hash: str) -> None: options = SubscriptionOptions(recoverable=True) sub = client.new_subscription( f"orderbook_v3:{market_hash}", BookHandler(market_hash), options ) await sub.subscribe() ``` See [Book versioning](/developers/book-versioning#the-apply-rule) for the full apply rule. ### Monitor your active orders Subscribe to `account:orders_v3_#{address}` to receive fills, cancellations, and new posts for your address in real time. This channel is the only place an order's terminal reason appears: ```javascript JavaScript theme={null} import { getAddress } from "viem"; const address = getAddress(myAddress); // checksummed, must match your token's `sub` const orders = new Map(); const seen = new Set(); const sub = client.newSubscription(`account:orders_v3_#${address}`, { recoverable: true, }); sub.on("publication", (ctx) => { const id = ctx.tags?.messageId; if (id !== undefined) { if (seen.has(id)) return; // drop replayed duplicates seen.add(id); } const o = ctx.data.order; orders.set(o.id, o); // subscribe-first ordering means the latest arrival wins if (o.status === "INACTIVE") console.log(`${o.id} died: ${o.inactiveReason}`); }); sub.on("subscribed", async (ctx) => { if (ctx.wasRecovering && ctx.recovered) return; // Fresh connect or failed recovery — seed active orders from REST const { data } = await fetch(`https://api.sx.bet/orders-v3`, authed).then((r) => r.json()); orders.clear(); for (const o of data.orders) orders.set(o.id, o); }); sub.on("unsubscribed", (ctx) => console.error("refused", ctx.code, ctx.reason)); sub.subscribe(); client.connect(); ``` ```python Python theme={null} from eth_utils import to_checksum_address from centrifuge import ( PublicationContext, SubscribedContext, SubscriptionEventHandler, SubscriptionOptions, ) address = to_checksum_address(my_address) # must match your token's `sub` orders: dict = {} seen: set = set() class OrdersHandler(SubscriptionEventHandler): async def on_publication(self, ctx: PublicationContext) -> None: msg_id = (ctx.tags or {}).get("messageId") if msg_id is not None: if msg_id in seen: # drop replayed duplicates return seen.add(msg_id) o = ctx.data["order"] orders[o["id"]] = o # subscribe-first ordering means the latest arrival wins if o["status"] == "INACTIVE": print(o["id"], "died:", o["inactiveReason"]) async def on_subscribed(self, ctx: SubscribedContext) -> None: if ctx.was_recovering and ctx.recovered: return body = await fetch_json("https://api.sx.bet/orders-v3") orders.clear() for o in body["data"]["orders"]: orders[o["id"]] = o options = SubscriptionOptions(recoverable=True) sub = client.new_subscription( f"account:orders_v3_#{address}", OrdersHandler(), options ) await sub.subscribe() ``` `GET /orders-v3` returns active orders only, so polling can tell you an order is gone but never why. See [Tracking your orders](/developers/my-orders). *** ## Common failures In most cases, you do not need to write custom retry logic around these errors. The SDK already handles reconnect and resubscribe automatically when the condition is retryable. The codes below are most useful for telemetry, debugging, and contacting support if an issue persists. ### Auth The `getToken` callback is called on initial connect and whenever the token needs to be refreshed. How you throw from it controls what the SDK does next: ```javascript JavaScript theme={null} import { Centrifuge, UnauthorizedError } from "centrifuge"; const client = new Centrifuge(WS_URL, { getToken: async () => { const res = await fetch(`${RELAYER_URL}/user/realtime-token-v3/api-key`, { headers: { "x-sx-api-key": apiKey }, }); if (res.status === 401 || res.status === 403) { throw new UnauthorizedError(); // permanent — stops all reconnect attempts } if (!res.ok) throw new Error(`Status ${res.status}`); // transient — retries with backoff const { token } = await res.json(); return token; }, }); ``` ```python Python theme={null} import os import aiohttp from centrifuge import Client, UnauthorizedError async def fetch_token(ctx=None): async with aiohttp.ClientSession() as session: async with session.get( f"{RELAYER_URL}/user/realtime-token-v3/api-key", headers={"x-sx-api-key": os.environ["SX_API_KEY"]}, ) as resp: if resp.status in (401, 403): raise UnauthorizedError() # permanent — stops all reconnect attempts if not resp.ok: raise Exception(f"Status {resp.status}") # transient — retries with backoff data = await resp.json() return data["token"] client = Client(WS_URL, get_token=fetch_token) ``` If your realtime-token endpoint returns `401` or `403`, throw `UnauthorizedError` so the connection stops retrying and moves to terminal `disconnected`. For transient failures like `429` or `5xx`, throw a normal error so the SDK keeps retrying with backoff. See [Rate limits](/developers/rate-limits). The server may also issue a terminal auth disconnect such as code `3500` (`"invalid token"`). In that case, the client stops reconnecting automatically. ### Subscribe errors Retryable subscription errors emit the subscription `error` event. Terminal subscription errors move the subscription to `unsubscribed`. ```javascript JavaScript theme={null} sub.on("error", (ctx) => { console.error(ctx.error.code, ctx.error.message); }); ``` ```python Python theme={null} from centrifuge import SubscriptionErrorContext, SubscriptionEventHandler async def on_error(ctx: SubscriptionErrorContext) -> None: print(ctx.error.code, ctx.error.message) handler = SubscriptionEventHandler(on_error=on_error) ``` | Code | Meaning | What happens next | | ----- | ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------- | | `100` | Internal server error | The subscription stays in `subscribing` and the SDK retries. | | `101` | Unauthorized | The subscription moves to terminal `unsubscribed`. | | `102` | Unknown channel | The subscription moves to terminal `unsubscribed`. | | `103` | Permission denied — an account-channel suffix that does not match your token's `sub` | The subscription moves to terminal `unsubscribed`. | | `106` | Limit exceeded — the connection is at the 512-channel cap | The subscription moves to terminal `unsubscribed`. | | `108` | No history on this channel | Returned by `history()`. | | `109` | Token expired | The subscription stays in `subscribing`; the SDK refreshes the token and retries. | | `111` | Too many requests | The subscription stays in `subscribing` and the SDK retries. | For the full list of built-in client error codes, see [Centrifugo client protocol codes](https://centrifugal.dev/docs/server/codes). ### Recovery lost / insufficient state If Centrifugo detects that recovery cannot continue from the current stream position, it may either resubscribe the affected subscription or reconnect the client, depending on where the problem is detected. This can surface as unsubscribe code `2500` or disconnect code `3010`, both with reason `"insufficient state"`. This is not terminal by itself. The next `subscribed` event tells you whether the replay succeeded: * `wasRecovering: true, recovered: true`: replay filled the gap * `wasRecovering: true, recovered: false`: replay could not fill the gap, so re-seed from REST If you see `insufficient state` frequently, it usually indicates a stream continuity problem rather than a client bug. ### Terminal disconnects The client reconnects automatically after most disconnects. It does **not** reconnect for built-in terminal disconnect codes in the `3500-3999` range. Common terminal examples include: * `3500` `invalid token` * `3501` `bad request` * `3503` `force disconnect` * `3507` `permission denied` For the full list of built-in disconnect codes, see [Centrifugo client protocol codes](https://centrifugal.dev/docs/server/codes). ### Slow consumer The server buffers about 1 MB per connection. If your `publication` handler is slow, that buffer fills faster than it drains and the server closes the connection. In Centrifugo this surfaces as disconnect code `3008` (`"slow"`), which is reconnectable but indicates your consumer cannot keep up. Keep handlers fast: receive the message and hand it off to a queue or async task immediately. *** ## Related Using `account:orders_v3` to monitor your open orders in real time. How to submit fills and monitor your trade history. Install, connect, and subscribe with the Centrifuge client. Responding to parlay quote requests via `parlay_markets:global`. # Channel and limit reference Source: https://docs.sx.bet/developers/realtime-reference Channels, per-namespace history, and connection limits for the SX Bet WebSocket API. | | | | --------- | ------------------------------------------------------------ | | Endpoint | `wss://realtime.sx.bet/connection/websocket` | | Transport | **WebSocket only.** There is no public HTTP API for realtime | | Server | Centrifugo v6.9.1 | ## The channels **Public** — subscribe with any valid connection: | Channel | Carries | | ----------------------------------------------------------------------------- | --------------------------------------------- | | [`orderbook_v3:{marketHash}`](/api-reference/channel-orderbook-v3) | The full aggregated book for one market | | [`orderbook_v3_event:{eventId}`](/api-reference/channel-orderbook-v3-event) | The same bodies, for every market in an event | | [`best_odds_v3:global`](/api-reference/channel-best-odds-v3) | Top-of-book changes across all markets | | [`recent_trades_v3:global`](/api-reference/channel-recent-trades-v3) | The anonymized public tape | | [`markets:global`](/api-reference/channel-markets) | Market create and update | | [`main_line:global`](/api-reference/channel-line-changes) | Main-line changes | | [`fixtures:global`](/api-reference/channel-fixtures) · `fixtures:live_scores` | Fixture metadata and live scores | | [`parlay_markets:global`](/api-reference/channel-parlay-requests) | Parlay quote requests | **Account** — scoped to you, and only reachable with a matching token: | Channel | Carries | | ------------------------------------------------------------------ | ------------------------ | | [`account:orders_v3_#{address}`](/api-reference/channel-orders-v3) | Your order state changes | | [`account:trades_v3_#{address}`](/api-reference/channel-trades-v3) | Your bets | | [`account:fills_v3_#{address}`](/api-reference/channel-fills-v3) | Your individual fills | ## History and recovery, per namespace The namespace is the part of the channel name before the colon, and it decides everything about what a reconnect can recover. | Namespace | History | Retention window | Client `history()` | | --------------------------------- | -------- | ---------------- | ------------------ | | `orderbook_v3` | **1** | **6 h** | **No** | | `orderbook_v3_event` | 100 | 5 min | Yes | | `account` | 100 | 5 min | Yes | | `recent_trades_v3` | 100 | 5 min | Yes | | `markets` | 1,000 | 5 min | Yes | | `main_line` | 500 | 5 min | Yes | | `fixtures` | 100 | 5 min | Yes | | `best_odds_v3` · `parlay_markets` | **None** | — | — | History is bounded by **both** the message limit and the retention window, whichever is reached first. A publication is only recoverable while it is still within the retention window **and** has not been pushed out by newer messages once the limit is hit. ## Limits | Parameter | Value | | ---------------------------------------- | ---------- | | Max channel subscriptions per connection | 512 | | Max history items returned per request | 1,000 | | Max messages recoverable per reconnect | 1,000 | | Ping interval | 30 seconds | | Pong timeout | 8 seconds | | Outbound queue limit | 1 MB | # Recovery & reliability Source: https://docs.sx.bet/developers/realtime-reliability Maintain consistent state across connects and reconnects on the SX Bet real-time API. How to keep local state consistent across reconnects: enabling recovery, reading the `subscribed` outcome, deduplicating replayed messages, and fetching history. Per-namespace history settings are on [Channel and limit reference](/developers/realtime-reference#history-and-recovery-per-namespace); the snapshot-plus-subscribe seed pattern is on [Initialization](/developers/realtime-initialization#snapshot--subscribe-pattern). ## Enabling recovery Pass options to `newSubscription` to control reliability behavior: | Flag | Type | Description | | ------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `recoverable` | `boolean` | Enables automatic recovery. On resubscribe, the client sends its last known stream position and the server replays missed publications from history. | Set `recoverable: true` on channels whose namespace has history. ```javascript JavaScript theme={null} const sub = client.newSubscription(`orderbook_v3_event:${eventId}`, { recoverable: true, }); ``` ```python Python theme={null} from centrifuge import SubscriptionOptions options = SubscriptionOptions(recoverable=True) sub = client.new_subscription(f"orderbook_v3_event:{event_id}", handler, options) ``` ## Interpreting subscribed after reconnect After a reconnect, the `subscribed` event fires with context that tells you whether your local state is still consistent: * `wasRecovering`: the client attempted to recover from a previous stream position * `recovered`: the server successfully replayed all missed publications * `positioned`: the subscription has stream position tracking enabled * `recoverable`: the subscription supports automatic recovery ```javascript JavaScript theme={null} sub.on("subscribed", (ctx) => { if (ctx.wasRecovering && ctx.recovered) { // Reconnected and gap was filled via history replay. // No need to re-fetch from REST — all missed messages were replayed. } else if (ctx.wasRecovering && !ctx.recovered) { // Reconnected but history was pruned before recovery could complete. // Too much time passed — re-seed your local state from REST. } else { // Fresh connect (first connection, or after a clean disconnect). // Seed initial state from REST, then rely on the subscription for updates. } }); ``` ```python Python theme={null} from centrifuge import SubscribedContext, SubscriptionEventHandler async def on_subscribed(ctx: SubscribedContext) -> None: if ctx.was_recovering and ctx.recovered: # Reconnected and gap was filled via history replay. # No need to re-fetch from REST — all missed messages were replayed. pass elif ctx.was_recovering and not ctx.recovered: # Reconnected but history was pruned before recovery could complete. # Too much time passed — re-seed your local state from REST. pass else: # Fresh connect (first connection, or after a clean disconnect). # Seed initial state from REST, then rely on the subscription for updates. pass handler = SubscriptionEventHandler(on_subscribed=on_subscribed) ``` | `wasRecovering` | `recovered` | State | What to do | | --------------- | ----------- | ------------- | ---------------------------------------------------- | | `true` | `true` | Recovered | History replay filled the gap — no action needed | | `true` | `false` | Unrecovered | History was pruned — re-seed from REST | | `false` | — | Fresh connect | First connection or clean reconnect — seed from REST | ## Delivery guarantees For namespaces with history enabled, Centrifugo provides **at-least-once delivery** within the recovery window — missed messages are replayed from server-side history on reconnect. When `recovered: true`, everything you missed was replayed and you do not need to re-seed from REST. If the disconnect outlasts it (or the message cap is reached first), `wasRecovering: true, recovered: false` fires and you must re-seed from REST. Recovery can fail even after a short disconnect if the server no longer has the missed publications in history or if the saved stream position is no longer valid. When this happens `recovered` is `false` and you must re-seed from REST. ## Dedup At-least-once delivery means a message may occasionally be replayed more than once during recovery. Every publication includes a `messageId` in `ctx.tags` — use it to deduplicate on the client side: ```javascript JavaScript theme={null} const seen = new Set(); const MAX_SEEN = 10_000; sub.on("publication", (ctx) => { const id = ctx.tags?.messageId; if (id !== undefined) { if (seen.has(id)) return; seen.add(id); if (seen.size > MAX_SEEN) { seen.delete(seen.values().next().value); } } applyUpdate(ctx.data); }); ``` ```python Python theme={null} from collections import OrderedDict from centrifuge import PublicationContext, SubscriptionEventHandler MAX_SEEN = 10_000 seen: "OrderedDict[str, None]" = OrderedDict() async def on_publication(ctx: PublicationContext) -> None: msg_id = (ctx.tags or {}).get("messageId") if msg_id is not None: if msg_id in seen: return seen[msg_id] = None if len(seen) > MAX_SEEN: seen.popitem(last=False) apply_update(ctx.data) handler = SubscriptionEventHandler(on_publication=on_publication) ``` `messageId` deduplicates any repeated publication, whatever the cause. There is no separate handling for recovery replay versus other duplicates — one `messageId` per publication, drop the ones you have already seen. [`orderbook_v3`](/api-reference/channel-orderbook-v3) and [`orderbook_v3_event`](/api-reference/channel-orderbook-v3-event) are the exception: their `version` both **orders** and de-duplicates the book. Apply a publication only when its `version` is strictly greater than the one you hold for that market — you do not need `messageId` there. See each channel's reference. ## History Each namespace with history enabled maintains a server-side log of recent publications. You can fetch this directly with `sub.history()` — useful for seeding initial state or auditing recent activity without a separate REST call. ```javascript JavaScript theme={null} const resp = await sub.history({ limit: 50 }); for (const pub of resp.publications) { applyUpdate(pub.data); } ``` ```python Python theme={null} resp = await sub.history(limit=50) for pub in resp.publications: apply_update(pub.data) ``` ### Parameters | Parameter | Type | Description | | --------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `limit` | `number` | Max publications to return. `0` returns only the current stream position (no publications). Omit for all history up to the server cap. | | `since` | `{ offset: number, epoch: string }` | Start from a known stream position. Useful for paginating through history. Omit to start from the beginning (forward) or end (reverse). | | `reverse` | `boolean` | `false` (default) = oldest first. `true` = newest first. | The response contains a `publications` array and the current stream `offset` and `epoch`. Each entry has `data`, `offset`, `tags`, and `info` fields — access the payload via `pub.data`, the same as `ctx.data` in a live publication event. ### Limits History fetches are bounded by the per-namespace caps in [Channel and limit reference](/developers/realtime-reference#history-and-recovery-per-namespace) and the global limit of 1,000 items per request. Calling `sub.history()` on a channel with no history enabled returns error code `108`. `orderbook_v3` keeps one message as a cache rather than a log, so the client is not granted history there. For the snapshot-plus-subscribe seed pattern, see [Initialization → Snapshot + subscribe pattern](/developers/realtime-initialization#snapshot--subscribe-pattern). *** The guide with full worked examples, channel reference, and common failures. # Exposure limits Source: https://docs.sx.bet/developers/risk-limits How SX Bet manages exposure Market making on SX Bet is capital-efficient by design: your balance is not divided across the markets you quote. The same funds can back liquidity everywhere at once. ## How maker exposure is limited When you post resting (`GTC`) liquidity, the exchange checks: ``` resting size + in-flight size + new order size ≤ your balance ``` That check runs **per market and outcome**. Orders on different markets — or on the opposite outcome of the same market — do not draw down each other's budget. Every market:outcome is allowed up to your full balance. An order that would push a single `market:outcome` over your balance is rejected. ## Example With a \$1000 balance you can post up to \$1000 of liquidity on **any number of markets at the same time**: * \$1000 on market A, outcome 1 * \$1000 on market B, outcome 1 * \$1000 on market C, outcome 2 Each is measured against the full \$1000 — there is no shared pool being divided up, so all three rest at once. What you cannot do is exceed \$1000 on a **single** `market:outcome`; if you already have \$600 resting on market A outcome 1, a further order there is capped at \$400. Anything larger is rejected. ## Capital is not locked until a match Posting an order does **not** move any funds into escrow, regardless of its [`timeInForce`](/developers/time-in-force). Capital is locked only when an order **matches** — the moment a taker fills your resting quote, or your own order crosses the book. ## Scale-down when your exposure exceeds your balance When a match locks capital and drops your available balance below what your resting orders still require, the exchange automatically cancels resting orders to bring you back within budget. Cancellation is **worst-odds-first, across all your markets**: 1. The orders **furthest from 50%** implied odds — the least likely to ever fill — are cancelled first. 2. Larger orders break ties, so the deficit is covered in as few cancellations as possible. 3. Cancellation stops as soon as the total exposure on each `market:outcome`is back within your balance. Orders already within budget are left untouched. Orders removed this way become `INACTIVE` with `inactiveReason: INSUFFICIENT_BALANCE`. Watch your [`account:orders_v3`](/api-reference/channel-orders-v3) stream to react. ## Takers work differently Non-resting (`IOC` / `FOK`) orders lock real capital the instant they fill. Unlike maker exposure, taker size is measured **globally** — the sum of your in-flight taker orders across every market must fit within the one balance, because each fill draws real funds immediately. A taker order that would exceed your balance is rejected with the same `Insufficient available balance for this order size` error. ## Related Quoting, exposure, and the re-quote loop. Build, sign, and submit an order. How offsetting positions release escrow at fill time. GTC rests; IOC and FOK execute now. # Taking liquidity Source: https://docs.sx.bet/developers/taking-liquidity How to fill existing orders on the SX Bet orderbook as a taker. Taking liquidity is the same request as [posting an order](/developers/posting-orders). There is no separate endpoint. Submit a signed limit order to [`POST /orders-v3`](/api-reference/post-orders-v3) with `timeInForce: "IOC"` or `"FOK"`, and the order executes against the book immediately instead of resting on it. ## Prerequisites Same as posting: your proxy must be deployed and funded, and auth is an API key plus your signature. See [Prerequisites](/developers/posting-orders#prerequisites) on Posting orders. ## The price you submit `percentageOdds` is the highest implied probability you're willing to pay — you match at that price or better, never worse. `percentageOdds` is an implied probability, so a lower number is a bigger payout: submitting `"48000000000000000000"` (48%) matches you at 46% but never at 50%. ## Steps 1. **Read the book in your frame** — `GET /orderbook-v3/snapshot` with `showTakerPerspective=true`, optionally, to avoid doing the conversion yourself. 2. **Pick the best level** for the outcome you want. 3. **Sign and submit an `IOC` or `FOK`** at that level's price. It matches at that price or better, or not at all. 4. **Read the result from your trades and fills** — an `IOC`/`FOK` never rests. 5. **Or listen on your account streams** — subscribe to [`account:fills_v3_#{address}`](/api-reference/channel-fills-v3), [`account:trades_v3_#{address}`](/api-reference/channel-trades-v3), or [`account:orders_v3_#{address}`](/api-reference/channel-orders-v3) to have the same updates pushed to you instead of polling. **The engine never matches you against your own resting orders.** ## Taking an IOC end to end ```js take_now.mjs theme={null} import { Wallet, TypedDataEncoder, getAddress, zeroPadValue, randomBytes, hexlify } from "ethers"; const API = "https://api.sx.bet"; // Mainnet — use https://api.toronto.sx.bet for testnet const MARKET = process.env.SX_MARKET_HASH; const wallet = new Wallet(process.env.SX_PRIVATE_KEY); const authed = (init = {}) => ({ ...init, headers: { "Content-Type": "application/json", "x-sx-api-key": process.env.SX_API_KEY, ...init.headers }, }); const { data: meta } = await fetch(`${API}/metadata/obv3`).then((r) => r.json()); // 1. Read the book in YOUR frame. showTakerPerspective=true shows each side as // the price someone wanting that outcome would pay. const { data: book } = await fetch( `${API}/orderbook-v3/snapshot?marketHash=${MARKET}&showTakerPerspective=true` ).then((r) => r.json()); // 2. Best available price for outcome TWO is index 0 of outcomeTwo. const best = book.outcomeTwo[0]; if (!best) throw new Error("no liquidity on outcome two"); // 3. Submit an IOC at that price. Anything at this price or better fills. const order = { marketHash: MARKET, maker: wallet.address, totalBetSize: "1000000", percentageOdds: best.percentageOdds, salt: hexlify(randomBytes(32)), expiry: Math.floor(Date.now() / 1000) + 3600, // unix seconds; here, 1 hour out baseToken: meta.activeAsset.baseToken, isMakerBettingOutcomeOne: false, // betting outcome TWO timeInForce: "IOC", }; const TYPES = { Order: [ { name: "marketHash", type: "bytes32" }, { name: "baseToken", type: "address" }, { name: "totalBetSize", type: "uint256" }, { name: "percentageOdds", type: "uint256" }, { name: "salt", type: "uint256" }, { name: "expiry", type: "uint256" }, { name: "maker", type: "address" }, { name: "isMakerBettingOutcomeOne", type: "bool" }, ] }; const domain = meta.domain; // complete EIP-712 domain const message = { marketHash: zeroPadValue(order.marketHash, 32), baseToken: getAddress(order.baseToken), totalBetSize: BigInt(order.totalBetSize), percentageOdds: BigInt(order.percentageOdds), salt: BigInt(order.salt), expiry: BigInt(order.expiry), maker: getAddress(order.maker), isMakerBettingOutcomeOne: order.isMakerBettingOutcomeOne, }; const orderSignature = await wallet.signTypedData(domain, TYPES, message); const orderId = TypedDataEncoder.hash(domain, TYPES, message).toLowerCase(); const res = await fetch(`${API}/orders-v3`, authed({ method: "POST", body: JSON.stringify({ orders: [{ ...order, orderSignature }] }), })); console.log(`HTTP ${res.status}`, JSON.stringify(await res.json())); // 4. The result is in your trades, not your orders — an IOC never rests. await new Promise((r) => setTimeout(r, 8000)); const { data: f } = await fetch(`${API}/fills-v3?orderId=${orderId}`, authed()).then((r) => r.json()); for (const x of f.fills) console.log(` ${x.fillAmount} @ ${x.fillOdds} → returns ${x.returnAmount}`); ``` ```python take_now.py theme={null} import json, os, secrets, time, requests from eth_account import Account from eth_account.messages import encode_typed_data from eth_utils import keccak, to_checksum_address API = "https://api.sx.bet" # Mainnet — use https://api.toronto.sx.bet for testnet MARKET = os.environ["SX_MARKET_HASH"] account = Account.from_key(os.environ["SX_PRIVATE_KEY"]) headers = {"Content-Type": "application/json", "x-sx-api-key": os.environ["SX_API_KEY"]} meta = requests.get(f"{API}/metadata/obv3").json()["data"] # 1. Read the book in YOUR frame. showTakerPerspective=true shows each side as # the price someone wanting that outcome would pay. book = requests.get( f"{API}/orderbook-v3/snapshot", params={"marketHash": MARKET, "showTakerPerspective": "true"} ).json()["data"] # 2. Best available price for outcome TWO is index 0 of outcomeTwo. outcome_two = book["outcomeTwo"] if not outcome_two: raise Exception("no liquidity on outcome two") best = outcome_two[0] # 3. Submit an IOC at that price. Anything at this price or better fills. order = { "marketHash": MARKET, "maker": account.address, "totalBetSize": "1000000", "percentageOdds": best["percentageOdds"], "salt": str(int.from_bytes(secrets.token_bytes(32), "big")), "expiry": int(time.time()) + 3600, # unix seconds; here, 1 hour out "baseToken": meta["activeAsset"]["baseToken"], "isMakerBettingOutcomeOne": False, # betting outcome TWO "timeInForce": "IOC", } TYPES = {"Order": [ {"name": "marketHash", "type": "bytes32"}, {"name": "baseToken", "type": "address"}, {"name": "totalBetSize", "type": "uint256"}, {"name": "percentageOdds", "type": "uint256"}, {"name": "salt", "type": "uint256"}, {"name": "expiry", "type": "uint256"}, {"name": "maker", "type": "address"}, {"name": "isMakerBettingOutcomeOne", "type": "bool"}, ]} domain = { "name": meta["domain"]["name"], "version": meta["domain"]["version"], "chainId": meta["chainId"], "verifyingContract": to_checksum_address(meta["activeAsset"]["escrowAddress"]), } message = { **order, "baseToken": to_checksum_address(order["baseToken"]), "totalBetSize": int(order["totalBetSize"]), "percentageOdds": int(order["percentageOdds"]), "salt": int(order["salt"]), "maker": to_checksum_address(order["maker"]), } del message["timeInForce"] # not part of the signed struct signable = encode_typed_data(domain_data=domain, message_types=TYPES, message_data=message) order_signature = account.sign_message(signable).signature.to_0x_hex() order_id = "0x" + keccak(b"\x19" + signable.version + signable.header + signable.body).hex() res = requests.post(f"{API}/orders-v3", headers=headers, json={"orders": [{**order, "orderSignature": order_signature}]}) print(f"HTTP {res.status_code}", res.json()) # 4. The result is in your trades, not your orders — an IOC never rests. time.sleep(8) fills = requests.get( f"{API}/fills-v3", headers=headers, params={"orderId": order_id} ).json()["data"]["fills"] for f in fills: print(f" {f['fillAmount']} @ {f['fillOdds']} -> returns {f['returnAmount']}") ``` ## Partial fills `IOC` discards whatever didn't match; `FOK` cancels entirely rather than executing partially — see [Time in force](/developers/time-in-force) for the full behavior table. An `IOC` that fills partially still reports `inactiveReason: "NO_LIQUIDITY"` on the order. That reason describes the discarded remainder. A `FOK` checks fillable depth first and cancels entirely if the depth can't cover your size. ## Related The same signing and submission path, from the maker's side. The global notional budget aggressive orders are checked against. Why IOC and FOK are the only two immediate types. The wait an order takes on before it matches. Fields, validation rules and every error body. Reading depth, and the prices from the taker's side. What happens to a bet after it matches. # Testnet & Mainnet Source: https://docs.sx.bet/developers/testnet-and-mainnet How to develop on SX Bet testnet and switch to mainnet when you're ready. ## Overview SX Bet runs a full testnet environment (called **Toronto**) that mirrors mainnet. Use testnet to develop and test your integration without risking real funds, then switch to mainnet by updating a few configuration values. ## Testnet setup Sign up at [toronto.sx.bet](https://toronto.sx.bet) and export your private key from the assets page, the same way you would on mainnet. Every order settles from a proxy wallet, and a deployed proxy is mandatory before your first order. Then you must transfer USDC to the proxy wallet. The simplest way to do this is through the [toronto.sx.bet](https://toronto.sx.bet) UI, which does both for you. You can also do it programmatically with [`POST /user/deploy-proxy`](/api-reference/post-user-deploy-proxy) and [`POST /user/transfer-to-proxy`](/api-reference/post-user-transfer-to-proxy). Open a support chat on [sx.bet](https://sx.bet) to receive testnet USDC. Go to your account page on [toronto.sx.bet](https://toronto.sx.bet) and generate an API key from the **API section**. Testnet keys are network-specific and won't work on mainnet. See [API keys](/api-reference/api-key). ## Configuration reference Everything that differs between testnet and mainnet: | Setting | Testnet (Toronto) | Mainnet | | ---------------------- | ---------------------------------------------------- | -------------------------------------------- | | **API Base URL** | `https://api.toronto.sx.bet` | `https://api.sx.bet` | | **App URL** | `https://toronto.sx.bet` | `https://sx.bet` | | **Chain ID** | `79479957` | `4162` | | **RPC URL** | `https://rpc-rollup.toronto.sx.technology` | `https://rpc-rollup.sx.technology` | | **Realtime WebSocket** | `wss://realtime.toronto.sx.bet/connection/websocket` | `wss://realtime.sx.bet/connection/websocket` | | **Explorer API** | `https://explorerl2.toronto.sx.technology/api` | `https://explorerl2.sx.technology/api` | | **USDC Address** | `0x1BC6326EA6aF2aB8E4b6Bc83418044B1923b2956` | `0x6629Ce1Cf35Cc1329ebB4F63202F3f197b3F050B` | | **OBv3 Escrow** | `0x007D30a86366EdA2a410a176329f991565d8CfA4` | `0xF946f2AE410bCeF6cFe53FB27D4F178A79B7863D` | | **Minimum order size** | `1` USDC | `5` USDC | | **Metadata** | `https://api.toronto.sx.bet/metadata/obv3` | `https://api.sx.bet/metadata/obv3` | ## Switching to mainnet checklist When you're ready to go live: * [ ] Update your config to use mainnet values * [ ] Use your **mainnet** private key and API key — testnet keys won't work on mainnet. See [API keys](/api-reference/api-key) * [ ] Deploy a mainnet proxy wallet and fund it with USDC. A proxy deployed on Toronto does not exist on mainnet * [ ] Confirm your order signing domain uses `chainId` and `activeAsset.escrowAddress` from `/metadata/obv3`. See [EIP-712 order signing](/api-reference/eip712-order-signing) * [ ] Size orders against `limits.orderSizeMinimumBaseUnits` * [ ] Round odds with `oddsLadderStepSize` from metadata. See [Odds rounding](/developers/odds-rounding) * [ ] Point your realtime client at the mainnet WebSocket URL. See [Initialization → Connect](/developers/realtime-initialization#connect) * [ ] Place one minimum-size order and cancel it before scaling up ## Common pitfalls ### Wrong chain ID in signatures If orders come back with `Invalid OBv3 order EIP-712 signature`, check that your signing domain's `chainId` matches the network you're targeting. Testnet is `79479957`, mainnet is `4162`. ### Wrong Escrow address `activeAsset.escrowAddress` is the EIP-712 domain's `verifyingContract` and differs per environment. Read it from [`GET /metadata/obv3`](/api-reference/get-metadata-obv3). ### Wrong token addresses USDC has different contract addresses on each network. ### No proxy on the new network Proxy wallets are per-environment. A first mainnet order without a deployed proxy returns `PROXY_NOT_DEPLOYED`. ### Testnet API key on mainnet API keys are network-specific. Generate a separate key for each environment. ## Related Full list of addresses, URLs, and chain IDs. The runtime config every integration reads at startup. Move USDC into your proxy on either network. End-to-end guide from setup to first order. # Time in force Source: https://docs.sx.bet/developers/time-in-force The order time-in-force options available on SX Bet: GTC, IOC, and FOK. Every order carries a required `timeInForce`. It decides how your order behaves in the order book. | `timeInForce` | Unmatched size | Use it for | | ------------------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------- | | **`GTC`** — good till cancelled | **Rests on the book** until filled, cancelled, or expired | Quoting. Providing liquidity. | | **`IOC`** — immediate or cancel | **Discarded.** Whatever matched is yours | Taking what is available now, accepting a partial fill. | | **`FOK`** — fill or kill | **Nothing executes.** The whole order is cancelled unless it fills completely | All-or-nothing execution. The only way to **take** parlay liquidity. | A `GTC` order can still be a taker. If it crosses the book on arrival it matches immediately, and only the remainder rests. ## Which one to use | You want to | Use | | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | | Quote a two-sided market and get filled over time | `GTC` | | Bet at the best available price, taking whatever size is there | `IOC` | | Bet a specific amount or nothing | `FOK` | | Take a parlay | `FOK` (`IOC` is rejected with `PARLAY_ORDER_MUST_NOT_BE_IOC`) | | Provide parlay liquidity | `GTC`, with a real `expiry` no later than the parlay's | | Use bet credits | `IOC` or `FOK` (a resting bet-credit order is rejected with `BET_CREDITS_REQUIRE_NON_RESTING`) | ## Related Build, sign and submit an order of any type. The immediate-execution path, end to end. `PENDING`, `ACTIVE`, `INACTIVE`, and every terminal reason. `timeInForce` is write-only — tag it in your own store when you post. # Unit conversions Source: https://docs.sx.bet/developers/unit-conversions How odds and amounts are represented on the SX Bet API, and how to convert them. Odds are 1e20-scaled integers and amounts are base unit integers. ## Quick reference | Value | Wire format | To human-readable | | ------------ | -------------------------------------------- | -------------------------------------------------------------- | | Odds | integer scaled by **10^20**, as a **string** | `BigInt(raw) * 100n / 10n**18n` → percent, to 3 decimal places | | USDC amounts | integer, **6 decimals**, as a **string** | `raw / 10**6` → dollars | **Use your library's BigInt or equivalent library to process these values** ## Odds Odds are an **implied probability scaled by 10^20**. There is no separate decimal or American field anywhere in the API. Those are display formats you compute yourself. ```javascript JavaScript theme={null} const SCALE = 10n ** 20n; // Display: exact, no float in the path until the last step. const toPercent = (raw) => Number(BigInt(raw) * 100000n / SCALE) / 1000; // 3 dp toPercent("40000000000000000000"); // → 40 toPercent("100000000000000000"); // → 0.1 // Build: from a percentage you chose. const fromPercent = (pct) => (BigInt(Math.round(pct * 1000)) * SCALE / 100000n).toString(); fromPercent(40); // → "40000000000000000000" fromPercent(0.125); // → "125000000000000000" ``` ```python Python theme={null} SCALE = 10 ** 20 def to_percent(raw: str) -> float: return int(raw) * 100000 // SCALE / 1000 # 3 dp, integer maths first def from_percent(pct: float) -> str: return str(round(pct * 1000) * SCALE // 100000) to_percent("40000000000000000000") # → 40.0 from_percent(40) # → '40000000000000000000' ``` ### Maker frame and taker frame ``` takerOdds = 10^20 − makerOdds ``` Example: | | Raw | Percent | | ------------------------------- | ---------------------- | ------- | | Maker is offering, and receives | `40000000000000000000` | 40.000% | | Taker is betting, and receives | `60000000000000000000` | 60.000% | A maker resting at 40% offers the taker a 60% price. ## Token amounts Amounts are integers in the token's smallest unit, as strings. Read the scale from [`GET /metadata/obv3`](/api-reference/get-metadata-obv3): ```json theme={null} { "activeAsset": { "symbol": "USDC", "baseToken": "0x1BC6326EA6aF2aB8E4b6Bc83418044B1923b2956", "escrowAddress": "0x007D30a86366EdA2a410a176329f991565d8CfA4", "decimals": 6 } } ``` | Token | Address | Decimals | | ----- | ------------------------------------------------------------------------------------- | -------- | | USDC | `activeAsset.baseToken` from [`GET /metadata/obv3`](/api-reference/get-metadata-obv3) | 6 | ``` baseUnits = nominal × 10^decimals nominal = baseUnits ÷ 10^decimals ``` ```javascript JavaScript theme={null} const DECIMALS = 6; // read from metadata, do not hard-code const toBaseUnits = (usdc) => (BigInt(Math.round(usdc * 10 ** DECIMALS))).toString(); const toUsdc = (base) => Number(BigInt(base)) / 10 ** DECIMALS; toBaseUnits(1); // → "1000000" toUsdc("1000000"); // → 1 ``` ```python Python theme={null} DECIMALS = 6 def to_base_units(usdc: float) -> str: return str(round(usdc * 10 ** DECIMALS)) def to_usdc(base: str) -> float: return int(base) / 10 ** DECIMALS ``` ## Payouts The payout relationship is the same on every grain: **return = stake ÷ your own implied odds.** ```javascript JavaScript theme={null} const SCALE = 10n ** 20n; // Both arguments in the SAME frame: the staker's own odds. function payout(stakeBaseUnits, ownOdds) { const stake = BigInt(stakeBaseUnits); const ret = (stake * SCALE) / BigInt(ownOdds); // integer division, matches the API return { stake, return: ret, profit: ret - stake }; } payout("1000000", "40000000000000000000"); // → { stake: 1000000n, return: 2500000n, profit: 1500000n } ``` ```python Python theme={null} SCALE = 10 ** 20 def payout(stake_base_units: str, own_odds: str) -> dict: stake = int(stake_base_units) ret = stake * SCALE // int(own_odds) return {"stake": stake, "return": ret, "profit": ret - stake} payout("1000000", "40000000000000000000") # → {'stake': 1000000, 'return': 2500000, 'profit': 1500000} def taker_frame(maker_odds: str) -> str: return str(SCALE - int(maker_odds)) ``` ## Display formats The API never sends decimal or American odds. Compute them yourselves: | From implied `p` | To | | ---------------------------------- | ------------------- | | `1 / p` | Decimal | | `p ≥ 0.5` → `−(p / (1 − p)) × 100` | American, favourite | | `p < 0.5` → `((1 − p) / p) × 100` | American, underdog | | Implied | Decimal | American | | ------- | ------- | -------- | | 40.000% | 2.500 | +150 | | 60.000% | 1.667 | −150 | | 33.333% | 3.000 | +200 | | 90.909% | 1.100 | −1000 | | 9.091% | 11.000 | +1000 | # What can you build? Source: https://docs.sx.bet/developers/what-can-you-build Explore the kinds of applications and tools you can build on top of SX Bet. SX Bet exposes a full REST API with access to markets, orderbooks, order management, and trade history. This opens up a wide range of possible applications — from fully automated trading systems to custom frontends and analytics tools. The most common use case. * **Market making** — post both sides of a market to capture the spread * **Arbitrage** — identify pricing discrepancies between SX Bet and other books * **Model-driven betting** — feed your own predictive models into automated order placement * **Closing positions** — hedge or exit existing positions programmatically Build your own interface on top of SX Bet's liquidity: * Specialized views for specific sports or leagues * Mobile-native betting apps * Social/community betting platforms * White-label sportsbook interfaces The open orderbook and trade history make SX Bet a deep data source: * Real-time odds tracking and movement alerts * Historical market data and closing line analysis * Flow analysis on the anonymised public tape — where size went, and at what price ## What you'll need Login to sx.bet and create an account Market data, the full order book and the public tape need no credentials. Anything about your own activity — orders, bets, positions, balance — needs your API key. Orders are denominated in USDC, in integer base units — `"1000000"` is 1.00 USDC. All guides and samples in this hub use Python and JavaScript. The API is REST, so any language works. ## Ready to start? Follow our step-by-step quickstart to fetch markets, read the orderbook, and place your first order. # What is SX Bet? Source: https://docs.sx.bet/developers/what-is-sx-bet An overview of how SX Bet works as a peer-to-peer prediction market. SX Bet is a decentralised, peer-to-peer sports prediction market. Instead of betting against a house, users trade against each other through an open order book — the model financial exchanges use, applied to sports. ## How it works A user signs an order naming a market, an outcome, a size and a price, and submits it. Two orders on opposite sides at compatible prices match, wholly or partly. Collateral is locked in escrow per match. Once the market is graded, the winning side is paid. A void is a settled bet with an outcome of zero, and stakes return. ## All bets in USDC You bet in USDC from your [proxy wallet](/developers/accounts), a wallet the exchange deploys for you that only you control. ## Visibility Markets, order books and the trade tape are public. Your orders, bets, positions and balances are private to you. ## Related Why maker and taker are a classification, not a choice. What an orderbook exchange makes possible. A resting order in about twenty minutes. The scales, and which side a price is quoted from. # Bet grains Source: https://docs.sx.bet/developers/which-grain How to choose between bets, fills, and positions when querying your trade data. ## The three grains | | **Positions** | **Bets** | **Fills** | | ------------ | ------------------------------------------------------ | ------------------------------------------------ | ---------------------------------------------- | | Route | [`GET /positions-v3`](/api-reference/get-positions-v3) | [`GET /trades-v3`](/api-reference/get-trades-v3) | [`GET /fills-v3`](/api-reference/get-fills-v3) | | A row is | one **market** | one **bet** you placed | one **match** against one counterparty | | Answers | *What am I exposed to?* | *What did I bet?* | *What did I get filled at?* | | Key | `marketHash` | `tradeId` | `matchId` | | Amount field | `totalStake` (summed) | `totalStake` | `fillAmount` | | Price field | `odds.outcomeOne/Two` (blended) | `weightedAverageOdds` (blended) | `fillOdds` (**actual**) | They nest strictly: one position contains N bets, one bet contains N fills. ## Positions One row per market, netted across all your bets in it. The grain for a portfolio view. ```js theme={null} const qs = new URLSearchParams({ status: "MATCHED,LOCKED", perPage: "50" }); const { data } = await (await fetch(`https://api.sx.bet/positions-v3?${qs}`, { headers: { "x-sx-api-key": process.env.SX_API_KEY }, })).json(); for (const p of data.positions) { const name = p.market?.outcomeOneName ?? p.marketHash.slice(0, 10) + "…"; console.log( `${name} staked ${(Number(p.totalStake) / 1e6).toFixed(2)} ` + `win +${(Number(p.maxWin) / 1e6).toFixed(2)} lose ${(Number(p.maxLoss) / 1e6).toFixed(2)}` ); } ``` ## Bets One row per bet you placed. All the fills for a bet are blended into a single row. ```js theme={null} const res = await fetch("https://api.sx.bet/trades-v3?perPage=25", { headers: { "x-sx-api-key": process.env.SX_API_KEY }, }); const { data } = await res.json(); for (const bet of data.trades) { const stake = Number(bet.totalStake) / 1e6; // USDC, 6 dp const profit = (Number(bet.totalReturn) - Number(bet.totalStake)) / 1e6; const odds = Number(bet.weightedAverageOdds) / 1e18; // → percent console.log(`${bet.betTime} ${bet.status} ${stake.toFixed(2)} USDC @ ${odds.toFixed(3)}% → +${profit.toFixed(2)} if it wins`); } ``` ## Fills One row per match against one counterparty. The finest grain, and the only one carrying the price you actually got and the order it came from. Narrow with `tradeId` for one bet, or `orderId` for the fills that came from one of your orders. ```js theme={null} const { data } = await (await fetch( `https://api.sx.bet/fills-v3?tradeId=${bet.tradeId}`, { headers: { "x-sx-api-key": process.env.SX_API_KEY } } )).json(); for (const f of data.fills) { console.log( `${(Number(f.fillAmount) / 1e6).toFixed(6)} USDC ` + `@ ${(Number(f.fillOdds) / 1e18).toFixed(3)}% → ${(Number(f.returnAmount) / 1e6).toFixed(6)}` ); } ``` ## Realtime equivalents Each grain has a channel, and they carry the same shapes as the REST rows: | Grain | Channel | | ----------- | ------------------------------------------------------------------ | | Bets | [`account:trades_v3_#{address}`](/api-reference/channel-trades-v3) | | Fills | [`account:fills_v3_#{address}`](/api-reference/channel-fills-v3) | | Positions | **None** — positions are a derived roll-up, so re-call the route | | Public tape | [`recent_trades_v3`](/api-reference/channel-recent-trades-v3) | ## Related The bet row in full and its filters. The fill row, its four ids, and the sort order. Every field, the required-status rule, and realised PnL. Keeping a live view of what is resting on the book. # Work With Us Source: https://docs.sx.bet/developers/work-with-us Building at scale or exploring a partnership? Let's talk. SX Bet's API is open to everyone — but if you're working on something larger, we want to help you get there faster. Whether you're looking to scale up your trading operations with SX, integrate our markets into your product, leverage our data in an application, or something completely different, we work directly with teams to make it happen. Please don't hesitate to reach out and tell us about what you're building — we'll figure out the best way to support your team. The two ways to represent your site's users on SX.bet. Tell us what you're building and what you need from us. # Deposit From Coinbase Source: https://docs.sx.bet/user-guides/deposit-withdraw/deposit-from-coinbase Learn how to deposit USDC on SX Bet directly from your Coinbase account. If you have a Coinbase account, you're able to connect to it through [sx.bet](https://sx.bet) and fund your wallet directly from Coinbase. You have three different funding options through the Coinbase integration: * Fund your wallet using a fiat balance on Coinbase * Fund your wallet using a debit or credit card on Coinbase * Fund your wallet using a crypto balance on Coinbase 1. Click "Deposit", then select "Coinbase" 2. Sign in to your Coinbase account 3. Select a deposit method from Coinbase Once you're on Coinbase, you can select from a few deposit options. If you already have some crypto or USD in your Coinbase account, you can fund your wallet using that balance. If you do not, you can use your credit/debit card attached to your Coinbase account to fund your wallet. 4. Confirm the transaction on Coinbase. Once the deposit is complete, you should receive USDC in your SX Bet wallet. You are ready to start betting! Bet at the best available odds and get matched instantly. Learn how to find odds and navigate the live order book. # Deposit From Crypto Wallet Source: https://docs.sx.bet/user-guides/deposit-withdraw/deposit-from-wallet Learn how to deposit USDC on SX Bet using your crypto wallet. It's easy to go from any token on any chain to USDC on SX Bet. If you have crypto in your wallet already, you can use it to deposit USDC in seconds. 1. Click "Deposit" 2. Select the "Wallet" option. From the list, select the type of wallet where your crypto balance is. 3. Enter the amount of USDC you want to deposit and click continue. If you have multiple tokens in your wallet, you may click "more options" to select which token balance to use. 4. Click "Deposit Now" and confirm the transaction in your wallet. Your transaction should be complete within a few seconds! You're all set to start betting. Bet at the best available odds and get matched instantly. Learn how to find odds and navigate the live order book. # Deposit With Interac e-Transfer Source: https://docs.sx.bet/user-guides/deposit-withdraw/interac Learn how to deposit USDC on SX Bet using Interac e-Transfer. 1. Click "Deposit", then select "Interac", then enter the amount of USDC you want to purchase and click continue on PayTrie. 2. Click "Buy", then proceed to login to your [PayTrie](https://paytrie.com/) account, or create one. 3. Click submit, then check your email inbox for a Request Money Transfer email from Interac and confirm. 4. Once you've confirmed the transaction, you will get an email notification when PayTrie accepts the funds and the USDC is in your account. This can take up to 60 minutes. Bet at the best available odds and get matched instantly. Learn how to find odds and navigate the live order book. # Large Withdrawals Source: https://docs.sx.bet/user-guides/deposit-withdraw/large-withdrawals Use the Arbitrum Native Bridge to save on fees for large withdrawals. While the [Glide bridge](https://sx.bet/wallet/bridge) offers no-fee deposits, withdrawals are subject to a 0.33% fee. The [Native Bridge](https://portal.arbitrum.io/bridge?destinationChain=ethereum\&sanitized=true\&sourceChain=sx\&token=0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48) facilitates USDC transfers between SX Bet and Ethereum. Users pay a small gas fee on each blockchain and must wait **12 hours** for their transfer to complete, but pay significantly lower fees overall on large transfers. *** To withdraw through the native bridge, you must be using a browser-based wallet (e.g. MetaMask). If you signed up for SX Bet with email, you can import your email wallet to MetaMask. Follow the tutorial here to [import your wallet to MetaMask](/user-guides/faq/export-wallet-metamask). 1. Transfer funds from your betting wallet into your EOA on the [Account Page](https://sx.bet/account) Screenshot 2026 09 01 At 11 51 37 AM 2. Go to the [Arbitrum Native Bridge](https://portal.arbitrum.io/bridge?destinationChain=ethereum\&sanitized=true\&sourceChain=sx\&token=0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48). 3. Connect your wallet. 3. Make sure you have some SX token in your wallet to cover network fees. If you don't have any SX, you can get some from the [faucet](https://faucet.sx.technology/). Enter the amount of USDC you would like to bridge, agree to the terms, and click "Move funds to Ethereum". You may need to add USD Token manually to the bridge by selecting "Add Token" Contract Address: 0x6629Ce1Cf35Cc1329ebB4F63202F3f197b3F050B Screenshot 2026 09 04 At 7 28 22 AM 1 5. Confirm the transaction in your wallet. 6. Wait 12 hours for your transfer to complete. When the bridge is complete, you may claim it from the same page by clicking the green "Claim" button. 7. Confirm the claim transaction in your wallet. You will need some ETH or USDC on Ethereum in your wallet to cover the network fee. Success! Fund your wallet when you're ready to bet again. Learn about trading fees, gas fees, and bridging costs. # Deposit By Transferring Crypto Source: https://docs.sx.bet/user-guides/deposit-withdraw/transfer-crypto Learn how to deposit USDC on SX Bet by transferring crypto from your wallet or exchange. What exchange you will use depends on your location, but some popular crypto exchanges include Coinbase, Kraken, Crypto.com, and Binance. We'll run through these steps using Coinbase, but you'll find most crypto exchanges have a similar layout. 1. Sign up or log in to [Coinbase](https://login.coinbase.com/) (or another crypto exchange) — click on "Buy", select USDC and input the amount you'd like to purchase. Confirm the transaction and payment details. **Now you have USDC!** 2. Login to SX Bet — click "Deposit" and select the "Deposit from anywhere" option. Choose USDC and copy the address. 3. Head back to your [Coinbase](https://login.coinbase.com/) account (or other crypto exchange) and click "Send", paste in your SX Bet Deposit Address, and the amount you would like to send. You **can keep the default setting as Ethereum Network** or choose Arbitrum, Optimism, Base, Avalanche or Polygon. 4. Select self-custody wallet — both MetaMask and email (Fortmatic) are self-custody wallets, meaning you stay in control of your funds. Confirm the transaction. 5. Your USDC will appear in your SX Bet wallet. 6. Now you can start betting! ### FAQ Yes — a fresh transfer address is generated each time you deposit. Sending funds to the address you were provided for a previous deposit will cause a delay in your deposit. Only transfer your funds to the deposit address provided for this specific session. No — deposits made with Native USDC have 0% fees. Deposits made with any other token (e.g. BTC, ETH, USDT) are subject to a 0.33% fee. If you're unsure how to get Native USDC, open a support chat on [SX Bet](https://sx.bet). We support deposits with 100+ tokens across 10+ blockchain networks. To see the full list of supported tokens: 1. Select **Deposit** > **Deposit from anywhere** 2. Use the dropdown menu to browse available tokens and chains. If your deposit is taking longer than expected, please open a support chat on [SX Bet](https://sx.bet/) or contact our payments provider (Glide) directly by emailing [support@paywithglide.xyz](mailto:support@paywithglide.xyz). Bet at the best available odds and get matched instantly. Learn how to find odds and navigate the live order book. # Withdraw With Interac e-Transfer Source: https://docs.sx.bet/user-guides/deposit-withdraw/withdraw-interac Withdraw your USDC to a Canadian bank account via Interac e-Transfer. If you are in Canada, PayTrie allows you to sell USDC directly on SX Bet via e-Transfer. 1. Go to the [Account Page](https://sx.bet/account) on SX Bet, click on "Withdraw" and select "Interac" 3. Enter your PayTrie account email. Click "Send verification code", then check your email inbox for a 4-digit verification code. Confirm the withdrawal by clicking "Withdraw now". 4. Once the transaction is complete, an Interac e-Transfer will be sent to your email. Fund your wallet again using Interac e-Transfer. Earn USDC by providing liquidity on SX Bet markets. # Withdraw USDC to a Crypto Exchange Source: https://docs.sx.bet/user-guides/deposit-withdraw/withdraw-to-exchange Want to cash your crypto in for real currency? Here is the guide. The exchange you will use depends on what's available in your location, but some popular crypto exchanges include Coinbase, Kraken, Crypto.com, Binance and eToro. We'll walk through these steps using Coinbase, but you'll find most crypto exchanges have a similar layout. 1. Login to [Coinbase](https://login.coinbase.com/) and click "Receive Crypto" — select USDC, Base network, then copy your Coinbase address. We're using the Base blockchain in this example, though you can send your USDC using Ethereum, Arbitrum, Polygon, Optimism, or Base network. Make sure that the receiving address you copy from your exchange account matches the network you withdraw to (e.g. if you are using your Base USDC address on Coinbase, you need to withdraw your USDC to Base network). 2. Go to the [Account Page](https://sx.bet/account) on SX Bet, click on "Withdraw" and select "Wallet" 3. Select "Withdraw", then select "Wallet". 4. Select Base as your withdrawal network, then paste in your Coinbase deposit address from step 1. 5. Enter the amount of USDC you would like to withdraw, click "Continue", then "Withdraw now". **If you are using MetaMask/Rabby, you will need to confirm the transaction in your wallet.** 6. Wait for the transaction to complete. 7. Go back to your Coinbase account and click "Sell" — connect your PayPal account to receive your funds in fiat currency, then complete the transaction. That's it! Now you have your funds back in fiat currency. Fund your wallet again when you're ready to bet. Earn USDC by providing liquidity on SX Bet markets. # Export Your Wallet Into MetaMask Source: https://docs.sx.bet/user-guides/faq/export-wallet-metamask Sign up with email? No problem — export your Fortmatic wallet into MetaMask. ## Fetch Your Wallet's Private Key 1. Visit the [Account Page](https://sx.bet/account) on SX Bet. 2. Select "Access Private Key" and follow the steps to reveal your private key. 3. Copy your private key to your clipboard. **Never share your private key with anyone.** Anyone with access to your private key can control your wallet and access your funds. SX Bet support will never ask you for it. ## Setting Up MetaMask 1. Visit [metamask.io](https://metamask.io) and select "Get MetaMask" — add the MetaMask extension to Chrome. 2. Select "Create a new wallet" (you will import your SX Bet wallet later). 3. Create a password, check the box, then select "Create a new wallet". 4. Secure your wallet by backing up your secret recovery phrase. 5. Open the extension and click on "Account". 6. Select "add account or hardware wallet". 7. Select "import account" and paste your private key into the box and click import. 8. Now that you are connected to MetaMask, go back to [SX Bet](https://sx.bet/) and select the dropdown beside your username then click "Logout". 9. Once logged out, select "Connect" in the top right corner and select "Connect MetaMask". 10. You are now connected on SX Bet via MetaMask. Fund your MetaMask wallet with USDC to start betting. Bet at the best available odds and get matched instantly. # How Does SX Bet Work? Source: https://docs.sx.bet/user-guides/getting-started/how-it-works Understand the peer-to-peer exchange model on SX Bet ## A Betting Exchange, Not a Sportsbook This peer-to-peer model means better odds, because you're not paying a bookmaker's fixed margin. Dynamic, competitive pricing sets the odds on SX. ## Makers and Takers Every bet on SX Bet involves two roles: Posts a limit order to the order book – this order sits open until it is cancelled or filled. Fills a maker's existing limit order from the orderbook with a market bet. This is like placing a bet on a traditional sportsbook. You don't need to choose one role — when you place a [**market order**](/user-guides/trading/market-orders) (betting at the best available odds), you're a taker. When you place a [**limit order**](/user-guides/trading/limit-orders) (requesting specific odds), you're a maker. ## Your Wallet, Your Funds SX Bet is non-custodial. Your account uses two connected wallets: **Signing wallet**
The wallet you control. It is used to sign and authorize actions on your account. **Trading wallet**
A secure smart-contract wallet linked to your signing wallet. Your USDC trading balance is held here and bets settle here. ## Betting Currency: USDC All bets on SX Bet are placed using **USDC**, a stablecoin pegged 1:1 to the US dollar. 1 USDC = \$1 USD, always. ## Fees | Bet Type | Fee | | ---------- | ---------------- | | Maker Bets | **0%** | | Taker Bets | **1%** on profit | | Parlays | **5%** on profit | SX Bet also covers all gas (transaction) fees for betting, so there are no hidden costs. Learn more about [fees on SX Bet](/user-guides/trading/fees). ## Next Steps Sign up, fund your wallet, and place your first bet. Learn how to find odds and navigate the live order book. # SX Bet Overview Source: https://docs.sx.bet/user-guides/getting-started/overview The prediction market for sports — better odds, open API, no limits.
SX Bet # Sports Prediction Market

Bet against other bettors — not the house. Better odds, open API, no limits.

## Why Bettors Choose SX Bet Because odds are set by the market, you'll find better prices than traditional sportsbooks. A competitive marketplace means tighter lines and more value. There are no account limits on SX Bet. Users are never restricted, banned, or charged premiums for winning — you are free to bet as much as you want on any market. Power users and builders can access the full SX Bet API at no cost. See the [Developer Docs](/developers/introduction) for more. ## Ready to Get Started? Learn how the peer-to-peer exchange model works. Sign up, fund your wallet, and place your first bet.
# Quickstart Source: https://docs.sx.bet/user-guides/getting-started/quickstart Sign up, fund your wallet, and place your first bet on SX Bet in minutes. Get started on SX Bet in three steps. Go to [sx.bet](https://sx.bet) and create an account. You have two options: Sign up with your email or Google account. SX Bet will automatically create a secure wallet for you — no crypto experience needed. Already have a crypto wallet like [MetaMask](https://metamask.io) or [Rabby](https://rabby.io)? Connect it directly to SX Bet. **If signing up with email:** you'll receive a confirmation email with a 3-digit code. Enter the code to verify your account, then choose a username. **If connecting a wallet:** confirm the connection in your wallet extension, then choose a username. To place bets, you'll need **USDC** in your SX Bet wallet. There are several ways to deposit: 1. Click **Deposit** on SX Bet and select **Deposit from anywhere**. 2. Select the token you will be using to deposit and the blockchain network you're sending it on. 3. Copy your transfer address. 4. Go to your exchange or wallet, click **Send**, paste your transfer address, and enter the amount you would like to deposit. 5. Confirm the transfer. Your USDC will appear in your SX Bet wallet shortly. See the full guide: [Deposit by Transferring Crypto](/user-guides/deposit-withdraw/transfer-crypto) Now that your wallet is funded, you're ready to bet. 1. **Browse markets** — find a game you want to bet on. Use the sidebar to browse by sport, or search for a specific team or event. 2. **Select your bet** — click on the outcome you want to bet on. This opens the bet slip. 3. **Enter your stake** — type in how much USDC you want to wager. You'll see your potential payout calculated automatically. 4. **Place your bet** — click **"Place Bet"** to confirm. If you're using MetaMask or Rabby, confirm the transaction in your wallet. Your bet is now live. You can track it on the [My Bets](https://sx.bet/my-bets) page. ## What's Next? Browse sports, leagues, and bet types on SX Bet. Bet at the best available odds and get matched instantly. Request your own odds and wait for someone to fill your order. # Finding Markets Source: https://docs.sx.bet/user-guides/markets/finding-markets How to search for events, switch between market types, and find alt lines on SX Bet. ## Search Click the **search bar** and type a team or event name. Results appear as you type, showing the matchup and start time. Click a result to go directly to that event. ## Market Types On a sport page, select a league to change market types (e.g. Game Lines, 1X2). On an event page, use the navigation at the top to switch between bet types (e.g. Game Lines, 1X2, First Half, First 5 Innings, Set Betting) and the tabs within each type to switch between its individual markets. Available market types vary by sport. ## Alt Lines For Spread and Total markets, SX Bet offers **alt lines** — alternative point spreads and totals with different odds. To access them: 1. On the Spread or Total tab, find the line selector next to a team's name (e.g., **+0.5 ▾**). 2. Click the dropdown to see all available lines. 3. Each line shows its corresponding odds. The currently active line is marked with a checkmark. 4. Select any line to switch the order book to that alt line. Alt lines let you shop for better odds by taking on more or less risk than the standard line. Learn how to read odds and liquidity once you've found a market. Bet at the best available odds and get matched instantly. # Betting Rules Source: https://docs.sx.bet/user-guides/more/betting-rules Sport-specific rules that govern how bets are settled on SX Bet. ## Other Can't find what you're looking for? View the full [Betting Rules collection](https://help.sx.bet/en/collections/2864899-betting-rules) for all sports and edge cases. # How to Use Bet Credits Source: https://docs.sx.bet/user-guides/rewards/bet-credits Learn how to use bet credits on SX Bet. 1. In the betslip check the "Bet credit" box. ## How Bet Credits Work Bet credits allow you to place bets without using your own funds. If you win, you keep the winnings, but not the original bet amount. Place bets at the best available odds. Compete for prizes in SX Bet tournaments. # SX Bet Maker Rewards Source: https://docs.sx.bet/user-guides/rewards/maker-rewards Earn USDC rewards by providing liquidity on SX Bet markets. The SX Market Maker Rewards Program pays users for improving market liquidity. By placing limit orders that offer better odds than the global consensus line (orange line on the order book), users can earn USDC rewards from dedicated prize pools. Users don't need to win a bet to earn — or even have the bet matched — just help make SX Bet's markets sharper. #### This program is designed to: * Incentivize liquidity throughout each game's entire lifecycle. * Reward market makers who post differentiated, competitive pricing. * Encourage tight, consistent liquidity that benefits all SX traders. Every eligible market features its own USDC prize pool, and rewards accrue automatically as qualifying limit orders remain active. The longer your liquidity is up and the tighter the odds, the more you can earn. ### Eligible Markets Look for the diamond symbol next to a market — these have active USDC prize pools and include a 5x multiplier for being the top order. Check the [Market Maker Rewards Page](https://sx.bet/rewards/incentives) to see a full list of eligible markets. ### How Points are Calculated Rewards are earned automatically when you provide liquidity that improves market pricing on selected markets. Your points are based on: **Amount of liquidity offered × (Taker odds / Global consensus odds – 1) × Top-offer multiplier.** Better odds = more points. More time live = more rewards. ### Qualifying Bets * Orders must remain live for at least 10 seconds to qualify. * Must be a limit order that is better than the Global Line and meet the minimum stake requirement (typically \$100, but may vary by market). * Top-of-book orders earn up to a 5× multiplier for the time they're at the top. * Limit order must be placed on the mainline (alt. lines not yet eligible). ### Reward Timing * You stop earning points once your order is matched or removed — points are only earned while your limit order remains active and unmatched on the order book. * Orders that are taken immediately (within 10s) do not earn points. * Rewards are checked at random time intervals to prevent gaming. * Rewards become claimable after the game begins for Pre-Match markets, and after the game ends for Live markets. ### Claiming Your Rewards Visit the [Market Maker Rewards Page](https://sx.bet/rewards/incentives) and click Claim Rewards. You can claim your rewards anytime. You'll need a small amount of SX Token to cover network gas fees. If you don't have any, visit the [SX Faucet](https://faucet.sx.technology/) to receive a small amount for free. ### FAQ That's okay! Qualifying limit orders only earn points for the time your qualifying order remains open. As long as your qualifying limit order is up long enough to accrue points, you keep your points and earn your share of the USDC reward pool. The Global Line (orange line on the order book) represents the global consensus odds across major sportsbooks (like Pinnacle). To qualify, your order must offer better odds than this global line. Technically, no. You don't need your bet to be matched to earn — you earn points for the time your qualifying order remains live. You stop earning points once your bet is matched or cancelled. No. No, not yet. Limit orders must be on the mainline. Choose a market, and select "Limit Order" in the bet slip. When you place a limit order on SX Bet, you're setting the exact odds you want for your bet — instead of taking the current market price. **Example:** You want to bet the Over 226.5 in the Detroit Pistons vs. Houston Rockets game. The Global Line (orange line) on the order book shows 1.97 on the other side of the book. To qualify for Market Maker Rewards, your limit order must beat that line — meaning you offer better odds than 1.97. Click Over 226.5, select Limit, and enter 2.00 as your requested odds. Your order now appears on the opposite side of the book (Under 226.5) in blue — visible as a new offer that's better than the global line. **To Qualify for Rewards:** * Beat the Global Line (orange line) on eligible markets. * Meet the minimum offer requirement (typically \$100, varies by market). * Place your limit order on the mainline (alt lines not yet eligible). If the market is included in the Maker Rewards but you don't see an orange line on the order book, you will not collect points by placing a limit order. Refresh the page or check the market later — sometimes it can take time for the Global Line to update. If the issue persists, message our support team. Post your own odds and earn the spread as a market maker. Learn how to read the order book and find the global consensus line. # Tournaments Source: https://docs.sx.bet/user-guides/rewards/tournaments Compete for prizes with SX Bet Tournaments. There are four kinds of Tournaments at SX Bet: * **Return** — standings are determined by your cumulative winnings only. * **Profit** — standings are determined by your +/-. * **Potential Return** — ranked by your potential win. Winning or losing your bet does not impact your standings. * **Volume** — ranked based on your total betting volume, not PnL. ### How to Join SX Bet Tournaments 1. Go to the [Rewards](https://sx.bet/rewards/tournaments) tab and click on the [Tournaments](https://sx.bet/rewards/tournaments) page. 2. Select the Tournament and click Join. You can check the Leaderboard by clicking on the tournament you are participating in. T\&C: General terms and conditions apply. No bet washing, or multi-accounting. SX reserves the right to remove individuals. Earn USDC by providing liquidity on eligible markets. Learn about bet credit promotions on SX Bet. # Managing Positions Source: https://docs.sx.bet/user-guides/trading/capital-efficiency Learn how SX Bet locks collateral and how you can unlock capital by trading both sides of a market. ## What is Capital Efficiency? When you place a bet, SX Bet only locks the amount of money you could actually lose — not your full stake on every individual bet. If you place bets on both sides of the same market, only your worst-case loss stays locked. Any extra collateral is automatically refunded to your wallet. This means your funds are freed up and available to use again, even before your bets settle. ## How It Works When you have bets on opposing outcomes in the same market, SX Bet: 1. Looks at all your positions on that market together 2. Calculates the worst possible loss across those positions 3. Keeps only that amount locked as collateral 4. Refunds the rest to your wallet instantly This applies to market orders, limit orders, partial fills, and both pre-match and live markets. **Quick Example** You place two bets on the same match — \$25 on Team A at 1.96 odds, and \$25 on Team B at 1.95 odds. Since only one team can win, the most you can lose is \$1.33. SX Bet recognizes this and only keeps \$1.33 locked as collateral — refunding the remaining \$48.66 to your wallet right away. ## Exiting a Position Early Want to lock in a profit or cut your losses before a match ends? You can exit your position at any time by placing a bet on the opposing outcome. Capital efficiency handles the rest — your worst-case loss stays locked and any excess collateral is returned to your wallet right away. **How to exit a position:** 1. Go to [sx.bet/my-bets/unsettled](https://sx.bet/my-bets/unsettled) to view your open bets 2. Expand the dropdown and select the bet you want to exit 3. Place a bet on the opposing outcome Once your bet is filled, only your net worst-case loss remains locked. Your freed-up collateral is available immediately — before your original bet even settles. **Pre-match odds: Team A @ 2.10 | Team B @ 1.85** 1. You stake $100 on Team A. If Team A wins, your payout is $100 × 2.10 = \$210. 2. After the first half, Team A is winning 2–0. Odds have shifted — Team A: 1.50 | Team B: 3.20. You want to guarantee a profit no matter the result. 3. To guarantee the same payout on both sides, calculate your hedge bet: $210 ÷ 3.20 = $65.62. You place \$65.62 on Team B at 3.20 odds. * If Team A wins: $100 × 2.10 = **$210 payout\*\* * If Team B wins: $65.62 × 3.20 = **$210 payout\*\* * Total staked: $100 + $65.62 = \$165.62 * **Guaranteed profit: \$44.38** 4. After placing the bet on Team B, you instantly receive $165.62 back. Since your worst-case scenario is a push (both bets refunded), none of that $165.62 needs to stay locked. 5. Your \$165.62 is now free to use on another bet — before the original match even finishes. **Pre-match odds: Team A @ 2.10 | Team B @ 1.85** 1. You stake $100 on Team A. If Team A wins, your payout is $100 × 2.10 = \$210. 2. After the first half, Team B is winning 2–0. Odds have shifted — Team A: 3.70 | Team B: 1.33. Things aren't looking good, but you can still limit your loss. 3. To guarantee the same payout on both sides, calculate your hedge bet: $210 ÷ 1.33 = $157.89. You place \$157.89 on Team B at 1.33 odds. * If Team A wins: $100 × 2.10 = **$210 payout\*\* * If Team B wins: $157.89 × 1.33 = **$210 payout\*\* * Total staked: $100 + $157.89 = \$257.89 * **Guaranteed loss capped at: \$47.89** 4. After placing the bet on Team B, you instantly receive \$210 back — your full guaranteed payout, returned before the match ends. 5. In the best case (a push), you receive an additional \$47.89 at settlement on top of that. ## Trading In and Out of Positions Capital efficiency also lets you move in and out of the same market without needing to lock new capital each time. As long as your worst-case loss is covered, you can: * Reduce risk without waiting for the market to close * Re-enter at better prices * Trade price movements instead of holding all the way to settlement Think of it like being able to reuse the same bankroll across multiple moves in the same game. ## For Market Makers If you're quoting both sides of a market, capital efficiency means you don't need to lock capital for every individual order. Instead, only your net worst-case exposure is locked. This lets you: * Quote both sides with less capital tied up * Provide continuous liquidity across outcomes * Deploy larger position sizes with the same bankroll * Scale your activity without repeated deposits or manual capital management Bet at the best available odds and get matched instantly. Understand the SX Bet fee structure, including 0% on single bets. # Fees Source: https://docs.sx.bet/user-guides/trading/fees Learn about trading fees, gas fees, and bridging fees on SX Bet. ### Trading Fees SX Bet charges a 1% fee on net profit per market from taker bets only. Maker bets pay 0% fees. | | Taker | Maker | Parlays | | ------ | ------------ | ----- | ------- | | SX Bet | 1% on profit | 0% | 5% | Fees are only applied to winning bets. *** ### Betting & Gas Fees SX Bet covers all gas fees for betting transactions, so the experience is seamless. *** ### Swaps, Sending, Staking SX is the native token of SX Bet and is needed for gas fees when performing [swaps](https://sx.bet/wallet/swap), [staking](https://sx.bet/staking), or sending crypto. If you don't have any, visit the faucet to receive SX Token for free. Go to the [SX Token Faucet](https://faucet.sx.technology/), enter your betting wallet address and click "Request SX Tokens". You must be signed up for SX Bet and can only request SX once every 24 hours. You'll receive 0.01 SX, which is enough to cover a swap. *** ### Bridging (Deposits & Withdrawals) Glide, MoonPay and PayTrie charge a small transaction fee. For large bridges, the [Native Arbitrum Bridge](https://portal.arbitrum.io/bridge?destinationChain=sx\&sanitized=true\&sourceChain=ethereum\&token=0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48) is usually the most cost-effective option, with only standard gas. Bet at the best available odds and get matched instantly. Post your own odds and act as the bookmaker. # Limit Orders (Maker) Source: https://docs.sx.bet/user-guides/trading/limit-orders Place a limit order to offer odds and act as the bookmaker on SX Bet. A **limit order** lets you **assume the role of a bookmaker.** Instead of betting on an outcome to happen, you are betting against it — offering odds to other users on the exchange and allowing them to take the other side. You can post limit orders across as many markets as you like at the same time. Your exposure is checked independently per market — so you can offer up to your full wallet balance on multiple markets simultaneously, without splitting your funds between them. To place a limit order, pull up the order book, select "Limit" from within the bet slip, enter your wager and the odds you are requesting, and click **"Request Bet"**. If you're using MetaMask, an additional approval will pop up in your wallet. **Example**: You think the odds for Grizzlies +15 should be higher than 2.00. You place a limit order to bet Grizzlies +15 at 2.02 odds. Now you wait for someone to fill your limit order. You can see your offer pop up in the order book on the other side. ## How to Cancel a Limit Order You can cancel a limit order anytime before it is matched. All limit orders are automatically cancelled when the game begins. **From the event page:** Click the **Offered Bets** tab below the order book, select the limit order you want to cancel using the checkbox, then click **Cancel Selected**. If you're using MetaMask or Rabby, confirm in your wallet. **From the portfolio page:** Go to [sx.bet/my-bets/offered](https://sx.bet/my-bets/offered) and click the **Offered Bets** tab. Select the limit order you want to cancel using the checkbox, then click **Cancel Selected**. If you're using MetaMask or Rabby, confirm in your wallet. *** Build on the SX Bet API to post limit orders at scale. No premium charges for API access. # Market Orders (Taker) Source: https://docs.sx.bet/user-guides/trading/market-orders Place a market order to bet at the best available odds on SX Bet. A **market order** is like placing a bet with a traditional sportsbook — you're betting on a specific outcome to occur at the best odds currently available. Click on your desired market to pull up the order book. You'll see a variety of odds available, **select the best odds, enter your wager and click "Place Bet"**. If you're using MetaMask, an additional approval will pop up in your wallet. **Example**: You bet the Grizzlies will cover +15. The current best odds are 2.00. You place a Market Order, your bet is matched instantly. ## Slippage Between the moment you submit a bet and when it's matched, the order book can change — a maker might cancel their order, or another bettor might fill it before you. Slippage tolerance is how much worse than your selected odds you're willing to accept for your bet to still go through. You can adjust slippage in two places: **Per-bet slippage** — overrides slippage for that individual bet only. **Default slippage** — sets your default slippage for all bets. Slippage is calculated on your **weighted average odds** across the entire bet — not on each individual order. This means your bet can fill across multiple price levels, and as long as the average odds stay within your slippage tolerance, it goes through. A single order at slightly worse odds won't necessarily cause your bet to fail if the rest of the fill brings the average back within range. For most pre-match bets, the default slippage is fine. For live/in-play markets where odds shift constantly, increasing your slippage tolerance reduces the chance of your bet failing to fill. Higher slippage means you may receive worse odds than you selected. Only increase it when getting matched quickly matters more than getting the exact price. Post your own odds and earn the spread as a market maker. Learn how to read the order book to find available odds and liquidity. # Reading the Order Book Source: https://docs.sx.bet/user-guides/trading/order-book Learn how to read the SX Bet order book to find available odds and liquidity. Click any market on SX Bet to open its order book. The order book shows two columns — one for each outcome. Each row shows: * **Dollar amount** — how much liquidity is available at that price * **Odds** — the odds you would receive at that level ## Changing Odds Format SX Bet supports decimal, American, and fractional odds. Your choice applies across the whole site. ## The Consensus Line The orange line across the order book marks the **consensus line**. It is the fair price for the market, based on activity across all books. It's a reference point, not a hard limit. Orders above and below it are all available to bet into. ## Placing a Bet Click any odds in the order book to open the bet slip pre-filled at that price. If you bet more than what's available at one price level, your order fills across several levels. Any remainder sits as an open limit order. Bet at the best available odds and get matched instantly. Request your own odds and wait for someone to fill your order. *** Build on the SX Bet API to fetch odds programmatically. No API key needed. # Peer-to-Peer Parlays Source: https://docs.sx.bet/user-guides/trading/parlays Learn about peer-to-peer parlays on SX Bet. ## What is a Parlay? A **Parlay Bet** combines multiple outcomes into a single wager. For a **"Parlay WIN"** bet to be successful, all legs (outcomes) of the parlay must win. If any leg is reported as a **Loss, Push or Void**, the entire Parlay bet is considered a loss. ## How Do Peer-to-Peer Parlays Work? SX Bet's peer-to-peer system lets users create and bet on parlays directly with one another, offering more dynamic odds and interaction. Bettors have **60 seconds** to place their parlay bet after submitting it, before the order book closes. Parlays can include up to **10 legs**. SX Bet also allows bettors to wager against a parlay by choosing **"Parlay LOSE."** In this case, for the bet to win, **one or more legs** of the parlay must result in a loss, and no leg can be reported as **Void** or a **Tie**. ## How to Place a Parlay on SX Bet 1. Add your selections to your **Betslip**. The system will automatically create a **Parlay Slip** for you. You can adjust your parlay by toggling bets on or off in the slip. 2. Choose whether to place a **Parlay WIN** (all legs must win) or a **Parlay LOSE** (one or more legs must lose) bet at the desired odds. 3. Submit your parlay and place the bet within 60 seconds before the order book closes. While fees on single bets are 0%, there is a 5% fee on winning parlays. ### Place Your Parlay, Your Way