SX BetSX BetBlog
Advancedpython

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.

45 minutes · 11 min read · April 16, 2026 · by Declan

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.bet before 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:

  1. Skew your quotes — make the filled side less attractive to reduce further accumulation
  2. 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

Add the SX Bet MCP server for AI-assisted development:

claude mcp add --transport http sx-bet https://docs.sx.bet/mcp

Start market making on SX Bet →

Build on SX Bet's Open API

No API key required. Fetch live odds, markets, and orderbook data with a single HTTP call.