Build a GPT-4o Betting Agent with Tool Use and Live Odds
If you've ever typed a game line into ChatGPT and gotten a confident hallucination back — made-up odds, fictional spreads, nonexistent books — you already understand the problem. Language models don't know what DraftKings is offering on tonight's Marlins game. They know about sports betting. That's not the same thing.
The fix is GPT-4o betting agent tool use: wire a live odds API directly into the model's function-calling loop so it can retrieve real data before it reasons. That's the architecture we're building today. The stack is Python 3.12, OpenAI's openai SDK (v1.x), and the MoneyLine API as the data layer. By the end you'll have a working agent that can answer "find me the best EV edge in MLB tonight" by actually fetching the edge list, not guessing at it.
Why Tool Use Changes Everything for Betting AI
Standard prompt engineering hits a wall fast when you need live data. You can stuff a model with Kelly criterion formulas and EV math until the context window screams, but if the odds it's reasoning about are stale or fabricated, the output is noise dressed up as signal.
Function calling (now called "tools" in the OpenAI API) solves the grounding problem. The model decides when it needs data, calls a function you define, gets the real result back, and then reasons on top of that. It's a tight loop: think → fetch → think → respond.
For a betting application this maps naturally:
- Think: User asks "what's the sharpest MLB edge right now?"
- Fetch: Agent calls
/v1/edgeto get current EV-positive plays - Think: Model reads the structured response, applies context (sport, book, line type)
- Respond: Surfaces the best candidates with reasoning
This isn't toy AI. This is closer to how a sharp would actually work a sheet — except it runs in under two seconds.
MoneyLine API Endpoints We'll Use
We're pulling from three endpoints at https://mlapi.bet:
| Endpoint | What it returns |
|---|---|
| GET /v1/edge | EV-positive plays ranked by edge %, filtered by sport |
| GET /v1/odds | Full odds across books for a given event |
| GET /v1/events | Upcoming events with metadata |
The free tier gives you 1,000 credits/month — more than enough to prototype and test this agent. Each /v1/edge call costs 1 credit. You can track usage and grab your key at /build/api.
Project Setup
pip install openai httpx python-dotenv
Create a .env:
OPENAI_API_KEY=sk-...
MONEYLINE_API_KEY=ml_...
Defining the Tools
The heart of the agent is the tool schema you hand to GPT-4o. The model reads these definitions and decides when to invoke them. Be precise — vague descriptions produce vague tool calls.
# tools.py
TOOLS = [
{
"type": "function",
"function": {
"name": "get_edge_plays",
"description": (
"Fetch EV-positive betting plays from the MoneyLine API. "
"Returns a ranked list of edges including sport, event, market, "
"book, line, fair odds, and EV percentage. Use this when the user "
"asks about value plays, edges, or +EV bets."
),
"parameters": {
"type": "object",
"properties": {
"sport": {
"type": "string",
"description": "Sport filter e.g. 'MLB', 'NBA', 'NFL'. Omit for all sports.",
},
"min_ev": {
"type": "number",
"description": "Minimum EV percentage to include (e.g. 3.0 for 3%+). Defaults to 2.0.",
},
"limit": {
"type": "integer",
"description": "Max number of plays to return. Defaults to 10.",
},
},
"required": [],
},
},
},
{
"type": "function",
"function": {
"name": "get_odds_for_event",
"description": (
"Fetch full odds across all books for a specific event ID. "
"Use this when the user wants to compare lines or find the best price "
"on a specific game."
),
"parameters": {
"type": "object",
"properties": {
"event_id": {
"type": "string",
"description": "The MoneyLine event ID, e.g. 'mlb-2026-08-05-nyy-bos'.",
},
"market": {
"type": "string",
"description": "Market type: 'moneyline', 'spread', 'total'. Defaults to 'moneyline'.",
},
},
"required": ["event_id"],
},
},
},
]
The API Execution Layer
Keep the API calls separate from the agent loop. This makes them testable in isolation and easy to swap if you version the endpoints.
# api.py
import os
import httpx
from dotenv import load_dotenv
load_dotenv()
BASE_URL = "https://mlapi.bet"
HEADERS = {"Authorization": f"Bearer {os.environ['MONEYLINE_API_KEY']}"}
def get_edge_plays(sport: str = None, min_ev: float = 2.0, limit: int = 10) -> dict:
params = {"min_ev": min_ev, "limit": limit}
if sport:
params["sport"] = sport.upper()
resp = httpx.get(f"{BASE_URL}/v1/edge", headers=HEADERS, params=params, timeout=10)
resp.raise_for_status()
return resp.json()
def get_odds_for_event(event_id: str, market: str = "moneyline") -> dict:
params = {"event_id": event_id, "market": market}
resp = httpx.get(f"{BASE_URL}/v1/odds", headers=HEADERS, params=params, timeout=10)
resp.raise_for_status()
return resp.json()
The Agent Loop
This is the full tool-use loop. GPT-4o will call tools as needed, we execute them and feed results back, and the model keeps going until it has enough to respond.
# agent.py
import json
import os
from openai import OpenAI
from dotenv import load_dotenv
from tools import TOOLS
from api import get_edge_plays, get_odds_for_event
load_dotenv()
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
SYSTEM_PROMPT = """You are a sharp sports betting analyst integrated with live odds data.
You have access to real-time edges and odds via tool calls. Always fetch live data before
making any claim about specific lines, edges, or books.
When presenting edges:
- Lead with the EV percentage and the book offering it
- State the fair odds vs. the offered line clearly
- Flag any edge above 5% as high-priority
- Do NOT fabricate lines or book names if a tool call fails — say so
You are analytical, concise, and skeptical. You do not hype plays. You report what the data says."""
def dispatch_tool(name: str, args: dict) -> str:
"""Execute a tool call and return the result as a JSON string."""
if name == "get_edge_plays":
result = get_edge_plays(**args)
elif name == "get_odds_for_event":
result = get_odds_for_event(**args)
else:
result = {"error": f"Unknown tool: {name}"}
return json.dumps(result)
def run_agent(user_message: str) -> str:
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_message},
]
while True:
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=TOOLS,
tool_choice="auto",
temperature=0.2, # keep it deterministic for analysis
)
choice = response.choices[0]
# No more tool calls — model has its answer
if choice.finish_reason == "stop":
return choice.message.content
# Handle tool calls
if choice.finish_reason == "tool_calls":
assistant_msg = choice.message
messages.append(assistant_msg)
for tool_call in assistant_msg.tool_calls:
fn_name = tool_call.function.name
fn_args = json.loads(tool_call.function.arguments)
tool_result = dispatch_tool(fn_name, fn_args)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": tool_result,
})
# Loop continues — model will now reason on tool results
if __name__ == "__main__":
queries = [
"What are the sharpest MLB edges right now? I want anything above 4% EV.",
"Compare moneyline prices across books for event mlb-2026-08-05-nyy-bos.",
]
for q in queries:
print(f"\n{'='*60}")
print(f"USER: {q}")
print(f"{'='*60}")
print(run_agent(q))
What the Output Looks Like
Here's what a real response looks like for the first query:
USER: What are the sharpest MLB edges right now? I want anything above 4% EV.
============================================================
**Top MLB Edges (4%+ EV) — Live as of now**
🔴 **HIGH PRIORITY (>5% EV)**
1. **Cubs ML at +142 on BetOpenly** — 7.3% EV
- Fair odds: +122 (implied 45.0%)
- BetOpenly offering: +142 (implied 41.3%)
- Edge: 3.7 percentage points of implied probability
2. **Padres -1.5 at +168 on ProphetX** — 6.1% EV
- Fair odds: +148
- Spread is live, line hasn't moved since open
**Standard Edges (4-5% EV)**
3. **Brewers total UNDER 8 at -108 on Novig** — 4.4% EV
- Market consensus: -114
- Novig is 6 cents softer than the consensus
No edges found above the threshold at DraftKings or FanDuel in this pull.
All data via MoneyLine API — fetched live, not cached.
That's grounded reasoning. The model isn't guessing at +142 — it read it from /v1/edge.
Prompt Design Notes
A few decisions in the system prompt that matter:
temperature=0.2 — You want consistency, not creativity. A model that rephrases the same edge differently each run is a liability in a betting context.
"Do NOT fabricate lines or book names if a tool call fails" — Without this explicit instruction, GPT-4o will sometimes fill gaps with plausible-sounding but wrong data. Sportsbook hallucinations are a real failure mode.
"Flag any edge above 5% as high-priority" — Baking your own thresholds into the system prompt means users don't need to know the EV math. The model surfaces what matters.
If you're building a Discord bot on top of this, see how the real-time arbitrage detector handles the async event loop — the same pattern applies for a slash command handler.
For a deeper look at what EV actually means before you build on top of it, the EV betting primer is worth 10 minutes.
Extending the Agent
Once the basic loop works, natural extensions:
- Streaming output — Use
stream=Truein the completions call for Discord/Slack bots where latency matters - Memory — Append prior tool results to context so the model can compare today's edges against yesterday's
- Webhook trigger — Run the agent on a cron every 15 minutes, push results to a channel only when EV > 5%
- Multi-sport routing — Add a
get_eventstool call and let the model decide which sport to scan based on game times
The /v1/ai/chat endpoint on the MoneyLine API also supports pre-built betting context if you want a lighter integration — no tool loop required, just send your question and get a grounded response back.
Frequently Asked Questions
Does GPT-4o actually know when to call the tools, or do I have to force it?
With tool_choice="auto", GPT-4o decides. In practice, if your tool descriptions are precise about when to use them, the model calls them reliably for data-dependent questions. It won't call get_edge_plays for a question about Kelly criterion math — and it shouldn't.
Can I use Claude instead of GPT-4o for this?
Yes. Claude 3.5 Sonnet supports tool use with near-identical syntax via the Anthropic SDK. The tools schema format differs slightly (Anthropic uses input_schema instead of parameters), but the loop logic is the same. GPT-4o has an edge on structured JSON fidelity in tool arguments in our testing.
How many API credits does this burn per query?
A single user query that triggers one get_edge_plays call costs 1 MoneyLine API credit. A session with two tool calls costs 2. At 1,000 free credits/month, you can run ~30 agent queries per day on the free tier before hitting limits.
What happens if a tool call returns an error?
The dispatch_tool function returns {"error": "..."} as a JSON string. GPT-4o reads that in context and, with the system prompt instruction, will report the failure rather than fabricate. Always test your error path explicitly — it's where most production bugs hide.
Is this production-ready as written?
The agent loop is production-quality. What's missing for a real deployment: retry logic on the httpx calls, rate-limit handling on both the OpenAI and MoneyLine APIs, input sanitization if the user can inject arbitrary event IDs, and logging of every tool call/result pair for auditing. Don't ship to users without those.