Betting Exchange Market Making Bot: How to Earn Maker Rewards on SX Bet
Build a betting exchange market making bot on SX Bet. Post limit orders, earn USDC Maker Rewards, and manage position inventory. Full Python code guide.
Betting Exchange Market Making Bot: How to Earn Maker Rewards on SX Bet
TL;DR Market making on SX Bet means posting limit orders (the maker side) rather than hitting existing ones (the taker side). Makers provide liquidity and earn USDC from SX Bet's Maker Rewards program. This guide builds the core loop: quote both sides of a market, manage inventory, cancel stale orders, and earn rewards. Full Python code throughout. Start with the API quickstart and use testnet at
api.toronto.sx.betbefore trading live.
On a traditional sportsbook, there's no maker/taker distinction — the house sets every line. On a betting exchange, someone has to post those odds. That's the market maker: a participant who posts limit orders on both sides of a market, earns the spread when orders fill, and earns rewards for providing liquidity the exchange would otherwise lack.
A betting exchange market making bot automates this process. On SX Bet, market makers can also earn USDC through the platform's Maker Rewards program — a direct incentive for providing consistent liquidity to the orderbook.
This guide explains how market making works on SX Bet, how the Maker Rewards program functions, and walks through a Python bot that posts two-sided quotes, manages inventory, and handles order cancellation cleanly.
Maker vs. Taker: What's the Difference?
On SX Bet, every bet has two sides:
| Role | What You Do | API Endpoint | Benefit |
|---|---|---|---|
| Maker | Post a limit order to the orderbook | POST /orders/new |
Sets price; eligible for Maker Rewards |
| Taker | Fill an existing maker order | POST /orders/fill/v2 |
Immediate execution at best available price |
A taker bet tutorial is in How to Build a Sports Betting Bot in Python. This article focuses on the maker side.
Why be a maker?
- You set the price instead of accepting the market price
- Maker Rewards: earn USDC by providing liquidity
- 0% commission on straight bet winnings (same as takers)
- Capital efficiency: SX Bet's system reduces escrow required for hedged positions
What's the risk?
- Adverse selection: if your prices are wrong, informed bettors take your orders first
- Inventory risk: you accumulate one-sided exposure as orders fill
- Stale quotes: old orders at wrong prices attract unfavorable fills
The bot design below addresses all three.
How Maker Rewards Work
SX Bet's Maker Rewards program pays USDC to accounts that consistently post limit orders and provide liquidity to the orderbook. The more liquidity you provide — measured by time in market and volume — the more you earn.
Key facts from the facts file:
- Rewards are paid in USDC (the platform's settlement currency)
- Eligibility requires posting limit orders (
POST /orders/new), not just taking - Rewards compound with consistent participation — sporadic market making earns proportionally less
For current reward rates and program details, check sx.bet directly. The program specifics can change, so always verify before sizing your operation around expected rewards.
Prerequisites
Same environment as the betting bot tutorial:
pip install requests eth-account python-dotenv
.env file:
PRIVATE_KEY=0xyour_private_key_here
WALLET_ADDRESS=0xyour_wallet_address
Use testnet during development:
BASE_URL = "https://api.toronto.sx.bet"
# Live: BASE_URL = "https://api.sx.bet"
Step 1: Choose Your Markets
Not every market is a good candidate for market making. Look for:
import requests
import os
from dotenv import load_dotenv
load_dotenv()
BASE_URL = "https://api.toronto.sx.bet" # Switch to api.sx.bet for live
WALLET_ADDRESS = os.getenv("WALLET_ADDRESS")
def get_active_markets(sport_id: int) -> list[dict]:
"""Fetch active markets for a sport."""
resp = requests.get(
f"{BASE_URL}/markets/active",
params={"sportIds": sport_id}
)
resp.raise_for_status()
return resp.json().get("data", [])
def get_best_odds(market_hash: str) -> dict:
"""Get current best odds for a market."""
resp = requests.get(
f"{BASE_URL}/orders/odds/best",
params={"marketHash": market_hash}
)
resp.raise_for_status()
return resp.json().get("data", {})
def select_markets(sport_id: int, min_liquidity_threshold: int = 0) -> list[dict]:
"""
Select markets suitable for market making.
Filters for markets with existing orderbook activity.
"""
markets = get_active_markets(sport_id)
candidates = []
for market in markets:
odds = get_best_odds(market["marketHash"])
# Markets with two-sided quotes have better MM opportunities
if odds.get("percentageOddsOutcomeOne") and odds.get("percentageOddsOutcomeTwo"):
candidates.append({
"marketHash": market["marketHash"],
"label": market.get("label", ""),
"bestOddsOne": odds["percentageOddsOutcomeOne"],
"bestOddsTwo": odds["percentageOddsOutcomeTwo"],
})
return candidates
Step 2: Calculate Your Quotes
As a market maker, you post bids on both sides of a market. The spread between your bid and offer is your theoretical edge.
def calculate_quotes(
best_odds_one: int,
best_odds_two: int,
spread_bps: int = 50 # 50 basis points = 0.5% spread
) -> dict:
"""
Calculate two-sided quotes from current best odds.
We post slightly worse odds than the current best,
giving us a spread if both sides fill.
Returns percentageOdds for our maker orders on each side.
"""
# Convert to implied probability (0–1)
prob_one = best_odds_one / 10000
prob_two = best_odds_two / 10000
# Apply spread: our maker bids are worse for the taker (better for us)
# A wider spread means more edge but less likely to fill
spread = spread_bps / 10000
our_prob_one = min(prob_one + spread, 0.99) # worse for takers backing outcome 1
our_prob_two = min(prob_two + spread, 0.99) # worse for takers backing outcome 2
return {
"quoteOne": int(our_prob_one * 10000),
"quoteTwo": int(our_prob_two * 10000),
}
# Example: NBA game, best odds 5200 / 4800 (52% / 48%)
quotes = calculate_quotes(best_odds_one=5200, best_odds_two=4800, spread_bps=75)
print(f"Our quotes: {quotes}")
# quoteOne: 5275, quoteTwo: 4875 (slightly less attractive for takers)
Spread sizing trade-off:
| Spread | Fill frequency | Edge per fill |
|---|---|---|
| 25 bps | High | Low |
| 50 bps | Medium | Medium |
| 100+ bps | Low | High |
Start wider during testing and tighten as you validate your inventory management.
Step 3: Post Limit Orders
Maker orders use POST /orders/new with EIP-712 signing (same mechanism as taker bets):
import time
import random
from eth_account import Account
PRIVATE_KEY = os.getenv("PRIVATE_KEY")
def post_maker_order(
market_hash: str,
pct_odds: int,
bet_size_usdc: float,
is_outcome_one: bool,
expiry_seconds: int = 3600
) -> dict:
"""
Post a limit (maker) order to the SX Bet orderbook.
pct_odds: percentageOdds for this order
bet_size_usdc: how much USDC to stake if filled
is_outcome_one: True = betting on outcome 1; False = outcome 2
"""
bet_size_units = int(bet_size_usdc * 1_000_000) # 6 decimals for USDC
# Build the order payload
# Full EIP-712 schema: see docs.sx.bet/api-reference
order = {
"marketHash": market_hash,
"totalBetSize": str(bet_size_units),
"percentageOdds": str(pct_odds),
"expiry": int(time.time()) + expiry_seconds,
"salt": str(random.randint(1, 2**256 - 1)),
"maker": WALLET_ADDRESS,
"isMakerBettingOutcomeOne": is_outcome_one,
# Add EIP-712 signature — see docs.sx.bet for full type definitions
"signature": sign_order(order_params) # implement per docs
}
resp = requests.post(
f"{BASE_URL}/orders/new",
json=order,
headers={"Content-Type": "application/json"}
)
resp.raise_for_status()
return resp.json()
def post_two_sided_quotes(
market_hash: str,
quotes: dict,
size_per_side_usdc: float = 10.0
) -> dict:
"""Post maker orders on both sides of a market."""
order_one = post_maker_order(
market_hash=market_hash,
pct_odds=quotes["quoteOne"],
bet_size_usdc=size_per_side_usdc,
is_outcome_one=True
)
order_two = post_maker_order(
market_hash=market_hash,
pct_odds=quotes["quoteTwo"],
bet_size_usdc=size_per_side_usdc,
is_outcome_one=False
)
return {"sideOne": order_one, "sideTwo": order_two}
For sign_order(), follow the EIP-712 typed data specification at docs.sx.bet/api-reference. The eth-account library's sign_typed_data method handles the encoding.
Step 4: Manage Inventory
When one side of your two-sided quote fills, you accumulate directional exposure. This is inventory risk — the core challenge of market making.
class InventoryTracker:
"""
Track net position across all markets.
Position is in USDC-equivalent exposure.
"""
def __init__(self, max_net_exposure_usdc: float = 200.0):
self.positions = {} # market_hash → net exposure (positive = long outcome 1)
self.max_exposure = max_net_exposure_usdc
def record_fill(self, market_hash: str, side: str, size_usdc: float):
"""Record a fill when one of our maker orders executes."""
current = self.positions.get(market_hash, 0.0)
if side == "one":
self.positions[market_hash] = current + size_usdc
else:
self.positions[market_hash] = current - size_usdc
def net_exposure(self, market_hash: str) -> float:
return self.positions.get(market_hash, 0.0)
def is_overexposed(self, market_hash: str) -> bool:
return abs(self.net_exposure(market_hash)) > self.max_exposure
def skew_needed(self, market_hash: str) -> str:
"""Return which side needs to be reduced."""
exp = self.net_exposure(market_hash)
if exp > 0:
return "reduce_one" # Long outcome 1; quote worse odds on outcome 1
elif exp < 0:
return "reduce_two" # Short outcome 1; quote worse odds on outcome 2
return "balanced"
When one side fills and you're overexposed, you have two options:
- Skew your quotes — make the filled side less attractive to reduce further accumulation
- Hedge — SX Bet's capital efficiency system reduces escrow on opposing positions in the same market, making hedging cheaper
Step 5: Cancel Stale Orders and Heartbeat
Old orders at stale prices are dangerous. When market odds move, your old quotes become the best price for informed bettors — adverse selection in action.
# Cancel orders for a specific event
def cancel_orders_by_event(event_id: str) -> dict:
resp = requests.delete(
f"{BASE_URL}/orders",
json={"eventId": event_id, "maker": WALLET_ADDRESS}
)
resp.raise_for_status()
return resp.json()
# Cancel all maker orders
def cancel_all_orders() -> dict:
resp = requests.delete(
f"{BASE_URL}/orders",
json={"maker": WALLET_ADDRESS}
)
resp.raise_for_status()
return resp.json()
# Heartbeat — keeps orders live and cancels them if connection drops
def heartbeat() -> None:
"""
The SX Bet heartbeat mechanism auto-cancels your orders
if this call stops arriving. Call every 30s.
"""
resp = requests.post(f"{BASE_URL}/orders/heartbeat")
resp.raise_for_status()
Rule of thumb: cancel and re-quote any market where odds have moved more than your spread since you posted.
Step 6: The Market Making Loop
import signal
import sys
inventory = InventoryTracker(max_net_exposure_usdc=200.0)
QUOTE_SIZE_USDC = 15.0 # Per-side bet size
SPREAD_BPS = 75 # 75 basis points spread
REQUOTE_INTERVAL = 60 # Re-quote every 60 seconds
MAX_MARKETS = 5 # Limit concurrent markets
def handle_shutdown(sig, frame):
print("Shutting down — cancelling all orders...")
cancel_all_orders()
sys.exit(0)
signal.signal(signal.SIGINT, handle_shutdown)
signal.signal(signal.SIGTERM, handle_shutdown)
def run_market_maker(sport_id: int = 2): # 2 = NBA
print(f"Starting market maker | Sport: {sport_id} | Spread: {SPREAD_BPS} bps")
last_quote_time = {}
while True:
try:
# Heartbeat — keep orders alive
heartbeat()
# Select markets
markets = select_markets(sport_id)[:MAX_MARKETS]
for market in markets:
mhash = market["marketHash"]
now = time.time()
# Re-quote if interval elapsed or never quoted
if now - last_quote_time.get(mhash, 0) < REQUOTE_INTERVAL:
continue
# Skip overexposed markets
if inventory.is_overexposed(mhash):
print(f"Skipping {market['label']} — overexposed")
continue
# Cancel existing quotes before re-posting
# (In production, track order IDs and cancel individually)
try:
cancel_orders_by_event(market.get("eventId", ""))
except Exception:
pass
# Calculate and post new quotes
quotes = calculate_quotes(
market["bestOddsOne"],
market["bestOddsTwo"],
spread_bps=SPREAD_BPS
)
result = post_two_sided_quotes(mhash, quotes, QUOTE_SIZE_USDC)
last_quote_time[mhash] = now
print(f"Quoted: {market['label']} | {quotes}")
time.sleep(30) # Heartbeat-aligned sleep
except KeyboardInterrupt:
handle_shutdown(None, None)
except Exception as e:
print(f"Error: {e}")
time.sleep(5)
# run_market_maker(sport_id=2) # Uncomment to run
Capital Efficiency: How It Reduces Your Escrow
When you have opposing positions in the same market — for example, a maker order on outcome 1 and one on outcome 2 — SX Bet's capital efficiency system recognises the hedge and reduces the USDC you need locked in escrow.
This is a direct economic benefit for market makers who post two-sided quotes: instead of locking 2x your quote size per market, the net required collateral is reduced proportionally to your hedge ratio.
The more balanced your inventory, the less capital you need tied up — improving your effective return on capital.
What to Expect
Realistic market-making performance depends on:
- Your spread vs. fill frequency — wider spread, fewer fills but more edge per fill
- Market selection — active markets with existing orderbook depth fill faster
- Model quality — makers with better probability estimates avoid adverse selection
- Maker Rewards — consistent presence in the orderbook earns rewards on top of spread income
The Maker Rewards program means you earn even on quiet days when your orders sit unfilled — the exchange pays for your liquidity provision regardless of fill rate.
Frequently Asked Questions
Q: How is market making different from just betting? A: A taker bettor takes existing odds. A market maker sets odds by posting limit orders and waits for someone to fill them. Market makers earn the spread (the difference between their bid and offer) and can earn additional USDC through the Maker Rewards program.
Q: Do market making bots get limited or banned on SX Bet? A: No. SX Bet does not limit or ban winning accounts — including systematic and algorithmic ones. Sharp and automated bettors are explicitly welcome because they improve market quality and liquidity.
Q: What capital do I need to start market making? A: There's no minimum deposit. Practically, you'll want enough USDC to cover several simultaneous two-sided quotes across multiple markets without hitting escrow limits. Start small on testnet, then scale.
Q: How does the heartbeat mechanism work? A: The heartbeat is a periodic API call that keeps your maker orders active. If your bot loses connectivity and stops sending heartbeats, SX Bet automatically cancels your open orders — protecting your wallet from filling at stale prices while you're offline.
Next Steps
- Read the full API reference → — order schemas, type definitions, contract addresses
- Build an arbitrage bot → — capture cross-exchange price differences
- Connect real-time data via WebSocket → — react to odds changes instantly instead of polling
Add the SX Bet MCP server for AI-assisted development:
claude mcp add --transport http sx-bet https://docs.sx.bet/mcp
Build on SX Bet's Open API
No API key required. Fetch live odds, markets, and orderbook data with a single HTTP call.