Build a Claude Betting Assistant with Tool Use
If you've ever wanted a Claude betting assistant with tool use that can pull live edges, explain what's moving a line, and reason about whether a play is worth making — this is the build. Not a chatbot that hallucinates injury news from its weights. A grounded agent that calls real endpoints, gets real numbers, and lets the model do what LLMs are actually good at: synthesizing noisy data into a coherent take.
We're using Claude 3.5 Sonnet (via Anthropic's Python SDK), the MoneyLine API for live odds and edges, and a straightforward tool-use loop. No LangChain. No Autogen. No abstraction layers that explode in production. Just the Anthropic SDK, httpx, and a clean tool definition.
By the end of this post you'll have a working agent you can drop into a Slack bot, a Discord slash command, or a CLI you run before placing a bet.
Why Tool Use Instead of RAG or Fine-Tuning
There's a temptation to solve "the model doesn't know today's odds" with retrieval-augmented generation — stuff context into the prompt and let the model search it. That works fine for static corpora. It breaks for live betting data because:
- Odds move in seconds. By the time you've embedded, indexed, and retrieved, the line is different.
- EV calculations require precision. You don't want the model interpolating odds from approximate embeddings. You want the exact number from the API.
- You want auditability. Tool use creates a clear call-response log. You know exactly what data the model was reasoning on. RAG pipelines obscure that.
Tool use is the right primitive here. Claude sees a tool definition, decides when to call it, gets back structured JSON, and then reasons. The model never invents numbers — it only interprets numbers you give it.
The Architecture
Here's the shape of the system:
User question
↓
Claude 3.5 Sonnet (system prompt + tool definitions)
↓ (tool_use block)
Tool dispatcher (Python)
↓
MoneyLine API: /v1/edge, /v1/odds, /v1/events
↓ (tool_result block)
Claude 3.5 Sonnet (reasoning pass)
↓
Final answer to user
One round-trip is usually enough. For compound questions ("what's the best MLB edge right now and is the line moving in our favor?") the model will call two tools sequentially before synthesizing.
Setting Up the Tools
We define three tools. Keep tool descriptions terse but precise — Claude reads these at inference time and uses them to decide when to call what.
# tools.py
TOOLS = [
{
"name": "get_top_edges",
"description": (
"Fetch the current top positive-EV edges from the MoneyLine API. "
"Returns a list of plays with sport, market, book, odds, fair odds, and EV percentage. "
"Use this when the user asks about value plays, +EV bets, or edges today."
),
"input_schema": {
"type": "object",
"properties": {
"sport": {
"type": "string",
"description": "Sport filter, e.g. 'MLB', 'NBA', 'NFL'. Leave empty for all sports."
},
"min_ev": {
"type": "number",
"description": "Minimum EV percentage to return. Default 5.0."
},
"limit": {
"type": "integer",
"description": "Max number of edges to return. Default 10."
}
},
"required": []
}
},
{
"name": "get_odds_for_event",
"description": (
"Fetch current odds across all books for a specific event. "
"Use this when the user asks about a specific game, matchup, or wants to compare lines."
),
"input_schema": {
"type": "object",
"properties": {
"event_id": {
"type": "string",
"description": "The MoneyLine event ID."
},
"query": {
"type": "string",
"description": "Natural language description of the event if event_id is unknown, e.g. 'Yankees vs Red Sox today'."
}
},
"required": []
}
},
{
"name": "get_events",
"description": (
"Search for upcoming events by sport or team name. "
"Returns event IDs, teams, start times, and available markets. "
"Use this to resolve event IDs before calling get_odds_for_event."
),
"input_schema": {
"type": "object",
"properties": {
"sport": {"type": "string", "description": "e.g. 'MLB', 'NBA'"},
"team": {"type": "string", "description": "Team name or partial name"}
},
"required": []
}
}
]
The Tool Dispatcher
This is the piece that actually calls the MoneyLine API endpoints and returns structured data back to Claude.
# dispatcher.py
import httpx
import os
API_BASE = "https://mlapi.bet"
API_KEY = os.environ["MONEYLINE_API_KEY"]
HEADERS = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"
}
def dispatch_tool(tool_name: str, tool_input: dict) -> dict:
if tool_name == "get_top_edges":
params = {
"min_ev": tool_input.get("min_ev", 5.0),
"limit": tool_input.get("limit", 10),
}
if sport := tool_input.get("sport"):
params["sport"] = sport
r = httpx.get(f"{API_BASE}/v1/edge", headers=HEADERS, params=params)
r.raise_for_status()
return r.json()
elif tool_name == "get_events":
params = {}
if sport := tool_input.get("sport"):
params["sport"] = sport
if team := tool_input.get("team"):
params["team"] = team
r = httpx.get(f"{API_BASE}/v1/events", headers=HEADERS, params=params)
r.raise_for_status()
return r.json()
elif tool_name == "get_odds_for_event":
event_id = tool_input.get("event_id")
if not event_id:
# Try to resolve via events search first
query = tool_input.get("query", "")
r = httpx.get(
f"{API_BASE}/v1/events",
headers=HEADERS,
params={"q": query}
)
r.raise_for_status()
events = r.json().get("events", [])
if not events:
return {"error": "No matching event found", "query": query}
event_id = events[0]["id"]
r = httpx.get(
f"{API_BASE}/v1/odds",
headers=HEADERS,
params={"event_id": event_id}
)
r.raise_for_status()
return r.json()
return {"error": f"Unknown tool: {tool_name}"}
The Agent Loop
Now the core loop. Claude may call one tool or chain several before producing a final text response. The loop handles both.
# agent.py
import anthropic
import json
from tools import TOOLS
from dispatcher import dispatch_tool
client = anthropic.Anthropic()
SYSTEM_PROMPT = """You are a sharp sports betting analyst with access to live odds and edge data.
Your job is to help bettors find +EV plays, understand line movement, and size bets correctly.
Rules:
- Always ground your analysis in the data returned by tools. Never invent odds or EV numbers.
- Be concise. Bettors want the signal, not the noise.
- When you cite an edge, include: the book, the market, the current odds, the fair odds, and the EV%.
- If asked about line movement, note whether the current odds represent value vs. the consensus.
- If the data shows no edge, say so directly. Don't manufacture confidence.
- Use American odds format unless the user specifies otherwise.
"""
def run_agent(user_message: str) -> str:
messages = [{"role": "user", "content": user_message}]
while True:
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=2048,
system=SYSTEM_PROMPT,
tools=TOOLS,
messages=messages
)
# Check if we're done
if response.stop_reason == "end_turn":
# Extract final text
for block in response.content:
if hasattr(block, "text"):
return block.text
return ""
# Handle tool use
if response.stop_reason == "tool_use":
# Append assistant's response (with tool_use blocks)
messages.append({"role": "assistant", "content": response.content})
# Execute each tool call and collect results
tool_results = []
for block in response.content:
if block.type == "tool_use":
print(f"[tool call] {block.name}({json.dumps(block.input)})")
result = dispatch_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result)
})
# Append tool results and continue
messages.append({"role": "user", "content": tool_results})
continue
# Unexpected stop reason
break
return "[agent error: unexpected stop reason]"
if __name__ == "__main__":
import sys
question = " ".join(sys.argv[1:]) or "What are the best MLB edges right now?"
print(run_agent(question))
What the User Actually Sees
Here's a real interaction. The user types:
"Is there anything worth betting on in today's MLB slate? I want at least 8% EV."
Claude calls get_top_edges with {"sport": "MLB", "min_ev": 8.0}. The API returns three edges. Claude responds:
Three MLB edges above 8% EV today:
1. Cardinals ML — BetOpenly (+165) Fair odds: +144 | EV: +11.3% The market has this closer to a coin flip. BetOpenly is 21 cents wide of fair value on the Cardinals. If you're sizing by Kelly, this is ~2% of bankroll at half-Kelly.
2. Cubs/Brewers Over 8.5 — DraftKings (-108) Fair odds: -120 | EV: +9.1% Sharp steam has been coming in on the over all morning. DraftKings hasn't adjusted their total yet. Line will move — take it now or not at all.
3. Mariners -1.5 — BetOpenly (+182) Fair odds: +160 | EV: +8.6% This is a runline with real value behind it. Seattle's bullpen splits favor covering against left-handed lineups. EV methodology here.
No hallucinated injury news. No made-up percentages. Every number came from a /v1/edge response that I can trace back to a timestamp.
Hardening for Production
A few things to add before you ship this:
Rate limiting awareness. The free tier is 1k credits/month. Each /v1/edge call costs credits. Cache aggressively — edge data doesn't need to refresh more than every 60–90 seconds for most use cases. Use Redis or even an in-process TTL cache.
Structured output enforcement. If you're building a UI on top of this (not just CLI), add a final formatting step. Ask Claude to return a JSON object with plays, reasoning, and confidence fields. Then render it properly rather than parsing markdown.
Tool errors. The dispatcher can fail. Wrap dispatch_tool in try/except and return {"error": "..."} cleanly. Claude handles tool errors gracefully if you format the tool_result correctly.
Prompt versioning. Your system prompt is a first-class artifact. Version it, A/B test it, track which version produces recommendations users actually act on. The model is the easy part. The prompt is the product.
If you want to explore what edges are available to build against, the MoneyLine API build docs walk through auth, credit costs per endpoint, and rate limit headers.
For a pure-Python arbitrage angle without an LLM in the loop, the Node.js arbitrage detector post covers the event-polling pattern that works just as well in Python.
Frequently Asked Questions
Q: Why Claude instead of GPT-4o for this?
Claude 3.5 Sonnet has tighter tool-use discipline — it's less likely to skip a tool call and reason from stale training data. In betting contexts where every hallucinated number is a real dollar lost, that matters. GPT-4o works fine too; the tool definitions and dispatcher are identical, you'd just swap the Anthropic client for OpenAI's.
Q: How many API credits does a typical agent query consume?
A single "what are today's MLB edges" query typically calls /v1/edge once — that's one credit. A compound query that also resolves event IDs might call /v1/events and /v1/odds — three credits total. The free tier at 1k credits/month is enough for 300–1000 queries depending on complexity.
Q: Can I use this as a Discord or Slack bot?
Yes. The run_agent(user_message) function is fully synchronous and returns a string. Wrap it in an async Discord on_message handler or a Slack Events API endpoint. Add a simple debounce so one user can't spam tool calls and drain your credit budget.
Q: Does the agent handle arbitrage opportunities or just EV?
As built, it's focused on +EV via /v1/edge. You can extend it with an find_arbitrage tool that calls /v1/odds for a given event and checks whether any combination of book prices sum below 100% implied probability. The arbitrage methodology is documented here if you want the math before you build.
Q: What's the right way to handle stale tool results in a long conversation?
Add a timestamp field to every tool result in the dispatcher. In the system prompt, instruct Claude to note when data is more than N minutes old and recommend refreshing. For live betting this threshold should be 2–3 minutes. For pre-game research, 15 minutes is fine.