API Documentation
Base URL: https://api.zenhodl.net
Quickstart
Get your first edge signal in 30 seconds.
Sign up at /pricing — 7-day free trial, no credit card required for free tier.
curl -H "X-API-Key: sk_live_YOUR_KEY" https://api.zenhodl.net/v1/edges
Each signal tells you: the model's fair probability, the market ask price, and the gap (edge) between them.
// Signal: NCAAMB, Purdue (away), fair_wp=0.74, market_ask=0.63, edge=+11c
// Action: BUY Purdue at 63c. Model says it's worth 74c. Hold to settlement.
import requests
resp = requests.get(
"https://api.zenhodl.net/v1/edges",
headers={"X-API-Key": "sk_live_YOUR_KEY"},
params={"min_edge": 8}
)
for signal in resp.json()["signals"]:
print(f"{signal['sport']} {signal['team']}: "
f"fair={signal['fair_wp']:.0%} ask={signal['market_ask']:.0%} "
f"edge=+{signal['edge']*100:.1f}c ({signal['confidence']})")
Key Concepts
Our ML model's estimate of the true probability a team wins, based on score, time, Elo, and sport-specific features. Ranges from 0.0 to 1.0. Independent from market prices — this is what makes edge detection possible.
The current Polymarket ask price for the contract. This is what you'd pay to buy. Ranges from 0.0 to 1.0 (equivalent to 0c to 100c).
edge = fair_wp - market_ask. When positive, the model thinks the contract is underpriced. An edge of 0.11 means +11 cents of expected value per share. Our default minimum threshold is 8c.
Prediction market contracts resolve to $1.00 (win) or $0.00 (lose) after the game ends. Buy at 63c, win = +37c profit. Buy at 63c, lose = -63c. The edge is your statistical advantage over many trades.
Authentication
All authenticated endpoints require an API key. Pass it via header (recommended) or query parameter:
# Header (recommended — keeps key out of server logs)
curl -H "X-API-Key: sk_live_your_key_here" https://api.zenhodl.net/v1/games
# Query parameter (convenient for testing)
curl "https://api.zenhodl.net/v1/games?api_key=sk_live_your_key_here"
# Python
import requests
headers = {"X-API-Key": "sk_live_your_key_here"}
games = requests.get("https://api.zenhodl.net/v1/games", headers=headers).json()
Get your API key at /pricing. Keys start with sk_live_. Keep it secret — treat it like a password.
Rate Limits
Rate limits are per-minute. Every response includes these headers:
X-RateLimit-Limit: 300X-RateLimit-Remaining: 297X-RateLimit-Tier: pro
| Tier | Requests/min | WebSocket | Sports | Delay |
|---|---|---|---|---|
| Free | 10 | - | NBA, NHL | 15 min |
| Starter ($49) | 60 | 1 | All 9 | Real-time |
| Pro ($149) | 300 | 5 | All 9 | Real-time |
| Enterprise ($299) | 1,000 | 20 | All 9 | Real-time |
Errors
All errors return JSON with a detail field:
{"detail": "Rate limit exceeded (300 req/min for pro tier)."}
| Code | Meaning | Example |
|---|---|---|
401 | Missing or invalid API key | "Invalid or inactive API key." |
402 | Payment required | "Payment not completed." |
403 | Tier too low / dashboard-only key | "Your plan includes dashboard access only." |
404 | Resource not found | "No predictions for 20260101." |
429 | Rate limit exceeded | "Rate limit exceeded (60 req/min for starter tier)." |
GET /v1/health
System health check. No authentication required. Use this to verify the API is running and check how many games are active.
curl https://api.zenhodl.net/v1/health
Response:
{
"status": "ok",
"uptime_seconds": 3600.5,
"active_games": 12,
"sports_loaded": ["NBA", "NCAAMB", "NCAAWB", "CFB", "NFL", "NHL", "MLB"],
"last_espn_poll": "2026-03-22T19:30:00Z",
"last_ws_update": "2026-03-22T19:30:01Z"
}
| Field | Description |
|---|---|
status | ok or starting |
active_games | Number of live games being tracked right now |
sports_loaded | Which sport models are loaded and active |
last_espn_poll | Last time ESPN scores were fetched (UTC) |
GET /v1/games
All live games with ML fair win probabilities and market prices. This is the primary endpoint for getting a full view of every tracked game.
| Param | Type | Description |
|---|---|---|
sport | string | Filter by sport (NBA, NCAAMB, NCAAWB, CFB, NFL, NHL, MLB). Optional. |
curl -H "X-API-Key: sk_live_..." "https://api.zenhodl.net/v1/games?sport=NBA"
import requests
games = requests.get(
"https://api.zenhodl.net/v1/games",
headers={"X-API-Key": "sk_live_..."},
params={"sport": "NBA"}
).json()
for g in games["games"]:
edge = (g["home_fair_wp"] - (g["home_market_ask"] or 0)) * 100
print(f"{g['event_title']} | {g['time_display']} | edge: {edge:+.1f}c")
Response:
{
"timestamp": "2026-03-22T19:30:00Z",
"count": 5,
"games": [{
"sport": "NBA",
"game_id": "401810889",
"event_title": "Knicks vs. Wizards",
"home_team": "NY", "away_team": "WSH",
"home_score": 78, "away_score": 65,
"period": 3, "seconds_remaining": 420.0,
"time_display": "Q3 7:00",
"status": "live",
"home_fair_wp": 0.82,
"away_fair_wp": 0.18,
"home_market_ask": 0.75,
"away_market_ask": 0.27,
"home_edge": 0.07,
"away_edge": -0.09,
"model_confidence": "high",
"is_score_change": false,
"last_updated": "2026-03-22T19:29:55Z"
}]
}
Response fields
home_fair_wp | Model's fair probability for home team (0.0–1.0) |
home_market_ask | Current Polymarket ask price (0.0–1.0). null if no market match |
home_edge | fair_wp - market_ask. Positive = underpriced. 0.07 = 7 cents edge |
model_confidence | high (edge >12c), medium (6–12c), low (<6c) |
is_score_change | true if a score change happened in the last 30 seconds (prices may lag) |
status | live, scheduled, final |
seconds_remaining | Total seconds left in regulation (e.g. 420 = 7 minutes) |
GET /v1/edges
Current edge signals — only games where the model disagrees with the market by at least min_edge. This is the "actionable trades" endpoint.
| Param | Type | Description |
|---|---|---|
sport | string | Filter by sport. Optional. |
min_edge | float | Minimum edge in cents. Default: 8.0 |
curl -H "X-API-Key: sk_live_..." "https://api.zenhodl.net/v1/edges?sport=NCAAMB&min_edge=10"
edges = requests.get(
"https://api.zenhodl.net/v1/edges",
headers={"X-API-Key": "sk_live_..."},
params={"sport": "NCAAMB", "min_edge": 10}
).json()
for s in edges["signals"]:
print(f"BUY {s['team']} @ {s['market_ask']:.0%} "
f"(fair: {s['fair_wp']:.0%}, edge: +{s['edge']*100:.1f}c)")
Response:
{
"timestamp": "2026-03-22T19:30:00Z",
"count": 1,
"min_edge": 0.10,
"signals": [{
"sport": "NCAAMB",
"game_id": "401810889",
"event_title": "Purdue vs. Illinois",
"team": "Purdue",
"side": "away",
"fair_wp": 0.74,
"market_ask": 0.63,
"edge": 0.11,
"period": 2,
"seconds_remaining": 480.0,
"score": "52-44",
"confidence": "high",
"is_score_change": false,
"timestamp": "2026-03-22T19:29:55Z"
}]
}
Response fields
team | Team name the signal is for |
side | home or away |
fair_wp | Model's fair probability (0.0–1.0) |
market_ask | Polymarket ask price (0.0–1.0) |
edge | fair_wp − market_ask. 0.11 = 11 cents expected value |
confidence | high (≥12c), medium (6–12c), low (<6c) |
GET /v1/sports
Available sports and model metadata. Useful for discovering what models are loaded and their backtest performance.
curl -H "X-API-Key: sk_live_..." https://api.zenhodl.net/v1/sports
Response:
{
"sports": [{
"sport": "NCAAMB",
"model_type": "xgb_isotonic",
"train_games": 15230,
"elo_teams": 362,
"features": ["score_diff", "seconds_remaining", "period",
"time_fraction", "elo_diff", "pregame_wp"],
"backtest_wr": 0.738,
"backtest_c_per_trade": 12.0
}]
}
GET /v1/predictions/latest
Download today's pre-game fair probability predictions as CSV. Also available by date: /v1/predictions/{YYYYMMDD}
# Today's predictions
curl -H "X-API-Key: sk_live_..." https://api.zenhodl.net/v1/predictions/latest -o predictions.csv
# Specific date
curl -H "X-API-Key: sk_live_..." https://api.zenhodl.net/v1/predictions/20260322 -o predictions.csv
Sample CSV row:
sport,game_id,home_team,away_team,start_time,home_fair_wp,away_fair_wp,elo_diff,model_pick,confidenceNCAAMB,401810889,Illinois,Purdue,2026-03-22T19:00:00Z,0.426,0.574,-92,Purdue,high
CSV files include a license header with your email and download token. See data license.
POST /v1/backtest
Pro tier and aboveRun a WP model backtest with custom strategy parameters against our 25M+ row dataset. Test different edge thresholds, periods, and fee assumptions.
| Field | Type | Default | Description |
|---|---|---|---|
sport | string | NBA | Sport to backtest (NBA, NCAAMB, CFB, NFL, etc.) |
min_edge_c | float | 8.0 | Minimum edge in cents to enter a trade |
max_edge_c | float | 50.0 | Maximum edge (filter out suspicious outliers) |
min_fair_wp_c | float | 65.0 | Only trade if model confidence ≥ this |
max_entry_c | float | 78.0 | Don't buy above this price |
min_entry_c | float | 35.0 | Don't buy below this price |
min_period | int | 2 | Earliest game period to enter |
max_per_game | int | 3 | Max entries per game (both sides combined) |
seasons | list | all | Filter by season, e.g. ["2024-25"] |
taker_fee_c | float | 2.0 | Platform taker fee per trade (cents) |
slippage_c | float | 1.0 | Expected slippage per trade (cents) |
curl -X POST -H "X-API-Key: sk_live_..." -H "Content-Type: application/json" \
-d '{"sport":"NCAAMB","min_edge_c":10,"min_period":2,"seasons":["2025-26"]}' \
https://api.zenhodl.net/v1/backtest
result = requests.post(
"https://api.zenhodl.net/v1/backtest",
headers={"X-API-Key": "sk_live_..."},
json={"sport": "NCAAMB", "min_edge_c": 10, "min_period": 2}
).json()
s = result["summary"]
print(f"{s['total_trades']} trades, {s['win_rate']:.1%} WR, "
f"+{s['c_per_trade_net']:.1f}c/trade net")
Response:
{
"summary": {
"total_trades": 854,
"wins": 630,
"losses": 224,
"win_rate": 0.738,
"gross_pnl_c": 12024.0,
"net_pnl_c": 10280.0,
"avg_edge_c": 11.8,
"c_per_trade_net": 12.0,
"max_drawdown_c": 340.0,
"best_streak": 18,
"worst_streak": 5
},
"by_edge": [
{"bucket": "8-10c", "trades": 412, "win_rate": 0.71, "c_per_trade": 9.8},
{"bucket": "10-12c", "trades": 198, "win_rate": 0.76, "c_per_trade": 14.2},
{"bucket": "12-15c", "trades": 144, "win_rate": 0.79, "c_per_trade": 16.5}
],
"by_period": [
{"period": 1, "trades": 320, "win_rate": 0.72, "c_per_trade": 10.1},
{"period": 2, "trades": 534, "win_rate": 0.75, "c_per_trade": 13.2}
],
"sample_trades": [{"game_id": "...", "side": "away", "edge_c": 11.1, "entry_c": 63.0, "profit_c": 37.0, "won": true}]
}
GET /v1/backtest/sports
List available sports and seasons for backtesting.
curl -H "X-API-Key: sk_live_..." https://api.zenhodl.net/v1/backtest/sports
Response:
{
"sports": {
"NCAAMB": {"seasons": ["2023-24", "2024-25", "2025-26"], "files": 3, "model_available": true},
"NBA": {"seasons": ["2023-24", "2024-25", "2025-26"], "files": 3, "model_available": true},
"CFB": {"seasons": ["2023", "2024"], "files": 2, "model_available": true}
}
}
WS /v1/ws/stream
Real-time WebSocket stream. Pushes game updates and edge alerts every 5 seconds. For bot builders who need sub-second reaction to new edges.
wss://api.zenhodl.net/v1/ws/stream?api_key=sk_live_...api_key query paramconst ws = new WebSocket("wss://api.zenhodl.net/v1/ws/stream?api_key=sk_live_...");
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === "edge_alert") {
const d = msg.data;
console.log(`EDGE: ${d.sport} ${d.team} | `
+ `fair=${d.fair_wp}c ask=${d.market_ask}c edge=+${d.edge}c`);
}
if (msg.type === "game_update") {
const d = msg.data;
console.log(`${d.event_title} ${d.time_display} | `
+ `fair=${d.home_fair_wp} ask=${d.home_market_ask}`);
}
};
ws.onclose = () => setTimeout(() => location.reload(), 5000); // auto-reconnect
import asyncio, websockets, json
async def stream():
uri = "wss://api.zenhodl.net/v1/ws/stream?api_key=sk_live_..."
async with websockets.connect(uri) as ws:
async for raw in ws:
msg = json.loads(raw)
if msg["type"] == "edge_alert":
d = msg["data"]
print(f"EDGE: {d['sport']} {d['team']} "
f"fair={d['fair_wp']}c ask={d['market_ask']}c "
f"edge=+{d['edge']}c ({d['confidence']})")
asyncio.run(stream())
Message types
game_update
Sent for every live game on each tick
{"type": "game_update", "data": {
"sport": "NBA", "game_id": "401810889", "event_title": "Knicks vs. Wizards",
"home_team": "NY", "away_team": "WSH", "home_score": 78, "away_score": 65,
"period": 3, "time_display": "Q3 7:00",
"home_fair_wp": 0.82, "away_fair_wp": 0.18,
"home_market_ask": 0.75, "away_market_ask": 0.27,
"home_edge": 0.07, "away_edge": -0.09
}}
edge_alert
Sent when edge exceeds threshold
{"type": "edge_alert", "data": {
"sport": "NCAAMB", "team": "Purdue", "side": "away",
"fair_wp": 74.1, "market_ask": 63.0, "edge": 11.1,
"score": "52-44", "confidence": "high"
}}
Free Samples
Two free endpoints — no API key needed.
GET /v1/predictions/sample.csv
Download a CSV with live edge signals + resolved trade examples showing actual P&L (wins and losses).
curl https://api.zenhodl.net/v1/predictions/sample.csv -o sample.csv
GET /v1/edges/sample
Top 3 edge signals as JSON, delayed 15 minutes, NBA + NHL only.
curl https://api.zenhodl.net/v1/edges/sample
{
"count": 2,
"delay_minutes": 15,
"note": "Delayed 15 min. Upgrade for real-time across 9 sports.",
"signals": [
{"sport": "NBA", "team": "NY", "edge": 11.2, "fair_wp": 78.5, "confidence": "high"},
{"sport": "NHL", "team": "BOS", "edge": 9.1, "fair_wp": 71.3, "confidence": "medium"}
]
}
Ready to get started?
Get your API key and start receiving real-time edge signals.