# API Key
Source: https://docs.sx.bet/api-reference/api-key
Generating and using an API key
An API Key is required to connect to the WebSocket for real-time updates and to register or cancel a heartbeat. Otherwise, you do NOT need an API Key to use the SX Bet API — all standard requests in this document can be made without one. A baseline rate limiter applies to all requests regardless of API key.
## Generating API Key
1. Visit sx.bet and register/login to your account. You can connect your MetaMask wallet, or login 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.
## Usage
```bash theme={null}
curl --location --request GET 'https://api.sx.bet/user/token' \
--header 'X-Api-Key: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'
```
Pass your API Key in the `X-Api-Key` header. The above fetches a realtime token used to authenticate WebSocket connections.
Once your API Key is generated (see above), you must add it as a HTTP Header with the name: `X-Api-Key`.
# Active Order Updates
Source: https://docs.sx.bet/api-reference/centrifugo-active-order-updates
Subscribe to real-time changes in a user's orders
Subscribe to changes in a particular user's orders. You will receive updates when orders are filled, cancelled, or posted. Note that for performance reasons, updates are delayed by at most 100ms.
**CHANNEL NAME FORMAT**
`active_orders:{maker}`
| Name | Type | Description |
| ----- | ------ | --------------------------------- |
| maker | string | The maker address to subscribe to |
The channel no longer includes a `baseToken` parameter. Updates for all tokens are now broadcast on a single channel per maker address.
**MESSAGE PAYLOAD FORMAT**
The message payload is an array of JSON objects. These are the same fields as in [the orders section](/api-reference/get-orders), with additional `status` and `updateTime` fields.
| Name | Type | Description |
| ------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| orderHash | string | A unique identifier for this order |
| marketHash | string | The market for this order |
| status | string | `"ACTIVE"` if still valid, `"INACTIVE"` if cancelled |
| fillAmount | string | How much this order has been filled in Ethereum units. See [unit conversion](/api-reference/unit-conversion). |
| pendingFillAmount | string | Amount pending fill in Ethereum units. See [unit conversion](/api-reference/unit-conversion). |
| totalBetSize | string | Total size of this order in Ethereum units. See [unit conversion](/api-reference/unit-conversion). |
| percentageOdds | string | The odds the `maker` receives in the sportx protocol format. To convert to implied odds divide by 10^20. To get taker implied odds: `takerOdds = 1 - percentageOdds / 10^20`. See [unit conversion](/api-reference/unit-conversion). |
| expiry | number | Deprecated. Always `2209006800`. |
| apiExpiry | number | The time in unix seconds after which this order is no longer valid |
| salt | string | A random number to differentiate identical orders |
| isMakerBettingOutcomeOne | boolean | `true` if the maker is betting outcome one |
| signature | string | Signature of the maker on this order |
| updateTime | string | Server-side clock time for the last modification of this order |
| sportXeventId | string | The event related to this order |
Messages are sent in batches as an array. If you receive two updates for the same `orderHash`, order them by `updateTime` after converting to a BigInt.
***
```javascript JavaScript theme={null}
// To subscribe
const maker = "0x082605F78dD03A8423113ecbEB794Fb3FFE478a2";
const sub = client.newSubscription(`active_orders:${maker}`, { positioned: true, 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():
maker = "0x082605F78dD03A8423113ecbEB794Fb3FFE478a2"
client = Client(
"wss://realtime.sx.bet/connection/websocket",
token="YOUR_TOKEN", # from /user/realtime-token/api-key
)
await client.connect()
handler = SubscriptionEventHandler(on_publication=on_publication)
sub = client.new_subscription(f"active_orders:{maker}", handler)
await sub.subscribe()
await asyncio.Future() # keep running
asyncio.run(main())
```
The above returns JSON structured like this:
```json theme={null}
[
{
"orderHash": "0xabcdef1234567890abcdef1234567890abcdef1234567890",
"marketHash": "0x1234567890abcdef1234567890abcdef1234567890abcd",
"status": "INACTIVE",
"fillAmount": "100000000000000000",
"pendingFillAmount": "500000000000000000",
"totalBetSize": "2000000000000000000",
"percentageOdds": "750000000000000000000",
"expiry": 1747500000000,
"apiExpiry": 1747500000000,
"salt": "12345678901234567890123456789012345678901",
"isMakerBettingOutcomeOne": false,
"signature": "0xbf099ab02255d5e2a9e063dc43a7afe96e65f5e8fc2ed3d2ba60b0a3fcadb3441bf32271293e85b7a795c9d86a2384035a0da3285113e746547e236bc58885e0",
"updateTime": 1747490000000,
"sportXeventId": "L13772588"
}
]
```
# Best Odds
Source: https://docs.sx.bet/api-reference/centrifugo-best-odds
Subscribe to real-time best odds updates
Subscribe to best odds changes across all order books. You will receive updates when orders are filled, cancelled, or posted. Note that for performance reasons, updates are delayed by at most 100ms.
**CHANNEL NAME**
`best_odds:global`
The channel no longer includes a `baseToken` parameter. All token denominations are now broadcast on this single global channel.
**MESSAGE PAYLOAD FORMAT**
| Name | Type | Description |
| ------------------------ | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| baseToken | string | The token for the best odds result order |
| marketHash | string | The market for the best odds result |
| isMakerBettingOutcomeOne | boolean | Whether the maker is betting outcome one |
| percentageOdds | string | The odds the `maker` receives in the sportx protocol format. To convert to implied odds divide by 10^20. To get taker implied odds: `takerOdds = 1 - percentageOdds / 10^20`. See [unit conversion](/api-reference/unit-conversion) for more details. |
| updatedAt | number | Timestamp in milliseconds for when these became the best odds |
| sportId | number | The sport ID for this market |
| leagueId | number | The league ID for this market |
Messages are sent in batches as an array.
***
```javascript JavaScript theme={null}
// To subscribe
const sub = client.newSubscription("best_odds: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/api-key
)
await client.connect()
handler = SubscriptionEventHandler(on_publication=on_publication)
sub = client.new_subscription("best_odds:global", handler)
await sub.subscribe()
await asyncio.Future() # keep running
asyncio.run(main())
```
The above returns JSON structured like this:
```json theme={null}
[
{
"baseToken": "0x1BC6326EA6aF2aB8E4b68c83418044B1923b2956",
"marketHash": "0xddaf2ef56d0db2317cf9a1e1dde3de2f2158e28bee55fe35a684389f4dce0cf6",
"isMakerBettingOutcomeOne": true,
"percentageOdds": "750000000000000000000",
"updatedAt": 1747500000000,
"sportId": 1,
"leagueId": 1236
}
]
```
# Reliability & Recovery
Source: https://docs.sx.bet/api-reference/centrifugo-best-practices
Key patterns for maintaining consistent state across connects and reconnects
The real-time channels are straightforward to subscribe to, but there are a few patterns worth following to avoid dropped or duplicated data. This page covers the essentials — see [Real-time Data](/developers/real-time) for full code examples.
## Snapshot + subscribe
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 a subscription with `positioned: true, recoverable: 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](#recovery)).
## Recovery flags
Pass both flags together on channels with history enabled:
```js theme={null}
client.newSubscription("order_book:market_abc123", {
positioned: true,
recoverable: true,
});
```
`positioned` bookmarks your place in the stream. `recoverable` uses that bookmark to replay missed messages on reconnect. Either flag alone does nothing useful — you need both. For channels without history (`best_odds`, `parlay_markets`), omit both and re-seed from REST on every reconnect.
See [Namespace history capabilities](/api-reference/centrifugo-overview#namespace-history-capabilities) for which channels support recovery.
## Recovery
After a reconnect, the `subscribed` event tells you whether your local state is still consistent:
| `wasRecovering` | `recovered` | What happened | Action |
| --------------- | ----------- | --------------------------------------- | ----------------------------- |
| `true` | `true` | History replay filled the gap | Nothing — state is consistent |
| `true` | `false` | History window expired before reconnect | Re-seed from REST |
| `false` | — | Fresh connect | Seed from REST |
The recovery window is 5 minutes. After that, or if the server stream was reset, `recovered` will be `false` and you must re-seed.
## Deduplication
At-least-once delivery means a message can be replayed more than once during recovery. Every publication includes a `messageId` in `ctx.tags` — track seen IDs and skip duplicates:
```js theme={null}
const seen = new Set();
sub.on("publication", (ctx) => {
const id = ctx.tags?.messageId;
if (seen.has(id)) return;
seen.add(id);
applyUpdate(ctx.data);
});
```
For long-running processes, cap your dedup set (e.g. last 1,000 IDs) to avoid unbounded memory growth.
## 512 channel limit
Each connection supports a maximum of 512 simultaneous subscriptions. If you exceed this, the subscribe call returns error code `106`. Create additional client instances for additional channels — each gets its own connection and its own 512-slot budget.
## Slow consumer
The server buffers up to 1 MB per connection. If your `publication` handler is slow, the buffer fills faster than it drains and the server closes the connection. Hand off incoming messages to a queue immediately — do not do heavy work inline. Unexpected disconnects that aren't auth errors are often a saturated buffer.
***
Full code examples for all patterns above, plus common failures and error codes.
# CE Refund Events
Source: https://docs.sx.bet/api-reference/centrifugo-ce-refund-events
Subscribe to real-time capital efficiency refund events
Subscribe to capital-efficiency refund events for a particular user. You receive updates when refunds are generated from reductions in maximum loss (MXL) for the user's market groups.
For field definitions, see [Get portfolio refunds](/api-reference/get-trades-refunds). For a high-level overview, see [Capital Efficiency](/developers/capital-efficiency).
**CHANNEL NAME FORMAT**
`ce_refunds:{bettor}`
| Name | Type | Description |
| ------ | ------ | -------------------------------- |
| bettor | string | The user address to subscribe to |
**MESSAGE PAYLOAD FORMAT**
The message payload matches a single element within the `GET /trades/portfolio/refunds` JSON results. See [Get portfolio refunds](/api-reference/get-trades-refunds) for schema details.
***
```javascript JavaScript theme={null}
// To subscribe
const bettor = "0xaD6A65315Cb20dD0b9D0Af56213516727a20C66F";
const sub = client.newSubscription(`ce_refunds:${bettor}`, { positioned: true, 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():
bettor = "0xaD6A65315Cb20dD0b9D0Af56213516727a20C66F"
client = Client(
"wss://realtime.sx.bet/connection/websocket",
token="YOUR_TOKEN", # from /user/realtime-token/api-key
)
await client.connect()
handler = SubscriptionEventHandler(on_publication=on_publication)
sub = client.new_subscription(f"ce_refunds:{bettor}", handler)
await sub.subscribe()
await asyncio.Future() # keep running
asyncio.run(main())
```
The above returns JSON structured like this:
```json theme={null}
[
{
"marketHash": "0x3e012cc2842849b96768547d4c92720d7ee8946e7706323f5114b6451708cf5e",
"baseToken": "0x1BC6326EA6aF2aB8E4b68c83418044B1923b2956",
"totalRefunded": 2.346033,
"events": [
{
"maker": false,
"amount": "2.346033",
"bettor": "0xaD6A65315Cb20dD0b9D0Af56213516727a20C66F",
"baseToken": "0x1BC6326EA6aF2aB8E4b68c83418044B1923b2956",
"createdAt": "2025-10-21T14:29:26.805266+00:00",
"marketHash": "0x3e012cc2842849b96768547d4c92720d7ee8946e7706323f5114b6451708cf5e",
"fillOrderHash": "0x7efa8ee211c5cbccebda722318252ee09cfadaa9c910bf4c433086d853784b02"
}
]
}
]
```
# Consolidated Trade Updates
Source: https://docs.sx.bet/api-reference/centrifugo-consolidated-trade-updates
Subscribe to real-time consolidated trade updates
Subscribe to all consolidated trade updates on the exchange. You will receive updates when a consolidated trade is settled or a new consolidated trade is available.
**CHANNEL NAME**
`recent_trades_consolidated:global`
**MESSAGE PAYLOAD FORMAT**
See [the trades section](/api-reference/get-trades-consolidated) for the format of the message.
***
```javascript JavaScript theme={null}
// To subscribe
const sub = client.newSubscription("recent_trades_consolidated:global", { positioned: true, 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/api-key
)
await client.connect()
handler = SubscriptionEventHandler(on_publication=on_publication)
sub = client.new_subscription("recent_trades_consolidated:global", handler)
await sub.subscribe()
await asyncio.Future() # keep running
asyncio.run(main())
```
The above returns JSON structured like this:
```json theme={null}
{
"baseToken": "0x5147891461a7C81075950f8eE6384e019e39ab90",
"tradeStatus": "PENDING",
"bettor": "0x1562258769E6c0527bd83502E9dfc803929fa446",
"totalStake": "10.0",
"weightedAverageOdds": "707500000000000000000",
"marketHash": "0x5bea2dc8ad1be455547d1ed043cea34457c0b49a4f6aad0d4ddcb19107e9057f3",
"maker": false,
"settled": false,
"fillHash": "0xd81d39b80f1336affc84c6f03944ad5bc6d6ee1cd7a6ba8318595812d8ad11c7",
"gameLabel": "Andrey Rublev vs Fabian Marozsan",
"sportXeventId": "L13351999",
"gameTime": "2024-07-25T16:00:00.000Z",
"leagueLabel": "ATP Umag",
"bettingOutcomeLabel": "Andrey Rublev",
"bettingOutcome": 1,
"chainVersion": "SXN"
}
```
# Initialization
Source: https://docs.sx.bet/api-reference/centrifugo-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 requests
```
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](/api-reference/centrifugo-overview#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 fetchCentrifugoToken(apiKey) {
const res = await fetch(`${RELAYER_URL}/user/realtime-token/api-key`, {
headers: { "x-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: () => fetchCentrifugoToken(YOUR_API_KEY),
});
client.connect();
```
```python Python theme={null}
import asyncio
import os
import requests
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
def fetch_token():
res = requests.get(
f"{RELAYER_URL}/user/realtime-token/api-key",
headers={"x-api-key": os.environ["SX_API_KEY"]},
)
if not res.ok:
body = res.text
raise Exception(f"Token endpoint returned {res.status_code}: {body}")
return res.json()["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", { positioned: true, 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
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/api-key
)
await client.connect()
handler = SubscriptionEventHandler(on_publication=on_publication)
sub = client.new_subscription("channel:name", handler)
await sub.subscribe()
await asyncio.Future() # keep running
asyncio.run(main())
```
Pass `positioned: true, recoverable: true` together on channels with history enabled to get at-least-once delivery across reconnects. See [Namespace history capabilities](/api-reference/centrifugo-overview#namespace-history-capabilities) for which channels support this, and [Best Practices](/api-reference/centrifugo-best-practices) 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 during history recovery (see [Real-time Data → Dedup](/developers/real-time#dedup)).
## 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)
```
# Line Changes
Source: https://docs.sx.bet/api-reference/centrifugo-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 active markets section](/api-reference/get-markets-active) 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 |
To get the actual line, fetch the market using the `marketHash`.
***
```javascript JavaScript theme={null}
// To subscribe
const sub = client.newSubscription("main_line:global", { positioned: true, 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/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}
{
"marketHash": "0x38cceead7bda65c18574a34994ebd8af154725d08aa735dcbf26247a7dcc67bd",
"marketType": 3,
"sportXEventId": "L7178624"
}
```
# Live Score & Fixture Updates
Source: https://docs.sx.bet/api-reference/centrifugo-live-score-updates
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 team one. Referring to `teamOneName` in the `Market` object |
| teamTwoScore | number | The current score for team two. Referring to `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", { positioned: true, 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/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 (e.g. status changes, game time updates).
**CHANNEL NAME**
`fixtures: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("fixtures:global", { positioned: true, 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/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())
```
# Market Updates
Source: https://docs.sx.bet/api-reference/centrifugo-market-updates
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", { positioned: true, 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/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}
[
{
"gameTime": 1625674200,
"group1": "MLB",
"leagueId": 171,
"leagueLabel": "MLB",
"line": 7,
"liveEnabled": false,
"marketHash": "0x384c6d8e17c9b522a17f7bb049ede7d3dd9dd1311232fe854e7f9f4708dfc4c",
"outcomeOneName": "Over 7.0",
"outcomeTwoName": "Under 7.0",
"outcomeVoidName": "NO_GAME_OR_EVEN",
"sportId": 3,
"sportLabel": "Baseball",
"sportXEventId": "L7186379",
"status": "ACTIVE",
"teamOneName": "Tampa Bay Rays",
"teamTwoName": "Cleveland Indians",
"type": 2
}
]
```
# Migrating from Ably
Source: https://docs.sx.bet/api-reference/centrifugo-migration
Step-by-step guide for migrating from the legacy Ably WebSocket API to Centrifugo.
The legacy Ably-based WebSocket API is being deprecated. This guide covers everything you need to migrate to the Centrifugo-based API.
**Deprecation deadline: July 1, 2026.** The legacy Ably WebSocket API will be shut down on this date. All integrations must complete migration to Centrifugo before then.
The core concepts are the same — channels, subscriptions, and message payloads are structurally identical. There are six concrete changes to make.
Uninstall `ably` and install `centrifuge`:
```bash theme={null}
npm uninstall ably
npm install centrifuge
```
The auth endpoint and callback pattern have both changed.
| | Ably (old) | Centrifugo (new) |
| -------- | ------------------------------------- | -------------------------------------------- |
| Endpoint | `GET /user/token` | `GET /user/realtime-token/api-key` |
| Callback | `authCallback(tokenParams, callback)` | `getToken: async () => token` |
| WS URL | Managed by SDK | `wss://realtime.sx.bet/connection/websocket` |
**Before:**
```javascript theme={null}
import * as ably from "ably";
const realtime = new ably.Realtime.Promise({
authCallback: async (tokenParams, callback) => {
try {
const response = await fetch("https://api.sx.bet/user/token", {
headers: { "X-Api-Key": process.env.SX_BET_API_KEY },
});
callback(null, await response.json());
} catch (error) {
callback(error, null);
}
},
});
await realtime.connection.once("connected");
```
**After:**
```javascript JavaScript theme={null}
import { Centrifuge } from "centrifuge";
const client = new Centrifuge("wss://realtime.sx.bet/connection/websocket", {
getToken: async () => {
const res = await fetch("https://api.sx.bet/user/realtime-token/api-key", {
headers: { "x-api-key": process.env.SX_BET_API_KEY },
});
if (!res.ok) throw new Error(`Token endpoint returned ${res.status}`);
const { token } = await res.json();
return token;
},
});
client.connect();
```
```python Python theme={null}
import asyncio
import os
import requests
from centrifuge import Client
def fetch_token():
res = requests.get(
"https://api.sx.bet/user/realtime-token/api-key",
headers={"x-api-key": os.environ["SX_BET_API_KEY"]},
)
if not res.ok:
raise Exception(f"Token endpoint returned {res.status_code}")
return res.json()["token"]
async def main():
client = Client(
"wss://realtime.sx.bet/connection/websocket",
get_token=fetch_token,
)
await client.connect()
await asyncio.Future() # keep running
asyncio.run(main())
```
`getToken` is called automatically on initial connect and on token refresh — no manual token management needed. To signal a permanent auth failure (so the client stops retrying), throw `UnauthorizedError` from `centrifuge` on a 401/403 response. Any other thrown error is treated as transient and retried with backoff. See [Real-time Data → Auth](/developers/real-time#auth) for the full pattern.
The subscription API has changed. Message data moves from `message.data` to `ctx.data`.
**Before:**
```javascript theme={null}
const channel = realtime.channels.get("order_book_v2:0x6629...:0xabc...", {
params: { rewind: "10s" },
});
channel.subscribe((message) => {
console.log(message.data);
});
```
**After:**
```javascript JavaScript theme={null}
const sub = client.newSubscription("order_book:market_0xabc...", {
positioned: true,
recoverable: true,
});
sub.on("subscribed", async (ctx) => {
if (!ctx.wasRecovering || (ctx.wasRecovering && !ctx.recovered)) {
// Seed state from REST on fresh connect or failed recovery
const res = await fetch(`https://api.sx.bet/orders?marketHashes=0xabc...`);
applySnapshot(await res.json());
}
});
sub.on("publication", (ctx) => {
applyUpdate(ctx.data);
});
sub.subscribe();
```
```python Python theme={null}
import asyncio
import aiohttp
from centrifuge import (
Client,
PublicationContext,
SubscribedContext,
SubscriptionEventHandler,
)
async def on_subscribed(ctx: SubscribedContext) -> None:
if not ctx.was_recovering or (ctx.was_recovering and not ctx.recovered):
async with aiohttp.ClientSession() as session:
async with session.get(
"https://api.sx.bet/orders?marketHashes=0xabc..."
) as res:
apply_snapshot(await res.json())
async def on_publication(ctx: PublicationContext) -> None:
apply_update(ctx.data)
async def main():
client = Client(
"wss://realtime.sx.bet/connection/websocket",
get_token=fetch_token,
)
await client.connect()
handler = SubscriptionEventHandler(
on_subscribed=on_subscribed,
on_publication=on_publication,
)
sub = client.new_subscription("order_book:market_0xabc...", handler)
await sub.subscribe()
await asyncio.Future()
asyncio.run(main())
```
**`rewind` is replaced by `positioned` + `recoverable`.** These two flags together give you at-least-once delivery with automatic gap recovery on reconnect — equivalent to what `rewind` was doing, but more reliable. See [Real-time Data → Recovery & reliability](/developers/real-time#recovery--reliability) for how to handle the three reconnect states.
All channel names have changed. Update every channel string in your code:
| Old channel (Ably) | New channel (Centrifuge) | Notes |
| ---------------------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `active_orders_v2:{baseToken}:{maker}` | `active_orders:{maker}` | `baseToken` removed, `_v2` dropped |
| `order_book_v2:{baseToken}:{marketHash}` | `order_book:market_{marketHash}` | `baseToken` removed |
| — | `order_book:event_{sportXEventId}` | New — alternative to `order_book:market_` that covers every market in an event with a single subscription |
| `best_odds:{baseToken}` | `best_odds:global` | `baseToken` removed from channel name |
| `markets` | `markets:global` | Renamed |
| `recent_trades` | `recent_trades:global` | Renamed |
| `recent_trades_consolidated` | `recent_trades_consolidated:global` | Renamed |
| `main_line` | `main_line:global` | Renamed |
| `fixtures:fixture_update` | `fixtures:global` | Renamed |
| `live_scores:{sportXEventId}` | `fixtures:live_scores` | Now global — filter by `sportXEventId` client-side |
| `ce_refunds:{user}` | `ce_refunds:{bettor}` | Parameter renamed `user` → `bettor` |
| `markets:parlay` | `parlay_markets:global` | Renamed |
All message payloads are unchanged.
**`live_scores` is now a global channel.** The old `live_scores:{sportXEventId}` channel was per-event. The new `fixtures:live_scores` delivers all live score updates globally — filter the `sportXEventId` field in the payload to isolate the events you care about.
The Ably `rewind` parameter (which replayed recent messages automatically on subscribe) has no direct equivalent in Centrifugo. The replacement is to set `positioned: true` and `recoverable: true` on the subscription, then seed from REST inside the `subscribed` handler — which fires on every connect and tells you whether history replay already filled the gap.
**Before:**
```javascript theme={null}
const channel = realtime.channels.get(`order_book_v2:${token}:${marketHash}`, {
params: { rewind: "10s" },
});
channel.subscribe((message) => {
applyUpdate(message.data);
});
```
**After:**
```javascript JavaScript theme={null}
let ready = false;
const buffer = [];
const sub = client.newSubscription(`order_book:market_${marketHash}`, {
positioned: true,
recoverable: true,
});
sub.on("publication", (ctx) => {
if (!ready) {
buffer.push(ctx.data);
} else {
applyUpdate(ctx.data);
}
});
sub.on("subscribed", async (ctx) => {
if (ctx.wasRecovering && ctx.recovered) {
// Centrifugo replayed all missed messages — no REST call needed.
ready = true;
return;
}
// Fresh connect or failed recovery (history gap too large / expired).
// Reset and re-seed from REST.
ready = false;
buffer.length = 0;
const res = await fetch(`https://api.sx.bet/orders?marketHashes=${marketHash}`);
applySnapshot(await res.json());
// Drain buffered publications on top of the snapshot.
// applyUpdate deduplicates by entity ID so any
// overlap between the snapshot and the buffer is handled safely.
for (const data of buffer) applyUpdate(data);
buffer.length = 0;
ready = true;
});
sub.subscribe();
```
```python Python theme={null}
import asyncio
import aiohttp
from centrifuge import (
Client,
PublicationContext,
SubscribedContext,
SubscriptionEventHandler,
SubscriptionOptions,
)
async def subscribe_order_book(client: Client, market_hash: str) -> None:
ready = False
buffer: list = []
async def on_subscribed(ctx: SubscribedContext) -> None:
nonlocal ready
if ctx.was_recovering and ctx.recovered:
ready = True
return
ready = False
buffer.clear()
async with aiohttp.ClientSession() as session:
async with session.get(
f"https://api.sx.bet/orders?marketHashes={market_hash}"
) as res:
apply_snapshot(await res.json())
for data in buffer:
apply_update(data)
buffer.clear()
ready = True
async def on_publication(ctx: PublicationContext) -> None:
if not ready:
buffer.append(ctx.data)
else:
apply_update(ctx.data)
handler = SubscriptionEventHandler(
on_subscribed=on_subscribed,
on_publication=on_publication,
)
options = SubscriptionOptions(positioned=True, recoverable=True)
sub = client.new_subscription(
f"order_book:market_{market_hash}", handler, options
)
await sub.subscribe()
```
For channels with no history (`best_odds:global`, `parlay_markets:global`), omit the flags and always seed from REST in the `subscribed` handler — recovery is not available for those channels. See [Real-time Data → Recovery & reliability](/developers/real-time#recovery--reliability) for the full three-state logic.
Centrifugo provides at-least-once delivery: during history recovery a message may be replayed more than once. Use `ctx.tags.messageId` to deduplicate:
```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)
```
For long-running processes, bound the set size (e.g., keep only the last 1,000 IDs) to avoid unbounded memory growth.
***
## Further reading
* [Real-time Data →](/developers/real-time) — connecting, subscribing, recovery, and common failures
* [WebSocket Channels →](/api-reference/centrifugo-overview) — namespace history capabilities and connection limits
* [WebSocket Initialization →](/api-reference/centrifugo-initialization) — full connection reference
# Order Book Updates
Source: https://docs.sx.bet/api-reference/centrifugo-order-book-updates
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 filled, cancelled, or posted. Note that for performance reasons, updates are delayed by at most 100ms.
There are two channels for order book data. Each publish is broadcast to both:
| Channel | Subscribe when you want to... |
| ---------------------------------- | ----------------------------------- |
| `order_book:market_{marketHash}` | Track a specific market |
| `order_book:event_{sportXeventId}` | Track all markets for a given event |
The channel no longer includes a `baseToken` parameter. The payload's `marketHash` field can be used to identify the token if needed.
***
## By market
**CHANNEL NAME FORMAT**
`order_book:market_{marketHash}`
| Name | Type | Description |
| ---------- | ------ | -------------------------- |
| marketHash | string | The market to subscribe to |
```javascript JavaScript theme={null}
// To subscribe
const marketHash = "0x04b9af76dfb92e71500975db77b1de0bb32a0b2413f1b3facbb25278987519a7";
const sub = client.newSubscription(`order_book:market_${marketHash}`, { positioned: true, 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 = "0x04b9af76dfb92e71500975db77b1de0bb32a0b2413f1b3facbb25278987519a7"
client = Client(
"wss://realtime.sx.bet/connection/websocket",
token="YOUR_TOKEN", # from /user/realtime-token/api-key
)
await client.connect()
handler = SubscriptionEventHandler(on_publication=on_publication)
sub = client.new_subscription(f"order_book:market_{market_hash}", handler)
await sub.subscribe()
await asyncio.Future() # keep running
asyncio.run(main())
```
***
## By event
**CHANNEL NAME FORMAT**
`order_book:event_{sportXeventId}`
| Name | Type | Description |
| ------------- | ------ | ----------------------------------------------------------------------------- |
| sportXeventId | string | The event to subscribe to. Receives updates for all markets under this event. |
```javascript JavaScript theme={null}
// To subscribe
const eventId = "L13772588";
const sub = client.newSubscription(`order_book:event_${eventId}`, { positioned: true, 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():
event_id = "L13772588"
client = Client(
"wss://realtime.sx.bet/connection/websocket",
token="YOUR_TOKEN", # from /user/realtime-token/api-key
)
await client.connect()
handler = SubscriptionEventHandler(on_publication=on_publication)
sub = client.new_subscription(f"order_book:event_{event_id}", handler)
await sub.subscribe()
await asyncio.Future() # keep running
asyncio.run(main())
```
***
## Message payload
Both channels share the same payload format. The message is an array of order objects with the fields below — the same fields as in [the orders section](/api-reference/get-orders), with additional `status` and `updateTime` fields.
| Name | Type | Description |
| ------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| orderHash | string | A unique identifier for this order |
| status | string | `"ACTIVE"` if still valid, `"INACTIVE"` if cancelled, `"FILLED"` if completely filled |
| fillAmount | string | How much this order has been filled in Ethereum units. See [unit conversion](/api-reference/unit-conversion). |
| pendingFillAmount | string | Amount pending fill in Ethereum units. See [unit conversion](/api-reference/unit-conversion). |
| maker | string | The market maker for this order |
| totalBetSize | string | Total size of this order in Ethereum units. See [unit conversion](/api-reference/unit-conversion). |
| percentageOdds | string | The odds the `maker` receives in the sportx protocol format. To convert to implied odds divide by 10^20. To get taker implied odds: `takerOdds = 1 - percentageOdds / 10^20`. See [unit conversion](/api-reference/unit-conversion). |
| expiry | number | Deprecated. Always `2209006800`. |
| apiExpiry | number | The time in unix seconds after which this order is no longer valid |
| salt | string | A random number to differentiate identical orders |
| isMakerBettingOutcomeOne | boolean | `true` if the maker is betting outcome one |
| signature | string | Signature of the maker on this order |
| updateTime | string | Server-side clock time for the last modification of this order |
| sportXeventId | string | The event related to this order |
Messages are sent in batches as an array. If you receive two updates for the same `orderHash`, order them by `updateTime` after converting to a BigInt.
```json theme={null}
[
{
"orderHash": "0x7bd76648f589f3e272d48294d8881fe68aae7704f7b2ef0a50bf612be44271",
"status": "INACTIVE",
"fillAmount": "2000000000",
"pendingFillAmount": "1000000000",
"maker": "0x9883D5e7dC023A441A81Ef95af406C69926a0AB6",
"totalBetSize": "500000000",
"percentageOdds": "750000000000000000000",
"expiry": 1747500000000,
"apiExpiry": 1747500000000,
"salt": "12345678901234567890",
"isMakerBettingOutcomeOne": false,
"signature": "0xbf099ab02255d5e2a9e063dc43a7afe96e65f5e8fc2ed3d2ba60b0a3fcadb3441bf32271293e85b7a795c9d86a2384035a0da3285113e746547e236bc58885e01",
"updateTime": 1747490000000,
"sportXeventId": "L13772588"
}
]
```
# Websocket Channels
Source: https://docs.sx.bet/api-reference/centrifugo-overview
Real-time data via the SX Bet WebSocket API
SX Bet's real-time API uses [Centrifugo](https://centrifugal.dev/) for WebSocket connections. You can subscribe to channels for live updates on order books, markets, trades, scores, and more.
You must have a valid API key to connect. See [API Key](/api-reference/api-key) for details.
Migrating from the legacy Ably API? See the [Migration Guide →](/api-reference/centrifugo-migration)
## Reference
### Namespace history capabilities
Server-side history powers the recovery feature — see [Real-time Data → Recovery & reliability](/developers/real-time#recovery--reliability) for how to use it.
| Namespace | History enabled | Max messages | Retention window |
| ---------------------------- | --------------- | ------------ | ---------------- |
| `order_book` | Yes | 5,000 | 5 minutes |
| `recent_trades` | Yes | 2,000 | 5 minutes |
| `recent_trades_consolidated` | Yes | 1,000 | 5 minutes |
| `markets` | Yes | 1,000 | 5 minutes |
| `main_line` | Yes | 500 | 5 minutes |
| `active_orders` | Yes | 100 | 5 minutes |
| `fixtures` | Yes | 100 | 5 minutes |
| `ce_refunds` | Yes | 100 | 5 minutes |
| `best_odds` | No | — | — |
| `parlay_markets` | No | — | — |
The 5-minute retention means messages are available for up to 5 minutes, but may become unavailable sooner if the namespace's message limit is reached first — newer messages push out older ones once the cap is hit. Recovery only works for publications still present in the history window at the time of reconnect.
### 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 |
# Parlay Market Requests
Source: https://docs.sx.bet/api-reference/centrifugo-parlay-market-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 |
| baseToken | string | The token this request is denominated in |
| requestSize | number | The size in baseTokens that the bettor is requesting. See [unit conversion](/api-reference/unit-conversion). 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/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",
"baseToken": "0x1BC6326EA6aF2aB8E4b6Bc83418044B1923b2956",
"legs": [
{
"marketHash": "0x22ed4cf508418f44e787f9c8e79f76eb31587efa20fe700b3582e09f01775944",
"bettingOutcomeOne": false
},
{
"marketHash": "0x186685cf65e1a22952ad42982e1ec75b6a457fa3bdec59fa6f891258a718ddfe",
"bettingOutcomeOne": true
}
]
}
```
# Trade Updates
Source: https://docs.sx.bet/api-reference/centrifugo-trade-updates
Subscribe to real-time trade updates
Subscribe to all trade updates on the exchange. You will receive updates when a trade is settled or a new trade is placed.
**CHANNEL NAME**
`recent_trades:global`
**MESSAGE PAYLOAD FORMAT**
See [the trades section](/api-reference/get-trades) for the format of the message.
***
```javascript JavaScript theme={null}
// To subscribe
const sub = client.newSubscription("recent_trades:global", { positioned: true, 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/api-key
)
await client.connect()
handler = SubscriptionEventHandler(on_publication=on_publication)
sub = client.new_subscription("recent_trades:global", handler)
await sub.subscribe()
await asyncio.Future() # keep running
asyncio.run(main())
```
The above returns JSON structured like this:
```json theme={null}
{
"baseToken": "0x8f3Cf7ad23Cd3CaDb09735AFf958023239c6A063",
"bettor": "0x814d79A9940CbC5Af4C19cAa118EC065a77CD31f",
"stake": "999999999999999999",
"odds": "484425713316886290000",
"orderHash": "0xf7321de419b8887eafe5756d25db37ed2796dfc2495a49e266f13c8533fddb67",
"marketHash": "0x32d6c7d300dc44c795e2bdb8c735d9ad74fd2bbece89012904a5ea8ec6b566f1",
"maker": false,
"betTime": 1625668455,
"settled": false,
"bettingOutcomeOne": true,
"fillHash": "0x027f3237d9dc9dfa6068b60d852c3e972776b683a8c43b2e1a43602918de924e",
"status": "SUCCESS",
"tradeStatus": "SUCCESS"
}
```
# Why Migrate
Source: https://docs.sx.bet/api-reference/centrifugo-why
Limitations of the legacy Ably WebSocket API and the improvements Centrifugo provides
This page is only relevant if you are currently using the legacy Ably WebSocket API. If you are starting a new integration, use the [Centrifugo-based API](/api-reference/centrifugo-initialization) directly.
The legacy Ably WebSocket API had three core limitations that motivated the switch to Centrifugo: a hard cap of 200 channels per connection that required clients to shard subscriptions, a 200 msg/sec per-channel throughput limit that silently dropped messages on active markets, and imprecise gap recovery based on time-window rewinds rather than exact message IDs. Centrifugo removes all three constraints — 512 channels per connection, no artificial throughput ceiling, and server-side history with exact message ID replay. It also delivers measurably lower latency across every channel.
| | Ably (old) | Centrifugo (new) |
| ----------------------- | ---------------------------------------------------------- | ---------------------------------------------------------- |
| Channels per connection | 200 | 512 |
| Throughput per channel | 200 msg/sec (enforced) | No artificial cap |
| Delivery guarantee | Partial — time-based rewind only | Exact — message IDs + server-side history |
| Gap recovery | Time-window rewind (imprecise, risk of gaps or duplicates) | Precise history replay from last message ID (5-min window) |
| Latency | Baseline | 16–127 ms lower end-to-end latency across channels |
***
## Channel capacity
**Ably:** 200 channels per connection required clients to shard subscriptions across multiple connections.
**Centrifugo:** 512 channels per connection. A single connection handles all subscriptions without sharding.
## Throughput rate limits
**Ably:** Ably enforced a 200 msg/sec rate limit per channel server-side. On active markets this limit was regularly hit, causing messages to be dropped silently on the client. To work around this, channel design was constrained to targeted, per-market subscriptions rather than broader firehose patterns — trading flexibility for staying under the cap.
**Centrifugo:** No per-channel throughput ceiling. Capacity is bounded by infrastructure, not an enforced message rate limit.
## Delivery guarantees
**Ably:** The rewind parameter replayed the last N seconds of channel history to bridge the REST-to-subscribe gap. Without message IDs, delivery was imprecise — no guarantee against gaps or duplicates, and no way to verify completeness.
**Centrifugo:** Centrifugo assigns a message ID to every publication and maintains server-side history per channel. The [snapshot + subscribe pattern](/developers/real-time#snapshot--subscribe-pattern) uses this to eliminate the REST-to-subscribe race condition.
## Gap recovery
**Ably:** Reconnects relied on the same time-based rewind, with the same precision problem — no guarantee of exactly-once delivery across the reconnect boundary.
**Centrifugo:** Centrifugo replays missed messages automatically from server-side history within a 5-minute recovery window.
## Latency
Measured end-to-end across production channels, Centrifugo delivers messages significantly faster:
| Channel | Latency improvement |
| ---------------------------- | ------------------- |
| `main_line` | \~127 ms |
| `best_odds` | \~120 ms |
| `order_book` | \~100 ms |
| `markets` | \~104 ms |
| `recent_trades_consolidated` | \~26 ms |
| `fixture_live_scores` | \~23 ms |
| `recent_trades` | \~21 ms |
| `fixtures` | \~19 ms |
| `activity_feed` | \~16 ms |
***
Steps required to move from Ably to Centrifugo.
# Order Signing
Source: https://docs.sx.bet/api-reference/eip712-signing
How to sign order and fill payloads with your private key
Some API operations require a cryptographic signature from your wallet. The examples below show how to generate one using a private key or an injected browser wallet (e.g. MetaMask).
## Private key signing example
```js JavaScript theme={null}
import { Wallet } from "ethers";
const wallet = new Wallet(process.env.SX_PRIVATE_KEY);
const domain = {
name: "CancelOrderSportX",
version: "1.0",
chainId: 4162, // Mainnet — use 79479957 for testnet
};
const types = {
Details: [
{ name: "message", type: "string" },
{ name: "orders", type: "string[]" },
],
};
const value = {
message: "Are you sure you want to cancel these orders",
orders: [
"0x550128e997978495eeae503c13e2e30243d747e969c65e1a0b565c609e097506",
],
};
const signature = await wallet.signTypedData(domain, types, value);
```
```python Python theme={null}
import os
from eth_account import Account
from eth_account.messages import encode_typed_data
structured_data = {
"types": {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
],
"Details": [
{"name": "message", "type": "string"},
{"name": "orders", "type": "string[]"},
],
},
"primaryType": "Details",
"domain": {
"name": "CancelOrderSportX",
"version": "1.0",
"chainId": 4162, # Mainnet — use 79479957 for testnet
},
"message": {
"message": "Are you sure you want to cancel these orders",
"orders": [
"0x550128e997978495eeae503c13e2e30243d747e969c65e1a0b565c609e097506",
],
},
}
private_key = os.environ["PRIVATE_KEY"]
signable_message = encode_typed_data(full_message=structured_data)
signed = Account.sign_message(signable_message, private_key=private_key)
signature = signed.signature.hex()
```
Here we use a private key directly which is the most straightforward way to sign data and does not require access to an authenticated node. It's also the fastest.
***
## Injected provider signing example
```js theme={null}
import { BrowserProvider } from "ethers";
const provider = new BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const domain = {
name: "CancelOrderSportX",
version: "1.0",
chainId: 4162, // Mainnet — use 79479957 for testnet
};
const types = {
Details: [
{ name: "message", type: "string" },
{ name: "orders", type: "string[]" },
],
};
const value = {
message: "Are you sure you want to cancel these orders",
orders: [
"0x550128e997978495eeae503c13e2e30243d747e969c65e1a0b565c609e097506",
],
};
const signature = await signer.signTypedData(domain, types, value);
```
Here we use ethers.js and show an example with MetaMask — any injected provider that exposes `eth_signTypedData_v4` will work.
> This signing scheme uses the [EIP-712 typed data standard](https://eips.ethereum.org/EIPS/eip-712).
# Enabling betting
Source: https://docs.sx.bet/api-reference/enabling-betting
To enable betting (filling or posting orders), you need to approve the `TokenTransferProxy` contract for each token for which you wish to trade. Otherwise, any endpoints that create/cancel or fill orders will fail. For example if you want to trade with both ETH and USDC, you'll need to approve the contract twice, once for each token. The address of the `TokenTransferProxy` is available at `https://api.sx.bet/metadata` and the address of each token is given in [the tokens section](/api-reference/references).
If you don't wish to do this programmatically, you can simply go to `https://sx.bet`, make a test bet with the account and token you'll be using, and you will be good to go.
If you want to do it programmatically, see the code sample below. Note you will need a tiny bit of SX to make this transaction.
Your assets must be on SX Network to place or fill orders via the API.
```javascript JavaScript theme={null}
import { Contract, JsonRpcProvider, MaxUint256, Wallet } from "ethers";
const tokenAddress = process.env.TOKEN_ADDRESS;
const tokenTransferProxyAddress = process.env.TOKEN_TRANSFER_PROXY_ADDRESS;
const provider = new JsonRpcProvider(process.env.RPC_URL); // find this under references
const wallet = new Wallet(process.env.SX_PRIVATE_KEY, provider);
const tokenContract = new Contract(
tokenAddress,
[
{
inputs: [
{ internalType: "address", name: "usr", type: "address" },
{ internalType: "uint256", name: "wad", type: "uint256" },
],
name: "approve",
outputs: [{ internalType: "bool", name: "", type: "bool" }],
stateMutability: "nonpayable",
type: "function",
},
],
wallet
);
await tokenContract.approve(tokenTransferProxyAddress, MaxUint256, {
gasLimit: 100000,
});
```
```python Python theme={null}
from web3 import Web3
import os
w3 = Web3(Web3.HTTPProvider(os.environ["RPC_URL"]))
account = w3.eth.account.from_key(os.environ["SX_PRIVATE_KEY"])
approve_abi = [
{
"inputs": [
{"name": "usr", "type": "address"},
{"name": "wad", "type": "uint256"},
],
"name": "approve",
"outputs": [{"name": "", "type": "bool"}],
"stateMutability": "nonpayable",
"type": "function",
}
]
token = w3.eth.contract(
address=os.environ["TOKEN_ADDRESS"], abi=approve_abi
)
tx = token.functions.approve(
os.environ["TOKEN_TRANSFER_PROXY_ADDRESS"], 2**256 - 1
).build_transaction({
"from": account.address,
"gas": 100000,
"nonce": w3.eth.get_transaction_count(account.address),
})
signed = account.sign_transaction(tx)
w3.eth.send_raw_transaction(signed.raw_transaction)
```
# Fees
Source: https://docs.sx.bet/api-reference/fees
Fee structure for trading on SX Bet
## Trading
Fees are currently 0% for makers and takers on single bets. SX Bet applies a 5% fee on profit to parlay bets.
# Fixture Statuses
Source: https://docs.sx.bet/api-reference/fixture-statuses
The possible statuses for a fixture are the following:
| 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 | The event has been postponed. Postponed is sent for events which are postponed and will be played at a later time. In case no new start date/time is available within 48 hours, the event will be cancelled. In case a new start time is available within 48 hours, we will update it to "Not started yet" with the new start time. |
| 6 | Interrupted | The event has been interrupted. Interrupted is sent for interrupted events (for example - rain delay). We will continue the coverage once the event is renewed, under the same event ID. |
| 7 | Abandoned | The event has been abandoned. Abandoned is a final status sent for abandoned events (player injury in Tennis for example), these matches will not resume. |
| 8 | Coverage lost | The coverage for this event has been lost |
| 9 | About to start | The event has not started but is about to. Note: this status will be shown up to 30 minutes before the event has started. |
# Best odds
Source: https://docs.sx.bet/api-reference/get-best-odds
GET /orders/odds/best
This endpoint returns the best available odds for the specified baseToken and marketHashes or leagueIds.
**Rate limit:** All `GET /orders/*` endpoints share a combined limit of 20 requests/10s. See [Rate Limits](/developers/rate-limits).
One of `marketHashes` or `leagueIds` is required.
Only one of `marketHashes` and `leagueIds` can be present.
# Fixtures
Source: https://docs.sx.bet/api-reference/get-fixture-active
GET /fixture/active
This endpoint returns current active fixtures for a particular league. A fixture can also be thought of as an event and multiple markets are under a particular event.
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
This endpoint returns the status of the passed event IDs.
# Teams
Source: https://docs.sx.bet/api-reference/get-league-teams
GET /teams
Returns all teams supported by SX.bet. Results are paginated starting at page 0 with a maximum of 500 records per page.
# Leagues
Source: https://docs.sx.bet/api-reference/get-leagues
GET /leagues
This endpoint returns all the leagues supported by SX.bet
# Active leagues
Source: https://docs.sx.bet/api-reference/get-leagues-active
GET /leagues/active
This endpoint returns all the currently active leagues with markets in them. Note that this endpoint is only updated every 10m. The response format is the same as the get leagues endpoint, with an additional field `eventsByType` which maps the number of unique events within a particular bet group (for example, `game-lines` or `outright-winner`).
# Live scores
Source: https://docs.sx.bet/api-reference/get-live-scores
GET /live-scores
This endpoint retrieves live scores for a particular event ID.
# Active markets
Source: https://docs.sx.bet/api-reference/get-markets-active
GET /markets/active
This endpoint retrieves active markets on the exchange. It does not return markets that have been settled or reported.
To retrieve odds for a particular market, you must query the [orders endpoint](/api-reference/get-orders) separately.
Only one of `type` and `betGroup` can be present. Not both.
# Specific Markets
Source: https://docs.sx.bet/api-reference/get-markets-find
GET /markets/find
This endpoint retrieves specific 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
This endpoint retrieves the top 10 popular markets by volume.
# Metadata
Source: https://docs.sx.bet/api-reference/get-metadata
GET /metadata
This endpoint retrieves metadata on the exchange itself and useful parameters to interact with the exchange.
This a sample response. Visit [https://api.sx.bet/metadata](https://api.sx.bet/metadata) for up-to-date info.
# Active orders
Source: https://docs.sx.bet/api-reference/get-orders
GET /orders
This endpoint returns active orders on the exchange based on a few parameters
**Rate limit:** All `GET /orders/*` endpoints share a combined limit of 20 requests/10s. See [Rate Limits](/developers/rate-limits).
One of `marketHashes` or `maker` is required.
Only one of `marketHashes` and `sportXeventId` can be present.
Note that `totalBetSize` and `fillAmount` are from *the perspective of the market maker*. `totalBetSize` can be thought of as the maximum amount of tokens the maker will be putting into the pot if the order was fully filled. `fillAmount` can be thought of as how many tokens the maker has already put into the pot. To compute how much space there is left from the taker's perspective, you can use the formula `remainingTakerSpace = (totalBetSize – fillAmount) * 10^20 / percentageOdds – (totalBetSize – fillAmount)`
# Sports
Source: https://docs.sx.bet/api-reference/get-sports
GET /sports
This endpoint retrieves all sports available on the exchange
# Active trades
Source: https://docs.sx.bet/api-reference/get-trades
GET /trades
This endpoint retrieves past trades on the exchange split up by order. This is a paginated endpoint. For example, if a trade fills more than one order at once, it will show up as two entries for the bettor.
**Rate limit:** All `GET /trades/*` endpoints share a combined limit of 200 requests/min. See [Rate Limits](/developers/rate-limits).
# Consolidated trades
Source: https://docs.sx.bet/api-reference/get-trades-consolidated
GET /trades/consolidated
This endpoint retrieves past consolidated trades on the exchange via pagination. If a trade fills multiple orders, it will show up as one entry here per bettor.
**Rate limit:** All `GET /trades/*` endpoints share a combined limit of 200 requests/min. See [Rate Limits](/developers/rate-limits).
# Trades by orderHash
Source: https://docs.sx.bet/api-reference/get-trades-orders
GET /trades/orders
This endpoint retrieves trades on the exchange for the given orderHashes.
**Rate limit:** All `GET /trades/*` endpoints share a combined limit of 200 requests/min. See [Rate Limits](/developers/rate-limits).
# Portfolio refunds
Source: https://docs.sx.bet/api-reference/get-trades-refunds
GET /trades/portfolio/refunds
This endpoint retrieves capital-efficient refund events for a bettor, aggregated by market.
**Rate limit:** All `GET /trades/*` endpoints share a combined limit of 200 requests/min. See [Rate Limits](/developers/rate-limits).
It supports reconciling cases where `settledNetReturn` may differ from `netReturn` when `marketHasRefunds` is `true` (see [Capital Efficiency Upgrades](/developers/capital-efficiency)). Refunds reflect decreases in worst-case exposure (MXL) from offsetting positions and are only generated for market groups without prior cash outs; conversely, cash outs cannot occur after capital-efficient refunds.
See [Capital Efficiency Upgrade](/developers/capital-efficiency) for a high-level overview of the Capital Efficiency Upgrade.
# Heartbeat
Source: https://docs.sx.bet/api-reference/heartbeat
Automatic order cancellation on connectivity loss
A user may register a `heartbeat` to SX Bet API using the Heartbeat service. An [API Key](/api-reference/api-key) is necessary for this service. Once you register a heartbeat with the desired timeout value, all your open orders will be automatically cancelled if another heartbeat request is not sent within that timeout period.
This will ensure that when loss of connectivity occurs between your service and SX Bet API, there will be no exposure left on the orderbook.
# API Reference
Source: https://docs.sx.bet/api-reference/introduction
Be your own bookmaker or fill orders programmatically with the SX.bet API!
API Reference
Fetch markets, post orders, fill bets, and subscribe to real-time updates.
## Explore the API
Full endpoint reference: markets, orders, trades, fixtures, and more.
Real-time updates via the Centrifugo WebSocket API.
Submit a maker or taker order to the exchange.
Per-endpoint rate limits and how to stay within them.
## Base URLs
| Environment | Base URL |
| ----------------- | ---------------------------- |
| Mainnet | `https://api.sx.bet` |
| Testnet (Toronto) | `https://api.toronto.sx.bet` |
## Authentication
| Operation | Required |
| ---------------------------- | ------------------------------ |
| Read markets, odds, trades | None |
| WebSocket subscriptions | API key (`X-Api-Key` header) |
| Post, fill, or cancel orders | Wallet signature (private key) |
See [Authentication →](/developers/authentication) for setup details.
## Rate Limits
| Endpoint group | Limit |
| -------------------- | ------------- |
| All `POST /orders/*` | 5,500 req/min |
| All `GET /orders/*` | 20 req/10s |
| All `GET /trades/*` | 200 req/min |
| All other endpoints | 500 req/min |
See [Rate Limits →](/developers/rate-limits) for the full table.
All ETH addresses in the API must use **checksum format** (EIP-55 mixed case). Lowercase-only addresses will return no results.
Technical questions or need support? [Open a support chat on sx.bet.](https://sx.bet)
# Market Types
Source: https://docs.sx.bet/api-reference/market-types
A MarketType can currently be one of the following:
| ID | Name | Has Lines | Description | Bet Group |
| ---- | --------------------------------- | --------- | ------------------------------------------------------------------------- | ------------------- |
| 1 | 1X2 | false | Who will win the game (1X2) | 1X2 |
| 52 | 12 | false | Who will win the game | game-lines |
| 88 | To Qualify | false | Which team will qualify | game-lines |
| 226 | 12 Including Overtime | false | Who will win the game including overtime (no draw) | game-lines |
| 3 | Asian Handicap | true | Who will win the game with handicap (no draw) | game-lines |
| 201 | Asian Handicap Games | true | Who will win more games with handicap (no draw) | game-lines |
| 342 | Asian Handicap Including Overtime | true | Who will win the game with handicap (no draw) including Overtime | game-lines |
| 2 | Under/Over | true | Will the score be under/over a specific line | game-lines |
| 835 | Asian Under/Over | true | Will the score be under/over specific asian line | game-lines |
| 28 | Under/Over Including Overtime | true | Will the score including overtime be over/under a specific line | game-lines |
| 29 | Under/Over Rounds | true | Will the number of rounds in the match will be under/over a specific line | game-lines |
| 166 | Under/Over Games | true | Number of games will be under/over a specific line | game-lines |
| 1536 | Under/Over Maps | true | Will the number of maps be under/over a specific line | game-lines |
| 274 | Outright Winner | false | Winner of a tournament, not a single match | outright-winner |
| 202 | First Period Winner | false | Who will win the 1st Period Home/Away | first-period-lines |
| 203 | Second Period Winner | false | Who will win the 2nd Period Home/Away | second-period-lines |
| 204 | Third Period Winner | false | Who will win the 3rd Period Home/Away | third-period-lines |
| 205 | Fourth Period Winner | false | Who will win the 4th Period Home/Away | fourth-period-lines |
| 866 | Set Spread | true | Which team/player will win more sets with handicap | set-betting |
| 165 | Set Total | true | Number of sets will be under/over a specific line | set-betting |
| 53 | Asian Handicap Halftime | true | Who will win the 1st half with handicap (no draw) | first-half-lines |
| 64 | Asian Handicap First Period | true | Who will win the 1st period with handicap (no draw) | first-period-lines |
| 65 | Asian Handicap Second Period | true | Who will win the 2nd period with handicap (no draw) | second-period-lines |
| 66 | Asian Handicap Third Period | true | Who will win the 3rd period with handicap (no draw) | third-period-lines |
| 63 | 12 Halftime | false | Who will win the 1st half (no draw) | first-half-lines |
| 77 | Under/Over Halftime | true | Will the score in the 1st half be under/over a specific line | first-half-lines |
| 21 | Under/Over First Period | true | Will the score in the 1st period be under/over a specific line | first-period-lines |
| 45 | Under/Over Second Period | true | Will the score in the 2nd period be under/over a specific line | second-period-lines |
| 46 | Under/Over Third Period | true | Will the score in the 3rd period be under/over a specific line | third-period-lines |
| 281 | 1st Five Innings Asian handicap | true | Who will win the 1st five innings with handicap (no draw) | first-five-innings |
| 1618 | 1st 5 Innings Winner-12 | false | Who will win in the 1st five innings | first-five-innings |
| 236 | 1st 5 Innings Under/Over | true | Will the score in the 1st five innings be under/over a specific line | first-five-innings |
# Odds ladder
Source: https://docs.sx.bet/api-reference/odds-ladder
We enforce an odds ladder to prevent diming. Your offer, in implied odds, must fall on one of the steps on the ladder. Currently, that is set to intervals of 0.125%, meaning that your offer cannot fall between the steps. An offer of 50.25% would be valid, but an offer of 50.20% would not. You can check if your odds would fall on the ladder by taking the modulus of your odds and 1.25 \* 10 ^ 17 and checking if it's equal to 0.
You can get the current interval from [`GET /metadata`](/api-reference/get-metadata). It will spit out a number from 0 to 1000, where 10 = 0.010%, and 125 = 0.125%.
Odds not on the ladder will be rejected and your order(s) will not be posted.
## Validating and rounding odds
```javascript JavaScript theme={null}
export const ODDS_LADDER_STEP_SIZE = 125n; // (0.125% = 125n, 0.25% = 250n, etc)
/**
* Check if the odds are valid, i.e., in one of the allowed steps
* @param odds Odds to check (as bigint)
*/
export function checkOddsLadderValid(odds: bigint): boolean {
// Logic:
// 100% = 10^20
// 10% = 10^19
// 1% = 10^18
// 0.1% = 10^17
// 0.001% = 10^15 → step = 10^15 * 125 = 1.25 * 10^17 = 0.125%
const step = 10n ** 15n * ODDS_LADDER_STEP_SIZE;
return odds % step === 0n;
}
/**
* Rounds odds down to the nearest step.
* @param odds Odds to round (as bigint).
*/
export function roundDownOddsToNearestStep(odds: bigint): bigint {
const step = 10n ** 15n * ODDS_LADDER_STEP_SIZE;
return (odds / step) * step;
}
```
```python Python theme={null}
# No external dependencies required — uses the standard library only.
ODDS_LADDER_STEP_SIZE = 125 # (0.125% = 125, 0.25% = 250, etc)
STEP = 10**15 * ODDS_LADDER_STEP_SIZE # 1.25 * 10^17 = 0.125%
def check_odds_ladder_valid(odds: int) -> bool:
"""Check if odds fall on an allowed ladder step."""
# 100% = 10^20, 0.001% = 10^15 → step = 10^15 * 125 = 0.125%
return odds % STEP == 0
def round_down_odds_to_nearest_step(odds: int) -> int:
"""Round odds down to the nearest ladder step."""
return (odds // STEP) * STEP
```
# 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/ws-parlay-market-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-new-order). 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 will be able to fill orders like any other non-parlay order.
Market makers can [cancel](/api-reference/post-cancel-orders) 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 `apiExpiry` on your orders accordingly.
Parlay Markets act as regular markets, but with additional fields to indicate the underlying legs that make up the Parlay.
# Approve order fill
Source: https://docs.sx.bet/api-reference/post-approve
POST /orders/approve
This endpoint approves the specified `value` to be spent by `spender` on behalf of `owner` for token transfers that occur as part of the [Filling orders](/api-reference/post-fill-order) flow according to Ethereum's [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612) Permit Extension. Note that `deadline` field here is only used during signature verification and that the `value` set will be the spender's allowance until changed or revoked.
# Cancel all orders
Source: https://docs.sx.bet/api-reference/post-cancel-all
POST /orders/cancel/all
This endpoint cancels ALL existing orders on the exchange that you placed as a market maker.
**Rate limit:** All `POST /orders/*` endpoints share a combined limit of 5,500 requests/min. See [Rate Limits](/developers/rate-limits).
# Cancel event orders
Source: https://docs.sx.bet/api-reference/post-cancel-event
POST /orders/cancel/event
This endpoint cancels existing orders on the exchange for a particular event that you placed as a market maker.
**Rate limit:** All `POST /orders/*` endpoints share a combined limit of 5,500 requests/min. See [Rate Limits](/developers/rate-limits).
# Cancel individual orders
Source: https://docs.sx.bet/api-reference/post-cancel-orders
POST /orders/cancel/v2
This endpoint cancels existing orders on the exchange that you placed as a market maker. If passed orders that do not exist, they simply fail silently while the others will succeed.
**Rate limit:** All `POST /orders/*` endpoints share a combined limit of 5,500 requests/min. See [Rate Limits](/developers/rate-limits).
# Filling orders
Source: https://docs.sx.bet/api-reference/post-fill-order
POST /orders/fill/v2
**Rate limit:** All `POST /orders/*` endpoints share a combined limit of 5,500 requests/min. See [Rate Limits](/developers/rate-limits).
This endpoint fills orders on the exchange based on the specified desiredOdds and oddsSlippage. Order matching is done internally after the built-in betting delay, optimizing the taker experience particularly during in-play betting. Furthermore, if any new orders with better odds are added during the betting delay window, those orders will be filled. Lastly, if there isn't sufficient size to support the full stake amount, the response will include a isPartialFill: true flag to indicate that the fill was only partially filled. Takers can then attempt to fill again if they choose to do so.
To fill orders on sx.bet via the API, make sure you first enable betting by following the steps [here](/api-reference/enabling-betting).
Your assets must be on SX Mainnet to place bets.
# Register Heartbeat
Source: https://docs.sx.bet/api-reference/post-heartbeat
POST /heartbeat
To register and refresh your heartbeat with 0 > `timeoutSeconds` <= 3600. Ping this endpoint to maintain your heartbeat after initial registration.
# Cancel Heartbeat
Source: https://docs.sx.bet/api-reference/post-heartbeat-cancel
POST https://api.sx.bet/heartbeat
Cancel a registered heartbeat
To cancel a registered heartbeat, you may use the same request to [register a heartbeat](/api-reference/post-heartbeat) but use `timeoutSeconds=0`. This will deactivate the heartbeat and **orders will NOT be cancelled automatically**.
```bash theme={null}
curl --location --request POST 'https://api.sx.bet/heartbeat' \
--header 'Content-Type: application/json' \
--header 'X-Api-Key: ' \
--data-raw '{
"requestor": "",
"timeoutSeconds": 0
}'
```
The above command returns JSON structured like this
```json theme={null}
{
"status": "success",
"data": {
"requestor": "",
"timeoutSeconds": 0,
"expiresAt": "2024-11-12T14:35:06.614Z"
}
}
```
# Post a new order
Source: https://docs.sx.bet/api-reference/post-new-order
POST /orders/new
This endpoint offers new orders on the exchange (market making). Offering orders does not cost any fee.
**Rate limit:** All `POST /orders/*` endpoints share a combined limit of 5,500 requests/min. See [Rate Limits](/developers/rate-limits).
Note you can offer as many orders as you wish, provided your total exposure for each token (as measured by `totalBetSize - fillAmount`) remains under your wallet balance. If your wallet balance dips under your total exposure, orders will be removed from the book until it reaches the minimum again.
Your assets must be on SX Mainnet to place orders.
If the API finds that your balance is consistently below your total exposure requiring orders to be cancelled, your account may be temporarily restricted.
To offer bets on sx.bet via the API, make sure you first enable betting by following the steps [here](/api-reference/enabling-betting).
## Status messages
The following status messages exist for each order:
| Message | Description |
| ---------------------- | ------------------------------------------------------------------------------- |
| `OK` | The order was created successfully |
| `INSUFFICIENT_BALANCE` | The order could not be created due to insufficient maker token balance |
| `INVALID_MARKET` | The order could not be created since the user specified non-unique marketHashes |
| `ORDERS_ALREADY_EXIST` | The order already exists |
Note that `totalBetSize` is from **the perspective of the market maker**. `totalBetSize` can be thought of as the maximum amount of tokens the maker (you) will be putting into the pot if the order was fully filled. This is the maximum amount you will risk.
# References
Source: https://docs.sx.bet/api-reference/references
Endpoints, chain IDs, contract addresses, and network details for mainnet and testnet
## Mainnet
| Reference Type | Data |
| ------------------ | -------------------------------------------- |
| Chain ID | `4162` |
| USDC Token Address | `0x6629Ce1Cf35Cc1329ebB4F63202F3f197b3F050B` |
| SX Application | `https://sx.bet` |
| SX API URL | `https://api.sx.bet` |
| RPC URL | `https://rpc-rollup.sx.technology` |
| Explorer Page | `https://explorerl2.sx.technology` |
| METADATA | `https://api.sx.bet/metadata` |
## Testnet
| Reference Type | Data |
| ------------------ | -------------------------------------------- |
| Chain ID | `79479957` |
| USDC Token Address | `0x1BC6326EA6aF2aB8E4b6Bc83418044B1923b2956` |
| SX Application | `https://toronto.sx.bet` |
| SX API URL | `https://api.toronto.sx.bet` |
| RPC URL | `https://rpc-rollup.toronto.sx.technology` |
| Explorer Page | `https://explorerl2.toronto.sx.technology` |
| METADATA | `https://api.toronto.sx.bet/metadata` |
## Versions of dependencies
These are the latest versions of js dependencies that are referenced in documentation:
* `ethers`: 6.16.0
# 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. Here is a table for the tokens supported by SX.bet and their associated decimals value
| Token | Address | Decimals |
| ----- | --------------------------------------------- | -------- |
| USDC | See `https://api.sx.bet/metadata` for address | 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` is specified in the above table.
***
## Odds
Odds are specified in an implied odds format like `839135214364235000`. To convert to a readable implied odds, divide by `10^20`. `839135214364235000` for example is 0.0839 or 8.39%
To convert from implied odds to decimal odds, inverse the number. For example, 0.0839 in decimal format is `1/0.0839 = 11.917`.
***
## Bookmaker odds
It's important to note how odds are displayed on sx.bet. Recall from [the order section](https://api.docs.sx.bet/#get-active-orders) that `percentageOdds` is from the perspective of the market maker. The odds that are displayed on sx.bet in the order books are what the taker will be receiving. Let's run through an example.
Suppose an order looks like the one on the right.
Here the maker is betting outcome one (`isMakerBettingOutcomeOne = true`) and receiving implied odds of `704552840724436400000 / 10^20 = 0.704552841`. Therefore the taker is betting *outcome two* and receiving implied odds of `1 - 0.704552841 = 0.295447159`. This would be displayed on sx.bet (what the user sees) under the second order book with odds of 29.5% in implied format, or `1 / 0.295447159 = 3.3847` in decimal format.
# Active Order Updates
Source: https://docs.sx.bet/api-reference/ws-active-order-updates
Subscribe to real-time changes in a user's orders
This WebSocket API will be deprecated on July 1, 2026. See the [Migration Guide](/api-reference/centrifugo-migration) to move to the Centrifuge-based API.
Subscribe to changes in a particular user's orders. You will receive updates when orders are filled, cancelled, or posted. Note that for performance reasons, updates are delayed by at most 100ms.
**CHANNEL NAME FORMAT**
`active_orders_v2:{token}:{user}`
| Name | Type | Description |
| ----- | ------ | --------------------------------------------------------- |
| token | string | Restrict updates to only orders denominated in this token |
| user | string | The user to subscribe to |
**MESSAGE PAYLOAD FORMAT**
The message payload is an array of JSON objects representing each object with the fields below. Note that these are the same fields as mentioned in the [the orders section](/api-reference/get-orders), with an additional `status` and `updateTime` field.
| Name | Type | Description |
| ------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| orderHash | string | A unique identifier for this order |
| marketHash | string | The market for this order |
| status | string | "ACTIVE" if this order is still valid, "INACTIVE" otherwise |
| fillAmount | string | How much this order has been filled in Ethereum units up to a max of `totalBetSize`. See [the token section](/api-reference/unit-conversion) of how to convert this into nominal amounts |
| pendingFillAmount | string | What amount is pending fill in Ethereum units up to a max of `totalBetSize`. See [the token section](/api-reference/unit-conversion) of how to convert this into nominal amounts |
| totalBetSize | string | The total size of this order in Ethereum units. See [the token section](/api-reference/unit-conversion) for how to convert this into nominal amounts. |
| percentageOdds | string | The odds that the `maker` receives in the sportx protocol format. To convert to an implied odds divide by 10^20. To convert to the odds that the taker would receive if this order would be filled in implied format, use the formula `takerOdds=1-percentageOdds/10^20`. See [the unit conversion section](/api-reference/unit-conversion) for more details. |
| expiry | number | Depcreated field: the time in unix seconds after which this order is no longer valid. Always 2209006800 |
| apiExpiry | number | The time in unix seconds after which this order is no longer valid |
| salt | string | A random number to differentiate identical orders |
| isMakerBettingOutcomeOne | boolean | `true` if the maker is betting outcome one (and hence taker is betting outcome two if filled) |
| signature | string | Signature of the maker on this order |
| updateTime | string | Server-side clock time for the last modification of this order. |
| sportXeventId | string | The event related to this order |
Note that the messages are sent in batches in an array. If you receive two updates for the same `orderHash` within an update, you can order them by `updateTime` after converting the `updateTime` to a BigInt or BigNumber.
***
```javascript theme={null}
const user = "0x082605F78dD03A8423113ecbEB794Fb3FFE478a2";
const token = process.env.USDC_TOKEN_ADDRESS; // get from https://api.sx.bet/metadata
const channel = realtime.channels.get(`active_orders_v2:${token}:${user}`);
channel.subscribe((message) => {
console.log(message.data);
});
```
The above command returns JSON structured like this
```json theme={null}
[
{
"orderHash": "0xabcdef1234567890abcdef1234567890abcdef1234567890",
"marketHash": "0x1234567890abcdef1234567890abcdef1234567890abcd",
"status": "INACTIVE",
"fillAmount": "100000000000000000",
"pendingFillAmount": "500000000000000000",
"totalBetSize": "2000000000000000000",
"percentageOdds": "750000000000000000000",
"expiry": 1747500000000,
"apiExpiry": 1747500000000,
"salt": "12345678901234567890123456789012345678901",
"isMakerBettingOutcomeOne": false,
"signature": "0xbf099ab02255d5e2a9e063dc43a7afe96e65f5e8fc2ed3d2ba60b0a3fcadb3441bf32271293e85b7a795c9d86a2384035a0da3285113e746547e236bc58885e0",
"updateTime": 1747490000000,
"sportXeventId": "L13772588"
}
]
```
# Best Odds
Source: https://docs.sx.bet/api-reference/ws-best-odds
Subscribe to real-time best odds changes
This WebSocket API will be deprecated on July 1, 2026. See the [Migration Guide](/api-reference/centrifugo-migration) to move to the Centrifuge-based API.
Subscribe to best odds changes in a particular order book for the given base token. You will receive updates when orders are filled, cancelled, or posted. Note that for performance reasons, updates are delayed by at most 100ms.
**CHANNEL NAME FORMAT**
`best_odds:{baseToken}`
| Name | Type | Description |
| --------- | ------ | --------------------------------------------------------- |
| baseToken | string | Restrict updates to only orders denominated in this token |
**MESSAGE PAYLOAD FORMAT**
The message payload is an array of JSON objects representing each object with the fields below.
| Name | Type | Description |
| ------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| baseToken | string | The token for the best odds result order |
| marketHash | string | The market for the best odds result |
| isMakerBettingOutcomeOne | boolean | Whether or not the maker is betting outcome one. If false, maker is betting outcome two. |
| percentageOdds | string | The odds that the `maker` receives in the sportx protocol format. To convert to an implied odds divide by 10^20. To convert to the odds that the taker would receive if this order would be filled in implied format, use the formula `takerOdds=1-percentageOdds/10^20`. See [the unit conversion section](/api-reference/unit-conversion) for more details. |
| updatedAt | number | The timestamp in milliseconds for when the odds became the best |
Note that the messages are sent in batches in an array. If you receive two updates for the same `orderHash` within an update, you can order them by `updateTime` after converting the `updateTime` to a BigInt or BigNumber.
***
```javascript theme={null}
const baseToken = "0xa25dA0331C0853FD17C47c8c34BCCBAaF516C438";
const channel = realtime.channels.get(`best_odds:${baseToken}`);
channel.subscribe((message) => {
console.log(message.data);
});
```
The above command returns JSON structured like this
```json theme={null}
[
{
"baseToken": "0x1BC6326EA6aF2aB8E4b68c83418044B1923b2956",
"marketHash": "0xddaf2ef56d0db2317cf9a1e1dde3de2f2158e28bee55fe35a684389f4dce0cf6",
"isMakerBettingOutcomeOne": true,
"percentageOdds": "750000000000000000000",
"updatedAt": 1747500000000
}
]
```
# Best Practices
Source: https://docs.sx.bet/api-reference/ws-best-practices
Recommended patterns for combining HTTP requests with WebSocket subscriptions
This WebSocket API will be deprecated on July 1, 2026. See the [Migration Guide](/api-reference/centrifugo-migration) to move to the Centrifuge-based API.
For optimal state updates, we recommend a combination of HTTP requests and channel subscriptions, utilizing the rewind parameter. HTTP requests provide the current state, while channel subscriptions keep the state updated. The rewind parameter ensures playback of past events, preventing any missed events between the HTTP call and subscription. See [this link](https://ably.com/docs/channels#rewind) for an overview of the rewind parameter and more.
***
```javascript theme={null}
const Ably = require("ably");
async function getOrders(marketHash, token) {
const response = await fetch(
`https://api.sx.bet/orders?marketHashes=${marketHash}&baseToken=${token}`,
{
headers: {
"Content-Type": "application/json",
"X-Api-Key": this.apiKey,
},
}
);
console.log(await response.json());
}
async function orderStream(realtime, marketHash, token) {
const channel = realtime.channels.get(`order_book:${token}:${marketHash}`, {
params: { rewind: "10s" },
});
channel.subscribe((message) => {
console.log(message.data);
});
}
async function createTokenRequest() {
console.log("createTokenRequest");
const response = await fetch("https://api.sx.bet/user/token", {
headers: {
"X-Api-Key": this.apiKey,
"Content-Type": "application/json",
},
});
return response.json();
}
async function initialize() {
const ablyClient = new Ably.Realtime.Promise({
authUrl: "https://ably.com/ably-auth/token/docs",
});
const realtime = new Ably.Realtime.Promise({
authCallback: async (tokenParams, callback) => {
try {
const tokenRequest = await createTokenRequest();
callback(null, tokenRequest);
} catch (error) {
callback(error, null);
}
},
});
await ablyClient.connection.once("connected");
return realtime;
}
async function main() {
const realtime = await initialize();
await getOrders(this.marketHash, this.token);
await orderStream(realtime, this.marketHash, this.token);
}
main();
```
# CE Refund Events
Source: https://docs.sx.bet/api-reference/ws-ce-refund-events
Subscribe to real-time capital-efficiency refund events
This WebSocket API will be deprecated on July 1, 2026. See the [Migration Guide](/api-reference/centrifugo-migration) to move to the Centrifuge-based API.
Subscribe to changes in a particular user's capital-efficient refund events. You receive updates when refunds are generated from reductions in maximum loss (MXL) for the user's market groups.
For field definitions, see [Get portfolio refunds](/api-reference/get-trades-refunds).
See [Capital Efficiency](/developers/capital-efficiency) for a high-level overview.
**CHANNEL NAME FORMAT**
`ce_refunds:{user}`
| Name | Type | Description |
| ---- | ------ | ------------------------ |
| user | string | The user to subscribe to |
**MESSAGE PAYLOAD FORMAT**
The message payload matches a single element within the `GET /trades/portfolio/refunds` JSON results. See [Get portfolio refunds](/api-reference/get-trades-refunds) for schema details.
***
```javascript theme={null}
const user = "0xaD6A65315Cb20dD0b9D0Af56213516727a20C66F";
const channel = realtime.channels.get(`ce_refunds:${user}`);
channel.subscribe((message) => {
console.log(message.data);
});
```
The above command returns JSON structured like this
```json theme={null}
[
{
"marketHash": "0x3e012cc2842849b96768547d4c92720d7ee8946e7706323f5114b6451708cf5e",
"baseToken": "0x1BC6326EA6aF2aB8E4b68c83418044B1923b2956",
"totalRefunded": 2.346033,
"events": [
{
"maker": false,
"amount": "2.346033",
"bettor": "0xaD6A65315Cb20dD0b9D0Af56213516727a20C66F",
"baseToken": "0x1BC6326EA6aF2aB8E4b68c83418044B1923b2956",
"createdAt": "2025-10-21T14:29:26.805266+00:00",
"marketHash": "0x3e012cc2842849b96768547d4c92720d7ee8946e7706323f5114b6451708cf5e",
"fillOrderHash": "0x7efa8ee211c5cbccebda722318252ee09cfadaa9c910bf4c433086d853784b02"
}
]
}
]
```
# Consolidated Trade Updates
Source: https://docs.sx.bet/api-reference/ws-consolidated-trade-updates
Subscribe to real-time consolidated trade updates
This WebSocket API will be deprecated on July 1, 2026. See the [Migration Guide](/api-reference/centrifugo-migration) to move to the Centrifuge-based API.
Subscribe to all consolidated trade updates on the exchange. You will receive updates when a consolidated trade is settled or a new consolidated trade available.
**CHANNEL NAME FORMAT**
`recent_trades_consolidated`
**MESSAGE PAYLOAD FORMAT**
See [the trades section](/api-reference/get-trades-consolidated) for the format of the message
***
```javascript theme={null}
const channel = realtime.channels.get('recent_trades_consolidated');
channel.subscribe((message) => {
console.log(message.data);
});
```
The above command returns JSON structured like this
```json theme={null}
{
"baseToken": "0x5147891461a7C81075950f8eE6384e019e39ab90",
"tradeStatus": "PENDING",
"bettor": "0x1562258769E6c0527bd83502E9dfc803929fa446",
"totalStake": "10.0",
"weightedAverageOdds": "707500000000000000000",
"marketHash": "0x5bea2dc8ad1be455547d1ed043cea34457c0b49a4f6aad0d4ddcb19107e9057f3",
"maker": false,
"settled": false,
"fillHash": "0xd81d39b80f1336affc84c6f03944ad5bc6d6ee1cd7a6ba8318595812d8ad11c7",
"gameLabel": "Andrey Rublev vs Fabian Marozsan",
"sportXeventId": "L13351999",
"gameTime": "2024-07-25T16:00:00.000Z",
"leagueLabel": "ATP Umag",
"bettingOutcomeLabel": "Andrey Rublev",
"bettingOutcome": 1,
"chainVersion": "SXN"
}
```
# Initialization
Source: https://docs.sx.bet/api-reference/ws-initialization
Connect to the SX Bet real-time WebSocket API using Ably
This WebSocket API will be deprecated on July 1, 2026. See the [Migration Guide](/api-reference/centrifugo-migration) to move to the Centrifuge-based API.
We use the Ably SDK to allow users to connect to our API. It supports pretty much every major language but all of the examples on this page will be in JavaScript. The API is relatively identical across languages though. See [this link](https://ably.com/docs) for a basic overview of the API in other languages.
You only need one instance of the `ably` object to connect to the API. Connections to multiple channels are multiplexed though the single network connection. If you create too many individual connections, you will be forcefully unsubscribed from all channels and disconnected.
All the examples following assume you have a `realtime` object in scope following the initialization code to the right.
***
```javascript theme={null}
import * as ably from "ably";
async function createTokenRequest() {
const response = await fetch("https://api.sx.bet/user/token", {
headers: {
"X-Api-Key": process.env.SX_BET_API_KEY,
},
});
return response.json();
}
async function initialize() {
const realtime = new ably.Realtime.Promise({
authCallback: async (tokenParams, callback) => {
try {
const tokenRequest = await createTokenRequest();
// Make a network request to GET /user/token passing in
// `X-Api-Key: [YOUR_API_KEY]` as a header
callback(null, tokenRequest);
} catch (error) {
callback(error, null);
}
},
});
await ablyClient.connection.once("connected");
}
```
# Line Changes
Source: https://docs.sx.bet/api-reference/ws-line-changes
Subscribe to real-time line changes
This WebSocket API will be deprecated on July 1, 2026. See the [Migration Guide](/api-reference/centrifugo-migration) to move to the Centrifuge-based API.
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 active markets section](/api-reference/get-markets-active) for details on which types have lines.
**CHANNEL NAME FORMAT**
`main_line`
**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 |
To get the actual line, you'll have to fetch the market using the `marketHash`
***
```javascript theme={null}
const channel = realtime.channels.get('main_line');
channel.subscribe((message) => {
console.log(message.data);
});
```
The above command returns JSON structured like this:
```json theme={null}
{
"marketHash": "0x38cceead7bda65c18574a34994ebd8af154725d08aa735dcbf26247a7dcc67bd",
"marketType": 3,
"sportXEventId": "L7178624"
}
```
# Live Score Updates
Source: https://docs.sx.bet/api-reference/ws-live-score-updates
Subscribe to real-time live score changes for a specific event
This WebSocket API will be deprecated on July 1, 2026. See the [Migration Guide](/api-reference/centrifugo-migration) to move to the Centrifuge-based API.
Subscribe to live score changes for a particular event.
**CHANNEL NAME FORMAT**
`live_scores:{sportXEventId}`
| Name | Type | Description |
| ------------- | ------ | ------------------------------------- |
| sportXEventId | string | The event ID you wish to subscribe to |
**MESSAGE PAYLOAD FORMAT**
| Name | Type | Description |
| ------------- | -------- | ---------------------------------------------------------------------------------------- |
| teamOneScore | number | The current score for team one. Referring to `teamOneName` in the `Market` object itself |
| teamTwoScore | number | The current score for team two. Referring to `teamTwoName` in the `Market` object itself |
| 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 (for example, in 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. Referring to `teamOneName` in the `Market` object itself |
| teamTwoScore | string | The score of team two. Referring to `teamTwoName` in the `Market` object itself |
To get the actual line, you'll have to fetch the market using the `marketHash`
***
```javascript theme={null}
const sportXEventId = "L7178624";
const channel = realtime.channels.get(`live_scores:${sportXEventId}`);
channel.subscribe((message) => {
console.log(message.data);
});
```
The above command 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": "2nd Set",
"isFinished": true,
"teamOneScore": "6",
"teamTwoScore": "3"
},
{
"label": "3rd Set",
"isFinished": true,
"teamOneScore": "7",
"teamTwoScore": "5"
},
{
"label": "4th Set",
"isFinished": false,
"teamOneScore": "1",
"teamTwoScore": "2"
},
{
"label": "Game",
"isFinished": false,
"teamOneScore": "0",
"teamTwoScore": "0"
}
],
"extra": "[{\"Name\":\"Turn\",\"Value\":\"2\"},{\"Name\":\"DoubleFaults\",\"Value\":\"{\\\"3/6\\\":\\\"0,0,0,0,0\\\",\\\"3/7\\\":\\\"0,2,0,0,0\\\",\\"
}
```
# Market Updates
Source: https://docs.sx.bet/api-reference/ws-market-updates
Subscribe to real-time market changes
This WebSocket API will be deprecated on July 1, 2026. See the [Migration Guide](/api-reference/centrifugo-migration) to move to the Centrifuge-based API.
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 FORMAT**
`markets`
**MESSAGE PAYLOAD FORMAT**
See [the markets section](/api-reference/get-markets-active) for the format of the message
***
```javascript theme={null}
const channel = realtime.channels.get('markets');
channel.subscribe((message) => {
console.log(message.data);
});
```
The above command returns JSON structured like this:
```json theme={null}
[
{
"gameTime": 1625674200,
"group1": "MLB",
"leagueId": 171,
"leagueLabel": "MLB",
"line": 7,
"liveEnabled": false,
"marketHash": "0x384c6d8e17c9b522a17f7bb049ede7d3dd9dd1311232fe854e7f9f4708dfc4c",
"outcomeOneName": "Over 7.0",
"outcomeTwoName": "Under 7.0",
"outcomeVoidName": "NO_GAME_OR_EVEN",
"sportId": 3,
"sportLabel": "Baseball",
"sportXEventId": "L7186379",
"status": "ACTIVE",
"teamOneName": "Tampa Bay Rays",
"teamTwoName": "Cleveland Indians",
"type": 2
}
]
```
# Order Book Updates
Source: https://docs.sx.bet/api-reference/ws-order-book-updates
Subscribe to real-time changes in a market's order book
This WebSocket API will be deprecated on July 1, 2026. See the [Migration Guide](/api-reference/centrifugo-migration) to move to the Centrifuge-based API.
Subscribe to changes in a particular order book. You will receive updates when orders are filled, cancelled, or posted. Note that for performance reasons, updates are delayed by at most 100ms.
**CHANNEL NAME FORMAT**
`order_book_v2:{token}:{marketHash}`
| Name | Type | Description |
| ---------- | ------ | --------------------------------------------------------- |
| token | string | Restrict updates to only orders denominated in this token |
| marketHash | string | The market to subscribe to |
**MESSAGE PAYLOAD FORMAT**
The message payload is an array of JSON objects representing each object with the fields below. Note that these are the same fields as mentioned in [the orders section](/api-reference/get-orders), with an additional `status` and `updateTime` field.
| Name | Type | Description |
| ------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| orderHash | string | A unique identifier for this order |
| status | string | "ACTIVE" if this order is still valid, "INACTIVE" if cancelled, "FILLED" if completely filled |
| fillAmount | string | How much this order has been filled in Ethereum units up to a max of `totalBetSize`. See [the token section](/api-reference/unit-conversion) of how to convert this into nominal amounts |
| pendingFillAmount | string | What amount is pending fill in Ethereum units up to a max of `totalBetSize`. See [the token section](/api-reference/unit-conversion) of how to convert this into nominal amounts |
| maker | string | The market maker for this order |
| totalBetSize | string | The total size of this order in Ethereum units. See [the token section](/api-reference/unit-conversion) for how to convert this into nominal amounts. |
| percentageOdds | string | The odds that the `maker` receives in the sportx protocol format. To convert to an implied odds divide by 10^20. To convert to the odds that the taker would receive if this order would be filled in implied format, use the formula `takerOdds=1-percentageOdds/10^20`. See [the unit conversion section](/api-reference/unit-conversion) for more details. |
| expiry | number | Depcreated field: the time in unix seconds after which this order is no longer valid. Always 2209006800 |
| apiExpiry | number | The time in unix seconds after which this order is no longer valid |
| salt | string | A random number to differentiate identical orders |
| isMakerBettingOutcomeOne | boolean | `true` if the maker is betting outcome one (and hence taker is betting outcome two if filled) |
| signature | string | Signature of the maker on this order |
| updateTime | string | Server-side clock time for the last modification of this order. |
| sportXeventId | string | The event related to this order |
Note that the messages are sent in batches in an array. If you receive two updates for the same `orderHash` within an update, you can order them by `updateTime` after converting the `updateTime` to a BigInt or BigNumber.
***
```javascript theme={null}
const marketHash =
"0x04b9af76dfb92e71500975db77b1de0bb32a0b2413f1b3facbb25278987519a7";
const token = "0xa25dA0331C0853FD17C47c8c34BCCBAaF516C438";
const channel = realtime.channels.get(`order_book_v2:${token}:${marketHash}`);
channel.subscribe((message) => {
console.log(message.data);
});
```
The above command returns JSON structured like this
```json theme={null}
[
{
"orderHash": "0x7bd76648f589f3e272d48294d8881fe68aae7704f7b2ef0a50bf612be44271",
"status": "INACTIVE",
"fillAmount": "2000000000",
"pendingFillAmount": "1000000000",
"maker": "0x9883D5e7dC023A441A81Ef95af406C69926a0AB6",
"totalBetSize": "500000000",
"percentageOdds": "750000000000000000000",
"expiry": 1747500000000,
"apiExpiry": 1747500000000,
"salt": "12345678901234567890",
"isMakerBettingOutcomeOne": false,
"signature": "0xbf099ab02255d5e2a9e063dc43a7afe96e65f5e8fc2ed3d2ba60b0a3fcadb3441bf32271293e85b7a795c9d86a2384035a0da3285113e746547e236bc58885e01",
"updateTime": 1747490000000,
"sportXeventId": "L13772588"
}
]
```
# Websocket API
Source: https://docs.sx.bet/api-reference/ws-overview
Real-time data via the SX Bet WebSocket API
This WebSocket API will be deprecated on July 1, 2026. See the [Migration Guide](/api-reference/centrifugo-migration) to move to the Centrifuge-based API.
You must have a valid API key to subscribe to realtime channels via the API. See [API Key](/api-reference/api-key) for more info.
You can connect to the websocket API and listen for realtime changes on several resources such as the order book, markets, scores, and line updates.
# Parlay Market Requests
Source: https://docs.sx.bet/api-reference/ws-parlay-market-requests
Subscribe to real-time parlay market requests
This WebSocket API will be deprecated on July 1, 2026. See the [Migration Guide](/api-reference/centrifugo-migration) to move to the Centrifuge-based API.
When a bettor requests a Parlay Market, a message is sent via the `markets:parlay` channel. In order to offer orders on Parlay Markets, you will need to subscribe to this channel. The payload will contain the `marketHash` that is 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 that the bettor is requesting. The `legs` in the payload contain the underlying legs that make up the parlay market. You can query for market data on each leg's `marketHash` to determine current orders for that individual market.
**`ParlayMarket` PAYLOAD FORMAT**
| Name | Type | Description |
| ----------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| marketHash | string | The parlay market associated with this request |
| baseToken | string | The token this request is denominated in |
| requestSize | number | The size in baseTokens that the bettor is requesting. See [the token section](/api-reference/unit-conversion) of how to convert this into nominal amounts |
| 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 an individual leg within the parlay |
Note that the `requestSize` is only indicating what the user is requesting, but does not limit market makers in how much they want to offer. You are allowed to offer any size.
***
```javascript theme={null}
const channel = realtime.channels.get('markets:parlay');
channel.subscribe((message) => {
console.log(message.data);
});
```
The above command returns JSON structured like this:
```json theme={null}
{
"marketHash": "0x38cceead7bda65c18574a34994ebd8af154725d08aa735dcbf26247a7dcc67bd",
"baseToken": "0x8f3Cf7ad23Cd3CaDb09735AFf958023239c6A063",
"requestSize": "100000000",
"legs": [
{
"marketHash": "0x0d64c52e8781acdada86920a2d1e5acd6f29dcfe285cf9cae367b671dff05f7d",
"bettingOutcomeOne": true
},
{
"marketHash": "0xe609a49d083cd41214a0db276c1ba323c4a947eefd2e4260386fec7b5d258188",
"bettingOutcomeOne": false
}
]
}
```
# Trade Updates
Source: https://docs.sx.bet/api-reference/ws-trade-updates
Subscribe to real-time trade updates
This WebSocket API will be deprecated on July 1, 2026. See the [Migration Guide](/api-reference/centrifugo-migration) to move to the Centrifuge-based API.
Subscribe to all trade updates on the exchange. You will receive updates when a trade is settled or a new trade is placed.
**CHANNEL NAME FORMAT**
`recent_trades`
**MESSAGE PAYLOAD FORMAT**
See [the trades section](/api-reference/get-trades) for the format of the message
***
```javascript theme={null}
const channel = realtime.channels.get('recent_trades');
channel.subscribe((message) => {
console.log(message.data);
});
```
The above command returns JSON structured like this
```json theme={null}
{
"baseToken": "0x8f3Cf7ad23Cd3CaDb09735AFf958023239c6A063",
"bettor": "0x814d79A9940CbC5Af4C19cAa118EC065a77CD31f",
"stake": "999999999999999999",
"odds": "484425713316886290000",
"orderHash": "0xf7321de419b8887eafe5756d25db37ed2796dfc2495a49e266f13c8533fddb67",
"marketHash": "0x32d6c7d300dc44c795e2bdb8c735d9ad74fd2bbece89012904a5ea8ec6b566f1",
"maker": false,
"betTime": 1625668455,
"settled": false,
"bettingOutcomeOne": true,
"fillHash": "0x027f3237d9dc9dfa6068b60d852c3e972776b683a8c43b2e1a43602918de924e",
"status": "SUCCESS",
"tradeStatus": "SUCCESS"
}
```
# Accounts
Source: https://docs.sx.bet/developers/accounts
Understand accounts and authentication with the SX Bet API.
## Creating an account
Sign up at [sx.bet](https://sx.bet) and choose a username. There are two ways to sign up:
* **Email or Google** — SX Bet creates a non-custodial [Magic](https://magic.link/) wallet for you, tied to your email address. Easiest for users with no prior crypto experience.
* **Connect a wallet** — If you already have a crypto wallet like [MetaMask](https://metamask.io) or [Rabby](https://rabby.io), you can connect it directly instead.
***
## Your wallet, your funds
SX Bet is **non-custodial** — your funds remain in your personal wallet at all times. This is fundamentally different from a traditional sportsbook where your deposit is held in an account the company controls.
Here's how it works in practice:
Your USDC sits in your personal crypto wallet — whether that's your email wallet or MetaMask/Rabby.
When you place a bet, you cryptographically sign the transaction with your private key. This authorizes the transfer — nothing moves without your explicit approval.
Your stake moves from your wallet into an escrow smart contract. The same happens for the users on the other side of your bet. Neither party can touch the funds while the market is open — the contract holds them trustlessly.
When the event concludes, the escrow contract automatically pays out the winnings directly to the winner's wallet.
***
## Your address and private key
Every wallet has two components:
* Your **address** is your public identity on SX Bet — it looks like `0x52adf738AAD93c31f798a30b2C74D658e1E9a562`. This is how the API identifies you, and it's safe to share.
* Your **private key** is what proves you own your wallet. It's used to sign every order and transaction. It never leaves your device and must be kept secret.
Anyone with your private key has full control of your wallet and funds. Never share it, never hardcode it in your code, and never commit it to version control. Always use environment variables.
If you signed up with email/Google, you can retrieve your private key from the [assets page](https://sx.bet/wallet/assets) on SX Bet. Store it securely in a `.env` file:
```bash theme={null}
SX_PRIVATE_KEY="0xyour_private_key_here"
```
***
## Funding your account
All bets on SX Bet are denominated in **USDC**. To place or post orders, you'll need USDC in your wallet.
See the [deposit & withdraw guides](/user-guides/deposit-withdraw/transfer-crypto) for instructions on depositing funds.
You can read all public data — sports, fixtures, markets, orderbooks — without an account or any funds. You only need a funded wallet to post or fill orders.
Test without real funds using the SX Bet testnet
Step-by-step guide to setting up your wallet
# 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 overview
Not everything requires authentication. SX Bet has three tiers:
| Operation | Auth required |
| ------------------------------------------------------ | --------------------- |
| Fetch markets, odds, trades, orders | None |
| Subscribe to WebSocket channels / register a heartbeat | API key |
| Post, fill, or cancel orders | Private key signature |
All REST API endpoints are public. You can fetch markets, orderbooks, trades, fixtures, and more without any credentials. A base rate limit applies to all requests.
```bash theme={null}
# No API key required
curl https://api.sx.bet/markets/active
```
To subscribe to real-time WebSocket channels (live odds updates, order book changes, trade events) or to register and cancel a heartbeat, you need an API key.
You can generate an API key from your account settings on [sx.bet](https://sx.bet).
SX Bet uses [Centrifugo](https://centrifugal.dev/) for WebSocket connections. Pass your API key to the `/user/realtime-token/api-key` endpoint to get a connection token, then supply it via the `getToken` callback:
```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() {
const res = await fetch(`${RELAYER_URL}/user/realtime-token/api-key`, {
headers: { "x-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,
});
client.connect();
```
```python Python theme={null}
import asyncio
import os
import requests
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
def fetch_token():
res = requests.get(
f"{RELAYER_URL}/user/realtime-token/api-key",
headers={"x-api-key": os.environ["SX_API_KEY"]},
)
if not res.ok:
raise Exception(f"Token endpoint returned {res.status_code}")
return res.json()["token"]
async def main():
client = Client(WS_URL, get_token=fetch_token)
await client.connect()
await asyncio.Future() # keep running
asyncio.run(main())
```
The `getToken` function is called automatically on initial connect and whenever the token needs to be refreshed — no manual token management is needed.
See the [WebSocket Channels](/api-reference/centrifugo-overview) overview and [Real-time Data](/developers/real-time) for full channel documentation.
Any action that moves funds or modifies your orders requires a **cryptographic signature** from your wallet's private key — proof that you — and only you — authorized that specific action. This includes:
* **Filling orders** — taking a position on a market
* **Posting orders** — placing a maker order on the book
* **Cancelling orders** — removing your open orders
Never expose your private key. Store it in a `.env` file, never commit it to version control, and never share it. Anyone with your private key has full control of your wallet.
For implementation details, see [Filling Orders](/developers/filling-orders) and [Posting Orders](/developers/posting-orders).
Sign and submit taker fills against open maker orders.
Subscribe to live order book and trade updates via WebSocket.
# Betting Delays
Source: https://docs.sx.bet/developers/betting-delays
Betting delays by sport added to guard against toxic flow and high spikes in latency.
See below for the betting delays by sport which are added to guard against toxic flow and high spikes in latency from the bookmaker's side. It is effectively protection for the bookmaker. As order matching is done after the betting delay, errors observed in the past due to order cancellations within the betting delay will now be avoided.
Delays are set first at the league level. If there is no league level setting, delays are set at the sport level. If there is no sport level setting, we use a default of 8 seconds.
## Pregame
| Sport | Delay (in seconds) |
| -------------------- | ------------------ |
| Default (all sports) | 0.5 |
## Live
### By league
| League | Delay (in seconds) |
| ---------------- | ------------------ |
| NFL | 3 |
| IPL | 4 |
| NBA | 4 |
| MLB | 5 |
| NHL | 5 |
| EPL | 6 |
| La Liga | 6 |
| Champions League | 6 |
| Serie A | 6 |
| Ligue 1 | 6 |
### By sport
| Sport | Delay (in seconds) |
| ------------------- | ------------------ |
| Tennis | 5 |
| MMA | 6 |
| Baseball | 8 |
| Basketball | 8 |
| Football | 8 |
| Hockey | 8 |
| Cricket | 8 |
| Soccer | 10 |
| Default (all other) | 8 |
## Related
Full guide to filling orders as a taker.
API reference for the fill endpoint.
# Build with LLMs
Source: https://docs.sx.bet/developers/build-with-llms
Use LLMs in your SX Bet integration workflow.
You can use large language models (LLMs) to assist in building SX Bet integrations. We provide a set of tools to give your AI assistant accurate, up-to-date context about the SX Bet API.
| Tool | What it does | Best for |
| ---------------------------- | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| **MCP server** | Lets your assistant search and read these docs live during your session | IDE-based workflows (Claude Code, Cursor, VS Code) |
| **Skill** | Teaches your assistant how to approach SX Bet integration tasks | Pairing with MCP for deeper guidance on workflows, signing patterns, and common mistakes |
| **llms.txt / llms-full.txt** | Plain-text index or full export of the documentation | Pasting context into ChatGPT, Gemini, or claude.ai conversations |
## MCP server
MCP (Model Context Protocol) is a standard that lets AI coding tools connect to external resources. Adding the SX Bet MCP server gives your assistant live access to search and read these docs during your session — so instead of copy-pasting documentation into your prompt, your assistant can look up exactly what it needs on demand.
Install the server once using the config for your tool, then ask questions as you code:
* *"How do I post a maker order on SX Bet?"*
* *"What WebSocket channel do I subscribe to for live odds?"*
* *"Show me the EIP-712 signing flow for filling an order."*
1. Go to [**Settings → Connectors**](https://claude.ai/settings/connectors) and select **Add custom connector**.
2. Enter the name `SX Bet` and URL `https://docs.sx.bet/mcp`, then select **Add**.
3. When chatting, select the attachments button (the **+** icon) and choose **SX Bet** to give Claude access for that conversation.
Or add directly to `claude_desktop_config.json`:
```json theme={null}
{
"mcpServers": {
"sx-bet": {
"url": "https://docs.sx.bet/mcp"
}
}
}
```
```bash theme={null}
claude mcp add --transport http sx-bet https://docs.sx.bet/mcp
```
Add to `.cursor/mcp.json` in your project root:
```json theme={null}
{
"mcpServers": {
"sx-bet": {
"url": "https://docs.sx.bet/mcp"
}
}
}
```
Or open the Command Palette (`Cmd+Shift+P`) and select **MCP: Add Server**.
Create `.vscode/mcp.json` in your project root:
```json theme={null}
{
"servers": {
"sx-bet": {
"type": "http",
"url": "https://docs.sx.bet/mcp"
}
}
}
```
Open **Windsurf Settings → MCP Configuration** and add:
```json theme={null}
{
"mcpServers": {
"sx-bet": {
"url": "https://docs.sx.bet/mcp"
}
}
}
```
## Skill
A skill is a structured instruction file that tells an AI assistant how to work with a specific API — covering workflows, signing patterns, configuration, and common mistakes. Where MCP gives your assistant access to search the docs, the skill teaches it how to approach SX Bet integration tasks.
Run this command in your project to install the SX Bet skill:
```bash theme={null}
npx skills add https://docs.sx.bet --yes
```
This works across Claude Code, Cursor, and other tools that support the [agentskills.io](https://agentskills.io) standard. Once installed, your assistant will automatically apply the skill when working on SX Bet integrations.
## Plain text docs
`llms.txt` and `llms-full.txt` are plain-text exports of this documentation — no HTML, no navigation chrome, just content. Use them when you're working outside your IDE and want to give an LLM SX Bet context directly: paste the contents into a ChatGPT, Gemini, or claude.ai conversation, or fetch and inject them into your own API calls.
| | URL | Use when |
| ----------------- | -------------------------------------------------------------- | ------------------------------------------------------------------ |
| **llms.txt** | [docs.sx.bet/llms.txt](https://docs.sx.bet/llms.txt) | You want a lightweight index of all pages to reference in a prompt |
| **llms-full.txt** | [docs.sx.bet/llms-full.txt](https://docs.sx.bet/llms-full.txt) | You want the complete documentation in a single paste |
For most cases, `llms-full.txt` is the better choice — it gives the LLM everything it needs without requiring follow-up fetches. Open the URL, copy the contents, and paste it at the start of your conversation before asking your question.
# 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 process: when your positions within a market group offset each other, the system calculates your true worst-case loss and returns any excess escrow to your wallet immediately — without waiting for the market to settle.
## How it works
Every market group has a maximum-loss (MXL) value tracked by the escrow contract. MXL represents the worst-case net loss across all your open positions in that group. When a new trade is filled, the system recalculates MXL for every party involved. If your MXL has decreased, the escrow adjusts down to match the new MXL level and releases the excess to your wallet on the spot.
This applies to both makers and takers. Refunds are computed at fill time across all parties to the trade, not just the taker who triggered it.
## Example
You bet \$100 on Team A at +100 odds. Your MXL is \$100 — the most you can lose, so \$100 is locked in escrow. Later you bet \$100 on Team B at +100 odds in the same game. In any outcome one side always pays out, so your worst-case net loss is \$0. The system recalculates MXL as \$0 and immediately refunds all \$200 from escrow.
## Tracking CE refunds
Use [`GET /trades/portfolio/refunds`](/api-reference/get-trades-refunds) to query refunds by `bettor`, `marketHash`, `baseToken`, or `fillOrderHash`. For real-time updates, subscribe to the [`ce_refunds:{bettor}`](/api-reference/centrifugo-ce-refund-events) channel.
Trade objects from [`GET /trades`](/api-reference/get-trades), [`GET /trades/orders`](/api-reference/get-trades-orders), and [`GET /trades/consolidated`](/api-reference/get-trades-consolidated) include a `marketHasRefunds` boolean. When `true`, one or more CE refunds have been issued for that market group.
## P\&L accounting
CE refunds are returned to your wallet before settlement, so they won't appear in `settledNetReturn`. When reconciling P\&L for a market where `marketHasRefunds` is `true`, add the refund amounts back in:
```
actual_return = settledNetReturn + sum(CE refunds for that market group)
```
## Related
Real-time WebSocket channel for refund events.
Query CE refunds by bettor, market, or trade.
# Error Codes
Source: https://docs.sx.bet/developers/error-codes
Reference of all error codes returned by the SX Bet API.
## Overview
When a request fails, the API returns an `errorCode` field in the response body alongside `"status": "failure"`. This page lists every error code by endpoint.
```json theme={null}
{
"status": "failure",
"errorCode": "INVALID_ODDS"
}
```
## POST /orders/new
These are returned in the `statuses` array for each submitted order. See [`POST /orders/new`](/api-reference/post-new-order).
| Status | Description |
| ---------------------- | ----------------------------------- |
| `OK` | Order created successfully |
| `INSUFFICIENT_BALANCE` | Insufficient maker token balance |
| `INVALID_MARKET` | Non-unique `marketHashes` specified |
| `ORDERS_ALREADY_EXIST` | The order already exists |
Request-level errors:
| Error code | Description |
| ----------------------------------- | ---------------------------------------------------- |
| `TOO_MANY_DIFFERENT_MARKETS` | More than 3 different markets in a single request |
| `ORDERS_MUST_HAVE_IDENTICAL_MARKET` | All orders must be for the same network (SXN or SXR) |
| `BAD_BASE_TOKEN` | All orders must be for the same base token |
## POST /orders/fill/v2
See [`POST /orders/fill/v2`](/api-reference/post-fill-order).
| Error code | Description |
| ---------------------------- | ------------------------------------------------------------------------------------------ |
| `INSUFFICIENT_KYC` | The taker has not met the minimum KYC level to fill |
| `AFTER_ORDER_EXPIRY` | One of the orders has expired |
| `BASE_TOKENS_NOT_SAME` | All orders must be for the same `baseToken` |
| `MARKETS_NOT_SAME` | All orders must be for the same market |
| `DIRECTIONS_NOT_SAME` | All orders must be betting on the same side (`isMakerBettingOutcomeOne`) |
| `INVALID_ORDERS` | Order is now inactive |
| `INVALID_ODDS` | Invalid `desiredOdds` — must be less than 10^20 |
| `INVALID_ODDS_SLIPPAGE` | Invalid `oddsSlippage` — must be an integer between 0 and 100 |
| `MATCH_STATE_INVALID` | The fixture is in an invalid state and is no longer bettable |
| `TAKER_SIGNATURE_MISMATCH` | The taker signature generated for the request is invalid |
| `PROXY_ACCOUNT_INVALID` | The proxy account is invalid (only applicable if `proxyTaker` was specified) |
| `TAKER_AMOUNT_TOO_LOW` | The `stakeWei` specified is too low for the current token |
| `META_TX_RATE_LIMIT_REACHED` | Cannot have more than 10 meta transactions at once |
| `INSUFFICIENT_SPACE` | Not enough space to fill the matched orders due to other pending fills |
| `FILL_ALREADY_SUBMITTED` | The fill has already been submitted |
| `ODDS_STALE` | No orders found for the `desiredOdds` and `oddsSlippage` — try again with greater slippage |
## POST /orders/cancel
See [`POST /orders/cancel`](/api-reference/post-cancel-orders).
| Error code | Description |
| ---------------------------------- | -------------------------------------------- |
| `CANCEL_REQUEST_ALREADY_PROCESSED` | This cancellation has already been processed |
## POST /orders/cancel/event
See [`POST /orders/cancel/event`](/api-reference/post-cancel-event).
| Error code | Description |
| ---------------------------------- | -------------------------------------------- |
| `CANCEL_REQUEST_ALREADY_PROCESSED` | This cancellation has already been processed |
## POST /orders/cancel/all
See [`POST /orders/cancel/all`](/api-reference/post-cancel-all).
| Error code | Description |
| ---------------------------------- | -------------------------------------------- |
| `CANCEL_REQUEST_ALREADY_PROCESSED` | This cancellation has already been processed |
## GET /orders
See [`GET /orders`](/api-reference/get-orders).
| Error code | Description |
| ----------------------------------------- | -------------------------------------------------------------------------- |
| `RATE_LIMIT_ORDER_REQUEST_MARKET_COUNT` | More than 1,000 `marketHashes` queried |
| `BOTH_SPORTXEVENTID_MARKETHASHES_PRESENT` | Cannot send both `marketHashes` and `sportXEventId` — use one or the other |
## GET /markets/find
See [`GET /markets/find`](/api-reference/get-markets-find).
| Error code | Description |
| ------------------- | ------------------------------------------------------------- |
| `BAD_MARKET_HASHES` | Invalid `marketHashes` or more than 30 `marketHashes` queried |
# 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 after a financial exchange, not a traditional sportsbook. The key difference: **there is no single counterparty or "house" taking 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:
Posts an order to the orderbook. Specifies an outcome, stake, and desired odds. The order sits open until a taker fills it.
Fills an existing maker order from the book.
The same user can be a maker on some trades and a taker on others — when you post an order, you're the maker; when you fill an order, you're the taker.
## Order lifecycle
```
Maker posts order
│
▼
Order sits in orderbook (visible to all)
│
▼
Taker fills order (fully or partially)
│
▼
Funds locked in Escrow contract
│
▼
Game plays out
│
▼
Reporter submits result on-chain
│
▼
Escrow pays winner automatically
```
## Partial fills
Orders don't have to be filled in full. A taker can fill any portion of a maker's available stake. This means:
* Large maker orders can be filled by multiple takers over time
* Takers can bet exactly how much they want, regardless of the order size
## Fees
SX Bet charges **0% trading fees** on straight bets. There is a **5% fee** on parlay bets.
## Next
# External Partner API
Source: https://docs.sx.bet/developers/external-partner-api
Leverage SX Bet markets and liquidity to power your platform with a dedicated partner integration.
SX Bet currently partners with a select number of commercial platforms that want to offer SX Bet's markets and liquidity to their users directly through their own interface.
This is handled through a dedicated **External Partner API**, which we provide directly to approved partners. There are two integration models depending on how you manage your users.
## Integration Models
Registers a single SX Bet account. All bets placed by your users routed through your commercial account are tagged with an `externalPartnerId` for per-user tracking on your end.
Ideal for platforms that want full control over wallet management.
Use an external partner API key to register individual users on SX Bet on their behalf. Users don't need to register themselves — they're created and managed through your platform.
Ideal for platforms seeking finer-grained per-user tracking and more flexibility in how you display bets, balances, and history within your product.
## Getting Access
If you're interested in leveraging the External Partner API to power your application, reach out to us below. We'll help you assess which model is the right fit and provide you with the full API documentation.
Tell us about your platform and what you're looking to build. We'll follow up with the integration guide.
# 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.
***
## 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's useful for looking up historical results.
***
## Filters
### Main lines only
For spread and total markets, multiple lines are usually available at once (e.g. 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` field — pass it as `paginationKey` in your next request. Keep paginating until `nextKey` is absent or empty. Maximum `pageSize` is 50.
***
## Real-time market updates
Rather than polling [`GET /markets/active`](/api-reference/get-markets-active), subscribe to WebSocket channels to receive market changes as they happen. Three channels are relevant when working with markets:
| Channel | What you receive |
| ---------------------- | ---------------------------------------------------------------- |
| `markets:global` | Market status changes — new markets added, suspended, or settled |
| `main_line:global` | Main line shifts on spread and totals markets |
| `fixtures:live_scores` | Live score updates for a specific event |
For setup and full payload references, see [Real-time Data →](/developers/real-time).
# Fetching Odds
Source: https://docs.sx.bet/developers/fetching-odds
How to fetch current odds and orderbook data for a market.
There are two ways to fetch odds depending on what you need:
| Endpoint | Use when |
| ----------------------- | ------------------------------------------------------------------------------------ |
| `GET /orders` | You need the full orderbook — all active orders, sizes, and individual order details |
| `GET /orders/odds/best` | You only need the current best price for each outcome |
All odds returned by the API are from the **maker's perspective**. To get the taker odds on the opposite outcome, use: `takerOdds = 1 - percentageOdds / 10^20`. For example, if a maker is asking for 51% on outcome 1, a taker gets outcome 2 at 49%.
***
## GET /orders
Use this when you need to inspect full order details — available sizes, fill status, or depth across both sides of a market. You can fetch orders for multiple markets at once, all orders for a specific event, or all orders posted by a specific maker address. See the [full parameter reference →](/api-reference/get-orders).
```bash theme={null}
curl "https://api.sx.bet/orders?marketHashes=0x1a46..."
```
Like the `percentageOdds` value, `totalBetSize` and `fillAmount` are from the maker's perspective. To calculate available taker liquidity, use: `remainingTakerSpace = (totalBetSize - fillAmount) * 10^20 / percentageOdds - (totalBetSize - fillAmount)`
***
## GET /orders/odds/best
Use this when you only need the best available price on each side — for example, to check odds before placing a fill (see [Filling Orders](/developers/filling-orders#step-1-find-orders-to-fill)) or to scan for value across a league. Note that this endpoint only returns the best price, not available liquidity at that price. If you need to know how much you can fill at those odds, use `GET /orders` to fetch the full orderbook. See the [full parameter reference →](/api-reference/get-best-odds).
```bash theme={null}
curl "https://api.sx.bet/orders/odds/best?marketHashes=0xbe62...&baseToken=0x6629..."
```
***
## Real-time best odds
Subscribe to `best_odds:global` to receive live best odds updates. Each message is a **complete replacement** for that market/outcome — not a delta. The channel is global, covering all markets and tokens in a single subscription.
Subscribe first, then seed from REST inside the `subscribed` handler:
```javascript theme={null}
const sub = client.newSubscription("best_odds:global");
sub.on("publication", (ctx) => {
for (const update of ctx.data) {
const key = `${update.marketHash}:${update.isMakerBettingOutcomeOne}`;
const existing = state[key];
if (existing && update.updatedAt <= existing.updatedAt) continue; // guard against out-of-order delivery
state[key] = update;
}
});
sub.on("subscribed", async () => {
// Re-seeds on every connect — REST uses outcomeOne/outcomeTwo; WS uses isMakerBettingOutcomeOne
const res = await fetch(`https://api.sx.bet/orders/odds/best?marketHashes=${marketHashes.join(",")}&baseToken=${baseToken}`);
const { data } = await res.json();
for (const market of data.bestOdds) {
state[`${market.marketHash}:true`] = { marketHash: market.marketHash, isMakerBettingOutcomeOne: true, percentageOdds: market.outcomeOne.percentageOdds, updatedAt: market.outcomeOne.updatedAt };
state[`${market.marketHash}:false`] = { marketHash: market.marketHash, isMakerBettingOutcomeOne: false, percentageOdds: market.outcomeTwo.percentageOdds, updatedAt: market.outcomeTwo.updatedAt };
}
});
sub.subscribe();
```
`best_odds:global` has no server-side history — recovery across reconnects is not available. The `subscribed` handler re-seeds from REST on every connect, keeping state consistent.
For payload field definitions, see [Best Odds →](/api-reference/centrifugo-best-odds).
***
## Real-time orderbook
For a live orderbook, subscribe to `order_book:market_{marketHash}` with `positioned: true, recoverable: true`. Centrifugo's history recovery handles the subscribe-to-fetch race condition automatically — you don't need to manually buffer messages.
See [Real-time Data → Snapshot + subscribe pattern](/developers/real-time#snapshot--subscribe-pattern) for the full pattern and code example.
# Fetching Trades
Source: https://docs.sx.bet/developers/fetching-trades
Query trade history from the SX Bet API.
Every trade made on SX Bet is publicly available through the SX Bet API. You can
query trades for a specific user, event, market, time period, and more.
| Endpoint | Best for |
| -------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `GET /trades` | Individual filled orders — split by order, so one taker bet filling five maker orders returns five records |
| `GET /trades/consolidated` | Aggregated history — that same bet appears as one record with weighted average odds |
| `GET /trades/orders` | When you have specific order hashes and want to see all fills against them |
***
## GET /trades
Use this when you need granular fill data — individual order matches, settlement
status, or trade history for a specific market or user. Filter by bettor, market
hash, time range, settlement status, and more. See the
[full parameter reference →](/api-reference/get-trades).
```bash theme={null}
curl "https://api.sx.bet/trades?bettor=0xYourAddress&marketHashes=0x1a46...&settled=false"
```
```json theme={null}
{
"status": "success",
"data": {
"trades": [
{
"fillHash": "0xabc123...",
"orderHash": "0xdef456...",
"marketHash": "0x1a46...",
"bettor": "0xYourAddress",
"stake": "1035620",
"normalizedStake": "1",
"odds": "50875000000000000000",
"maker": true,
"bettingOutcomeOne": true,
"betTime": 1774491706,
"betTimeValue": 1.03562,
"settleValue": 0,
"settled": false,
"tradeStatus": "SUCCESS",
"valid": true,
"betType": 0,
"netReturn": "2.035617",
"fillOrderHash": "0x789abc...",
"fillOrderHashAttempts": ["0x789abc..."],
"lastMineAttemptTime": "2026-03-26T02:21:48.098Z",
"targetNonce": 27614,
"contractsVersion": "6.0",
"chainVersion": "SXR",
"sportXeventId": "L16068923",
"marketHasRefunds": false,
"baseToken": "0x6629Ce1Cf35Cc1329ebB4F63202F3f197b3F050B",
"createdAt": "2026-03-26T02:21:47.041Z",
"updatedAt": "2026-03-26T02:21:48.224Z",
"_id": "69c4983a23f5052c3b0a6dc2"
}
],
"nextKey": "66ae05484c2e5e971bcb7f29",
"pageSize": 100,
"count": 142
}
}
```
`odds` is the bettor's implied probability as a fixed-point integer — divide by
10^20 to get a decimal. `50875000000000000000` = 50.875% (1.97 in decimal odds format).
`stake` and `normalizedStake` reflect the bettor's stake — `stake` is in raw token units, `normalizedStake` is in whole token units. See
[Unit Conversions](/api-reference/unit-conversion) for token decimal details.
Results are paginated using a cursor — each response includes a `nextKey` field inside `data`.
Pass it as `paginationKey` in your next request. Default page size is 100, max 300.
When `nextKey` is absent, you've reached the last page.
***
## GET /trades/consolidated
Use this when you want one record per bet regardless of how many maker orders it
filled — for portfolio views, PnL tracking, or reviewing a user's betting history.
Fills that crossed multiple maker orders are rolled up with weighted average odds.
See the [full parameter reference →](/api-reference/get-trades-consolidated).
`page`, `perPage`, `sortBy`, `sortAsc`, and `settled` are all required parameters for this endpoint.
```bash theme={null}
curl "https://api.sx.bet/trades/consolidated?bettor=0xYourAddress&settled=false&page=0&perPage=100&sortBy=gameTime&sortAsc=false"
```
```json theme={null}
{
"status": "success",
"data": {
"trades": [
{
"fillHash": "0xabc123...",
"marketHash": "0x1a46...",
"bettor": "0xYourAddress",
"totalStake": "5000000000",
"weightedAverageOdds": "54200000000000000000",
"bettingOutcomeLabel": "Los Angeles Lakers",
"gameLabel": "Los Angeles Lakers vs Boston Celtics",
"leagueLabel": "NBA",
"gameTime": "2025-11-15T02:00:00.000Z",
"bettingOutcome": 1,
"maker": false,
"settled": false,
"tradeStatus": "SUCCESS",
"sportXeventId": "L16068923",
"baseToken": "0x6629Ce1Cf35Cc1329ebB4F63202F3f197b3F050B"
}
],
"count": 142
}
}
```
`count` is the total number of matching trades — use it to calculate total pages:
`Math.ceil(count / perPage)`. Pagination starts at page `0`.
***
## GET /trades/orders
Use this when you have specific order hashes and want to see exactly how and when
they were filled. See the [full parameter reference →](/api-reference/get-trades-orders).
```bash theme={null}
curl "https://api.sx.bet/trades/orders?orderHashes=0xdef456...,0x789abc..."
```
```json theme={null}
{
"status": "success",
"data": {
"trades": [
{
"fillHash": "0xabc123...",
"orderHash": "0xdef456...",
"marketHash": "0x1a46...",
"bettor": "0xYourAddress",
"stake": "1035620",
"normalizedStake": "1",
"odds": "50875000000000000000",
"maker": true,
"bettingOutcomeOne": true,
"betTime": "2026-03-26T02:21:46.964Z",
"betTimeValue": 1.03562,
"settleValue": 0,
"settled": false,
"tradeStatus": "SUCCESS",
"valid": true,
"betType": 0,
"affiliate": "0x0000000000000000000000000000000000000000",
"netReturn": "2.035617",
"fillOrderHash": "0x789abc...",
"fillOrderHashAttempts": ["0x789abc..."],
"contractsVersion": "6.0",
"chainVersion": "SXR",
"sportXeventId": "L16068923",
"providerEventId": "17438473",
"marketHasRefunds": false,
"baseToken": "0x6629Ce1Cf35Cc1329ebB4F63202F3f197b3F050B",
"id": "69c4983a23f5052c3b0a6dc2"
}
]
}
}
```
Each order hash returns two trade records — one for the maker and one for the taker. `odds` is from each bettor's perspective as a fixed-point integer — divide by 10^20 to get a decimal.
***
## Real-time trades
Rather than polling, subscribe to WebSocket channels to receive trades as they happen:
* **`recent_trades:global`** — Global feed of every trade on the exchange, one message per order fill.
* **`recent_trades_consolidated:global`** — Same feed, but one message per taker bet regardless of how many maker orders it filled.
For setup and full payload references, see [Real-time Data →](/developers/real-time).
# Filling Orders
Source: https://docs.sx.bet/developers/filling-orders
How to fill existing orders on the SX Bet orderbook as a taker.
## Overview
Filling an order means taking the other side of an existing maker order on the orderbook. As a taker, you get immediate execution at the price a maker has posted.
SX Bet uses [`POST /orders/fill/v2`](/api-reference/post-fill-order) to submit fills. The exchange matches your fill against the best available orders after a short betting delay.
## Prerequisites
1. **Create an account** at [sx.bet](https://sx.bet) and export your private key from the [assets page](https://sx.bet/wallet/assets)
2. **Fund your account** with USDC
3. **Enable betting** by approving the `TokenTransferProxy` contract — see [Enabling Betting](/api-reference/enabling-betting)
## Fill fields
Each fill submitted to [`POST /orders/fill/v2`](/api-reference/post-fill-order) requires these fields:
| Field | Type | Description |
| -------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------- |
| `market` | string | User-facing string for the market. Set to `"N/A"` when using the API |
| `baseToken` | string | The token this fill is denominated in (e.g., USDC). See [References](/api-reference/references) for token addresses |
| `isTakerBettingOutcomeOne` | boolean | `true` if you're betting on outcome one, `false` for outcome two |
| `stakeWei` | string | Your stake in base token units (Ethereum format). See [Unit Conversion](/api-reference/unit-conversion) |
| `desiredOdds` | string | The worst taker odds you'll accept, in SX protocol format (`implied * 10^20`) |
| `oddsSlippage` | integer | Percentage tolerance (0–100) applied to `desiredOdds`. `0` means exact odds only |
| `fillSalt` | string | A random 32-byte value to identify this fill |
| `taker` | string | Your wallet address |
| `takerSig` | string | Your wallet signature over the fill payload |
| `message` | string | A user-facing message included in the signing payload. Can be anything (e.g., `"N/A"`) |
### Understanding `desiredOdds`
This is the implied probability **from your perspective as the taker**. It's the inverse of the maker's `percentageOdds`:
```
taker_implied = 10^20 - maker_percentageOdds
```
For example, if the best maker order on outcome two has `percentageOdds = "52500000000000000000"` (52.5%), the taker odds for betting outcome one are:
```
10^20 - 52500000000000000000 = 47500000000000000000 (47.5%)
```
You'd set `desiredOdds = "47500000000000000000"` to fill at that price.
Orders with taker odds **better** than your `desiredOdds` will also fill. The `desiredOdds` is a floor, not an exact match.
### Understanding `oddsSlippage`
The `oddsSlippage` field is an integer from 0 to 100 representing a percentage tolerance on your `desiredOdds`. This is useful for volatile in-play markets where odds shift quickly.
* `0` — only accept `desiredOdds` or better (no slippage)
* `5` — accept up to 5% worse than `desiredOdds`
For pre-game markets, `0` is usually fine. For in-play markets, a small slippage (2–5) helps ensure your fill goes through.
### Understanding `stakeWei`
This is the amount of tokens **you** are putting up for the bet. It's in Ethereum units — for USDC (6 decimals), `"50000000"` = 50 USDC.
If there isn't enough liquidity to fill your full stake, the exchange will partially fill you for the available amount.
The minimum taker stake is **1 USDC**.
## Step 1: Find orders to fill
Check what odds are available on the market you want to bet on using [`GET /orders/odds/best`](/api-reference/get-best-odds):
```python Python theme={null}
import requests
BASE_URL = "https://api.sx.bet" # Mainnet — use https://api.toronto.sx.bet for testnet
ODDS_PRECISION = 10 ** 20
market_hash = "YOUR_MARKET_HASH"
base_token = "0x6629Ce1Cf35Cc1329ebB4F63202F3f197b3F050B" # Mainnet USDC — see References for testnet address
best = requests.get(
f"{BASE_URL}/orders/odds/best",
params={"marketHashes": market_hash, "baseToken": base_token}
).json()["data"]["bestOdds"][0]
o1_maker = int(best["outcomeOne"]["percentageOdds"]) / ODDS_PRECISION
o2_maker = int(best["outcomeTwo"]["percentageOdds"]) / ODDS_PRECISION
# Taker odds are the inverse of the opposite outcome's maker odds
print(f"Taker odds for outcome 1: {1 - o2_maker:.2%}")
print(f"Taker odds for outcome 2: {1 - o1_maker:.2%}")
```
```javascript JavaScript theme={null}
const BASE_URL = "https://api.sx.bet"; // Mainnet — use https://api.toronto.sx.bet for testnet
const ODDS_PRECISION = 1e20;
const marketHash = "YOUR_MARKET_HASH";
const baseToken = "0x6629Ce1Cf35Cc1329ebB4F63202F3f197b3F050B"; // Mainnet USDC — see References for testnet address
const best = await fetch(
`${BASE_URL}/orders/odds/best?${new URLSearchParams({ marketHashes: marketHash, baseToken })}`
).then((r) => r.json());
const odds = best.data.bestOdds[0];
const o1Maker = Number(odds.outcomeOne.percentageOdds) / ODDS_PRECISION;
const o2Maker = Number(odds.outcomeTwo.percentageOdds) / ODDS_PRECISION;
// Taker odds are the inverse of the opposite outcome's maker odds
console.log(`Taker odds for outcome 1: ${((1 - o2Maker) * 100).toFixed(2)}%`);
console.log(`Taker odds for outcome 2: ${((1 - o1Maker) * 100).toFixed(2)}%`);
```
For deeper orderbook inspection, use [`GET /orders`](/api-reference/get-orders) to see all open orders and their available sizes. See [Navigating the Orderbook](/developers/navigating-the-orderbook) for more on reading depth.
## Step 2: Sign and submit the fill
Fills require a typed data signature from your wallet. This is a different signing method from maker orders — the code below handles it for you. See [Order Signing](/api-reference/eip712-signing) for the full reference.
The `verifyingContract`, `chainId`, and `baseToken` values below are hardcoded to mainnet. Using the wrong values will cause signature failures. Fetch `verifyingContract` from [`GET /metadata`](/api-reference/get-metadata) → `EIP712FillHasher`, and get the correct `baseToken` for your network from [References](/api-reference/references). See [Testnet & Mainnet](/developers/testnet-and-mainnet) for the recommended config pattern.
```python Python theme={null}
import os
import secrets
import requests
from eth_account import Account
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"
base_token = "0x6629Ce1Cf35Cc1329ebB4F63202F3f197b3F050B" # Mainnet USDC — see References for testnet address
stake_wei = "50000000" # 50 USDC (6 decimals)
# Taker odds: 10^20 - best maker odds on the opposite outcome
desired_odds = "47500000000000000000" # 47.5% taker implied
fill_salt = int.from_bytes(secrets.token_bytes(32), "big")
# --- Sign the fill ---
DOMAIN = {
"name": "SX Bet",
"version": "6.0",
"chainId": 4162,
"verifyingContract": "0x845a2Da2D70fEDe8474b1C8518200798c60aC364", # Mainnet — use GET /metadata → EIP712FillHasher for testnet
}
FILL_TYPES = {
"Details": [
{"name": "action", "type": "string"},
{"name": "market", "type": "string"},
{"name": "betting", "type": "string"},
{"name": "stake", "type": "string"},
{"name": "worstOdds", "type": "string"},
{"name": "worstReturning", "type": "string"},
{"name": "fills", "type": "FillObject"},
],
"FillObject": [
{"name": "stakeWei", "type": "string"},
{"name": "marketHash", "type": "string"},
{"name": "baseToken", "type": "string"},
{"name": "desiredOdds", "type": "string"},
{"name": "oddsSlippage", "type": "uint256"},
{"name": "isTakerBettingOutcomeOne", "type": "bool"},
{"name": "fillSalt", "type": "uint256"},
{"name": "beneficiary", "type": "address"},
{"name": "beneficiaryType", "type": "uint8"},
{"name": "cashOutTarget", "type": "bytes32"},
],
}
signed = Account.sign_typed_data(
account.key,
domain_data=DOMAIN,
message_types=FILL_TYPES,
message_data={
"action": "N/A",
"market": market_hash,
"betting": "N/A",
"stake": "N/A",
"worstOdds": "N/A",
"worstReturning": "N/A",
"fills": {
"stakeWei": stake_wei,
"marketHash": market_hash,
"baseToken": base_token,
"desiredOdds": desired_odds,
"oddsSlippage": 0,
"isTakerBettingOutcomeOne": True,
"fillSalt": fill_salt,
"beneficiary": "0x0000000000000000000000000000000000000000",
"beneficiaryType": 0,
"cashOutTarget": b"\x00" * 32,
},
},
)
taker_sig = "0x" + signed.signature.hex()
# --- Submit ---
response = requests.post(f"{BASE_URL}/orders/fill/v2", json={
"market": market_hash,
"baseToken": base_token,
"isTakerBettingOutcomeOne": True,
"stakeWei": stake_wei,
"desiredOdds": desired_odds,
"oddsSlippage": 0,
"taker": account.address,
"takerSig": taker_sig,
"fillSalt": str(fill_salt),
})
result = response.json()
print("Status:", result["status"])
print("Data:", result["data"])
```
```javascript JavaScript theme={null}
import "dotenv/config";
import { Wallet, ZeroAddress, ZeroHash, 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";
const baseToken = "0x6629Ce1Cf35Cc1329ebB4F63202F3f197b3F050B"; // Mainnet USDC — see References for testnet address
const stakeWei = "50000000"; // 50 USDC (6 decimals)
// Taker odds: 10^20 - best maker odds on the opposite outcome
const desiredOdds = "47500000000000000000"; // 47.5% taker implied
const oddsSlippage = 0;
const isTakerBettingOutcomeOne = true;
const fillSalt = BigInt(hexlify(randomBytes(32))).toString();
// --- Sign the fill ---
const domain = {
name: "SX Bet",
version: "6.0",
chainId: 4162,
verifyingContract: "0x845a2Da2D70fEDe8474b1C8518200798c60aC364", // Mainnet — use GET /metadata → EIP712FillHasher for testnet
};
const types = {
Details: [
{ name: "action", type: "string" },
{ name: "market", type: "string" },
{ name: "betting", type: "string" },
{ name: "stake", type: "string" },
{ name: "worstOdds", type: "string" },
{ name: "worstReturning", type: "string" },
{ name: "fills", type: "FillObject" },
],
FillObject: [
{ name: "stakeWei", type: "string" },
{ name: "marketHash", type: "string" },
{ name: "baseToken", type: "string" },
{ name: "desiredOdds", type: "string" },
{ name: "oddsSlippage", type: "uint256" },
{ name: "isTakerBettingOutcomeOne", type: "bool" },
{ name: "fillSalt", type: "uint256" },
{ name: "beneficiary", type: "address" },
{ name: "beneficiaryType", type: "uint8" },
{ name: "cashOutTarget", type: "bytes32" },
],
};
const message = {
action: "N/A",
market: marketHash,
betting: "N/A",
stake: "N/A",
worstOdds: "N/A",
worstReturning: "N/A",
fills: {
stakeWei,
marketHash,
baseToken,
desiredOdds,
oddsSlippage,
isTakerBettingOutcomeOne,
fillSalt,
beneficiary: ZeroAddress,
beneficiaryType: 0,
cashOutTarget: ZeroHash,
},
};
const takerSig = await wallet.signTypedData(domain, types, message);
// --- Submit ---
const response = await fetch(`${BASE_URL}/orders/fill/v2`, {
method: "POST",
body: JSON.stringify({
market: "N/A",
baseToken,
isTakerBettingOutcomeOne,
stakeWei,
desiredOdds,
oddsSlippage,
taker: wallet.address,
takerSig,
fillSalt,
message: "N/A",
}),
headers: { "Content-Type": "application/json" },
});
const result = await response.json();
console.log("Status:", result.status);
console.log("Data:", result.data);
```
## How filling works
After you submit a fill:
1. The exchange queues your fill and applies a short **betting delay** (a few seconds for pre-game, optimized for in-play)
2. After the delay, the exchange matches your fill against the best available maker orders at or better than your `desiredOdds` (accounting for `oddsSlippage`)
3. If enough liquidity exists, your fill executes. If not, you get a partial fill for the available amount
4. The resulting trade(s) appear in your [trade history](/api-reference/get-trades)
The betting delay prevents front-running on in-play markets where odds shift rapidly. Your fill is matched against the orderbook state **after** the delay, not at submission time.
## Monitoring your fills
Subscribe to the [`recent_trades:global` WebSocket channel](/api-reference/centrifugo-trade-updates) to get real-time notifications when fills execute. This is a global feed — filter messages to your address in the handler:
```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() {
const res = await fetch(`${RELAYER_URL}/user/realtime-token/api-key`, {
headers: { "x-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 userAddress = wallet.address;
const sub = client.newSubscription("recent_trades:global", {
positioned: true,
recoverable: true,
});
sub.on("publication", (ctx) => {
const trades = Array.isArray(ctx.data) ? ctx.data : [ctx.data];
for (const trade of trades) {
if (trade.bettor.toLowerCase() !== userAddress.toLowerCase()) continue;
console.log(`Trade: market=${trade.marketHash}, stake=${trade.stake}, odds=${trade.odds}`);
}
});
sub.subscribe();
client.connect();
```
```python Python theme={null}
import asyncio
import aiohttp
from centrifuge import Client, PublicationContext, SubscriptionEventHandler, SubscriptionOptions
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
async def fetch_token():
async with aiohttp.ClientSession() as session:
async with session.get(
f"{RELAYER_URL}/user/realtime-token/api-key",
headers={"x-api-key": os.environ["SX_API_KEY"]},
) as resp:
data = await resp.json()
return data["token"]
async def main():
client = Client(WS_URL, get_token=fetch_token)
user_address = account.address
async def on_publication(ctx: PublicationContext) -> None:
trades = ctx.data if isinstance(ctx.data, list) else [ctx.data]
for trade in trades:
if trade["bettor"].lower() != user_address.lower():
continue
print(f"Trade: market={trade['marketHash']}, stake={trade['stake']}, odds={trade['odds']}")
handler = SubscriptionEventHandler(on_publication=on_publication)
options = SubscriptionOptions(positioned=True, recoverable=True)
sub = client.new_subscription("recent_trades:global", handler, options)
await client.connect()
await sub.subscribe()
await asyncio.Future()
asyncio.run(main())
```
You can also poll your trade history via [`GET /trades`](/api-reference/get-trades) with the `bettor` parameter.
## Real-time orderbook and odds
If you're actively scanning markets before filling — especially in-play — polling REST adds latency. Two WebSocket channels are useful here:
* **`order_book:market_{marketHash}`** — full orderbook updates for a specific market. Subscribe with `positioned: true, recoverable: true` before fetching the snapshot to avoid gaps. See [Fetching Odds → Real-time orderbook](/developers/fetching-odds#real-time-orderbook) for the correct subscribe-then-fetch pattern.
* **`best_odds:global`** — fires whenever the best available odds change across any market. Useful for watching multiple markets without subscribing to each orderbook individually. See [Fetching Odds → Real-time best odds](/developers/fetching-odds#real-time-best-odds).
## Common issues
### Fill rejected — betting not enabled
You need to approve the `TokenTransferProxy` contract for each token you trade. See [Enabling Betting](/api-reference/enabling-betting).
### Fill rejected — insufficient balance
Your wallet must have enough of the base token to cover `stakeWei`. Fund your account before submitting.
### Fill returns no trades
This means there wasn't enough liquidity at your `desiredOdds` (accounting for `oddsSlippage`). Either relax your odds, increase slippage, or wait for more maker orders to appear on the book.
### In-play fills timing out
For in-play markets, odds move quickly. Use a small `oddsSlippage` (2–5) to give your fill a better chance of executing. Check the latest odds via [`GET /orders/odds/best`](/api-reference/get-best-odds) before each fill.
## Related
Full signing reference for fills and orders.
Full API reference for the fill endpoint.
Reading depth and finding the best prices.
Converting between raw values and human-readable amounts.
# Glossary
Source: https://docs.sx.bet/developers/glossary
Definitions of key terms used across the SX Bet API and developer docs.
### API Key
A credential required for posting orders, cancelling orders, and accessing real-time WebSocket updates. Generated from your account at [sx.bet](https://sx.bet). Passed as an `X-Api-Key` HTTP header for REST calls, and used to obtain a connection token for WebSocket subscriptions. See [API Key](/api-reference/api-key).
### apiExpiry
A Unix timestamp (in seconds) specifying when an order expires from the API's perspective. After this time, the order is removed from the orderbook. Distinct from the on-chain `expiry` field, which is deprecated and must always be `2209006800`.
### Base token
The ERC-20 token used to denominate a bet. On SX Bet, the primary base token is USDC (6 decimals). Referenced by contract address in API calls. See [References](/api-reference/references).
### Bet group
A categorical grouping of related market types (e.g., `"game-lines"`, `"outright-winner"`, `"set-betting"`). Returned in market objects from the API.
### Betting delay
A short waiting period between when a fill is submitted and when the exchange matches it against the orderbook. Protects makers from toxic flow during in-play betting. See [Betting Delays](/developers/betting-delays).
### Capital efficiency (CE)
A system that recognizes hedged exposure across opposing outcomes within a market group and releases escrowed funds early. Driven by the maximum-loss (MXL) algorithm. See [Capital Efficiency](/developers/capital-efficiency).
### Cash out
The ability to exit a position early by selling it back. Mutually exclusive with CE refunds within the same market group.
### Chain ID
A numeric identifier for a blockchain network. SX mainnet is `4162`, SX testnet (Toronto) is `79479957`.
### Checksum format
A way of writing Ethereum addresses with mixed uppercase and lowercase letters (EIP-55) to catch typos. All addresses in the SX Bet API must use checksum format.
### Diming
Posting odds in tiny increments to gain an unfair edge over existing orders. The [odds ladder](/developers/odds-rounding) prevents this by enforcing minimum step sizes.
### EIP-712
An Ethereum standard for signing structured typed data. Used by SX Bet to sign fill payloads and cancel requests. See [EIP-712 Signing](/api-reference/eip712-signing).
### Escrow
A smart contract that holds both sides' funds while a bet is active. Automatically pays out the winner after settlement.
### Exchange model
SX Bet's peer-to-peer model where users trade directly with each other rather than against a house or sportsbook. See [The Exchange Model](/developers/exchange-model).
### Executor
The SX Bet exchange contract address that executes matched orders on-chain. Retrieved from [`GET /metadata`](/api-reference/get-metadata) and required in every maker order.
### Exposure
The total amount of capital at risk across your open orders. Calculated as the sum of `totalBetSize - fillAmount` for all active orders. Must stay under your wallet balance.
### fillAmount
The portion of a maker order that has already been matched by takers. The remaining fillable amount is `totalBetSize - fillAmount`.
### fillSalt
A random 32-byte value that uniquely identifies a fill submission. Must match between the EIP-712 signature and the API request body.
### Fixture
An individual sporting event (game, match, race, etc.) that betting markets are created for. Each fixture has a status indicating its current state. See [Fixture Statuses](/api-reference/fixture-statuses).
### Heartbeat
A safety mechanism that automatically cancels all your open orders if your service stops sending periodic heartbeat requests within a configured timeout. See [Heartbeat](/api-reference/heartbeat).
### Implied probability
Odds expressed as a probability between 0% and 100%. The native odds format on SX Bet. Stored as a fixed-point integer with 20 decimal places (e.g., 52.5% = `52500000000000000000`).
### isMakerBettingOutcomeOne
A boolean field on orders indicating which outcome the maker is backing. `true` = outcome one, `false` = outcome two. The taker always takes the opposite side.
### Maker
A user who posts an order to the orderbook, specifying an outcome, stake, and odds. Makers wait for takers to fill their orders. Makers pay 0% fees on SX Bet.
### Market
A specific betting proposition on a fixture, defined by its sport, league, fixture, market type, and line. Identified by its `marketHash`.
### Market group
A collection of related markets for a single fixture (e.g., all markets for one game). Used for calculating maximum loss and CE refunds.
### marketHash
A unique keccak256 hash identifying a specific market. The primary key used in API calls to reference markets.
### Market type
The category of bet — moneyline (1), spread (2), total/over-under (3), Asian handicap, etc. See [Market Types](/api-reference/market-types).
### Maximum loss (MXL)
The worst-case net loss across all open positions within a market group. The CE algorithm calculates this to determine when escrow funds can be released.
### Moneyline
A market type where you predict which team or player will win outright, with no point spread. Market type ID `1`.
### Odds ladder
The set of valid odds values you can post orders at. Currently enforced at 0.125% intervals to prevent diming. See [Odds Rounding](/developers/odds-rounding).
### oddsSlippage
An integer (0–100) representing the percentage tolerance on `desiredOdds` when filling. Applied to the weighted average odds across all matched orders. See [Slippage](/developers/slippage).
### Orderbook
The collection of all active, unfilled maker orders for a given market. Fully transparent — all users can see all orders. See [The Orderbook](/developers/orderbook).
### orderHash
A unique identifier for a specific maker order, computed from the order fields. Returned when an order is successfully posted.
### Outcome
One of two bettable sides in a market. Outcome one is typically the first team/option listed, outcome two is the second. Some markets also have a void outcome (e.g., draw).
### Parlay
A multi-leg bet combining multiple markets. All legs must win for the parlay to pay out. SX Bet applies a 5% fee on parlay profits. See [Parlays](/developers/parlays).
### Partial fill
When a taker fills only a portion of a maker's available stake. The remaining unfilled portion stays on the orderbook for other takers.
### percentageOdds
The odds for a maker order expressed as the maker's implied probability multiplied by 10^20. For example, 52.5% implied = `"52500000000000000000"`. The taker receives the complementary odds on the opposite outcome.
### RFQ (Request for Quote)
The system used for parlay betting. When a bettor requests a parlay, makers receive the request via WebSocket and have 3 seconds to post orders. See [Parlays](/developers/parlays).
### Salt
A random number included in an order to differentiate otherwise identical orders. Ensures each order produces a unique hash.
### Settlement
The process of determining the outcome of a market after the event concludes and distributing funds to winners. Handled by the on-chain ReporterService contract.
### solidityKeccak256
A hashing function used to compute maker order hashes. Takes ABI-encoded order fields and produces a bytes32 hash that the maker signs.
### Spread
A market type where you predict whether a team will win by more or less than a specified point margin (the "line"). Market type ID `2`.
### sportXeventId
A string identifier for sporting events within SX Bet's system. Used to group markets by fixture and for event-level cancellations.
### stakeWei
The taker's bet amount in base token units (Ethereum format). For USDC with 6 decimals, `"50000000"` = 50 USDC. See [Unit Conversion](/api-reference/unit-conversion).
### SX Network
The blockchain that SX Bet runs on. Mainnet chain ID `4162`.
### Taker
A user who fills an existing order from the orderbook. Takers get immediate execution at the posted price. Takers pay 0% fees on single bets, 5% on parlay profits.
### TokenTransferProxy
A smart contract that must be approved (via ERC-20 `approve`) before you can place bets. It transfers tokens on your behalf when orders are matched. See [Enabling Betting](/api-reference/enabling-betting).
### Total (Over/Under)
A market type where you predict whether a combined score or statistic will be over or under a specified line. Market type ID `3`.
### totalBetSize
The maximum amount of tokens a maker puts at risk if the order is fully filled. Expressed in base token units (e.g., `"100000000"` = 100 USDC).
### Vig (vigorish)
The commission or house edge charged by a sportsbook. SX Bet charges 0% vig — odds are set entirely by makers and takers.
# 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.
Developer Hub
Build trading bots, custom frontends, and analytics tools on the only open, peer-to-peer sports prediction market.
Fetch markets, read the orderbook, and place your first order end-to-end.
Full endpoint reference with request and response schemas.
Subscribe to live odds, order fills, and trade events via WebSocket.
Trading bots, custom frontends, analytics tools, and more.
## Try the API
The REST API is fully public and free to access — no credentials needed to start reading data.
```javascript JavaScript theme={null}
const res = await fetch("https://api.sx.bet/markets/active?sportIds=5");
const { data } = await res.json();
console.log(data.markets); // array of active markets
```
```python Python theme={null}
import requests
res = requests.get("https://api.sx.bet/markets/active", params={"sportIds": 5})
markets = res.json()["data"]["markets"]
print(markets)
```
## Explore the platform
Wallet-based authentication and signing orders.
Understand how makers, takers, and the orderbook interact.
How markets are structured and which sports are supported.
Query live odds and orderbook data from the API.
Post limit orders and capture the spread as a maker.
Become a taker: fill existing orders at market prices.
# Latency & Server Locations
Source: https://docs.sx.bet/developers/latency
Expected response times and server locations for the SX Bet API.
SX Bet's servers are located in **Montreal, Canada**. For the lowest latency, use a server in the same region.
## Response time targets
Target response times at p95 (95th percentile):
| Operation | p95 latency |
| -------------------------------------- | ----------- |
| Order create (`POST /orders/new`) | 100ms |
| Order cancel (`POST /orders/cancel/*`) | 50ms |
These figures assume your servers are in Montreal. Higher latency is expected from more distant regions.
# Market Making on SX Bet
Source: https://docs.sx.bet/developers/market-making
An overview of market making on SX Bet — how the maker role works, how to construct a spread, and how the pieces fit together.
## Overview
Market making on SX Bet means posting orders to the orderbook that other users (takers) can fill. You provide liquidity by offering odds on one or both sides of a market, and earn the spread between your posted price and what takers receive.
SX Bet charges **0% maker fees**, making it well-suited for systematic market making strategies.
## Prerequisites
Before you start:
1. **Create an account** at [sx.bet](https://sx.bet) and export your private key from the [assets page](https://sx.bet/wallet/assets)
2. **Get an API key** — see [API Key](/api-reference/api-key)
3. **Enable betting** by approving the `TokenTransferProxy` contract — see [Enabling Betting](/api-reference/enabling-betting)
4. **Fund your account** with USDC
## The maker's role
As a maker, you post orders specifying an outcome, stake size, and odds. Takers see the complementary side of your order — when they fill it, you're matched.
Your orders sit on the book until they're filled, cancelled, or expired. You can post on one side, or both sides of a market:
* **Single side** — post on one outcome if you want specific exposure to that result at your chosen odds
* **Two-sided (spread)** — post on both outcomes to earn the difference between your posted price and what takers pay
Two-sided example:
```
You post outcome 1 at 52.5% implied → taker gets 47.5%
You post outcome 2 at 52.5% implied → taker gets 47.5%
Your spread: 5%
```
The `totalBetSize` field is from **your perspective as the maker**. It's the maximum amount of tokens you'd put into the pot if the order was fully filled — your maximum risk on that order.
## How the flow works
A typical market making loop:
1. **Find markets** — query [`GET /markets/active`](/api-reference/get-markets-active) for the sports and market types you want to cover. See [Markets and Sports](/developers/markets-and-sports) for sport IDs and market types.
2. **Check current odds** — use [`GET /orders/odds/best`](/api-reference/get-best-odds) and [`GET /orders`](/api-reference/get-orders) to see what's already on the book before deciding where to post.
3. **Post orders** — construct, sign, and submit orders via [`POST /orders/new`](/api-reference/post-new-order). Your `percentageOdds` must land on the [odds ladder](/developers/odds-rounding). See [Posting Orders](/developers/posting-orders) for the full field reference and signing instructions.
4. **Manage your orders** — monitor fills in real-time, cancel stale orders, and run a heartbeat so your orders are auto-cancelled if your service goes down. See [Order Management](/developers/order-management).
## Exposure management
You can post as many orders as you wish across the exchange as long as your total exposure per market remains below your wallet balance. If your total exposure exceeds your wallet balance on any given market, your orders will be removed from the orderbook until your balance reaches the minimum again.
If the API repeatedly finds your balance below your total exposure, your account may be temporarily restricted.
See [Order Management → Exposure management](/developers/order-management#exposure-management) for a worked example with concrete numbers.
## Real-time market signals
Beyond tracking your own orders, three channels carry signals that market makers use to decide when to reprice or cancel:
| Channel | Signal | When it matters |
| ---------------------- | ----------------------------------------- | --------------------------------------------------------------- |
| `main_line:global` | Main line shifts on spread/totals markets | Reprice or cancel spread/total orders when the line moves |
| `markets:global` | Market added, suspended, or settled | Cancel orders immediately when a market is suspended or settled |
| `fixtures:live_scores` | Live score and period updates | Adjust in-play odds or cancel exposure on goal/score changes |
These channels are documented with code examples on the [Real-time Data](/developers/real-time) page.
## Next steps
Full field reference, signing instructions, and order submission.
Monitoring fills, cancelling orders, heartbeat setup, and exposure management.
Validate and round odds to the ladder before posting.
The full flow for making on parlay RFQ requests.
# 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: connecting to the WebSocket, receiving parlay requests, analyzing legs, calculating odds, and posting orders — all within the 3-second RFQ window.
## Step 1: Connect to the parlay channel
Subscribe to the [`parlay_markets:global` WebSocket channel](/api-reference/centrifugo-parlay-market-requests) to receive parlay requests in real-time. You'll need a token from the [`GET /user/realtime-token/api-key`](/api-reference/centrifugo-initialization) endpoint.
```javascript JavaScript theme={null}
import { Centrifuge } from "centrifuge";
const API_KEY = process.env.SX_API_KEY;
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() {
const res = await fetch(`${RELAYER_URL}/user/realtime-token/api-key`, {
headers: { "x-api-key": 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 aiohttp
from centrifuge import Client, PublicationContext, SubscriptionEventHandler
API_KEY = os.environ["SX_API_KEY"]
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
async def fetch_token():
async with aiohttp.ClientSession() as session:
async with session.get(
f"{RELAYER_URL}/user/realtime-token/api-key",
headers={"x-api-key": API_KEY},
) as resp:
data = await resp.json()
return data["token"]
async def main():
client = Client(WS_URL, get_token=fetch_token)
async def on_publication(ctx: PublicationContext) -> None:
handle_parlay_request(ctx.data)
handler = SubscriptionEventHandler(on_publication=on_publication)
sub = client.new_subscription("parlay_markets:global", handler)
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",
"baseToken": "0x8f3Cf7ad23Cd3CaDb09735AFf958023239c6A063",
"requestSize": "100000000",
"legs": [
{
"marketHash": "0x0d64c52e8781acdada86920a2d1e5acd6f29dcfe285cf9cae367b671dff05f7d",
"bettingOutcomeOne": true
},
{
"marketHash": "0xe609a49d083cd41214a0db276c1ba323c4a947eefd2e4260386fec7b5d258188",
"bettingOutcomeOne": false
}
]
}
```
| Field | Description |
| ------------- | -------------------------------------------------------------------------------------------- |
| `marketHash` | The parlay market to post your order against |
| `baseToken` | Token the bettor wants to bet in |
| `requestSize` | Requested size in base token units (see [`unit conversion`](/api-reference/unit-conversion)) |
| `legs` | Array of individual markets and the outcomes the bettor selected |
The `requestSize` is what the bettor requested, but you can offer any size you want. You are not limited to matching their request.
## Step 3: Look up each leg
Query each leg's market via [`GET /markets/find`](/api-reference/get-markets-find) to understand what's being bet on, and check current odds via [`GET /orders/odds/best`](/api-reference/get-best-odds):
```javascript JavaScript theme={null}
async function handleParlayRequest(parlayRequest) {
const { marketHash: parlayMarketHash, baseToken, 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/odds/best?${new URLSearchParams({ marketHashes: leg.marketHash })}`
).then((r) => r.json());
const bestOdds = ordersResponse.data.bestOdds;
return {
...leg,
market,
bestOdds,
};
})
);
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, baseToken, odds);
}
}
```
```python Python theme={null}
def handle_parlay_request(parlay_request):
parlay_market_hash = parlay_request["marketHash"]
base_token = parlay_request["baseToken"]
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/odds/best",
params={"marketHashes": leg["marketHash"]}
)
best_odds = odds_resp.json()["data"]["bestOdds"]
leg_data.append({**leg, "market": market, "bestOdds": best_odds})
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, base_token, 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
// In practice, you may want to account for correlation between legs
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
```
The `percentageOdds` field represents the **maker's** implied probability in the SX protocol format (multiply by 10^20). The taker's implied odds are `1 - percentageOdds / 10^20`. See [odds formats](/developers/odds-formats) and [unit conversion](/api-reference/unit-conversion) for details.
## Step 5: Post your order
Post an order to the parlay `marketHash` using [`POST /orders/new`](/api-reference/post-new-order), the same endpoint as single bets. The parlay market hash from the request is your `marketHash`. The `executor` address is available from [`GET /metadata`](/api-reference/get-metadata).
```javascript JavaScript theme={null}
import { Wallet, solidityPackedKeccak256, getBytes, randomBytes, hexlify } from "ethers";
async function postParlayOrder(parlayMarketHash, baseToken, percentageOdds) {
const wallet = new Wallet(process.env.SX_PRIVATE_KEY);
const order = {
marketHash: parlayMarketHash,
maker: wallet.address,
baseToken: baseToken,
totalBetSize: "1000000000", // your offered size in base token units
percentageOdds: percentageOdds,
expiry: 2209006800,
apiExpiry: Math.floor(Date.now() / 1000) + 60, // 1 minute from now
executor: "0x...", // from GET /metadata → executorAddress
salt: BigInt(hexlify(randomBytes(32))).toString(),
isMakerBettingOutcomeOne: false, // maker takes the opposite side
};
const orderHash = getBytes(
solidityPackedKeccak256(
["bytes32", "address", "uint256", "uint256", "uint256", "uint256", "address", "address", "bool"],
[order.marketHash, order.baseToken, order.totalBetSize, order.percentageOdds, order.expiry, order.salt, order.maker, order.executor, order.isMakerBettingOutcomeOne]
)
);
const signature = await wallet.signMessage(orderHash);
const response = await fetch(`${BASE_URL}/orders/new`, {
method: "POST",
headers: { "Content-Type": "application/json", "X-Api-Key": API_KEY },
body: JSON.stringify({ orders: [{ ...order, signature }] }),
}).then((r) => r.json());
console.log("Order posted:", response);
}
```
```python Python theme={null}
import secrets
from eth_account import Account
from eth_account.messages import encode_defunct
from web3 import Web3
def post_parlay_order(parlay_market_hash, base_token, percentage_odds):
account = Account.from_key(os.environ["SX_PRIVATE_KEY"])
order = {
"marketHash": parlay_market_hash,
"maker": account.address,
"baseToken": base_token,
"totalBetSize": "1000000000", # your offered size
"percentageOdds": percentage_odds,
"expiry": 2209006800,
"apiExpiry": int(time.time()) + 60, # 1 minute from now
"executor": "0x...", # from GET /metadata → executorAddress
"salt": str(int.from_bytes(secrets.token_bytes(32), "big")),
"isMakerBettingOutcomeOne": False,
}
order_hash = Web3.solidity_keccak(
["bytes32", "address", "uint256", "uint256", "uint256", "uint256", "address", "address", "bool"],
[
order["marketHash"],
order["baseToken"],
int(order["totalBetSize"]),
int(order["percentageOdds"]),
order["expiry"],
int(order["salt"]),
order["maker"],
order["executor"],
order["isMakerBettingOutcomeOne"],
]
)
message = encode_defunct(primitive=order_hash)
signed = account.sign_message(message)
signature = "0x" + signed.signature.hex()
response = requests.post(
f"{BASE_URL}/orders/new",
json={"orders": [{**order, "signature": signature}]},
headers={"X-Api-Key": API_KEY},
)
print("Order posted:", response.json())
```
You have a **3-second window** from when the parlay request is broadcast to post your order.
## Step 6: Manage your orders
After posting, you can [cancel](/api-reference/post-cancel-orders) parlay orders the same way as any other order:
```javascript JavaScript theme={null}
import { hexlify, randomBytes } from "ethers";
// Cancel a specific parlay order
const salt = hexlify(randomBytes(32));
const timestamp = Math.floor(Date.now() / 1000);
const chainId = 4162; // Mainnet — use 79479957 for testnet
const cancelSig = await wallet.signTypedData(
{ name: "CancelOrderV2SportX", version: "1.0", chainId, salt },
{ Details: [{ name: "orderHashes", type: "string[]" }, { name: "timestamp", type: "uint256" }] },
{ orderHashes: [orderHash], timestamp },
);
const response = await fetch(`${BASE_URL}/orders/cancel/v2`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ orderHashes: [orderHash], signature: cancelSig, salt, maker: wallet.address, timestamp }),
}).then((r) => r.json());
```
```python Python theme={null}
import secrets, time
from eth_account import Account
# Cancel a specific parlay order
cancel_salt = "0x" + secrets.token_bytes(32).hex()
timestamp = int(time.time())
chain_id = 4162 # Mainnet — use 79479957 for testnet
signed = Account.sign_typed_data(
account.key,
domain_data={"name": "CancelOrderV2SportX", "version": "1.0", "chainId": chain_id, "salt": cancel_salt},
message_types={"Details": [{"name": "orderHashes", "type": "string[]"}, {"name": "timestamp", "type": "uint256"}]},
message_data={"orderHashes": [order_hash], "timestamp": timestamp},
)
response = requests.post(
f"{BASE_URL}/orders/cancel/v2",
json={"orderHashes": [order_hash], "signature": "0x" + signed.signature.hex(), "salt": cancel_salt, "maker": account.address, "timestamp": timestamp},
)
```
## Summary
The full maker flow at a glance:
| Step | Action | Time constraint |
| ---- | -------------------------------------------- | --------------- |
| 1 | Subscribe to `parlay_markets:global` channel | Once at startup |
| 2 | Receive parlay request with legs | — |
| 3 | Look up each leg's market data | Within 3s |
| 4 | Calculate combined odds + your margin | Within 3s |
| 5 | Post order to the parlay `marketHash` | Within 3s |
| 6 | Optionally cancel if not filled | Anytime |
Parlay orders expire like normal orders. Set `apiExpiry` appropriately — a short expiry (e.g., 60 seconds) is recommended since parlay orderbooks are ephemeral.
## Related
How the RFQ parlay system works conceptually.
API reference for the order submission endpoint.
How to sign orders for the SX Bet protocol.
WebSocket channel reference for parlay requests.
# Markets
Source: https://docs.sx.bet/developers/markets-and-sports
A deep dive into what a market is on SX Bet and everything you need to know to work with them.
## 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:
* Fetch orders on a market
* Post and cancel orders
* Query your trade history
* Subscribe to orderbook updates via WebSocket
Whenever you're working with a specific market, you'll need its `marketHash`.
***
## 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.
***
## 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 filled |
| `INACTIVE` | The market is closed for trading |
Markets returned from `GET /markets/active` will always have `status: ACTIVE`. Once a game starts or a market is suspended, it will no longer appear in that endpoint.
***
## 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 [slippage](/developers/slippage) and [betting delays](/developers/betting-delays) when filling live orders.
***
## 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,
"__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 first team/player participating |
| `teamTwoName` | The name of the second team/player participating |
| `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 |
| `group1` | Indicator to the client of how to display this market |
| `group2` | Indicator to the client of how to display this market |
# 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`, `onlyMainLine` |
| Orders | `GET /orders` | Active maker orders on a market | `marketHashes`, `maker`, `sportXeventId`, `orderHash` |
## 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, and it will display 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 following endpoints use `sportXeventId` instead of `eventId` as a query parameter: `GET /trades/consolidated`, `GET /orders`
The following endpoints use `sportXEventIds` instead of `eventId` as a query parameter: `GET /live-scores`, `GET /fixture/status`
***
## 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.
Learn how to query active markets from the SX Bet API
# Odds & Tokens
Source: https://docs.sx.bet/developers/odds-and-tokens
How odds and tokens are represented on the SX Bet API
## Odds format
The SX Bet API represents odds as an **implied probability** stored as a fixed-point integer with 20 decimal places (units of 10^20).
### The formula
```
Human-readable implied probability = percentageOdds / 10^20
```
### Examples
| `percentageOdds` value | Implied probability | American odds (approx) |
| ---------------------- | ------------------- | ---------------------- |
| `60000000000000000000` | 60.00% | -150 |
| `52500000000000000000` | 52.50% | -111 |
| `50000000000000000000` | 50.00% | +100 |
| `40000000000000000000` | 40.00% | +150 |
### Converting in code
```python Python theme={null}
ODDS_PRECISION = 10 ** 20
def to_implied(percentage_odds: int) -> float:
"""Convert raw percentageOdds to decimal implied probability (0–1)."""
return percentage_odds / ODDS_PRECISION
def to_raw(implied: float) -> int:
"""Convert decimal implied probability to raw percentageOdds."""
return int(implied * ODDS_PRECISION)
def implied_to_american(implied: float) -> float:
"""Convert implied probability to American odds."""
if implied >= 0.5:
return -(implied / (1 - implied)) * 100
else:
return ((1 - implied) / implied) * 100
# Examples
raw = 52631578947368421052
print(to_implied(raw)) # 0.5263...
print(implied_to_american(0.5263)) # ~-111
```
```javascript JavaScript theme={null}
const ODDS_PRECISION = BigInt(10 ** 20);
function toImplied(percentageOdds) {
// Use floating point for display purposes
return Number(percentageOdds) / Number(ODDS_PRECISION);
}
function toRaw(implied) {
return BigInt(Math.round(implied * Number(ODDS_PRECISION)));
}
function impliedToAmerican(implied) {
if (implied >= 0.5) {
return -(implied / (1 - implied)) * 100;
} else {
return ((1 - implied) / implied) * 100;
}
}
// Examples
const raw = 52631578947368421052n;
console.log(toImplied(raw)); // 0.5263...
console.log(impliedToAmerican(0.5263)); // ~-111
```
`percentageOdds` values are very large integers. Use `BigInt` in JavaScript to avoid precision loss when doing arithmetic.
## Taker odds
When you fill an order, you automatically receive the complementary odds:
```
taker_implied = 1 - maker_implied
```
For example, if a maker posts an order with desired odds of 52.25% implied odds on outcome 1, the taker gets 47.75% implied odds on outcome 2.
## Token amounts (USDC)
All stake and payout amounts are in **USDC with 6 decimal places**.
```
Human-readable USDC = rawAmount / 1000000
```
### Examples
| Raw value | Human-readable |
| ---------- | -------------- |
| `1000000` | \$1.00 |
| `10000000` | \$10.00 |
| `5500000` | \$5.50 |
### Converting in code
```python Python theme={null}
USDC_DECIMALS = 10 ** 6
def to_usdc(raw: int) -> float:
return raw / USDC_DECIMALS
def from_usdc(amount: float) -> int:
return int(amount * USDC_DECIMALS)
print(to_usdc(10000000)) # 10.0
print(from_usdc(5.50)) # 5500000
```
```javascript JavaScript theme={null}
const USDC_DECIMALS = 1000000;
function toUsdc(raw) {
return raw / USDC_DECIMALS;
}
function fromUsdc(amount) {
return Math.round(amount * USDC_DECIMALS);
}
console.log(toUsdc(10000000)); // 10
console.log(fromUsdc(5.50)); // 5500000
```
## Calculating payout
For a given fill:
```
taker_payout = fill_amount / taker_implied
taker_profit = taker_payout - fill_amount
```
```python Python theme={null}
def calculate_payout(fill_amount_raw: int, maker_odds_raw: int) -> dict:
maker_implied = maker_odds_raw / ODDS_PRECISION
taker_implied = 1 - maker_implied
payout_raw = fill_amount_raw / taker_implied
profit_raw = payout_raw - fill_amount_raw
return {
"stake_usdc": to_usdc(fill_amount_raw),
"payout_usdc": to_usdc(payout_raw),
"profit_usdc": to_usdc(profit_raw),
}
result = calculate_payout(
fill_amount_raw=10000000, # $10 stake
maker_odds_raw=52631578947368421052, # maker at 52.63%
)
# stake: $10, payout: ~$21.17, profit: ~$11.17
```
```javascript JavaScript theme={null}
function calculatePayout(fillAmountRaw, makerOddsRaw) {
const makerImplied = Number(makerOddsRaw) / Number(ODDS_PRECISION);
const takerImplied = 1 - makerImplied;
const payoutRaw = fillAmountRaw / takerImplied;
const profitRaw = payoutRaw - fillAmountRaw;
return {
stakeUsdc: toUsdc(fillAmountRaw),
payoutUsdc: toUsdc(payoutRaw),
profitUsdc: toUsdc(profitRaw),
};
}
```
# Odds Rounding
Source: https://docs.sx.bet/developers/odds-rounding
How to validate and round odds to the SX Bet odds ladder.
## The odds ladder
SX Bet enforces an **odds ladder** to prevent diming (posting odds in tiny increments to gain an unfair edge). Your `percentageOdds` value must land exactly on one of the allowed steps, or your order will be rejected.
The ladder works in intervals of the implied probability. The current step size is **0.125%**, meaning valid implied odds are:
```
50.000%, 50.125%, 50.250%, 50.375%, 50.500%, ...
```
An offer of **50.25%** is valid. An offer of **50.20%** is not.
Orders with odds not on the ladder will be rejected and will not be posted.
## Getting the current step size
The step size is available from [`GET /metadata`](/api-reference/get-metadata). It returns a number from 0 to 1000:
| Metadata value | Step size |
| -------------- | --------- |
| 10 | 0.010% |
| 25 | 0.025% |
| 125 | 0.125% |
The step size in raw `percentageOdds` units is:
```
stepInRaw = metadataValue * 10^15
```
For the current default of 125: `125 * 10^15 = 1.25 * 10^17`
## Checking if your odds are valid
To check if a `percentageOdds` value falls on the ladder, take the modulus with the step size and check if it equals 0:
```python Python theme={null}
ODDS_PRECISION = 10 ** 20
ODDS_LADDER_STEP_SIZE = 125 # from GET /metadata
def is_odds_valid(percentage_odds: int) -> bool:
step = ODDS_LADDER_STEP_SIZE * (10 ** 15)
return percentage_odds % step == 0
# Valid: 50.250% implied = 50250000000000000000
print(is_odds_valid(50250000000000000000)) # True
# Invalid: 50.200% implied = 50200000000000000000
print(is_odds_valid(50200000000000000000)) # False
```
```javascript JavaScript theme={null}
const ODDS_PRECISION = 10n ** 20n;
const ODDS_LADDER_STEP_SIZE = 125n; // from GET /metadata
function isOddsValid(percentageOdds) {
const step = ODDS_LADDER_STEP_SIZE * 10n ** 15n;
return BigInt(percentageOdds) % step === 0n;
}
// Valid: 50.250% implied
console.log(isOddsValid("50250000000000000000")); // true
// Invalid: 50.200% implied
console.log(isOddsValid("50200000000000000000")); // false
```
## Rounding to the nearest valid step
If your calculated odds don't land on the ladder, round down to the nearest valid step before submitting:
```python Python theme={null}
def round_odds_down(percentage_odds: int) -> int:
"""Round odds down to the nearest valid step on the ladder."""
step = ODDS_LADDER_STEP_SIZE * (10 ** 15)
return (percentage_odds // step) * step
# 50.200% → 50.125%
raw = 50200000000000000000
rounded = round_odds_down(raw)
print(f"{raw / ODDS_PRECISION:.3%} → {rounded / ODDS_PRECISION:.3%}")
# 50.200% → 50.125%
# 67.800% → 67.750%
raw = 67800000000000000000
rounded = round_odds_down(raw)
print(f"{raw / ODDS_PRECISION:.3%} → {rounded / ODDS_PRECISION:.3%}")
# 67.800% → 67.750%
```
```javascript JavaScript theme={null}
function roundOddsDown(percentageOdds) {
const step = ODDS_LADDER_STEP_SIZE * 10n ** 15n;
const odds = BigInt(percentageOdds);
return (odds / step) * step;
}
// 50.200% → 50.125%
console.log(roundOddsDown("50200000000000000000").toString());
// "50125000000000000000"
// 67.800% → 67.750%
console.log(roundOddsDown("67800000000000000000").toString());
// "67750000000000000000"
```
## Full example: implied probability to valid `percentageOdds`
A common workflow: you have a fair probability (e.g., from your model), add margin, and need to convert it to a valid `percentageOdds` value.
```python Python theme={null}
def implied_to_valid_odds(implied: float) -> int:
"""Convert an implied probability to the nearest valid percentageOdds (rounded down)."""
raw = int(implied * ODDS_PRECISION)
return round_odds_down(raw)
# Your model says 54.3% implied, you want to post at that price
odds = implied_to_valid_odds(0.543)
print(f"percentageOdds: {odds}")
# 54250000000000000000 (54.250%)
print(f"Implied: {odds / ODDS_PRECISION:.3%}")
# 54.250%
print(f"Valid: {is_odds_valid(odds)}")
# True
```
```javascript JavaScript theme={null}
function impliedToValidOdds(implied) {
const raw = BigInt(Math.round(implied * 1e20));
return roundOddsDown(raw);
}
// Your model says 54.3% implied, you want to post at that price
const odds = impliedToValidOdds(0.543);
console.log(`percentageOdds: ${odds}`);
// 54250000000000000000 (54.250%)
console.log(`Valid: ${isOddsValid(odds)}`);
// true
```
Always round **down** when posting maker orders. Rounding up would give worse odds for you. If you need to round to the nearest step in either direction, compare the distance to the step above and below.
## Related
Converting between implied, American, and decimal odds.
Full reference for odds and token conversions.
API reference for submitting orders.
Retrieve the current odds ladder step size.
# Order Management
Source: https://docs.sx.bet/developers/order-management
Monitor your open orders in real-time, cancel orders, and set up a heartbeat to protect against connectivity loss.
## Overview
Once your orders are on the book, you need to track fills, react to market changes, and cancel orders that are no longer valid. This page covers the three tools for that: real-time order monitoring, order cancellation, and the heartbeat safety mechanism.
## Monitor your orders
Subscribe to the [`active_orders:{maker}` WebSocket channel](/api-reference/centrifugo-active-order-updates) to receive real-time updates when your orders are posted, partially filled, fully filled, or cancelled. Updates are batched and delayed by at most 100ms.
```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() {
const res = await fetch(`${RELAYER_URL}/user/realtime-token/api-key`, {
headers: { "x-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(`active_orders:${wallet.address}`, {
positioned: true,
recoverable: true,
});
sub.on("publication", (ctx) => {
for (const update of ctx.data) {
console.log(`Order ${update.orderHash}: status=${update.status}, filled=${update.fillAmount}`);
}
});
sub.subscribe();
client.connect();
```
```python Python theme={null}
import asyncio
import aiohttp
from centrifuge import Client, PublicationContext, SubscriptionEventHandler, SubscriptionOptions
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
async def fetch_token():
async with aiohttp.ClientSession() as session:
async with session.get(
f"{RELAYER_URL}/user/realtime-token/api-key",
headers={"x-api-key": os.environ["SX_API_KEY"]},
) as resp:
data = await resp.json()
return data["token"]
async def main():
client = Client(WS_URL, get_token=fetch_token)
async def on_publication(ctx: PublicationContext) -> None:
for update in ctx.data:
print(f"Order {update['orderHash']}: status={update['status']}, filled={update['fillAmount']}")
handler = SubscriptionEventHandler(on_publication=on_publication)
options = SubscriptionOptions(positioned=True, recoverable=True)
sub = client.new_subscription(f"active_orders:{account.address}", handler, options)
await client.connect()
await sub.subscribe()
await asyncio.Future()
asyncio.run(main())
```
Create only one `Centrifuge` instance per process. All subscriptions share the same connection — maximum 512 channel subscriptions per connection.
You can also poll your open orders via [`GET /orders`](/api-reference/get-orders) with the `maker` parameter — useful for reconciling state on startup before subscribing.
Full payload reference: [Active Order Updates →](/api-reference/centrifugo-active-order-updates)
## Cancel orders
All cancellation endpoints require an EIP-712 wallet signature. To update odds on a market, cancel the old order and post a new one — there is no in-place edit.
Three cancellation scopes are available:
| Endpoint | When to use |
| --------------------------------------------------------------- | --------------------------------------------------- |
| [`POST /orders/cancel/v2`](/api-reference/post-cancel-orders) | Cancel specific orders by hash |
| [`POST /orders/cancel/event`](/api-reference/post-cancel-event) | Cancel all orders tied to a specific event |
| [`POST /orders/cancel/all`](/api-reference/post-cancel-all) | Cancel everything — useful on shutdown or repricing |
Each endpoint uses the same pattern: generate a `salt` and `timestamp`, sign them with your wallet, and include the signature in the request. The domain name varies by endpoint — `"CancelOrderV2SportX"`, `"CancelOrderEventsSportX"`, or `"CancelAllOrdersSportX"`. See the API reference pages above for full signing code.
```python Python theme={null}
import os, secrets, requests
from eth_account import Account
BASE_URL = "https://api.sx.bet" # Mainnet — use https://api.toronto.sx.bet for testnet
CHAIN_ID = 4162 # Mainnet — use 79479957 for testnet
account = Account.from_key(os.environ["SX_PRIVATE_KEY"])
salt = "0x" + secrets.token_bytes(32).hex()
timestamp = int(__import__("time").time())
# Cancel specific orders by hash
signed = Account.sign_typed_data(
account.key,
domain_data={"name": "CancelOrderV2SportX", "version": "1.0", "chainId": CHAIN_ID, "salt": salt},
message_types={"Details": [{"name": "orderHashes", "type": "string[]"}, {"name": "timestamp", "type": "uint256"}]},
message_data={"orderHashes": ["0xabc..."], "timestamp": timestamp},
)
requests.post(f"{BASE_URL}/orders/cancel/v2", json={
"orderHashes": ["0xabc..."],
"signature": "0x" + signed.signature.hex(),
"salt": salt,
"maker": account.address,
"timestamp": timestamp,
})
# Cancel all orders for a specific event
signed = Account.sign_typed_data(
account.key,
domain_data={"name": "CancelOrderEventsSportX", "version": "1.0", "chainId": CHAIN_ID, "salt": salt},
message_types={"Details": [{"name": "sportXeventId", "type": "string"}, {"name": "timestamp", "type": "uint256"}]},
message_data={"sportXeventId": "L15468276", "timestamp": timestamp},
)
requests.post(f"{BASE_URL}/orders/cancel/event", json={
"sportXeventId": "L15468276",
"signature": "0x" + signed.signature.hex(),
"salt": salt,
"maker": account.address,
"timestamp": timestamp,
})
# Cancel all open orders
signed = Account.sign_typed_data(
account.key,
domain_data={"name": "CancelAllOrdersSportX", "version": "1.0", "chainId": CHAIN_ID, "salt": salt},
message_types={"Details": [{"name": "timestamp", "type": "uint256"}]},
message_data={"timestamp": timestamp},
)
requests.post(f"{BASE_URL}/orders/cancel/all", json={
"signature": "0x" + signed.signature.hex(),
"salt": salt,
"maker": account.address,
"timestamp": timestamp,
})
```
```javascript JavaScript theme={null}
import { Wallet, hexlify, randomBytes } from "ethers";
const BASE_URL = "https://api.sx.bet"; // Mainnet — use https://api.toronto.sx.bet for testnet
const CHAIN_ID = 4162; // Mainnet — use 79479957 for testnet
const wallet = new Wallet(process.env.SX_PRIVATE_KEY);
const salt = hexlify(randomBytes(32));
const timestamp = Math.floor(Date.now() / 1000);
// Cancel specific orders by hash
const sigV2 = await wallet.signTypedData(
{ name: "CancelOrderV2SportX", version: "1.0", chainId: CHAIN_ID, salt },
{ Details: [{ name: "orderHashes", type: "string[]" }, { name: "timestamp", type: "uint256" }] },
{ orderHashes: ["0xabc..."], timestamp },
);
await fetch(`${BASE_URL}/orders/cancel/v2`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ orderHashes: ["0xabc..."], signature: sigV2, salt, maker: wallet.address, timestamp }),
});
// Cancel all orders for a specific event
const sigEvent = await wallet.signTypedData(
{ name: "CancelOrderEventsSportX", version: "1.0", chainId: CHAIN_ID, salt },
{ Details: [{ name: "sportXeventId", type: "string" }, { name: "timestamp", type: "uint256" }] },
{ sportXeventId: "L15468276", timestamp },
);
await fetch(`${BASE_URL}/orders/cancel/event`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sportXeventId: "L15468276", signature: sigEvent, salt, maker: wallet.address, timestamp }),
});
// Cancel all open orders
const sigAll = await wallet.signTypedData(
{ name: "CancelAllOrdersSportX", version: "1.0", chainId: CHAIN_ID, salt },
{ Details: [{ name: "timestamp", type: "uint256" }] },
{ timestamp },
);
await fetch(`${BASE_URL}/orders/cancel/all`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ signature: sigAll, salt, maker: wallet.address, timestamp }),
});
```
## Heartbeat
The heartbeat is a safety mechanism that automatically cancels all your open orders if your service loses connectivity. This prevents stale orders from sitting on the book when you can't manage them.
Register a heartbeat with an interval (in seconds). If SX Bet doesn't receive a heartbeat ping within that window, all your orders are cancelled.
```python Python theme={null}
import requests
import time
import os
BASE_URL = "https://api.sx.bet" # Mainnet — use https://api.toronto.sx.bet for testnet
headers = {"X-Api-Key": os.environ["SX_API_KEY"]}
# Register a heartbeat with a 30-second timeout
requests.post(
f"{BASE_URL}/user/heartbeat",
json={"interval": 30},
headers=headers,
)
# Send heartbeats in a loop to keep orders alive
while True:
requests.post(
f"{BASE_URL}/user/heartbeat",
json={"interval": 30},
headers=headers,
)
time.sleep(15) # send well within the 30s window
```
```javascript JavaScript theme={null}
const BASE_URL = "https://api.sx.bet"; // Mainnet — use https://api.toronto.sx.bet for testnet
const headers = { "X-Api-Key": process.env.SX_API_KEY, "Content-Type": "application/json" };
// Register a heartbeat with a 30-second timeout
await fetch(`${BASE_URL}/user/heartbeat`, {
method: "POST",
headers,
body: JSON.stringify({ interval: 30 }),
});
// Send heartbeats in a loop to keep orders alive
setInterval(async () => {
await fetch(`${BASE_URL}/user/heartbeat`, {
method: "POST",
headers,
body: JSON.stringify({ interval: 30 }),
});
}, 15_000); // send well within the 30s window
```
Always set up a heartbeat before going live. If your service goes down and misses a heartbeat, all open orders will be cancelled automatically.
To stop the heartbeat and prevent auto-cancellation, call [`POST /user/heartbeat/cancel`](/api-reference/post-heartbeat-cancel).
Full reference: [Heartbeat →](/api-reference/heartbeat)
## Exposure management
You can post as many orders as you wish across the exchange as long as your total exposure per market remains below your wallet balance.
Total exposure can be calculated as the sum of `totalBetSize - fillAmount` across all your open orders on that market hash. Each market hash is checked independently, so you can post up to your full balance on as many market hashes as you like simultaneously.
**Example: posting across multiple markets**
Your wallet balance is 100 USDC. You post orders across three different markets:
| Market | Side | totalBetSize | fillAmount | Exposure |
| ------------------ | --------- | ------------ | ---------- | -------- |
| Spread (hash A) | Outcome 1 | 50 USDC | 0 | 50 USDC |
| Spread (hash A) | Outcome 2 | 50 USDC | 0 | 50 USDC |
| Moneyline (hash B) | Outcome 1 | 100 USDC | 0 | 100 USDC |
| Totals (hash C) | Outcome 1 | 50 USDC | 0 | 50 USDC |
| Totals (hash C) | Outcome 2 | 50 USDC | 0 | 50 USDC |
Per-hash exposure: hash A = 100 USDC, hash B = 100 USDC, hash C = 100 USDC. All equal your balance — all orders are valid.
If your total exposure exceeds your wallet balance on any given market, your orders will be removed from the orderbook until your balance reaches the minimum again.
If the API finds that your balance is consistently below your total exposure requiring orders to be removed, your account may be temporarily restricted.
Tips for managing exposure:
* Track your open exposure in real-time using the [`active_orders:{maker}` WebSocket channel](/api-reference/centrifugo-active-order-updates)
* Set `apiExpiry` on orders to automatically cancel orders after a set amount of time
* Use the heartbeat to auto-cancel if your service goes down
* Orders posted pre-match will be automatically cancelled when the match begins
## Related
Overview of the maker role, spreads, and exposure management.
Constructing, signing, and submitting orders.
All WebSocket channels including orderbook and market signals.
Full heartbeat endpoint reference.
Expected response times and co-location recommendations.
# Orderbook
Source: https://docs.sx.bet/developers/orderbook-core
How the SX Bet orderbook is structured and how to read it.
## What is the orderbook?
The orderbook is the collection of all active, unfilled orders for a given market. Every order in the book is a maker's open ask to bet on a specific outcome at specific odds.
When you fetch orders from the API, you're reading the live orderbook from the maker's perspective.
```bash theme={null}
curl "https://api.sx.bet/orders?marketHashes={marketHash}"
```
Orders on the SX Bet API are always from the maker's perspective.
***
## Reading an order
Each order in the API response contains:
```json theme={null}
{
"orderHash": "0xc19c8838b4ec0f5b6f51e8a18d6ca9c46c9b7a2e1234567890abcdef12345678",
"marketHash": "0x6750c579d780a9b04fcda4f3e0428d9e5e0a6b1c2d3e4f5a6b7c8d9e0f1a2b3",
"maker": "0xE52ed002A3d13e3DB3F5A29F7c98A49B4e5C6D7E",
"totalBetSize": "50000000",
"percentageOdds": "51000000000000000000",
"fillAmount": "0",
"expiry": 2209006800,
"apiExpiry": 1772737200,
"baseToken": "0xe2aa35C25aE3d9f4386d4Df7fE1177A3d044f2e7",
"executor": "0x8f3Bc9b2E3e98765FAC412e1d0A3B5c6d7e8F9A0",
"salt": "69837538621348917669822801580188867159099562954597560424559338173619670254332",
"isMakerBettingOutcomeOne": false,
"signature": "0x4e8b3f2a1c9d7e6b5a4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b",
"sportXeventId": "L18148217"
}
```
The critical fields for understanding an order:
| Field |
What it tells you |
| `marketHash` |
The market this order belongs to |
| `isMakerBettingOutcomeOne` |
Which side the maker is on (`true` = backing outcome 1, `false` = outcome 2) |
| `percentageOdds` |
The maker's implied odds — divide by 10^20 to get a decimal (e.g. 0.51 = 51%) |
| `totalBetSize` |
The maker's total stake (USDC, 6 decimals) |
| `fillAmount` |
Already filled portion — available = `totalBetSize - fillAmount` |
***
## Converting Odds to Taker's Perspective
To bet on outcome 1 as a taker, you need to find orders where the maker is betting on outcome 2 — because you're taking the other side.
The `isMakerBettingOutcomeOne` field tells you which side a maker has taken:
| `isMakerBettingOutcomeOne` | Maker is betting on | You can use this order to bet on |
| -------------------------- | ------------------- | -------------------------------- |
| `true` | Outcome 1 | **Outcome 2** |
| `false` | Outcome 2 | **Outcome 1** |
Let's take a real market — **Cleveland Cavaliers vs Boston Celtics** (moneyline) — and trace how raw maker orders from the API become the taker orderbook you see on sx.bet.
* Outcome 1 = Cleveland Cavaliers
* Outcome 2 = Boston Celtics
### Step 1: Raw maker orders from `GET /orders`
```bash theme={null}
curl "https://api.sx.bet/orders?marketHashes=0x1a46..."
```
Each row is labeled so you can trace it into the taker orderbook below.
|
maker |
totalBetSize |
percentageOdds |
isMakerBettingOutcomeOne |
| A |
0x5B34...eb42 |
758990000 |
42000000000000000000 |
false |
| B |
0xcFF3...32b |
530000000 |
46000000000000000000 |
false |
| C |
0xDFd1...530 |
4836776618 |
46000000000000000000 |
false |
| D |
0x740d...1D84 |
292187500 |
46750000000000000000 |
false |
| E |
0xDFd1...530 |
4616060608 |
50000000000000000000 |
true |
| F |
0xcFF3...32b |
799000000 |
51500000000000000000 |
true |
| G |
0x5B34...eb42 |
999650000 |
52000000000000000000 |
true |
| H |
0xcFF3...32b |
752000000 |
52000000000000000000 |
true |
|
Maker |
Bet Size (USDC) |
Maker Odds |
isMakerBettingOutcomeOne |
| A |
0x5B34...eb42 |
\$758.99 |
42.00% |
false |
| B |
0xcFF3...32b |
\$530.00 |
46.00% |
false |
| C |
0xDFd1...530 |
\$4,836.78 |
46.00% |
false |
| D |
0x740d...1D84 |
\$292.19 |
46.75% |
false |
| E |
0xDFd1...530 |
\$4,616.06 |
50.00% |
true |
| F |
0xcFF3...32b |
\$799.00 |
51.50% |
true |
| G |
0x5B34...eb42 |
\$999.65 |
52.00% |
true |
| H |
0xcFF3...32b |
\$752.00 |
52.00% |
true |
### Step 2: What the taker sees on sx.bet
Those maker orders create the following orderbook for takers.
The taker's price = `1 - (percentageOdds / 10^20)`. Best taker odds are at the top.
| Outcome 1 |
| Cleveland Cavaliers |
| Size | Taker Price |
| D |
\$292.19 |
53.25¢ |
| B |
\$530.00 |
54.0¢ |
| C |
\$4,836.78 |
54.0¢ |
| A |
\$758.99 |
58.0¢ |
| Outcome 2 |
| Boston Celtics |
| Size | Taker Price |
| G |
\$999.65 |
48.0¢ |
| H |
\$752.00 |
48.0¢ |
| F |
\$799.00 |
48.5¢ |
| E |
\$4,616.06 |
50.0¢ |
***
## Converting Maker Bet Sizes to Available Liquidity
Like `percentageOdds`, `totalBetSize` and `fillAmount` for a given order are from the maker's perspective. To find how much liquidity is available for a taker to fill, you will need to convert the remaining maker space (`totalBetSize - fillAmount`) into taker space.
### The formula
```
remainingTakerSpace = (totalBetSize - fillAmount) * 10^20 / percentageOdds - (totalBetSize - fillAmount)
```
On any given bet, the maker's stake and the taker's stake together form the total payout pool. The maker posts an order at their implied odds (`percentageOdds / 10^20`), so the total pot for any filled amount is `makerStake / makerOdds`. The taker's maximum stake is that total pot minus the maker's stake.
### Example
```json theme={null}
{
"totalBetSize": "10000000",
"fillAmount": "4000000",
"percentageOdds": "52500000000000000000"
}
```
```
remainingTakerSpace = (10000000 - 4000000) * 10^20 / 52500000000000000000 - (10000000 - 4000000)
≈ 5428571 (~5.43 USDC)
```
So while 6.00 USDC of maker stake remains, a taker can fill up to \~5.43 USDC on the other side.
### In code
```python Python theme={null}
ODDS_PRECISION = 10 ** 20
USDC_DECIMALS = 10 ** 6
def taker_liquidity(order: dict) -> float:
remaining_maker = int(order["totalBetSize"]) - int(order["fillAmount"])
remaining_taker = remaining_maker * ODDS_PRECISION // int(order["percentageOdds"]) - remaining_maker
return remaining_taker / USDC_DECIMALS
# Example
order = {
"totalBetSize": "10000000",
"fillAmount": "4000000",
"percentageOdds": "52500000000000000000",
}
print(f"${taker_liquidity(order):.2f} available to fill") # ~$5.43
```
```javascript JavaScript theme={null}
const ODDS_PRECISION = BigInt(10 ** 20);
const USDC_DECIMALS = 1e6;
function takerLiquidity(order) {
const remainingMaker = BigInt(order.totalBetSize) - BigInt(order.fillAmount);
const remainingTaker = remainingMaker * ODDS_PRECISION / BigInt(order.percentageOdds) - remainingMaker;
return Number(remainingTaker) / USDC_DECIMALS;
}
// Example
const order = {
totalBetSize: "10000000",
fillAmount: "4000000",
percentageOdds: "52500000000000000000",
};
console.log(`$${takerLiquidity(order).toFixed(2)} available to fill`); // ~$5.43
```
# Parlays (RFQ System)
Source: https://docs.sx.bet/developers/parlays
How SX Bet's request-for-quote parlay system works.
## Overview
SX Bet supports parlays (multi-leg bets) through a **Request-for-Quote (RFQ)** system. Unlike single bets where makers post orders to a standing orderbook, parlays are created on-demand when a bettor requests a custom combination of legs.
This means there is no persistent parlay orderbook. Every parlay market is created fresh in response to a bettor's request, and market makers compete in real-time to offer odds.
## How it works
```
Bettor selects multiple legs on sx.bet
│
▼
Parlay request broadcast via WebSocket
(channel: parlay_markets:global)
│
▼
Market makers receive the request
│
▼
3-second window for makers to
post orders to the parlay
│
▼
All submitted orders shown to taker
simultaneously after window closes
│
▼
Taker has 1 minute to fill an order
before the orderbook closes
│
▼
Taker chooses an order to fill
│
▼
Trade executes like any other bet
```
## Key concepts
### Parlay markets are real markets
When a bettor submits a parlay request, SX Bet creates a real market with its own `marketHash`. This market behaves like any other market on the exchange — you post orders to it, fill orders on it, and cancel orders on it using the same API endpoints.
The difference is that a parlay market has **legs** — an array of underlying single markets and the outcomes the bettor selected for each.
### The 3-second RFQ window
Once a parlay request is broadcast:
1. Market makers have **3 seconds** to analyze the legs and post orders
2. After the window closes, the taker sees all available orders at once
3. The taker has **1 minute** to fill an order before the orderbook closes
4. The taker picks the best offer and fills it
Your orders may remain active even after the bettor's orderbook window closes. Set `apiExpiry` on your orders to control how long they stay valid.
### Sample parlay request
When a taker submits a parlay, makers receive a message like this on the `parlay_markets:global` WebSocket channel:
```json theme={null}
{
"marketHash": "0x38cceead7bda65c18574a34994ebd8af154725d08aa735dcbf26247a7dcc67bd",
"baseToken": "0x6629Ce1Cf35Cc1329ebB4F63202F3f197b3F050B", // Mainnet USDC — see References for testnet address
"requestSize": "100000000",
"legs": [
{
"marketHash": "0x0d64c52e8781acdada86920a2d1e5acd6f29dcfe285cf9cae367b671dff05f7d",
"bettingOutcomeOne": true
},
{
"marketHash": "0xe609a49d083cd41214a0db276c1ba323c4a947eefd2e4260386fec7b5d258188",
"bettingOutcomeOne": false
}
]
}
```
The `marketHash` at the top level is the parlay market you post orders against. The `legs` array contains the individual markets and outcomes the taker selected.
### Legs and pricing
Each leg in a parlay request contains:
| Field | Description |
| ------------------- | -------------------------------------------------------- |
| `marketHash` | The single market for this leg |
| `bettingOutcomeOne` | Which outcome the bettor selected (`true` = outcome one) |
As a market maker, you need to:
* Look up each leg's market via [`GET /markets/find`](/api-reference/get-markets-find) to understand the matchup
* Check current odds for each leg via [`GET /orders/odds/best`](/api-reference/get-best-odds)
* Calculate combined odds across all legs (accounting for correlation if applicable)
* Decide your offer price and size, then post via [`POST /orders/new`](/api-reference/post-new-order)
For example, given the sample request above with two legs:
| Leg | Market | Taker's Pick | Your Fair Probability |
| ------------ | ----------- | -------------------------------- | ----------------------- |
| 1 | `0x0d64...` | Outcome One (e.g. Team A to win) | 55% |
| 2 | `0xe609...` | Outcome Two (e.g. Under 220.5) | 48% |
| **Combined** | | | **26.4%** (0.55 × 0.48) |
You'd then add your margin on top of the combined probability to arrive at the odds you offer. For example, with a 3% edge you'd price the parlay at \~29.4% implied probability for the maker side.
### Fees
SX Bet charges **0% fees** on single bets but applies a **5% fee on profit** for parlay bets.
## 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/fill anytime | 3-second maker window |
| **Market creation** | Markets exist for scheduled events | Market created when bettor requests |
| **Legs** | Single market | Multiple underlying markets |
| **Fees** | 0% maker and taker | 5% on profit |
## Next steps
The full guide to listening for, pricing, and submitting orders on parlay requests.
API reference for parlay market behavior and fields.
# Posting Orders
Source: https://docs.sx.bet/developers/posting-orders
How to construct, sign, and submit maker orders to the SX Bet orderbook.
## Overview
Posting an order places it on the SX Bet orderbook for takers to fill. This guide covers every field in the order payload, how to sign it, and how to handle the response.
## Order fields
Each order submitted to [`POST /orders/new`](/api-reference/post-new-order) requires these fields:
| Field | Type | Description |
| -------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `marketHash` | string | The market you're posting to. Get this from [`GET /markets/active`](/api-reference/get-markets-active) or [`GET /markets/find`](/api-reference/get-markets-find) |
| `maker` | string | Your wallet address |
| `baseToken` | string | The token this order is denominated in (e.g., USDC). See [References](/api-reference/references) for token addresses |
| `totalBetSize` | string | Your maximum risk in base token units (Ethereum format). See [Unit Conversion](/api-reference/unit-conversion) |
| `percentageOdds` | string | The odds **you the maker** receive, in SX protocol format (`implied * 10^20`). Must be on the [odds ladder](/developers/odds-rounding) |
| `expiry` | number | Deprecated. Must always be `2209006800` |
| `apiExpiry` | number | Unix timestamp (seconds) after which this order expires |
| `executor` | string | The SX Bet exchange address. Get this from [`GET /metadata`](/api-reference/get-metadata) |
| `salt` | string | A random number to differentiate otherwise identical orders |
| `isMakerBettingOutcomeOne` | boolean | `true` if you're betting on outcome one, `false` for outcome two |
| `signature` | string | Your wallet signature over the order hash |
### Understanding `percentageOdds`
This is the implied probability **from your perspective as the maker**. The taker gets the complementary odds on the opposite outcome:
```
taker_implied = 1 - (percentageOdds / 10^20)
```
For example, if you post at `percentageOdds = "52500000000000000000"` (52.5% maker implied), the taker sees 47.5% implied odds on the opposite outcome.
See [Odds Formats](/developers/odds-formats) for conversions between implied, American, and decimal odds.
### Understanding `totalBetSize`
This is the maximum amount of tokens **you** would put into the pot if the order is fully filled. It's in Ethereum units — for USDC (6 decimals), `"100000000"` = 100 USDC.
A taker can partially fill your order. The remaining unfilled portion stays on the book.
The minimum maker order size is **10 USDC**.
## Example
```javascript JavaScript theme={null}
import "dotenv/config";
import { Wallet, solidityPackedKeccak256, getBytes, 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);
// 1. Build the order
const order = {
marketHash: "0x8eeace4a9bbf6235bc59695258a419ed3a85a2c8e3b6a58fb71a0d9e6b031c2b",
maker: wallet.address,
baseToken: "0x6629Ce1Cf35Cc1329ebB4F63202F3f197b3F050B", // Mainnet USDC — see References for testnet address
totalBetSize: "100000000", // 100 USDC
percentageOdds: "52500000000000000000", // 52.5% maker implied
expiry: 2209006800,
apiExpiry: Math.floor(Date.now() / 1000) + 3600, // expires in 1 hour
executor: "0x52adf738AAD93c31f798a30b2C74D658e1E9a562", // Mainnet — use GET /metadata → executorAddress for testnet
salt: BigInt(hexlify(randomBytes(32))).toString(),
isMakerBettingOutcomeOne: true,
};
// 2. Sign
const orderHash = getBytes(
solidityPackedKeccak256(
[
"bytes32", // marketHash
"address", // baseToken
"uint256", // totalBetSize
"uint256", // percentageOdds
"uint256", // expiry
"uint256", // salt
"address", // maker
"address", // executor
"bool", // isMakerBettingOutcomeOne
],
[
order.marketHash,
order.baseToken,
order.totalBetSize,
order.percentageOdds,
order.expiry,
order.salt,
order.maker,
order.executor,
order.isMakerBettingOutcomeOne,
]
)
);
const signature = await wallet.signMessage(orderHash);
// For browser wallets (e.g. MetaMask), use an injected provider instead:
// const provider = new BrowserProvider(window.ethereum);
// const signer = await provider.getSigner();
// const signature = await signer.signMessage(orderHash);
// 3. Submit
const response = await fetch(`${BASE_URL}/orders/new`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ orders: [{ ...order, signature }] }),
}).then((r) => r.json());
console.log(response);
```
```python Python theme={null}
import os
import time
import secrets
import requests
from dotenv import load_dotenv
from eth_account import Account
from eth_account.messages import encode_defunct
from web3 import Web3
load_dotenv()
BASE_URL = "https://api.sx.bet" # Mainnet — use https://api.toronto.sx.bet for testnet
account = Account.from_key(os.environ["SX_PRIVATE_KEY"])
# 1. Build the order
order = {
"marketHash": "0x8eeace4a9bbf6235bc59695258a419ed3a85a2c8e3b6a58fb71a0d9e6b031c2b",
"maker": account.address,
"baseToken": "0x6629Ce1Cf35Cc1329ebB4F63202F3f197b3F050B", # Mainnet USDC — see References for testnet address
"totalBetSize": "100000000", # 100 USDC
"percentageOdds": "52500000000000000000", # 52.5% maker implied
"expiry": 2209006800,
"apiExpiry": int(time.time()) + 3600, # expires in 1 hour
"executor": "0x52adf738AAD93c31f798a30b2C74D658e1E9a562", # Mainnet — use GET /metadata → executorAddress for testnet
"salt": str(int.from_bytes(secrets.token_bytes(32), "big")),
"isMakerBettingOutcomeOne": True,
}
# 2. Sign
order_hash = Web3.solidity_keccak(
[
"bytes32", # marketHash
"address", # baseToken
"uint256", # totalBetSize
"uint256", # percentageOdds
"uint256", # expiry
"uint256", # salt
"address", # maker
"address", # executor
"bool", # isMakerBettingOutcomeOne
],
[
order["marketHash"],
order["baseToken"],
int(order["totalBetSize"]),
int(order["percentageOdds"]),
order["expiry"],
int(order["salt"]),
order["maker"],
order["executor"],
order["isMakerBettingOutcomeOne"],
]
)
message = encode_defunct(primitive=order_hash)
signed = account.sign_message(message)
signature = "0x" + signed.signature.hex()
# 3. Submit
response = requests.post(
f"{BASE_URL}/orders/new",
json={"orders": [{**order, "signature": signature}]},
)
print(response.json())
```
## Response
A successful response looks like this:
```json theme={null}
{
"status": "success",
"data": {
"orders": [
"0x7a9d420551c4a635849013dd908f7894766e97aee25fe656d0c5ac857e166fac"
],
"statuses": {
"0x7a9d420551c4a635849013dd908f7894766e97aee25fe656d0c5ac857e166fac": "OK"
},
"inserted": 1
}
}
```
Each order gets a status:
| Status | Description |
| ---------------------- | --------------------------------- |
| `OK` | Order created successfully |
| `INSUFFICIENT_BALANCE` | Insufficient maker token balance |
| `INVALID_MARKET` | Non-unique marketHashes specified |
| `ORDERS_ALREADY_EXIST` | The order already exists |
## Common issues
### Order rejected — odds not on ladder
Your `percentageOdds` must land on the odds ladder (currently 0.125% intervals). See [Odds Rounding](/developers/odds-rounding) for how to validate and round your odds.
### Order rejected — exposure too high
Your total exposure per market hash (`sum of totalBetSize - fillAmount` across all open orders on that market hash) must stay under your wallet balance for that token. Cancel some orders or add funds.
### Order rejected — betting not enabled
You need to approve the `TokenTransferProxy` contract for each token you trade. See [Enabling Betting](/api-reference/enabling-betting).
### `apiExpiry` tips
* Set a reasonable expiry — orders left open indefinitely consume exposure
* For volatile markets, use shorter expiries (minutes) and repost
* For stable pre-game markets, longer expiries (hours) are fine
* The `expiry` field is deprecated and must always be `2209006800`
## Related
Validate and round odds to the ladder.
Full API reference for the endpoint.
Converting between raw values and human-readable amounts.
Expected response times and co-location recommendations.
# Querying Balances
Source: https://docs.sx.bet/developers/querying-balances
How to check your USDC balance on SX Bet using the blockchain explorer API.
## Overview
SX Bet provides a blockchain explorer API that lets you query your USDC balance.
## Base URL
```
https://explorerl2.sx.technology/api
```
For testnet:
```
https://explorerl2.toronto.sx.technology/api
```
## Check your USDC balance
Query your ERC-20 token balance using the `tokenbalance` action. You need the token contract address and your wallet address.
```python Python theme={null}
import requests
EXPLORER_API = "https://explorerl2.sx.technology/api"
USDC_ADDRESS = "0x6629Ce1Cf35Cc1329ebB4F63202F3f197b3F050B"
WALLET = "0xYourWalletAddress"
response = requests.get(EXPLORER_API, params={
"module": "account",
"action": "tokenbalance",
"contractaddress": USDC_ADDRESS,
"address": WALLET,
})
result = response.json()
raw_balance = int(result["result"])
usdc_balance = raw_balance / 10**6 # USDC has 6 decimals
print(f"USDC balance: {usdc_balance:.2f}")
```
```javascript JavaScript theme={null}
const EXPLORER_API = "https://explorerl2.sx.technology/api";
const USDC_ADDRESS = "0x6629Ce1Cf35Cc1329ebB4F63202F3f197b3F050B";
const WALLET = "0xYourWalletAddress";
const data = await fetch(
`${EXPLORER_API}?${new URLSearchParams({
module: "account",
action: "tokenbalance",
contractaddress: USDC_ADDRESS,
address: WALLET,
})}`
).then((r) => r.json());
const rawBalance = BigInt(data.result);
const usdcBalance = Number(rawBalance) / 1e6; // USDC has 6 decimals
console.log(`USDC balance: ${usdcBalance.toFixed(2)}`);
```
Response:
```json theme={null}
{
"message": "OK",
"status": "1",
"result": "135499"
}
```
The `result` is the raw token balance. Divide by `10^decimals` to get the human-readable amount (USDC = 6 decimals). See [Unit Conversion](/api-reference/unit-conversion) for more on token decimals.
## Token addresses
| Token | Mainnet | Testnet |
| ----- | -------------------------------------------- | -------------------------------------------- |
| USDC | `0x6629Ce1Cf35Cc1329ebB4F63202F3f197b3F050B` | `0x1BC6326EA6aF2aB8E4b6Bc83418044B1923b2956` |
See [References](/api-reference/references) for the full list of addresses and network details.
## Related
Converting between raw token values and human-readable amounts.
Contract addresses, RPC URLs, and chain IDs.
Approve the TokenTransferProxy before placing bets.
Use your balance to fill orders as a taker.
# 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 fetched live markets, read current odds, and filled a real order.
## What you'll need
* An account on SX Bet (for signing orders)
* USDC (to fill an order)
Market data and orderbook reads are public — no authentication needed. You only need an account for posting or filling orders.
## Base URL
All API requests go to:
```
https://api.sx.bet
```
## Install dependencies
```bash Python theme={null}
pip install requests eth-account python-dotenv
```
```bash JavaScript theme={null}
npm install ethers dotenv
```
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 will need this to sign orders through the API.
Store it securely in a `.env` file:
```bash theme={null}
SX_PRIVATE_KEY="0xyour_private_key_here"
```
Fetch active markets 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"
```
```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"
).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`).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);
```
Get the best available odds for your market (from the maker's perspective) using the `marketHash` from the previous step. You'll use these to derive your taker odds in the next step.
```bash cURL theme={null}
curl "https://api.sx.bet/orders/odds/best?marketHashes=YOUR_MARKET_HASH&baseToken=0x6629Ce1Cf35Cc1329ebB4F63202F3f197b3F050B"
```
```python Python theme={null}
market_hash = "YOUR_MARKET_HASH" # from step 2
response = requests.get(
f"{BASE_URL}/orders/odds/best",
params={
"marketHashes": market_hash,
"baseToken": "0x6629Ce1Cf35Cc1329ebB4F63202F3f197b3F050B", # Mainnet USDC — see References for testnet address
}
)
odds = response.json()["data"]["bestOdds"][0]
# Maker odds — the best prices available from makers on each outcome
# percentageOdds is null if no orders exist on that side
pct_one = odds["outcomeOne"]["percentageOdds"]
pct_two = odds["outcomeTwo"]["percentageOdds"]
outcome_one_maker = int(pct_one) / 10**20 if pct_one is not None else None
outcome_two_maker = int(pct_two) / 10**20 if pct_two is not None else None
print(f"Maker odds on outcome 1: {outcome_one_maker:.2%}" if outcome_one_maker is not None else "Maker odds on outcome 1: no orders")
print(f"Maker odds on outcome 2: {outcome_two_maker:.2%}" if outcome_two_maker is not None else "Maker odds on outcome 2: no orders")
```
```javascript JavaScript theme={null}
const BASE_URL = "https://api.sx.bet"; // Mainnet — use https://api.toronto.sx.bet for testnet
const marketHash = "YOUR_MARKET_HASH"; // from step 2
const response = await fetch(
`${BASE_URL}/orders/odds/best?${new URLSearchParams({
marketHashes: marketHash,
baseToken: "0x6629Ce1Cf35Cc1329ebB4F63202F3f197b3F050B", // Mainnet USDC — see References for testnet address
})}`
).then((r) => r.json());
const odds = response.data.bestOdds[0];
// Maker odds — the best prices available from makers on each outcome
// percentageOdds is null if no orders exist on that side
const pctOne = odds.outcomeOne.percentageOdds;
const pctTwo = odds.outcomeTwo.percentageOdds;
const outcomeOneMaker = pctOne !== null ? Number(pctOne) / 1e20 : null;
const outcomeTwoMaker = pctTwo !== null ? Number(pctTwo) / 1e20 : null;
console.log(outcomeOneMaker !== null ? `Maker odds on outcome 1: ${(outcomeOneMaker * 100).toFixed(2)}%` : "Maker odds on outcome 1: no orders");
console.log(outcomeTwoMaker !== null ? `Maker odds on outcome 2: ${(outcomeTwoMaker * 100).toFixed(2)}%` : "Maker odds on outcome 2: no orders");
```
Submit a fill against the market from step 2. `desiredOdds` is from the **taker's perspective** — the inverse of the maker's best odds on the opposite outcome from step 3.
The `verifyingContract` and `baseToken` values below are hardcoded to mainnet. Using the wrong values will cause signature failures. Fetch `verifyingContract` from [`GET /metadata`](/api-reference/get-metadata) → `EIP712FillHasher`, and get the correct `baseToken` for your network from [References](/api-reference/references). See [Testnet & Mainnet](/developers/testnet-and-mainnet) for the recommended config pattern.
```python Python theme={null}
import os
import secrets
import requests
from eth_account import Account
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 2
base_token = "0x6629Ce1Cf35Cc1329ebB4F63202F3f197b3F050B" # Mainnet USDC — see References for testnet address
stake_wei = "50000000" # 50 USDC (6 decimals)
# Desired odds (taker's perspective)
# To get best available taker odds: 10^20 - opposite outcome's maker odds from step 3
desired_odds = "YOUR_DESIRED_ODDS"
is_taker_betting_outcome_one = True # True = bet outcome 1, False = bet outcome 2
fill_salt = int.from_bytes(secrets.token_bytes(32), "big")
# --- Sign the fill ---
DOMAIN = {
"name": "SX Bet",
"version": "6.0",
"chainId": 4162, # Mainnet — use 79479957 for testnet
"verifyingContract": "0x845a2Da2D70fEDe8474b1C8518200798c60aC364", # Mainnet — use GET /metadata → EIP712FillHasher for testnet
}
FILL_TYPES = {
"Details": [
{"name": "action", "type": "string"},
{"name": "market", "type": "string"},
{"name": "betting", "type": "string"},
{"name": "stake", "type": "string"},
{"name": "worstOdds", "type": "string"},
{"name": "worstReturning", "type": "string"},
{"name": "fills", "type": "FillObject"},
],
"FillObject": [
{"name": "stakeWei", "type": "string"},
{"name": "marketHash", "type": "string"},
{"name": "baseToken", "type": "string"},
{"name": "desiredOdds", "type": "string"},
{"name": "oddsSlippage", "type": "uint256"},
{"name": "isTakerBettingOutcomeOne", "type": "bool"},
{"name": "fillSalt", "type": "uint256"},
{"name": "beneficiary", "type": "address"},
{"name": "beneficiaryType", "type": "uint8"},
{"name": "cashOutTarget", "type": "bytes32"},
],
}
signed = Account.sign_typed_data(
account.key,
domain_data=DOMAIN,
message_types=FILL_TYPES,
message_data={
"action": "N/A",
"market": market_hash,
"betting": "N/A",
"stake": "N/A",
"worstOdds": "N/A",
"worstReturning": "N/A",
"fills": {
"stakeWei": stake_wei,
"marketHash": market_hash,
"baseToken": base_token,
"desiredOdds": desired_odds,
"oddsSlippage": 0,
"isTakerBettingOutcomeOne": is_taker_betting_outcome_one,
"fillSalt": fill_salt,
"beneficiary": "0x0000000000000000000000000000000000000000",
"beneficiaryType": 0,
"cashOutTarget": b"\x00" * 32,
},
},
)
taker_sig = "0x" + signed.signature.hex()
# --- Submit ---
response = requests.post(f"{BASE_URL}/orders/fill/v2", json={
"market": market_hash,
"baseToken": base_token,
"isTakerBettingOutcomeOne": is_taker_betting_outcome_one,
"stakeWei": stake_wei,
"desiredOdds": desired_odds,
"oddsSlippage": 0,
"taker": account.address,
"takerSig": taker_sig,
"fillSalt": str(fill_salt),
})
print("Fill result:", response.json())
```
```javascript JavaScript theme={null}
import "dotenv/config";
import { Wallet, ZeroAddress, ZeroHash, 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 2
const baseToken = "0x6629Ce1Cf35Cc1329ebB4F63202F3f197b3F050B"; // Mainnet USDC — see References for testnet address
const stakeWei = "50000000"; // 50 USDC (6 decimals)
// Desired odds (taker's perspective)
// To get best available taker odds: 10^20 - opposite outcome's maker odds from step 3
const desiredOdds = "YOUR_DESIRED_ODDS";
const oddsSlippage = 0;
const isTakerBettingOutcomeOne = true; // true = bet outcome 1, false = bet outcome 2
const fillSalt = BigInt(hexlify(randomBytes(32))).toString();
// --- Sign the fill ---
const domain = {
name: "SX Bet",
version: "6.0",
chainId: 4162, // Mainnet — use 79479957 for testnet
verifyingContract: "0x845a2Da2D70fEDe8474b1C8518200798c60aC364", // Mainnet — use GET /metadata → EIP712FillHasher for testnet
};
const types = {
Details: [
{ name: "action", type: "string" },
{ name: "market", type: "string" },
{ name: "betting", type: "string" },
{ name: "stake", type: "string" },
{ name: "worstOdds", type: "string" },
{ name: "worstReturning", type: "string" },
{ name: "fills", type: "FillObject" },
],
FillObject: [
{ name: "stakeWei", type: "string" },
{ name: "marketHash", type: "string" },
{ name: "baseToken", type: "string" },
{ name: "desiredOdds", type: "string" },
{ name: "oddsSlippage", type: "uint256" },
{ name: "isTakerBettingOutcomeOne", type: "bool" },
{ name: "fillSalt", type: "uint256" },
{ name: "beneficiary", type: "address" },
{ name: "beneficiaryType", type: "uint8" },
{ name: "cashOutTarget", type: "bytes32" },
],
};
const message = {
action: "N/A",
market: marketHash,
betting: "N/A",
stake: "N/A",
worstOdds: "N/A",
worstReturning: "N/A",
fills: {
stakeWei,
marketHash,
baseToken,
desiredOdds,
oddsSlippage,
isTakerBettingOutcomeOne,
fillSalt,
beneficiary: ZeroAddress,
beneficiaryType: 0,
cashOutTarget: ZeroHash,
},
};
const takerSig = await wallet.signTypedData(domain, types, message);
// --- Submit ---
const response = await fetch(`${BASE_URL}/orders/fill/v2`, {
method: "POST",
body: JSON.stringify({
market: marketHash,
baseToken,
isTakerBettingOutcomeOne,
stakeWei,
desiredOdds,
oddsSlippage,
taker: wallet.address,
takerSig,
fillSalt,
}),
headers: { "Content-Type": "application/json" },
});
console.log("Fill result:", await response.json());
```
If your fill returns `"status": "success"`, you're live on mainnet. Check your open positions at [sx.bet/my-bets](https://sx.bet/my-bets).
Understand how the exchange and orderbook work in depth.
Step-by-step guide to signing and submitting fills.
# Rate Limits
Source: https://docs.sx.bet/developers/rate-limits
Request limits for the SX Bet REST API.
Rate limits are applied at the **endpoint group** level. All requests within a group count toward the same shared bucket — not a per-endpoint limit.
## Limits
| Endpoint group | Limit |
| ------------------------------ | ----------------------- |
| All `POST /orders/*` endpoints | 5,500 requests/min |
| All `GET /orders/*` endpoints | 20 requests/10s |
| All `GET /trades/*` endpoints | 200 requests/min |
| All other endpoints | 500 requests/min (base) |
| All traffic (global ceiling) | 35,000 requests/10min |
The global ceiling of 35,000 requests/10min applies across all traffic regardless of endpoint group.
## What counts toward each limit
**`POST /orders/*`** — `POST /orders/new`, `POST /orders/cancel/v2`, `POST /orders/cancel/event`, `POST /orders/cancel/all`, and `POST /orders/fill/v2` all draw from the same 5,500 req/min bucket.
**`GET /orders/*`** — `GET /orders` (active orders) and `GET /orders/odds/best` share the same 20 req/10s bucket.
**`GET /trades/*`** — `GET /trades`, `GET /trades/consolidated`, `GET /trades/orders`, and `GET /trades/portfolio/refunds` all draw from the same 200 req/min bucket.
## When limits are exceeded
Requests that exceed a limit return HTTP `429 Too Many Requests`.
## Staying within limits
For high-frequency use cases — such as continuously monitoring odds or tracking order state — we recommend combining REST requests with WebSocket subscriptions rather than polling via REST alone. WebSocket subscriptions deliver real-time updates without consuming your request quota.
| Use case | WebSocket channel |
| --------------------------------- | ---------------------------------------- |
| Best odds across all markets | `best_odds:{baseToken}` |
| Full orderbook for a market | `order_book_v2:{token}:{marketHash}` |
| Your open orders (fills, cancels) | `active_orders_v2:{baseToken}:{address}` |
| Recent trade activity | `recent_trades` |
See [Real-time Data](/developers/real-time) for setup guides and payload references for each channel.
# Real-time Data
Source: https://docs.sx.bet/developers/real-time
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/) and require an API key. Centrifugo provides [official client SDKs](https://centrifugal.dev/docs/transports/client_sdk) for JavaScript, Python, Go, Dart, Swift, Java, and C#.
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** — 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
### Authenticate
To connect, you need a realtime token from the relayer. Fetch it from `/user/realtime-token/api-key` and pass your API key in the `x-api-key` header. In the client SDK, provide a `getToken` callback that returns this token.
The SDK calls `getToken` again whenever the current token expires, so passing a function instead of a static token is all that's needed to keep the connection alive. See [Common failures → Auth](#auth) for how to signal permanent auth failure vs. a transient fetch error.
### Connect
Create one `Centrifuge` client per process and reuse it for all subscriptions. All channel subscriptions are multiplexed over the same connection.
```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/api-key`, {
headers: { "x-api-key": apiKey },
});
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(YOUR_API_KEY),
});
client.connect();
```
```python Python theme={null}
import asyncio
import os
import requests
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
def fetch_token():
res = requests.get(
f"{RELAYER_URL}/user/realtime-token/api-key",
headers={"x-api-key": os.environ["SX_API_KEY"]},
)
if not res.ok:
raise Exception(f"Token endpoint returned {res.status_code}")
return res.json()["token"]
async def main():
client = Client(WS_URL, get_token=fetch_token)
await client.connect()
await asyncio.Future() # keep running
asyncio.run(main())
```
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.
### Subscribe
Use `client.newSubscription(channel, options)` to create a subscription, attach event handlers, then call `.subscribe()`:
If you need at-least-once delivery across reconnects, pass `positioned: true` and `recoverable: true` together for channels that support recovery. See [Recovery & reliability](#recovery-%26-reliability) for details.
```javascript JavaScript theme={null}
const sub = client.newSubscription("markets:global");
sub.on("publication", (ctx) => {
console.log(ctx.data);
});
sub.subscribe();
```
```python Python theme={null}
from centrifuge import Client, PublicationContext, SubscriptionEventHandler
async def on_publication(ctx: PublicationContext) -> None:
print(ctx.data)
handler = SubscriptionEventHandler(on_publication=on_publication)
sub = client.new_subscription("markets:global", handler)
await sub.subscribe()
```
For the full Centrifugo client SDK specification, see [Client SDK specification](https://centrifugal.dev/docs/transports/client_api).
***
## Channels
| Channel | What you receive | Payload reference |
| ----------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `active_orders:{maker}` | Your orders being filled, cancelled, or posted | [Active Order Updates →](/api-reference/centrifugo-active-order-updates) |
| `order_book:market_{marketHash}` | All order changes for a specific market | [Order Book Updates →](/api-reference/centrifugo-order-book-updates) |
| `order_book:event_{sportXEventId}` | All order changes for every market in an event | [Order Book Updates →](/api-reference/centrifugo-order-book-updates) |
| `best_odds:global` | Best available odds changes across all markets | [Best Odds →](/api-reference/centrifugo-best-odds) |
| `recent_trades:global` | Global feed of all exchange trades | [Trade Updates →](/api-reference/centrifugo-trade-updates) |
| `recent_trades_consolidated:global` | Consolidated trade feed — one entry per fill, not per order | [Consolidated Trade Updates →](/api-reference/centrifugo-consolidated-trade-updates) |
| `markets:global` | Market status changes, suspension, settlement | [Market Updates →](/api-reference/centrifugo-market-updates) |
| `main_line:global` | Main line shifts on spread and totals markets | [Line Changes →](/api-reference/centrifugo-line-changes) |
| `fixtures:global` | Fixture metadata updates | [Live Score & Fixture Updates →](/api-reference/centrifugo-live-score-updates) |
| `fixtures:live_scores` | Live match scores | [Live Score Updates →](/api-reference/centrifugo-live-score-updates) |
| `ce_refunds:{bettor}` | Chain enforcer refund events for your address | [CE Refund Events →](/api-reference/centrifugo-ce-refund-events) |
| `parlay_markets:global` | Incoming parlay RFQ requests | [Parlay Market Requests →](/api-reference/centrifugo-parlay-market-requests) |
For channel name changes from the legacy Ably API, see [WebSocket Channels → Migration Guide](/api-reference/centrifugo-overview#migration-guide).
***
## Recovery & reliability
### Enabling recovery
Pass options to `newSubscription` to control reliability behavior:
| Flag | Type | Description |
| ------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `positioned` | `boolean` | Enables stream position tracking for the subscription. This lets the client keep its current offset and allows the server to signal when the stream position has become invalid. |
| `recoverable` | `boolean` | Enables automatic recovery for the subscription. On resubscribe, the client sends its last known stream position and the server tries to replay missed publications from history. |
**Both flags are required together to get at-least-once delivery across reconnects.** Think of it in two steps: `positioned` bookmarks your place in the stream while you're connected; `recoverable` uses that bookmark to fetch the missed pages when you come back.
```javascript JavaScript theme={null}
// Correct — both flags together
const sub = client.newSubscription("order_book:market_abc123", {
positioned: true,
recoverable: true,
});
```
```python Python theme={null}
from centrifuge import SubscriptionOptions
# Correct — both flags together
options = SubscriptionOptions(positioned=True, recoverable=True)
sub = client.new_subscription("order_book:market_abc123", handler, options)
```
For channels without history where you always re-seed from REST on reconnect (e.g., `best_odds:global`, `parlay_markets:global`), both flags can be omitted.
### Interpreting subscribed after reconnect
After a reconnect, the `subscribed` event fires with context that tells you whether your local state is still consistent:
The `subscribed` context includes:
* `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. The recovery window is 5 minutes, but may be shorter if the namespace's message cap is reached first. After the window expires, `wasRecovering: true, recovered: false` fires and you must re-seed from REST.
`best_odds` and `parlay_markets` do not have history enabled — recovery is not available on those channels.
**Note on epoch:** 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. This is rare, but when it happens `recovered` will be `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)
```
For long-running processes, bound the size of your dedup set (e.g., keep only the last 1,000 IDs) to avoid unbounded memory growth.
### 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 in `publications` 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 [Namespace history capabilities](/api-reference/centrifugo-overview#namespace-history-capabilities) and the global limit of 1,000 items per request. Calling `sub.history()` on a channel with no history enabled returns error code `108`.
### Snapshot + subscribe pattern
Subscribe with `positioned: true, recoverable: true` and seed from REST inside the `subscribed` handler. The handler fires on every connect and tells you whether recovery filled the gap — so you only hit REST when you actually need to:
```javascript JavaScript theme={null}
let ready = false;
const buffer = [];
const sub = client.newSubscription(`order_book:market_${marketHash}`, {
positioned: true,
recoverable: true,
});
sub.on("publication", (ctx) => {
if (!ready) {
buffer.push(ctx.data);
} else {
applyUpdate(ctx.data);
}
});
sub.on("subscribed", async (ctx) => {
if (ctx.wasRecovering && ctx.recovered) {
// Centrifugo replayed all missed messages — no REST call needed.
ready = true;
return;
}
// Fresh connect or failed recovery (history gap too large / expired).
// Reset and re-seed from REST.
ready = false;
buffer.length = 0;
const res = await fetch(`https://api.sx.bet/orders?marketHashes=${marketHash}`);
applySnapshot(await res.json());
// Drain buffered publications on top of the snapshot.
// applyUpdate deduplicates by entity ID so any
// overlap between the snapshot and the buffer is handled safely.
for (const data of buffer) applyUpdate(data);
buffer.length = 0;
ready = true;
});
sub.subscribe();
client.connect();
```
```python Python theme={null}
import asyncio
import aiohttp
from centrifuge import (
Client,
PublicationContext,
SubscribedContext,
SubscriptionEventHandler,
SubscriptionOptions,
)
async def subscribe_order_book(client: Client, market_hash: str) -> None:
ready = False
buffer: list = []
async def on_subscribed(ctx: SubscribedContext) -> None:
nonlocal ready
if ctx.was_recovering and ctx.recovered:
ready = True
return
ready = False
buffer.clear()
async with aiohttp.ClientSession() as session:
async with session.get(
f"https://api.sx.bet/orders?marketHashes={market_hash}"
) as res:
apply_snapshot(await res.json())
for data in buffer:
apply_update(data)
buffer.clear()
ready = True
async def on_publication(ctx: PublicationContext) -> None:
if not ready:
buffer.append(ctx.data)
else:
apply_update(ctx.data)
handler = SubscriptionEventHandler(
on_subscribed=on_subscribed,
on_publication=on_publication,
)
options = SubscriptionOptions(positioned=True, recoverable=True)
sub = client.new_subscription(
f"order_book:market_{market_hash}", handler, options
)
await sub.subscribe()
```
See [Fetching Odds → Real-time best odds](/developers/fetching-odds#real-time-best-odds) for a worked example applying this pattern to the `best_odds:global` channel.
***
## 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);
});
client.on("connected", (ctx) => {
console.log("connected", ctx);
});
client.on("disconnected", (ctx) => {
console.log("disconnected", ctx);
});
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);
});
sub.on("subscribed", (ctx) => {
console.log("subscribed", ctx);
});
sub.on("unsubscribed", (ctx) => {
console.log("unsubscribed", ctx);
});
sub.on("publication", (ctx) => {
console.log("publication", ctx);
});
sub.on("error", (ctx) => {
console.error("subscription error", ctx);
});
```
To start a terminally unsubscribed subscription again, call `sub.subscribe()` explicitly.
### Retryable vs terminal conditions
In practice, this means:
* temporary transport loss moves the client back to `connecting`
* reconnectable subscription interruptions move the subscription back to `subscribing`
* calling `client.disconnect()` or hitting a terminal disconnect condition moves the client to `disconnected`
* calling `sub.unsubscribe()` or hitting a terminal subscription condition moves the subscription to `unsubscribed`
Recovery outcome is reported on the next `subscribed` event via `wasRecovering` and `recovered`. See [Recovery & reliability](#recovery--reliability) for how to interpret those fields.
***
## 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 Getting started above
});
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
Use `positioned: true` and `recoverable: true` together, then handle the three subscription states to maintain consistent local state across reconnects:
```javascript JavaScript theme={null}
async function subscribeOrderBook(client, marketHash) {
const sub = client.newSubscription(`order_book:market_${marketHash}`, {
positioned: true,
recoverable: true,
});
sub.on("subscribed", async (ctx) => {
if (!ctx.wasRecovering || (ctx.wasRecovering && !ctx.recovered)) {
// Fresh connect or failed recovery — seed state from REST
const orders = await fetchOrdersFromRest(marketHash);
applySnapshot(orders);
}
// wasRecovering && recovered: history replay handled it, nothing to do
});
sub.on("publication", (ctx) => {
applyUpdate(ctx.data);
});
sub.subscribe();
}
```
```python Python theme={null}
from centrifuge import (
Client,
PublicationContext,
SubscribedContext,
SubscriptionEventHandler,
SubscriptionOptions,
)
async def subscribe_order_book(client: Client, market_hash: str) -> None:
async def on_subscribed(ctx: SubscribedContext) -> None:
if not ctx.was_recovering or (ctx.was_recovering and not ctx.recovered):
# Fresh connect or failed recovery — seed state from REST
orders = await fetch_orders_from_rest(market_hash)
apply_snapshot(orders)
# was_recovering and recovered: history replay handled it, nothing to do
async def on_publication(ctx: PublicationContext) -> None:
apply_update(ctx.data)
handler = SubscriptionEventHandler(
on_subscribed=on_subscribed,
on_publication=on_publication,
)
options = SubscriptionOptions(positioned=True, recoverable=True)
sub = client.new_subscription(f"order_book:market_{market_hash}", handler, options)
await sub.subscribe()
```
### Monitor your active orders
Subscribe to `active_orders:{maker}` to receive fills, cancellations, and new posts for your address in real time:
```javascript JavaScript theme={null}
import { Centrifuge } from "centrifuge";
const client = new Centrifuge("wss://realtime.sx.bet/connection/websocket", {
getToken: () => fetchToken(YOUR_API_KEY),
});
const sub = client.newSubscription(`active_orders:${YOUR_ADDRESS}`, {
positioned: true,
recoverable: true,
});
sub.on("subscribed", async (ctx) => {
if (!ctx.wasRecovering || (ctx.wasRecovering && !ctx.recovered)) {
// Seed current open orders from REST on fresh connect or failed recovery
const orders = await fetchActiveOrdersFromRest(YOUR_ADDRESS);
applySnapshot(orders);
}
});
sub.on("publication", (ctx) => {
for (const update of ctx.data) {
console.log(`order ${update.orderHash}: ${update.status}`);
applyUpdate(update);
}
});
sub.subscribe();
client.connect();
```
```python Python theme={null}
import asyncio
from centrifuge import (
Client,
PublicationContext,
SubscribedContext,
SubscriptionEventHandler,
SubscriptionOptions,
)
async def main():
client = Client(
"wss://realtime.sx.bet/connection/websocket",
get_token=fetch_token,
)
async def on_subscribed(ctx: SubscribedContext) -> None:
if not ctx.was_recovering or (ctx.was_recovering and not ctx.recovered):
orders = await fetch_active_orders_from_rest(YOUR_ADDRESS)
apply_snapshot(orders)
async def on_publication(ctx: PublicationContext) -> None:
for update in ctx.data:
print(f"order {update['orderHash']}: {update['status']}")
apply_update(update)
handler = SubscriptionEventHandler(
on_subscribed=on_subscribed,
on_publication=on_publication,
)
options = SubscriptionOptions(positioned=True, recoverable=True)
sub = client.new_subscription(f"active_orders:{YOUR_ADDRESS}", handler, options)
await client.connect()
await sub.subscribe()
await asyncio.Future() # keep running
asyncio.run(main())
```
***
## 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/api-key`, {
headers: { "x-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 requests
from centrifuge import Client, UnauthorizedError
def fetch_token():
res = requests.get(
f"{RELAYER_URL}/user/realtime-token/api-key",
headers={"x-api-key": os.environ["SX_API_KEY"]},
)
if res.status_code in (401, 403):
raise UnauthorizedError() # permanent — stops all reconnect attempts
if not res.ok:
raise Exception(f"Status {res.status_code}") # transient — retries with backoff
return res.json()["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.
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 | The subscription moves to terminal `unsubscribed`. |
| `106` | Limit exceeded — connection is at the 512 channel cap | The subscription moves to terminal `unsubscribed`. |
| `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 up to 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 can surface 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. Unexpected disconnects that are not auth-related are often caused by a saturated buffer.
For the full list of built-in unsubscribe and disconnect codes, see [Centrifugo client protocol codes](https://centrifugal.dev/docs/server/codes).
## Related
Using active\_orders to monitor your open orders in real-time.
How to submit fills and monitor your trade history.
Connecting and subscribing with the Centrifuge client.
Responding to parlay RFQ requests via parlay\_markets:global.
# Slippage
Source: https://docs.sx.bet/developers/slippage
How odds slippage works when filling orders on SX Bet.
## Overview
When you fill an order on SX Bet, you set a `desiredOdds` and an `oddsSlippage`. Together, these control the worst **weighted average odds** you're willing to accept.
* `oddsSlippage = 0` — only fill at `desiredOdds` or better
* `oddsSlippage = 3` — accept up to 3% worse than `desiredOdds` on a weighted average basis
The `oddsSlippage` field is an integer from 0 to 100 representing a percentage.
## Why slippage exists
Between the moment you submit a fill and the moment the exchange matches it (after the [betting delay](/developers/filling-orders#how-filling-works)), the orderbook can change:
* The order you targeted may get **cancelled** by the maker
* Another taker may **fill** the order before you
* The maker may **update** their odds by cancelling and reposting
* For in-play markets, odds shift constantly as the game progresses
Without slippage, any of these changes would cause your fill to fail entirely. With a small slippage tolerance, your bet can still go through at slightly worse odds rather than failing outright.
## How slippage is calculated
Slippage applies to the **weighted average odds** across all orders your fill matches — not to each individual order.
The exchange checks:
```
weighted_average_odds >= desiredOdds * (1 - oddsSlippage / 100)
```
This means individual orders in your fill can have odds worse than your `desiredOdds`, as long as the overall weighted average stays within your slippage tolerance.
### Example 1: Filling across multiple price levels
You submit a fill for **\$10,000** at `desiredOdds` of **2.10** with **5% slippage**.
The orderbook has taker liquidity at these levels:
| Taker odds | Available liquidity |
| ---------- | ------------------- |
| 2.10 | \$2,000 |
| 2.09 | \$3,000 |
| 2.07 | \$15,000 |
The exchange fills your \$10,000 across all three levels:
| Taker odds | Amount filled | Contribution to average |
| ---------- | ------------- | ----------------------- |
| 2.10 | \$2,000 | $2,000 × 2.10 = $4,200 |
| 2.09 | \$3,000 | $3,000 × 2.09 = $6,270 |
| 2.07 | \$5,000 | $5,000 × 2.07 = $10,350 |
**Weighted average odds** = ($4,200 + $6,270 + $10,350) / $10,000 = **2.082**
Your minimum acceptable odds = `2.10 * (1 - 0.05) = 1.995`. Since 2.082 > 1.995, the fill succeeds — even though some of the liquidity was at 2.07.
### Example 2: Slippage protects you from filling at bad odds
Slippage is based on your **weighted average** across all matched orders. If the orderbook is mostly stale or far off your target price, the weighted average will fall below your threshold and the fill will fail — protecting you from getting a bad price.
You submit a fill for **\$50** at `desiredOdds` of **2.30** with **3% slippage**.
The orderbook has:
| Taker odds | Available liquidity |
| ---------- | ------------------- |
| 2.30 | \$10 |
| 2.00 | \$40 |
Your minimum acceptable odds = `2.30 * (1 - 0.03) = 2.231`.
The exchange would need to fill across both levels:
| Taker odds | Amount filled | Contribution to average |
| ---------- | ------------- | ----------------------- |
| 2.30 | \$10 | $10 × 2.30 = $23.00 |
| 2.00 | \$40 | $40 × 2.00 = $80.00 |
**Weighted average odds** = ($23.00 + $80.00) / \$50 = **2.06**
Since 2.06 \< 2.231, the fill **fails**. The 2.00 liquidity drags the weighted average well below your threshold, so you receive an `ODDS_STALE` error rather than being filled at a price far from what you intended.
## Choosing a slippage value
| Scenario | Recommended slippage | Reasoning |
| --------------------- | -------------------- | ------------------------------------------------------------------------- |
| Pre-game markets | `0` | Odds are stable; you want exactly what you see |
| Pre-game, large fills | `1`–`2` | Slight tolerance for orders getting taken between submission and matching |
| In-play markets | `3`–`5` | Odds shift constantly; some tolerance prevents repeated failures |
| Fast-moving in-play | `5`–`10` | Prioritize execution over price precision |
Higher slippage means you may get filled at worse odds. Only increase slippage when execution speed matters more than getting the exact price.
## Error: `ODDS_STALE`
If the exchange can't find any orders within your slippage tolerance, it returns an `ODDS_STALE` error. This means:
* The orders that existed when you checked have been filled or cancelled
* The remaining liquidity is at odds worse than your `desiredOdds` plus slippage allows
## Related
Full guide to filling orders as a taker.
Full API reference for the fill endpoint.
Converting between implied, American, and decimal odds.
Reading orderbook depth and finding the best prices.
# 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
### 1. Create a testnet account
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.
### 2. Get testnet funds
Open a support chat on [sx.bet](https://sx.bet) to receive testnet USDC.
### 3. Enable betting on testnet
Approve the `TokenTransferProxy` on the testnet chain. The simplest way is to place a test bet through the [toronto.sx.bet](https://toronto.sx.bet) UI. To do it programmatically, see [Enabling Betting](/api-reference/enabling-betting) — just use the testnet RPC URL and contract addresses below.
## 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` |
| **Explorer API** | `https://explorerl2.toronto.sx.technology/api` | `https://explorerl2.sx.technology/api` |
| **USDC Address** | `0x1BC6326EA6aF2aB8E4b6Bc83418044B1923b2956` | `0x6629Ce1Cf35Cc1329ebB4F63202F3f197b3F050B` |
| **Metadata** | `https://api.toronto.sx.bet/metadata` | `https://api.sx.bet/metadata` |
The `executor`, `EIP712FillHasher`, and `TokenTransferProxy` addresses are also different between environments. You can fetch them from the `/metadata` endpoint.
## Structuring your code for easy switching
The cleanest approach is to store all environment-specific values in a config object and swap based on an environment variable:
```python Python theme={null}
import os
ENV = os.environ.get("SX_ENV", "testnet")
CONFIG = {
"testnet": {
"api_url": "https://api.toronto.sx.bet",
"chain_id": 79479957,
"rpc_url": "https://rpc-rollup.toronto.sx.technology",
"usdc_address": "0x1BC6326EA6aF2aB8E4b6Bc83418044B1923b2956",
"wsx_address": "0x5c02932f8422D943647682E95c87ea0333191e08",
"explorer_api": "https://explorerl2.toronto.sx.technology/api",
},
"mainnet": {
"api_url": "https://api.sx.bet",
"chain_id": 4162,
"rpc_url": "https://rpc-rollup.sx.technology",
"usdc_address": "0x6629Ce1Cf35Cc1329ebB4F63202F3f197b3F050B",
"wsx_address": "0x3E96B0a25d51e3Cc89C557f152797c33B839968f",
"explorer_api": "https://explorerl2.sx.technology/api",
},
}
cfg = CONFIG[ENV]
# Fetch executor and TokenTransferProxy from metadata — these differ per network
import requests
metadata = requests.get(f"{cfg['api_url']}/metadata").json()["data"]
cfg["executor"] = metadata["executorAddress"]
cfg["token_transfer_proxy"] = metadata["TokenTransferProxy"]
cfg["domain_version"] = metadata["domainVersion"]
cfg["eip712_fill_hasher"] = metadata["EIP712FillHasher"]
```
```javascript JavaScript theme={null}
const ENV = process.env.SX_ENV || "testnet";
const CONFIG = {
testnet: {
apiUrl: "https://api.toronto.sx.bet",
chainId: 79479957,
rpcUrl: "https://rpc-rollup.toronto.sx.technology",
usdcAddress: "0x1BC6326EA6aF2aB8E4b6Bc83418044B1923b2956",
wsxAddress: "0x5c02932f8422D943647682E95c87ea0333191e08",
explorerApi: "https://explorerl2.toronto.sx.technology/api",
},
mainnet: {
apiUrl: "https://api.sx.bet",
chainId: 4162,
rpcUrl: "https://rpc-rollup.sx.technology",
usdcAddress: "0x6629Ce1Cf35Cc1329ebB4F63202F3f197b3F050B",
wsxAddress: "0x3E96B0a25d51e3Cc89C557f152797c33B839968f",
explorerApi: "https://explorerl2.sx.technology/api",
},
};
const cfg = CONFIG[ENV];
// Fetch executor and TokenTransferProxy from metadata — these differ per network
const metadata = await fetch(`${cfg.apiUrl}/metadata`).then((r) => r.json()).then((r) => r.data);
cfg.executor = metadata.executorAddress;
cfg.tokenTransferProxy = metadata.TokenTransferProxy;
cfg.domainVersion = metadata.domainVersion;
cfg.eip712FillHasher = metadata.EIP712FillHasher;
```
Then reference `cfg` everywhere instead of hardcoding values:
```python Python theme={null}
# API calls use cfg["api_url"]
markets = requests.get(f"{cfg['api_url']}/markets/active").json()
# Fill signing domain uses cfg["chain_id"] and values from metadata
DOMAIN = {
"name": "SX Bet",
"version": cfg["domain_version"],
"chainId": cfg["chain_id"],
"verifyingContract": cfg["eip712_fill_hasher"],
}
# Token addresses use cfg
base_token = cfg["usdc_address"]
```
```javascript JavaScript theme={null}
// API calls use cfg.apiUrl
const markets = await fetch(`${cfg.apiUrl}/markets/active`).then((r) => r.json());
// Fill signing domain uses cfg.chainId and values from metadata
const DOMAIN = {
name: "SX Bet",
version: cfg.domainVersion,
chainId: cfg.chainId,
verifyingContract: cfg.eip712FillHasher,
};
// Token addresses use cfg
const baseToken = cfg.usdcAddress;
```
To switch to mainnet, change one environment variable:
```bash theme={null}
# Testnet (default)
SX_ENV=testnet node your-bot.js
# Mainnet
SX_ENV=mainnet node your-bot.js
```
## Switching to mainnet checklist
When you're ready to go live:
* [ ] Set `SX_ENV=mainnet` (or update your config to use mainnet values)
* [ ] Use your **mainnet** private key and API key (testnet keys won't work on mainnet)
* [ ] Fund your mainnet wallet with USDC
* [ ] Approve the `TokenTransferProxy` on mainnet — either through [sx.bet](https://sx.bet) or [programmatically](/api-reference/enabling-betting)
* [ ] Confirm your fill signing domain uses chain ID `4162` and `EIP712FillHasher` from `/metadata` as `verifyingContract`
* [ ] Test with a small order before scaling up
## Common pitfalls
### Wrong chain ID in signatures
If you see `TAKER_SIGNATURE_MISMATCH` or orders being rejected, check that your signing domain's `chainId` matches the network you're targeting. Testnet is `79479957`, mainnet is `4162`.
### Wrong token addresses
USDC has different contract addresses on each network. Using a testnet token address on mainnet (or vice versa) will result in `BAD_BASE_TOKEN` errors.
### Hardcoded executor or TokenTransferProxy
These addresses differ between networks. You can fetch them from [`GET /metadata`](/api-reference/get-metadata) at startup and cache them in your config.
### 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.
Approve the TokenTransferProxy for trading.
Check your balance on either network.
End-to-end guide from setup to first fill.
# Unit Conversions
Source: https://docs.sx.bet/developers/unit-conversions
Converting between SX Bet's raw API values and human-readable formats.
The SX Bet API uses raw integers for odds and token amounts. Here's everything you need to convert them.
## Quick reference
| Value | Raw format | Convert to human-readable |
| ---------- | ------------------------ | ----------------------------------------- |
| Odds | Integer, scaled by 10^20 | `raw / 10^20` → implied probability (0–1) |
| USDC | Integer, 6 decimals | `raw / 1,000,000` → dollar amount |
| Timestamps | Unix seconds | Standard Unix timestamp |
***
## Odds
SX Bet stores odds as **implied probability** scaled to avoid floating point: `percentageOdds = implied × 10^20`
### Raw ↔ implied probability
```python Python theme={null}
ODDS_PRECISION = 10 ** 20
raw_to_implied = lambda raw: raw / ODDS_PRECISION
implied_to_raw = lambda implied: int(implied * ODDS_PRECISION)
# 52631578947368421052 → 0.5263 (52.63%)
```
```javascript JavaScript theme={null}
const rawToImplied = (raw) => Number(raw) / 1e20;
const impliedToRaw = (implied) => BigInt(Math.round(implied * 1e20));
```
### Maker → taker odds
On SX Bet, makers post odds at a specific implied probability. The taker automatically receives the complementary side — the two sides always sum to 1 (no house spread):
```
taker_implied = 1 - maker_implied
taker_raw = 10^20 - maker_raw
```
**Example:** Maker posts at `50125000000000000000` (\~50.125%). Taker receives `49875000000000000000` (\~49.875%).
### Implied ↔ American
```
Favorite (implied ≥ 0.5): American = -(implied / (1 - implied)) × 100
Underdog (implied < 0.5): American = ((1 - implied) / implied) × 100
```
```python Python theme={null}
def implied_to_american(implied: float) -> float:
if implied >= 0.5:
return -(implied / (1 - implied)) * 100
return ((1 - implied) / implied) * 100
def american_to_implied(american: float) -> float:
if american < 0:
return (-american) / (-american + 100)
return 100 / (american + 100)
```
```javascript JavaScript theme={null}
function impliedToAmerican(implied) {
return implied >= 0.5
? -(implied / (1 - implied)) * 100
: ((1 - implied) / implied) * 100;
}
function americanToImplied(american) {
return american < 0
? (-american) / (-american + 100)
: 100 / (american + 100);
}
```
### Implied ↔ Decimal
```
decimal = 1 / implied
implied = 1 / decimal
```
***
## Token amounts
USDC on SX Network uses 6 decimal places, so all token amounts in the API are integers representing millionths of a dollar. For example, `10000000` is 10.00 USDC, and `1500000` is 1.50 USDC.
```python Python theme={null}
USDC_DECIMALS = 10 ** 6
raw_to_usdc = lambda raw: raw / USDC_DECIMALS
usdc_to_raw = lambda amount: int(round(amount * USDC_DECIMALS))
raw_to_usdc(10_000_000) # → 10.0
usdc_to_raw(10.00) # → 10000000
```
```javascript JavaScript theme={null}
const rawToUsdc = (raw) => raw / 1_000_000;
const usdcToRaw = (amount) => Math.round(amount * 1_000_000);
rawToUsdc(10_000_000); // → 10
usdcToRaw(10.00); // → 10000000
```
# 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 developer use case. Because SX Bet is a true orderbook exchange, you can implement systematic trading strategies just like in financial markets:
* **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 rich data source:
* Real-time odds tracking and movement alerts
* Historical market data and closing line analysis
* Bettor profiling and edge detection
Track performance and manage risk across your activity:
* Bet trackers with P\&L dashboards
* Position monitoring and alerts
* Multi-account management tools
* Risk management and bankroll calculators
## What you'll need
You'll need a wallet and account to interact with the exchange. [Create one →](/developers/quickstart)
The REST API is publicly accessible for reads. Posting orders requires signing with your wallet private key.
All orders are denominated in USDC. You'll need USDC to post or fill orders.
All guides and samples in this hub use Python and JavaScript. The API is REST-based 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 decentralized, peer-to-peer sports prediction market. Instead of betting against a house, users trade directly against each other through an open orderbook — the same model used by financial exchanges, applied to sports markets.
On SX, bettors set their own odds, and the market determines fair prices through competition.
## How it works
A user (the maker) submits an order specifying an outcome, stake amount, and desired odds. This order sits in the orderbook until matched.
Another user (the taker) submits a fill order against it. Both sides' funds are locked in the Escrow smart contract.
After the game concludes, a reporter submits the result on-chain. The Escrow contract automatically pays out to the winner.
## Tokens
SX Bet uses **USDC** as the primary wagering token.
## The exchange model
Because SX Bet is a true orderbook exchange — not a traditional sportsbook — the dynamics are different from what most bettors are used to.
Understand how makers, takers, and the orderbook interact.
How SX Bet represents odds and token amounts internally.
# 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.
Tell us what you're working on and what you're hoping to accomplish.
# 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.
Glide makes it 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. Go to the [Arbitrum Native Bridge](https://portal.arbitrum.io/bridge?destinationChain=ethereum\&sanitized=true\&sourceChain=sx\&token=0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48).
2. 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/).
4. Enter the amount of USDC you would like to bridge, agree to the terms, and click "Move funds to Ethereum".
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 — Glide generates a fresh transfer address 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 — Glide offers 0% fees on deposits made with Native USDC. 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 [bridge page](https://sx.bet/wallet/bridge) on SX Bet and select "Bridge with Glide".
2. Select "Withdraw", then select "Interac". Enter the amount you want to withdraw then click "Continue".
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 [bridge page](https://sx.bet/wallet/bridge) on SX Bet and select "Bridge with Glide".
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 [Assets Page](https://sx.bet/wallet/assets) 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.
## 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 funds stay in your personal wallet, not in an account controlled by SX Bet. This is fundamentally different from traditional sportsbooks where you deposit money into a company's account.
Your USDC sits in your personal wallet at all times until you place a bet.
When you bet, you authorize the transfer by signing the transaction. Nothing moves without your approval.
Your stake and the other bettor's stake are held in a secure escrow smart contract. Neither party can touch the funds — the contract holds them until the game ends.
When the event concludes, the escrow contract automatically pays out the winnings directly to the winner's wallet.
## 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 |
| ----------- | -------------------------- |
| Single bets | **0%** |
| Parlays | **5%** (winning bets only) |
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.
# Overview
Source: https://docs.sx.bet/user-guides/getting-started/overview
The prediction market for sports — better odds, no fees, no limits.
Sports Prediction Market
Bet against other bettors — not the house. Better odds, no fees, 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.
SX Bet charges **0% fees** on single bets. A 5% fee applies only to winning parlays. No hidden premiums or commission.
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 icon** (top right of the navbar) 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.
# API Changelog
Source: https://docs.sx.bet/user-guides/more/api-changelog
# API Reference
Source: https://docs.sx.bet/user-guides/more/api-reference
# 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.
# Developer Guides
Source: https://docs.sx.bet/user-guides/more/developer-guides
# 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. Make sure you are on **"SX Rollup"** blockchain.
2. Click the currency tab on the menu then click the **"Use Bet Credits"** toggle switch, and start using your bet credits. You can also check the "Use Bet Credits" checkbox from within your bet slip.
## 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 currently has no trading fees on single bets. This underscores SX Bet's commitment to reducing the vig in sports betting to zero, significantly enhancing fairness for users, and becoming the global liquidity hub for sports betting.
| | Taker | Maker | Parlays |
| ------ | ----- | ----- | ------- |
| SX Bet | 0% | 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 on 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 — whichever you switch to will apply across the whole site.
## The Consensus Line
The orange line running across the order book marks the **consensus line** — the globally agreed-upon 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 a single price level, your order will fill across multiple levels or sit as an open limit order for the remainder.
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
Place a standard single-leg bet at the best available odds.
Understand the 5% winning parlay fee and single-bet fee structure.