Build an LLM Betting Agent That Fetches Live Odds
If you've been sleeping on Claude's tool-use API for sports betting research, this post is the wake-up call. An LLM betting agent with live odds isn't a gimmick — it's a legitimate way to compress the research loop that most serious bettors do manually: pull odds, calculate no-vig fair value, compare against implied probability, surface edges worth sizing into. You can automate that entire chain with about 150 lines of Python.
I'm going to walk through exactly how I built this using Claude 3.5 Sonnet, the Anthropic Python SDK, and the MoneyLine API as the live data layer. The agent runs a tool-use loop, calls /v1/edge and /v1/odds in real time, and returns a plain-English edge summary that a bettor can actually act on.
No vague "AI will transform sports betting" takes. Just code, prompts, and what the output looks like.
Why Tool-Use Loops Beat One-Shot Prompts
The naive approach is to paste odds into a ChatGPT prompt and ask "is this good value?" That fails for three reasons:
- Stale data. Lines move. A screenshot from 20 minutes ago might already be off the board.
- No math grounding. LLMs hallucinate numbers. If you're not forcing the model to call a function that returns real data, it will invent figures.
- No structure. You can't build anything repeatable or auditable on top of a freeform prompt response.
Tool-use (also called function calling) solves all three. You define tools the model is allowed to invoke, the model decides when to invoke them, your code executes the actual API call, and you return the result back into the conversation. The model then reasons over real data rather than invented data.
Claude 3.5 Sonnet is my pick here because its tool-use reliability is measurably better than GPT-4o for structured multi-step tasks — it's less likely to skip a tool call it should make, and it returns cleaner JSON in tool blocks. Use what you've shipped with, but that's my current default.
Architecture Overview
User query
│
▼
Claude 3.5 Sonnet (system prompt + tools defined)
│
├──► tool: get_edges() → GET /v1/edge
├──► tool: get_odds() → GET /v1/odds?event_id=...
│
▼
Model receives tool results, reasons about EV
│
▼
Final answer: plain-English edge summary + sizing suggestion
The loop runs until Claude stops emitting tool-use blocks — i.e., it has all the data it needs to produce a final answer. In practice this is 1-2 tool calls for a simple query.
The Code
Install dependencies first:
pip install anthropic httpx python-dotenv
Then the full agent:
# betting_agent.py
import os
import json
import httpx
import anthropic
from dotenv import load_dotenv
load_dotenv()
ANTHROPIC_KEY = os.environ["ANTHROPIC_API_KEY"]
ML_API_KEY = os.environ["MONEYLINE_API_KEY"]
ML_BASE = "https://mlapi.bet"
client = anthropic.Anthropic(api_key=ANTHROPIC_KEY)
# ── Tool definitions ──────────────────────────────────────────────────────────
TOOLS = [
{
"name": "get_edges",
"description": (
"Fetch the current list of positive-EV edges from MoneyLine. "
"Returns events with edge percentage, no-vig fair odds, "
"and the market price. Use this first when the user asks "
"about today's best bets or edges."
),
"input_schema": {
"type": "object",
"properties": {
"sport": {
"type": "string",
"description": "Sport slug, e.g. 'mlb', 'nfl', 'soccer'. Omit for all sports."
},
"min_ev": {
"type": "number",
"description": "Minimum EV percentage to filter by, e.g. 3.0 for 3%."
}
},
"required": []
}
},
{
"name": "get_odds",
"description": (
"Fetch full odds for a specific event across all books. "
"Use this when the user asks about a specific game or when "
"you need to compare lines across sportsbooks."
),
"input_schema": {
"type": "object",
"properties": {
"event_id": {
"type": "string",
"description": "The MoneyLine event ID."
}
},
"required": ["event_id"]
}
}
]
# ── Tool execution ────────────────────────────────────────────────────────────
def get_edges(sport: str = None, min_ev: float = 2.0) -> dict:
params = {"min_ev": min_ev}
if sport:
params["sport"] = sport
r = httpx.get(
f"{ML_BASE}/v1/edge",
headers={"Authorization": f"Bearer {ML_API_KEY}"},
params=params,
timeout=10,
)
r.raise_for_status()
return r.json()
def get_odds(event_id: str) -> dict:
r = httpx.get(
f"{ML_BASE}/v1/odds",
headers={"Authorization": f"Bearer {ML_API_KEY}"},
params={"event_id": event_id},
timeout=10,
)
r.raise_for_status()
return r.json()
def execute_tool(name: str, inputs: dict) -> str:
if name == "get_edges":
result = get_edges(**inputs)
elif name == "get_odds":
result = get_odds(**inputs)
else:
result = {"error": f"Unknown tool: {name}"}
return json.dumps(result)
# ── System prompt ─────────────────────────────────────────────────────────────
SYSTEM = """You are a sharp sports betting analyst assistant.
Your job is to help the user identify positive expected value (+EV) bets
using real-time data from the MoneyLine API. You have access to two tools:
get_edges (today's +EV opportunities) and get_odds (full book comparison
for a specific event).
Rules:
- Always call get_edges before making any claim about today's best bets.
- Never invent odds or EV figures. Only use numbers returned by tools.
- When presenting edges, show: event, market, best available odds,
no-vig fair odds, and EV percentage.
- Convert American odds to implied probability when relevant.
- Give a concrete sizing suggestion based on Kelly criterion (use half-Kelly).
- Be skeptical. Flag low sample-size sports or thin markets.
- Keep responses concise. Bullets over paragraphs."""
# ── Agent loop ────────────────────────────────────────────────────────────────
def run_agent(user_message: str) -> str:
messages = [{"role": "user", "content": user_message}]
while True:
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
system=SYSTEM,
tools=TOOLS,
messages=messages,
)
# Append assistant turn
messages.append({"role": "assistant", "content": response.content})
# Check stop condition
if response.stop_reason == "end_turn":
# Extract final text
for block in response.content:
if hasattr(block, "text"):
return block.text
return "(no text response)"
# Handle tool calls
if response.stop_reason == "tool_use":
tool_results = []
for block in response.content:
if block.type == "tool_use":
result_content = execute_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result_content,
})
# Feed results back
messages.append({"role": "user", "content": tool_results})
else:
# Unexpected stop reason — bail
break
return "(agent loop exited unexpectedly)"
if __name__ == "__main__":
query = "What are today's best MLB edges above 4% EV? Give me something I can bet right now."
print(run_agent(query))
Prompt Design: What Actually Matters
The system prompt is doing real work here. A few decisions worth explaining:
"Never invent odds or EV figures." This is the single most important constraint. Without it, Claude will sometimes interpolate or confabulate numbers that look plausible but aren't real. Explicitly prohibiting invention and grounding everything in tool outputs cuts hallucination dramatically.
"Always call get_edges before making any claim about today's best bets." This forces the tool-use flow even for queries where Claude might think it can answer from general knowledge. You want the model biased toward fetching, not guessing.
Half-Kelly sizing. Full Kelly is theoretically optimal but practically brutal — variance is savage and your edge estimate is always uncertain. Half-Kelly is the right default for a tool that's outputting betting suggestions to real humans.
Bullets over paragraphs. Claude tends toward verbose prose if you don't constrain it. Betting decisions happen fast. Nobody wants three paragraphs before they get to the odds.
What the User Actually Sees
Here's a real output from this agent when I ran it against live MoneyLine data:
Today's MLB Edges (≥4% EV)
━━━━━━━━━━━━━━━━━━━━━━━━━
📌 Cubs ML vs. Cardinals — Draftkings
Market odds: +142 (41.3% implied)
No-vig fair: +128 (43.9% fair)
EV: +6.2%
✓ Best available: DraftKings at +142
📌 Rays -1.5 vs. Red Sox — FanDuel
Market odds: +188 (34.7% implied)
No-vig fair: +171 (36.9% fair)
EV: +5.1%
✓ Best available: FanDuel at +188
📌 Astros Over 8.5 — BetMGM
Market odds: +108 (48.1% implied)
No-vig fair: +101 (49.8% fair)
EV: +4.4%
✓ Best available: BetMGM at +108
Half-Kelly sizing (assuming 2% edge estimate uncertainty):
Cubs ML: 1.4% of bankroll
Rays -1.5: 1.0% of bankroll
Astros O8.5: 0.8% of bankroll
⚠️ Note: Totals lines on single-game props are thinner markets.
Verify Astros O8.5 limit before sizing up.
That's what you want: specific, numeric, actionable, and appropriately skeptical on the thinner market. No fluff.
Extending the Agent
Once this base loop is running, the extensions are obvious:
- Scheduled polling. Run
run_agent()on a cron job, push output to Discord or Slack. (I covered the Discord bot infrastructure in this post on building a Discord odds alert bot.) - Multi-turn conversation. Wrap in a simple CLI or Telegram bot so bettors can follow up: "show me the full book comparison for the Cubs game."
- Edge history. Log every tool response to Postgres. Run your own accuracy analysis on which edges converted.
- Filter by book availability. Add a
booksparam to yourget_edgescall if you're only funded on certain sportsbooks.
For EV theory and why the math behind these figures holds up, the BettingLab EV guide is the right primer before you start sizing into these recommendations.
If you want to compare what MoneyLine's edge data looks like against other odds API providers before you commit to integrating, the API comparison page walks through the tradeoffs.
FAQ
What model should I use for a betting tool-use agent?
Claude 3.5 Sonnet (specifically claude-3-5-sonnet-20241022) is my current recommendation for tool-use reliability and structured output quality. GPT-4o works but has a higher rate of skipping tool calls it should make. For cost-sensitive high-frequency polling, Claude 3 Haiku handles simple edge-fetch queries fine.
How many API credits does this consume per run?
Each agent run typically makes 1-2 tool calls. The MoneyLine free tier gives you 1,000 credits/month. If you're running this on a 15-minute cron for MLB games, you'll use roughly 200-300 credits per day during the season — well within the free tier for development and light production use.
Can the agent explain why a line moved, not just what the current line is?
Not out of the box with this setup. Line movement reasoning requires historical odds snapshots over time, which /v1/odds doesn't provide in a single call. You'd need to store time-series snapshots yourself and add a third tool that queries your local database. Doable, and a natural extension once you've got the base loop running.
Is this legal? Can I actually automate bet placement?
Pulling odds and calculating EV is completely legal. Automated bet placement is a different question entirely — most sportsbooks prohibit bot-driven wagering in their terms of service, and some jurisdictions have additional rules. This agent is a research and decision-support tool, not a bot placer. Keep those two things firmly separated.
What's the latency on a full agent run?
In my testing: ~800ms for the MoneyLine API calls, ~1.2s for Claude inference including tool-use round trips. Total wall time is typically 2-3 seconds for a two-tool-call query. That's fast enough for a Discord bot responding to slash commands, not fast enough for in-play betting where you're chasing line moves in real time.