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 theparlay_markets:global WebSocket channel to receive parlay requests in real-time. You’ll need a token from the GET /user/realtime-token/api-key endpoint.
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();
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’smarketHash, the requested token and size, and the legs:
{
"marketHash": "0x38cceead7bda65c18574a34994ebd8af154725d08aa735dcbf26247a7dcc67bd",
"requestorId": "08e489e45313812cf236b43ab68c6236e3e8d63375a07d6bafdec1ecbed85c1b",
"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) |
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 viaGET /markets/find to understand what’s being bet on, and optionally check current single-market odds via GET /orders/odds/best if you want a reference point for pricing:
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);
}
}
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:const ODDS_PRECISION = 10n ** 20n;
function calculateParlayOdds(legData) {
// Simple approach: multiply implied probabilities of each leg
let combinedProbability = 1.0;
for (const leg of legData) {
// Use your own fair odds estimate per leg
// This example uses a fixed estimate — replace with your model
const legProbability = estimateLegProbability(leg);
combinedProbability *= legProbability;
}
// Add your margin
const margin = 0.03; // 3% edge
const makerProbability = combinedProbability + margin;
// Convert to SX protocol format (maker's percentageOdds)
// percentageOdds = maker's implied probability * 10^20
const percentageOdds = BigInt(Math.round(makerProbability * 1e20));
return percentageOdds.toString();
}
function estimateLegProbability(leg) {
// Replace with your actual pricing model
// This is just a placeholder
return 0.5;
}
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 and unit conversion for details.Step 5: Post your order
Post an order to the parlaymarketHash using POST /orders/new, the same endpoint as single bets. The parlay market hash from the request is your marketHash.
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);
}
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 parlay orders the same way as any other order: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());
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 | Post order to the parlay marketHash | Within 3s |
| 5 | 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
Parlays (RFQ System) →
How the RFQ parlay system works conceptually.
Post New Order →
API reference for the order submission endpoint.
Order Signing →
How to sign orders for the SX Bet protocol.
WebSocket Parlay Requests →
WebSocket channel reference for parlay requests.
