BettingLab

Build a Claude Betting Agent with Tool-Use and Odds API

Marcus Hale
Marcus Hale

Build a Claude Betting Agent with Tool-Use and Odds API

If you've typed "claude betting agent tool use" into a search bar recently, you're probably not looking for a chatbot that tells you the Cubs "have been playing well lately." You want something that actually fetches real odds, reasons about them structurally, and tells you where value lives. That's what this post builds.

We're going to construct a Python agent using Anthropic's Claude 3.5 Sonnet and the MoneyLine API — specifically the /v1/edge and /v1/odds endpoints — wired together through Claude's native tool-use (function calling) interface. The agent will decide which tools to call, interpret the results, and produce a structured play recommendation. No LangChain magic, no abstraction soup. Just the Anthropic SDK, httpx, and clear prompt design.


Why Tool-Use (Not RAG, Not Fine-Tuning)

The instinct when people hear "AI betting assistant" is to either dump a bunch of historical data into a vector store or fine-tune a model on past results. Both approaches have their place but they're the wrong tool for real-time odds work.

The core problem: odds move. A probability estimate that was correct at noon may be a trap by 3 PM. What you need is a model that can reach out and grab fresh data at inference time, reason over it, and return a synthesized answer.

That's exactly what tool-use enables. Claude can inspect a tool schema, decide it needs to call get_edge_plays to answer your question, receive the structured JSON back, and then reason over the live numbers — without any of that data being baked into weights.

This is fundamentally different from asking GPT-4 "who should I bet tonight?" and getting a hallucinated opinion. The model is anchored to live API responses.

What Claude 3.5 Sonnet Brings Here

Claude 3.5 Sonnet is the right model for this task for a few concrete reasons:

The tradeoff: it's more expensive than GPT-4o-mini at scale. For an interactive assistant or a low-volume daily runner, it's fine. For a high-frequency scanner, look at batching or caching.


Prompt Architecture and Tool Schema Design

Before writing code, think about what the agent needs to know and what it's allowed to do.

System Prompt

The system prompt does three jobs: defines the agent's role, constrains hallucination vectors, and tells it how to use the tools it has.

SYSTEM_PROMPT = """
You are a sharp, data-grounded sports betting analyst assistant.
Your job is to identify positive expected value (+EV) betting opportunities using live odds and edge data.

Rules:
- Never state odds or probabilities you have not retrieved via a tool call.
- Always cite the specific market, book, and edge percentage from the tool response.
- If edge data is insufficient or stale, say so explicitly.
- Recommend at most 3 plays per session. Prioritize by edge percentage, then by market liquidity.
- Output recommendations in this format:
    PLAY: [Team / Market]
    BOOK: [Sportsbook]
    LINE: [American odds]
    EDGE: [% edge]
    FAIR VALUE: [implied probability]
    RATIONALE: [1-2 sentences grounded in the data]

Do not editorialize about team quality, injuries, or narrative unless you have tool data supporting it.
"""

The "never state odds you haven't retrieved" instruction is load-bearing. Without it, Claude will occasionally confabulate plausible-looking lines. It's not malicious — it's completing a pattern — but it's dangerous in a betting context.

Tool Definitions

We define two tools: one for edge plays, one for raw odds on a specific event.

tools = [
    {
        "name": "get_edge_plays",
        "description": (
            "Fetch current positive-EV edge plays from the MoneyLine API. "
            "Returns a list of plays with edge percentage, fair value, book, and market."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "sport": {
                    "type": "string",
                    "description": "Sport slug, e.g. 'baseball_mlb', 'basketball_nba', 'football_nfl'."
                },
                "min_edge": {
                    "type": "number",
                    "description": "Minimum edge percentage to filter results, e.g. 5.0 for 5%."
                },
                "market_type": {
                    "type": "string",
                    "description": "Market type filter: 'spreads', 'totals', 'h2h', or 'props'."
                }
            },
            "required": ["sport"]
        }
    },
    {
        "name": "get_event_odds",
        "description": (
            "Fetch current odds for a specific event across multiple books. "
            "Returns book-by-book lines for the requested market."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "event_id": {
                    "type": "string",
                    "description": "The MoneyLine API event ID."
                },
                "market_type": {
                    "type": "string",
                    "description": "Market to fetch: 'h2h', 'spreads', or 'totals'."
                }
            },
            "required": ["event_id", "market_type"]
        }
    }
]

The Tool Execution Layer

This is the boring-but-critical part. When Claude decides to call a tool, your code has to actually execute that call against the MoneyLine API and return the result. Here's the full implementation:

import anthropic
import httpx
import json
import os

MLAPI_BASE = "https://mlapi.bet"
MLAPI_KEY = os.environ["MONEYLINE_API_KEY"]
ANTHROPIC_KEY = os.environ["ANTHROPIC_API_KEY"]

client = anthropic.Anthropic(api_key=ANTHROPIC_KEY)

def get_edge_plays(sport: str, min_edge: float = 3.0, market_type: str = None) -> dict:
    params = {
        "sport": sport,
        "min_edge": min_edge,
    }
    if market_type:
        params["market_type"] = market_type

    resp = httpx.get(
        f"{MLAPI_BASE}/v1/edge",
        headers={"Authorization": f"Bearer {MLAPI_KEY}"},
        params=params,
        timeout=10,
    )
    resp.raise_for_status()
    return resp.json()


def get_event_odds(event_id: str, market_type: str) -> dict:
    resp = httpx.get(
        f"{MLAPI_BASE}/v1/odds",
        headers={"Authorization": f"Bearer {MLAPI_KEY}"},
        params={"event_id": event_id, "market": market_type},
        timeout=10,
    )
    resp.raise_for_status()
    return resp.json()


def execute_tool(tool_name: str, tool_input: dict) -> str:
    if tool_name == "get_edge_plays":
        result = get_edge_plays(**tool_input)
    elif tool_name == "get_event_odds":
        result = get_event_odds(**tool_input)
    else:
        result = {"error": f"Unknown tool: {tool_name}"}
    return json.dumps(result)

The Agent Loop

Claude's tool-use works as a multi-turn loop: the model returns a tool_use block, you execute the tool, you send the result back, and the model continues. Here's the complete loop:

def run_betting_agent(user_query: str) -> str:
    messages = [{"role": "user", "content": user_query}]

    while True:
        response = client.messages.create(
            model="claude-3-5-sonnet-20241022",
            max_tokens=2048,
            system=SYSTEM_PROMPT,
            tools=tools,
            messages=messages,
        )

        # If Claude is done (no more tool calls), return the final text
        if response.stop_reason == "end_turn":
            return next(
                block.text for block in response.content
                if block.type == "text"
            )

        # Process tool use blocks
        tool_results = []
        for block in response.content:
            if block.type == "tool_use":
                tool_output = execute_tool(block.name, block.input)
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": tool_output,
                })

        # Append assistant turn + tool results and loop
        messages.append({"role": "assistant", "content": response.content})
        messages.append({"role": "user", "content": tool_results})


if __name__ == "__main__":
    query = "Find me the best MLB spread plays with at least 8% edge tonight."
    print(run_betting_agent(query))

What the User Actually Sees

When you run this with a live API key against tonight's MLB slate, you get output structured like:

PLAY: Houston Astros -1.5
BOOK: BetOpenly
LINE: +162
EDGE: 12.4%
FAIR VALUE: 51.3% implied probability
RATIONALE: Pinnacle consensus places Astros ML at -118 implying 54.1% win probability; BetOpenly spread pricing implies only 41.7%, creating a 12.4% edge against the closing line model.

PLAY: Chicago Cubs / Milwaukee Brewers Under 8.5
BOOK: ProphetX
LINE: -108
EDGE: 9.1%
FAIR VALUE: 53.8% implied probability
RATIONALE: Sharp book consensus on the total sits at -118 to -122 range; ProphetX's -108 represents meaningful positive deviation with sufficient liquidity to absorb a unit bet.

PLAY: San Diego Padres ML
BOOK: Kalshi
LINE: +145
EDGE: 8.7%
FAIR VALUE: 49.3% implied probability
RATIONALE: Fair value derived from no-vig consensus across Pinnacle, Circa, and BetOnline places Padres ML at +134; Kalshi's +145 is the outlier offering real edge.

This is what separates tool-grounded output from a generic chatbot response. Every number in that block came from a live API call. The model synthesized it, but it didn't invent it.

For deeper context on how the edge calculations work before they even reach the agent, see how MoneyLine calculates EV.


Productionizing the Agent

A few things you'll want to handle before this runs in production:

Credit management. The free tier on MoneyLine API gives you 1,000 credits/month. Each /v1/edge call is cheap but plan your call frequency accordingly. Cache edge responses for 90 seconds — lines don't move that fast on most markets, and you'll burn credits otherwise.

Error handling and retries. The httpx calls above have no retry logic. Wrap them with tenacity or a simple exponential backoff. API timeouts happen, especially around game time when load spikes.

Rate limiting Claude. If you're building a multi-user Discord bot or Slack app on top of this, you'll hit Anthropic's rate limits faster than expected. Use a queue (Redis + rq works fine) and limit concurrent agent runs per user.

Prompt versioning. The system prompt is doing real work here. Treat it like code — version control it, test changes against a fixture set of API responses before deploying.

For a comparison of how this compares to building with other odds data providers, see our odds API comparison guide.


FAQ

Q: Why use Claude instead of GPT-4o for this?

Claude 3.5 Sonnet's tool-use reliability is strong and its instruction adherence for structured output formats is slightly more consistent in my testing. GPT-4o works too — the tool schema design translates directly to OpenAI's function-calling format. Pick the one you have credits for and can iterate on faster.

Q: Can I run this agent on a schedule instead of interactively?

Yes. Replace the user query with a fixed prompt like "scan MLB spreads for edges above 7% and return the top 3" and run it as a cron job. The output format is consistent enough to parse downstream, or just pipe it to a Slack webhook.

Q: How do I keep the agent from recommending plays after lines have moved?

Add a timestamp check: compare the last_updated field in the /v1/edge response against your current time. If a play's data is older than 5 minutes, have the agent discard it. You can encode this in the system prompt as an additional rule.

Q: What's the difference between /v1/edge and /v1/odds?

/v1/edge returns pre-computed EV calculations — MoneyLine has already done the no-vig math and comparison against sharp book consensus. /v1/odds gives you raw book-by-book lines for a specific event. The agent uses both: edge endpoint for discovery, odds endpoint for drill-down on a specific matchup.

Q: How many API credits does a typical agent session use?

A single query that calls get_edge_plays once and get_event_odds once uses 2 credits. A session that loops through 3 events for odds confirmation uses roughly 4-5. You can run several hundred sessions on the free tier, or hundreds of thousands if you upgrade your MoneyLine API plan.


Where to Take This Next

This agent is a working foundation. From here you can extend it toward a Discord bot that responds to !edges mlb with live plays, or wire in /v1/players/trending to add prop context, or build a thin FastAPI layer so your frontend can query it. The tool schema pattern scales — add a get_arbitrage tool pointing at your arbitrage scanner and the model will pull that data when the query warrants it.

The piece that makes all of it work is the API layer underneath. Stale, thin, or rate-limited odds data ruins the agent regardless of how good the prompts are. That's why the foundation matters — check the full API reference to understand what's available before you design around it.

Build with the same data we use.

MoneyLine API powers BettingLab's edge calculations. Free tier, 1k credits/month.