SX BetSX BetBlog
Intermediatepython

Build a Sports Betting MCP Server for Claude Code

Sports betting MCP server tutorial: wire SX Bet's live odds API into Claude Code or Cursor using FastMCP. Full Python implementation, tool definitions, Claude Code config.

30 minutes · 12 min read · April 23, 2026 · by Declan

Build a Sports Betting MCP Server for Claude Code

TL;DR An MCP (Model Context Protocol) server exposes your SX Bet API connection as tools that Claude Code or Cursor can call during a conversation. Once configured, you can prompt Claude with "What are the best NBA odds right now?" and it will fetch live data from SX Bet, analyze it, and respond — no switching contexts, no manual API calls. This guide builds a complete MCP server in Python using FastMCP, with tools for fetching markets, reading orderbooks, and placing orders. Final step: wiring it into Claude Code's settings.json.

Access the SX Bet API free — no key required → docs.sx.bet


What Is an MCP Server?

Model Context Protocol (MCP) is an open standard that lets AI assistants like Claude call external tools during a conversation. An MCP server is a lightweight process that declares tools — functions with names, descriptions, and parameters — and executes them when the AI requests it.

In practical terms: instead of manually fetching odds, pasting them into a chat, and asking Claude to analyze them, you configure an MCP server once. From then on, Claude can fetch the data itself within the conversation.

The sports betting use case:

Without MCP:

You: Here are today's NBA markets [paste 500 lines of JSON]. Which has the best value?
Claude: [analyzes static snapshot]

With MCP:

You: What are the best NBA odds right now, and is anything showing value against a 52% model?
Claude: [calls fetch_markets → calls get_orderbook → analyzes live data → responds]

The data is live, the tool calls are transparent, and the workflow removes manual copy-paste from your process.


Prerequisites

  • Python 3.11+
  • Claude Code (desktop app or CLI) or Cursor
  • An SX Bet account with a wallet private key (for order placement; read tools need no key)
  • pip install fastmcp requests eth-account python-dotenv

Step 1: Install FastMCP

FastMCP is the Python library for building MCP servers with minimal boilerplate. It handles protocol negotiation, tool registration, and stdio transport automatically.

pip install fastmcp requests eth-account python-dotenv

Step 2: Build the MCP Server

Create sx_bet_mcp.py:

# sx_bet_mcp.py
"""
SX Bet MCP Server
Exposes SX Bet exchange API as tools for Claude Code and Cursor.
"""

import os
import json
import time
import requests
from typing import Optional
from dotenv import load_dotenv
from fastmcp import FastMCP

load_dotenv()

# Configuration
USE_TESTNET = os.getenv("USE_TESTNET", "true").lower() == "true"
BASE_URL = "https://api.toronto.sx.bet" if USE_TESTNET else "https://api.sx.bet"
PRIVATE_KEY = os.getenv("PRIVATE_KEY")  # Required only for place_order tool

mcp = FastMCP("SX Bet Sports Betting Exchange")

# ---------------------------------------------------------------------------
# Tool 1: Fetch Active Markets
# ---------------------------------------------------------------------------

@mcp.tool()
def fetch_markets(
    sport_id: Optional[int] = None,
    limit: int = 20,
) -> str:
    """
    Fetch active betting markets from SX Bet exchange.

    Args:
        sport_id: Filter by sport ID. Common IDs:
                  4=NBA, 29=NFL, 7=NHL, 5=MLB, 6=NCAAB, 11=Soccer.
                  Omit to return all active sports.
        limit: Maximum number of markets to return (default 20, max 100).

    Returns:
        JSON string with active markets including game labels, market types,
        start times, and market hashes needed for orderbook lookups.
    """
    params = {}
    if sport_id:
        params["sportId"] = sport_id

    try:
        response = requests.get(f"{BASE_URL}/markets/active", params=params, timeout=10)
        response.raise_for_status()
        markets = response.json().get("data", [])[:limit]

        # Return a clean summary rather than raw API response
        summary = []
        for m in markets:
            summary.append({
                "market_hash": m.get("marketHash"),
                "game": m.get("gameLabel", ""),
                "market_type": m.get("label", ""),
                "sport_id": m.get("sportId"),
                "start_time": m.get("gameTime"),
                "status": m.get("status"),
            })

        return json.dumps({
            "count": len(summary),
            "environment": "testnet" if USE_TESTNET else "mainnet",
            "markets": summary,
        }, indent=2)

    except requests.RequestException as e:
        return json.dumps({"error": str(e)})


# ---------------------------------------------------------------------------
# Tool 2: Get Orderbook
# ---------------------------------------------------------------------------

@mcp.tool()
def get_orderbook(
    market_hash: str,
    depth: int = 5,
) -> str:
    """
    Fetch the live orderbook for a specific betting market.

    Returns the best available back odds (betting for an outcome) with
    available stake sizes. Use fetch_markets() first to get market_hash values.

    Args:
        market_hash: The unique market identifier (from fetch_markets).
        depth: Number of price levels to return (default 5).

    Returns:
        JSON with best available odds, implied probabilities, and available
        stake at each price level.
    """
    try:
        response = requests.get(
            f"{BASE_URL}/orders",
            params={
                "marketHash": market_hash,
                "bettor": "0x0000000000000000000000000000000000000000",
            },
            timeout=10,
        )
        response.raise_for_status()
        orders = response.json().get("data", [])

        if not orders:
            return json.dumps({"market_hash": market_hash, "orders": [], "note": "No open orders"})

        # Sort by best odds for taker (lowest percentageOdds = best for bettor)
        sorted_orders = sorted(orders, key=lambda o: o.get("percentageOdds", 999999))[:depth]

        formatted = []
        for o in sorted_orders:
            pct = o.get("percentageOdds", 0)
            implied_prob = pct / 10000  # Convert basis points to probability
            decimal_odds = 1 / implied_prob if implied_prob > 0 else 0
            formatted.append({
                "order_hash": o.get("orderHash"),
                "implied_probability": f"{implied_prob:.4f}",
                "implied_probability_pct": f"{implied_prob * 100:.2f}%",
                "decimal_odds": f"{decimal_odds:.4f}",
                "available_stake_usdc": float(o.get("takerAmount", 0)) / 1e6,
            })

        return json.dumps({
            "market_hash": market_hash,
            "environment": "testnet" if USE_TESTNET else "mainnet",
            "best_available": formatted,
        }, indent=2)

    except requests.RequestException as e:
        return json.dumps({"error": str(e)})


# ---------------------------------------------------------------------------
# Tool 3: Get Market Summary (combines fetch + orderbook for a quick read)
# ---------------------------------------------------------------------------

@mcp.tool()
def get_market_summary(
    sport_id: int = 4,
    limit: int = 10,
) -> str:
    """
    Get a summary of active markets with their best available odds.
    Combines market listing and top orderbook prices in one call.
    Useful for quick screening: "show me the best NBA lines right now."

    Args:
        sport_id: Sport to query. 4=NBA, 29=NFL, 7=NHL, 5=MLB.
        limit: Number of markets to summarize (default 10).

    Returns:
        JSON summary of markets with best odds, useful for value screening.
    """
    try:
        markets_resp = requests.get(
            f"{BASE_URL}/markets/active",
            params={"sportId": sport_id},
            timeout=10,
        )
        markets_resp.raise_for_status()
        markets = markets_resp.json().get("data", [])[:limit]

        results = []
        for market in markets:
            mhash = market.get("marketHash")
            orders_resp = requests.get(
                f"{BASE_URL}/orders",
                params={"marketHash": mhash, "bettor": "0x0000000000000000000000000000000000000000"},
                timeout=10,
            )
            orders = orders_resp.json().get("data", []) if orders_resp.ok else []
            best_odds = None
            if orders:
                best = min(orders, key=lambda o: o.get("percentageOdds", 999999))
                ip = best["percentageOdds"] / 10000
                best_odds = {
                    "implied_probability_pct": f"{ip * 100:.2f}%",
                    "decimal_odds": f"{1/ip:.3f}" if ip > 0 else "N/A",
                    "available_usdc": float(best.get("takerAmount", 0)) / 1e6,
                }

            results.append({
                "game": market.get("gameLabel", ""),
                "market": market.get("label", ""),
                "market_hash": mhash,
                "start_time": market.get("gameTime"),
                "best_odds": best_odds,
            })

        return json.dumps({
            "sport_id": sport_id,
            "count": len(results),
            "environment": "testnet" if USE_TESTNET else "mainnet",
            "markets": results,
        }, indent=2)

    except requests.RequestException as e:
        return json.dumps({"error": str(e)})


# ---------------------------------------------------------------------------
# Tool 4: Place Order (requires wallet private key)
# ---------------------------------------------------------------------------

@mcp.tool()
def place_order(
    order_hash: str,
    stake_usdc: float,
) -> str:
    """
    Place a taker order on SX Bet exchange (fills an existing maker order).
    REQUIRES a wallet private key configured in .env (PRIVATE_KEY).
    Uses the testnet by default — set USE_TESTNET=false in .env for mainnet.

    Args:
        order_hash: The order hash to fill (from get_orderbook).
        stake_usdc: How much USDC to stake (will be capped at order's available size).

    Returns:
        JSON with order placement result or error details.

    IMPORTANT: This places a real bet. On mainnet, this spends real funds.
    Always verify the market and odds with get_orderbook() first.
    """
    if not PRIVATE_KEY:
        return json.dumps({
            "error": "No PRIVATE_KEY configured. Add it to your .env file to enable order placement.",
            "docs": "https://docs.sx.bet/api-reference/orders/place-order",
        })

    try:
        from eth_account import Account
        from eth_account.messages import encode_structured_data

        account = Account.from_key(PRIVATE_KEY)
        stake_wei = int(stake_usdc * 1e6)
        fill_salt = str(int(time.time() * 1000))

        payload = {
            "types": {
                "EIP712Domain": [
                    {"name": "name", "type": "string"},
                    {"name": "version", "type": "string"},
                    {"name": "chainId", "type": "uint256"},
                ],
                "TakerOrder": [
                    {"name": "orderHash", "type": "bytes32"},
                    {"name": "takerAmount", "type": "uint256"},
                    {"name": "fillSalt", "type": "uint256"},
                    {"name": "action", "type": "string"},
                    {"name": "market", "type": "string"},
                    {"name": "betting", "type": "string"},
                    {"name": "stake", "type": "string"},
                    {"name": "odds", "type": "string"},
                    {"name": "returnAmount", "type": "string"},
                ],
            },
            "domain": {"name": "SX Bet", "version": "1", "chainId": 4162},
            "primaryType": "TakerOrder",
            "message": {
                "orderHash": order_hash,
                "takerAmount": str(stake_wei),
                "fillSalt": fill_salt,
                "action": "N/A",
                "market": "",
                "betting": "For",
                "stake": f"{stake_usdc:.2f} USDC",
                "odds": "market",
                "returnAmount": "calculated",
            },
        }

        signed = account.sign_message(encode_structured_data(payload))

        order_request = {
            "orderHash": order_hash,
            "takerAmount": str(stake_wei),
            "fillSalt": fill_salt,
            "taker": account.address,
            "takerSignature": signed.signature.hex(),
        }

        response = requests.post(
            f"{BASE_URL}/orders/taker",
            json=order_request,
            headers={"Content-Type": "application/json"},
            timeout=10,
        )
        response.raise_for_status()
        result = response.json()

        return json.dumps({
            "success": True,
            "environment": "testnet" if USE_TESTNET else "mainnet",
            "stake_usdc": stake_usdc,
            "taker_address": account.address,
            "result": result,
        }, indent=2)

    except Exception as e:
        return json.dumps({"error": str(e), "success": False})


# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------

if __name__ == "__main__":
    print(f"Starting SX Bet MCP Server ({'testnet' if USE_TESTNET else 'mainnet'})")
    print(f"API: {BASE_URL}")
    print(f"Order placement: {'enabled' if PRIVATE_KEY else 'disabled (no PRIVATE_KEY)'}")
    mcp.run()

Step 3: Configure Claude Code

Add the MCP server to Claude Code's configuration. Open your Claude Code settings file:

macOS / Linux: ~/.claude/settings.json Windows: %APPDATA%\Claude\settings.json

Add the mcpServers block:

{
  "mcpServers": {
    "sx-bet": {
      "command": "python",
      "args": ["/absolute/path/to/sx_bet_mcp.py"],
      "env": {
        "USE_TESTNET": "true",
        "PRIVATE_KEY": ""
      }
    }
  }
}

Replace /absolute/path/to/sx_bet_mcp.py with the actual path. Leave PRIVATE_KEY empty to start — read-only tools work without it.

Restart Claude Code after saving. You'll see "SX Bet Sports Betting Exchange" appear in the MCP tools panel if the server connected successfully.


Step 4: Configure Cursor

In Cursor, go to Settings → MCP → Add MCP Server:

{
  "name": "sx-bet",
  "command": "python /absolute/path/to/sx_bet_mcp.py",
  "env": {
    "USE_TESTNET": "true"
  }
}

Step 5: Test It

With the MCP server running and Claude Code configured, try these prompts:

Basic market scan:

What NBA games are active right now on SX Bet? Show me the best available odds.

Value screening:

Fetch the top 10 NBA markets. For each one, tell me the implied probability
from the best available odds. Flag any where the implied probability is below 50%.

Orderbook analysis:

Get the orderbook for [market_hash]. How much liquidity is available
at each price level? Is there enough depth to place a $50 bet?

Full analysis workflow:

1. Fetch active NBA markets
2. Get the orderbook for the first three markets
3. Calculate which has the lowest implied probability (best odds for the bettor)
4. Tell me the decimal odds and how much is available to bet at that price

Pre-bet verification:

Before I place a $25 bet on order [order_hash], get the current orderbook
and confirm the order is still available and the odds haven't moved.

Security Considerations

Private key handling:

  • Store PRIVATE_KEY in .env only — never hardcode it in sx_bet_mcp.py
  • Add .env to .gitignore before initializing a git repo
  • For production, use a dedicated wallet funded only with what you're willing to risk
  • The MCP server inherits the permissions of its process — on a shared machine, consider restricting file permissions on .env

Order placement safeguards:

  • The place_order tool includes a warning that returns an error without a configured key
  • Default USE_TESTNET=true means all signed orders go to the testnet — no real funds
  • Claude Code will show you the tool call and parameters before executing; you can review before confirming

Prompt injection defense:

  • The MCP server only calls the SX Bet API — it doesn't read files, execute shell commands, or make calls to arbitrary URLs
  • If you extend the server, keep tool scope narrow: each tool should do one thing

Example Claude Code Session

Here's what a real session looks like with the MCP server running:


User: Show me the best available NBA moneyline odds right now and flag anything where the implied probability is under 45%.

Claude: (calls get_market_summary(sport_id=4, limit=15))

I fetched 15 active NBA markets. Here's what stands out:

Game Market Implied Prob Decimal Odds Available
Lakers vs Warriors Warriors ML 43.2% 2.315 $340 USDC
Celtics vs Heat Heat ML 38.7% 2.584 $180 USDC
Nuggets vs Clippers Clippers ML 44.8% 2.232 $520 USDC

Three markets are below 45% implied probability. The Heat at 38.7% is the most mispriced if your model has them above 40%. The Warriors at 43.2% have the deepest liquidity ($340 available).

Want me to get deeper orderbook data for any of these?


Is It Possible to Use AI for Sports Betting?

Yes — and the MCP pattern is one of the most practical ways to do it. The AI doesn't make autonomous decisions without you; it calls tools at your direction, surfaces data, and helps you analyze it. The actual bet placement step still requires your explicit instruction.

SX Bet's exchange explicitly supports automated and AI-assisted trading. See the full context: Are Betting Bots Illegal?


What Is the Best Sports Betting Bot?

The most effective setup for 2026 is a hybrid: an MCP server that gives an AI assistant live exchange access, combined with a standalone Python bot for automated execution. The AI handles ad-hoc analysis and strategy exploration; the bot handles systematic execution at scale.


Frequently Asked Questions

Q: Are betting bots illegal? A: Automating sports bets is legal where betting itself is legal. Traditional sportsbooks restrict systematic accounts. SX Bet is a peer-to-peer exchange that explicitly supports algorithmic trading. Full breakdown: Are Betting Bots Illegal?

Q: Is it possible to use AI for sports betting? A: Yes. This tutorial is exactly that. Claude uses MCP tools to fetch live odds and analyze value. The bet placement step is under your control. No jurisdiction criminalizes using AI to inform betting decisions.

Q: What is the best sports betting bot? A: The right tool depends on the use case. For interactive analysis and strategy exploration: an MCP server like the one in this guide. For continuous automated execution: a standalone Python bot. See How to Build a Sports Betting Bot in Python.

Q: Does this work with Cursor? A: Yes. Cursor supports the MCP protocol. The settings.json config differs slightly (see Step 4), but the server code is identical.

Q: Can I use this with Claude.ai (web) instead of Claude Code? A: Not directly — claude.ai doesn't support local MCP servers. Claude Code (desktop app or CLI) and Cursor are the supported runtimes for local MCP servers.


Access the SX Bet API free — no key required → docs.sx.bet

Read next: Sports Betting API: The Developer's Complete Hub · Build a Sports Betting Bot with AI

Build on SX Bet's Open API

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