Sports Prediction API Documentation
Base URL: https://zenhodl.net
ZenHodl Weekly
Follow dataset releases, research and API changes.
Updates on dataset releases, research notes, corrections and API changes.
For builders and researchers following the data, endpoints and published evidence.
Quickstart
Start with a games or edges request. Response values below are illustrative; game availability, model versions and source freshness change. For background, read our research summary. research summary.
Create a free account at /signup (no credit card required), or choose a paid plan at /pricing.
curl -H "X-API-Key: sk_live_YOUR_KEY" https://zenhodl.net/v1/edges
Each row tells you: the model's fair probability, the venue's ask price, and the gap between them — so you can line-shop and avoid overpaying the vig.
// NCAAMB, Purdue (away), fair_wp=0.74, market_ask=0.63, gap=+11c
// Our fair value is 74c; this venue asks 63c. Compare venues and take the best price.
pip install zenhodl
Zero dependencies, built-in timeouts and retries. Try it before signing up — the sample endpoint needs no key:
from zenhodl import ZenHodl
client = ZenHodl() # keyless: sample + health
sample = client.edges_sample() # keyless preview: 15-min delay, NBA+NHL only — NOT your Free tier
With your API key (games and edges are 5 minutes delayed on Free):
client = ZenHodl("sk_live_YOUR_KEY")
for s in client.edges(min_edge=8)["signals"]:
print(f"{s['sport']} {s['team']}: "
f"fair={s['fair_wp']:.0%} ask={s['market_ask']:.0%} "
f"edge=+{s['edge']*100:.1f}c ({s['confidence']})")
games = client.games(sport="MLB") # delayed on Free; real-time on Starter+
Starter and higher plans can also request the predictions CSV:
csv_bytes = client.predictions()
Prefer raw HTTP? Every endpoint below works with any client — pass your key in the X-API-Key header.
Which Endpoint Should I Use?
Free keys can call /v1/games and /v1/edges with a 5-minute delay and a 500 request monthly cap. Real-time predictions, fair lines, WebSocket, and webhooks require an eligible paid tier.
Use /v1/edges. It returns just the games where our fair win probability and the venue ask diverge by your threshold — the fastest first integration for line-shopping alerts, dashboards, and scanners.
Use /v1/games. This is the best endpoint for dashboards and game-level monitoring.
Use /v1/predict/{sport}/live or /v1/predict/{sport}/pregame. These are the cleanest prediction endpoints for model consumers. Starter+ tier.
Use /v1/ws/stream for browser/app clients, or /v1/webhooks for server-to-server delivery.
Use /v1/watchlists and /v1/preferences to save filters and alert settings tied to a user account.
Key Concepts
Our ML model's estimate of the probability a team wins, based on score, time, Elo, and sport-specific features. Ranges from 0.0 to 1.0. The API exposes the documented pre-blend model estimate; this does not certify independence of every upstream feature. Compare the named model’s evaluated feature set and version.
The stored Polymarket ask price for the contract, from 0.0 to 1.0 (0c to 100c). Check the venue quote age and available size; a displayed ask does not guarantee a fill at that price.
edge = fair_wp - market_ask. When positive, the model thinks the contract is underpriced. An edge of 0.11 means the model sees an 11-cent probability gap before fees and slippage. Not the same as expected profit — actual profit depends on execution costs (~2-3c) and model accuracy. 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 field is the gap between our fair-value reference and the venue price before fees, slippage, and model error — not a guaranteed advantage.
Authentication
All authenticated endpoints require an API key. Pass it via the X-API-Key header:
Security Note
Always pass your API key via the X-API-Key header, not in URL parameters. URL parameters appear in browser history, server logs, and referrer headers.
# curl
curl -H "X-API-Key: sk_live_your_key_here" https://zenhodl.net/v1/games
# Python
import requests
headers = {"X-API-Key": "sk_live_your_key_here"}
games = requests.get("https://zenhodl.net/v1/games", headers=headers).json()
// JavaScript (fetch)
const resp = await fetch("https://zenhodl.net/v1/games", {
headers: { "X-API-Key": "sk_live_your_key_here" }
});
const data = await resp.json();
Create a free account to receive a key for delayed games and edges, or choose a paid plan for real-time access. Keys start with sk_live_. Keep it secret — treat it like a password.
Rate Limits
Rate limits are per-minute. Every authenticated API response includes these headers (Remaining already counts the current request). Public endpoints return 429 with Retry-After.
X-RateLimit-Limit: 300X-RateLimit-Remaining: 297X-RateLimit-Tier: pro
| Tier | Requests/min | Monthly Cap | WebSocket | Sports | Delay |
|---|---|---|---|---|---|
| Free | 10 | 500 | - | All 8 | 5 min (games/edges); predict + fair lines Starter+ |
| Starter ($49) | 60 | 30,000 | 1 | All 8 | Real-time |
| Pro ($149) | 300 | 100,000 | 5 | All 8 | Real-time |
| Enterprise (Custom) | 1,000 | Unlimited | 20 | All 8 | Real-time |
Monthly usage is metered on authenticated REST endpoints. /v1/usage is excluded so you can always inspect your own usage without consuming quota. WebSocket connection limits are enforced separately from monthly REST usage.
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)." |
Transparency endpoints (free, no auth)
Anyone can audit our results by downloading the strategy-redacted trade ledger and the analysis code. These endpoints are unauthenticated and serve the proof data behind our public results pages — including the 2026-07-21 retraction of our earlier CLV-gap claim. We publish retractions with the same prominence we gave the claims.
GET
/api/trades.jsonl
Strategy-redacted admitted live trades as line-delimited JSON. Each row carries only the proof fields needed to reproduce CLV: sport, entry price, closing price where captured, CLV, resolution, outcome, and a one-way public_fill_id when exchange correction is available. Raw order ids, token ids, event names, model fair values, edge thresholds, sizes, scores, book depth, and cash P&L are not published here.
curl -O https://zenhodl.net/api/trades.jsonl
Use case: reproduce the unconditioned CLV scorecard, build your own dashboards, and audit per-sport results.
GET
/api/exchange_fill_reconciliation.jsonl
The public CLV price-correction sidecar. One-way public_fill_id matches replace
historical submitted-limit prices with authenticated exchange execution prices. The public file
intentionally excludes raw exchange identities, share size, cost, realized P&L, and reconciliation
diagnostics; those fields are not needed to reproduce CLV.
curl -O https://zenhodl.net/api/exchange_fill_reconciliation.jsonl
The verifier treats a missing, malformed, or duplicate sidecar as an error rather than falling back to known-biased historical prices. It still supports legacy raw artifacts for audit replay.
GET
/api/verify_clv_gap.py
The script behind our earlier headline “78-pp CLV gap” — a claim we retracted on 2026-07-21: for in-play trades the captured close is near-terminal, so the statistic largely encodes the outcome rather than skill (our model's rejected side showed the same split). The script is preserved, with a retraction notice, for the audit trail. It also prints the current honest aggregate and requires both public JSONL artifacts.
curl -O https://zenhodl.net/api/trades.jsonl
curl -O https://zenhodl.net/api/exchange_fill_reconciliation.jsonl
curl -O https://zenhodl.net/api/verify_clv_gap.py
python3 verify_clv_gap.py trades.jsonl
Read the retraction at /clv-evidence before interpreting its output. No dependencies beyond Python stdlib.
HTML /clv-evidence
The retraction of our CLV-gap whitepaper (2026-07-21). The original claim (a 78-pp win-rate gap, z = 24.27) was a measurement artifact: on in-play trades the captured close encodes the outcome, so the test also “passed” for the trades our model rejected. The page documents exactly what went wrong and the honest aggregate we now report (beat-close rate and mean CLV). We keep it public because honest measurement is the product.
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://zenhodl.net/v1/health
Response:
{
"status": "ok",
"uptime_seconds": 3600.5,
"active_games": 12,
"sports_loaded": ["NBA", "WNBA", "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, starting, or degraded |
active_games | Number of live games being tracked right now |
sports_loaded | Which public API sport models loaded successfully; this does not describe live-money bot status |
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, WNBA, NCAAMB, NCAAWB, CFB, NFL, NHL, MLB). Optional. |
venue | string | Filter venue_prices to a specific venue (polymarket, kalshi, draftkings, fanduel, betmgm). Optional. Starter tier or above — free tier keys get 400. |
curl -H "X-API-Key: sk_live_..." "https://zenhodl.net/v1/games?sport=NBA&venue=polymarket"
import requests
games = requests.get(
"https://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": "medium",
"is_score_change": false,
"last_updated": "2026-03-22T19:29:55Z",
"venue_prices": {
"polymarket": {
"venue": "polymarket",
"home_ask_c": 75.0, "away_ask_c": 27.0,
"home_bid_c": 74.0, "away_bid_c": 26.0,
"quote_ts": "2026-03-22T19:29:53Z",
"quote_age_seconds": 2.1
},
"kalshi": {
"venue": "kalshi",
"home_ask_c": 76.0, "away_ask_c": 26.0,
"home_bid_c": 74.0, "away_bid_c": 25.0,
"quote_ts": "2026-03-22T18:59:10Z",
"quote_age_seconds": 1843.0
}
},
"best_home_venue": "polymarket",
"best_away_venue": "kalshi",
"batting_half": null
}]
}
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) |
venue_prices | Per-venue price map, keyed by venue id (polymarket, kalshi, sportsbook keys). Each entry is independent — two venues on the same game can carry different prices AND different ages. |
venue_prices.<venue>.quote_ts | UTC timestamp (ISO 8601) of THIS VENUE's own last quote update. Independent per venue — do not assume it matches the game-level last_updated above. |
venue_prices.<venue>.quote_age_seconds | Age of this venue's quote in seconds, as of this response's timestamp. null only if that venue never reported an update time. A large value on one venue while another is small means that venue's book has gone quiet — treat its price as stale rather than averaging it in. |
batting_half | MLB only: "top" (away team batting) or "bottom" (home team batting). null for every other sport. Added 2026-09-25 — period and time_display (e.g. "Inn 7, 1 out") do not encode this on their own. |
GET /v1/edges
Where our fair value differs from a venue's price — only games where our fair win probability and the venue ask diverge by at least min_edge. Use it for line-shopping and best-price comparison, not as a profit promise. The edge field is just fair_wp − market_ask (before fees and slippage); see the CLV retraction and amendment for the limits of terminal in-play closing-line measurements.
How these edges relate to our own trading (disclosure, 2026-09-25)
fair_wp, edge and the min_edge filter are model values, taken before the market blend our own bot applies. The bot does not trade this number: after the model it runs a production calibration step and then, for most sports, blends 50/50 with the market price, which halves the edge. A 12c edge here is roughly a 6c edge on the bot's own chain. Many signals listed here are never traded by our bot.
Each signal therefore also carries the bot-side view in cents: bot_fair_c and bot_edge_c are the bot's calibration and blend applied to this quote, model_fair_c is the model fair in cents, and edge_basis (model_pre_blend) names the basis of the legacy fields. confidence is computed from bot_edge_c.
bot_lane is shadow for sports our bot evaluates without placing any live orders (as of 2026-09-25: NFL, CFB, NBA, NCAAMB, NCAAWB, NHL, plus any sport whose rating file fails the bot's startup check, the _elo_generation_unverified set) and live otherwise. live only means the sport is not shadowed: floors, caps, circuit breakers and sizing can still decline any signal. It is null for venues the bot never trades (only Polymarket is traded).
MLB exception (the AB_LIVE_ARM_SPORTS A/B arm, MLB by default): when the MLB edge on the chain above misses the bot's floor, the bot can still trade using a second, reference calibration (the older GLOBAL-map path) if that reference edge clears the floor. bot_edge_c shows only the main chain, so for MLB it can understate what the bot acts on.
Limits: bot-only in-play adjustments (for example NBA injury and rest, MLB game-state and weather adjustments) are not included, so bot_fair_c can differ from the value the bot logs. Delayed (free-tier) responses carry null bot fields; edge_basis is model_pre_blend on every row. /v1/games edges and model_confidence remain model-based.
| Param | Type | Description |
|---|---|---|
sport | string | Filter by sport. Optional. |
min_edge | float | Minimum model edge in cents, applied to edge (not bot_edge_c). Default: 8.0. The response's min_edge field echoes this back as a decimal (0.0–1.0); min_edge_c echoes it back in the same cents unit you sent. |
venue | string | Filter by venue (polymarket, kalshi, draftkings, etc.). Optional. Starter tier or above — free tier keys get 400. |
curl -H "X-API-Key: sk_live_..." "https://zenhodl.net/v1/edges?sport=NCAAMB&min_edge=10"
edges = requests.get(
"https://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,
"min_edge_c": 10.0,
"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,
"edge_c": 11.0,
"period": 2,
"seconds_remaining": 480.0,
"score": "52-44",
"confidence": "low",
"is_score_change": false,
"timestamp": "2026-03-22T19:29:55Z",
"model_fair_c": 74.0,
"bot_fair_c": 68.5,
"bot_edge_c": 5.5,
"bot_lane": "shadow",
"edge_basis": "model_pre_blend"
}]
}
Illustrative values. Here an 11c model edge becomes 5.5c after the bot's 50/50 market blend, so confidence is low; NCAAMB is a shadow sport for our bot.
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-cent probability gap (before fees/slippage) |
edge_c | Added 2026-09-25: edge in cents (0–100), i.e. edge * 100 — same MODEL basis, matching the min_edge query parameter's unit |
confidence | From bot_edge_c: high (≥12c), medium (6–12c), low (<6c). Falls back to the model edge only if the bot chain is unavailable. |
model_fair_c | Model fair probability in cents (0–100); same value as fair_wp |
bot_fair_c | Our bot's fair value for this quote in cents: production calibration, then the 50/50 market blend unless the sport is exempt. null if unavailable |
bot_edge_c | bot_fair_c − the ask, in cents (before fees/slippage) |
bot_lane | live or shadow (shadow sports place no live orders); null for venues the bot never trades |
edge_basis | model_pre_blend: the basis of fair_wp / edge |
GET /v1/sports
Available sports and model metadata. Useful for discovering what models are loaded and their backtest performance.
Public API contract: NBA, WNBA, NCAAMB, NCAAWB, CFB, NFL, NHL and MLB. Internal soccer, tennis, CS2 and League of Legends runners are separate and are not promised by this endpoint.
curl -H "X-API-Key: sk_live_..." https://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://zenhodl.net/v1/predictions/latest -o predictions.csv
# Specific date
curl -H "X-API-Key: sk_live_..." https://zenhodl.net/v1/predictions/20260322 -o predictions.csv
Sample CSV row:
sport,game_id,home_team,away_team,home_full_name,away_full_name,start_time,venue,home_fair_wp,away_fair_wp,home_elo,away_elo,elo_diff,model_pick,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://zenhodl.net/v1/backtest
result = requests.post(
"https://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
Pro tier and aboveList available sports and seasons for backtesting.
curl -H "X-API-Key: sk_live_..." https://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
WebSocket pushes are scheduled every 5 seconds. That is a transport interval, not a guarantee that a new game state or fresh quote exists for every source. Inspect the source timestamps and quote-age fields.
wss://zenhodl.net/v1/ws/streamconst API_KEY = "sk_live_your_key_here";
const ws = new WebSocket(
"wss://zenhodl.net/v1/ws/stream",
["zenhodl.v1", API_KEY]
);
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
API_KEY = "sk_live_your_key_here"
async def stream():
uri = "wss://zenhodl.net/v1/ws/stream"
async with websockets.connect(uri, subprotocols=["zenhodl.v1", API_KEY]) 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": "low"
}}
confidence is computed from the bot-side edge (see /v1/edges); edge is the model edge.
GET /v1/predict/{sport}/live
All live games with win probabilities and multi-venue edges. Returns fair ML odds and per-venue pricing from Polymarket, Kalshi, and major sportsbooks. Starter+ tier (Free keys get a 403; use the delayed /v1/games and /v1/edges).
curl -H "X-API-Key: sk_live_..." "https://zenhodl.net/v1/predict/NBA/live"
import requests
resp = requests.get(
"https://zenhodl.net/v1/predict/NBA/live",
headers={"X-API-Key": "sk_live_..."}
).json()
for g in resp["games"]:
print(f"{g['home_team']} vs {g['away_team']}: "
f"P(home)={g['home_win_prob']:.0%} ML={g['fair_home_ml']}")
for venue, v in (g.get("venues") or {}).items():
if v.get("home_edge") and v["home_edge"] > 0.05:
print(f" model minus {venue} price: {v['home_edge']:.1%}")
const resp = await fetch("https://zenhodl.net/v1/predict/NBA/live", {
headers: { "X-API-Key": "sk_live_..." }
});
const { games } = await resp.json();
games.forEach(g => console.log(`${g.home_team} ${g.fair_home_ml} | ${g.away_team} ${g.fair_away_ml}`));
Response:
{
"timestamp": "2026-03-28T22:00:00Z",
"sport": "NBA",
"count": 5,
"games": [{
"game_id": "401585432",
"sport": "NBA",
"home_team": "LAL", "away_team": "BOS",
"home_score": 78, "away_score": 75,
"period": 3, "clock": "Q3 8:42",
"home_win_prob": 0.72,
"away_win_prob": 0.28,
"model_confidence": "high",
"fair_home_ml": -257,
"fair_away_ml": 215,
"venues": {
"Polymarket": {"home_ask": 0.62, "home_edge": 0.10, "quote_ts": "2026-03-28T21:59:58Z", "quote_age_seconds": 2.4},
"Kalshi": {"home_ask": 0.63, "home_edge": 0.09, "quote_ts": "2026-03-28T21:30:12Z", "quote_age_seconds": 1787.6},
"DraftKings": {"home_ml": "-150", "home_implied": 0.60, "home_edge": 0.12, "quote_ts": "2026-03-28T21:59:40Z", "quote_age_seconds": 20.1},
"FanDuel": {"home_ml": "-165", "home_implied": 0.623, "home_edge": 0.097, "quote_ts": "2026-03-28T21:59:41Z", "quote_age_seconds": 19.0}
},
"best_home_venue": "DraftKings",
"batting_half": null
}]
}
batting_half (added 2026-09-25): MLB only, "top" (away team batting) or "bottom" (home team batting); null for every other sport and for pregame predictions. period and clock do not encode this on their own.
Per-venue freshness
venues.<Venue>.quote_ts | UTC timestamp (ISO 8601) of THAT venue's own last quote update — independent per venue. |
venues.<Venue>.quote_age_seconds | Age of that venue's quote in seconds, as of this response's timestamp. In the example above Kalshi is ~30 minutes stale while Polymarket is ~2 seconds old — compare this before trusting best_home_venue or any single venue's price. null only if that venue never reported an update time. |
GET /v1/predict/{sport}/pregame
Today's upcoming games with Elo-based pregame win probabilities. No live game state needed — uses Elo ratings only. Starter+ tier.
curl -H "X-API-Key: sk_live_..." "https://zenhodl.net/v1/predict/NBA/pregame"
Response:
{
"sport": "NBA", "count": 8,
"games": [{
"game_id": "401810900",
"home_team": "LAL", "away_team": "BOS",
"home_win_prob": 0.42, "away_win_prob": 0.58,
"fair_home_ml": 138, "fair_away_ml": -138,
"status": "scheduled"
}]
}
GET /v1/predict/{sport}/{game_id}
Single game detail with full model output, multi-venue edges, and game state. Starter+ tier.
curl -H "X-API-Key: sk_live_..." "https://zenhodl.net/v1/predict/NBA/401585432"
GET /v1/fair-lines/{sport}
Fair moneyline odds in American format for all live games. Designed for integration into odds comparison tools. Starter+ tier.
curl -H "X-API-Key: sk_live_..." "https://zenhodl.net/v1/fair-lines/NBA"
Response:
{
"sport": "NBA", "count": 5,
"lines": [{
"game_id": "401585432",
"home_team": "LAL", "away_team": "BOS",
"home_win_prob": 0.72, "away_win_prob": 0.28,
"fair_home_ml": -257, "fair_away_ml": 215,
"clock": "Q3 8:42", "status": "live"
}]
}
GET /v1/usage
Your current month's API usage. Track request counts against your monthly cap.
curl -H "X-API-Key: sk_live_..." "https://zenhodl.net/v1/usage"
Response:
{
"month": "2026-03",
"requests_used": 4521,
"requests_cap": 30000,
"by_endpoint": {
"/v1/predict/NBA/live": 3200,
"/v1/predict/NHL/live": 821,
"/v1/fair-lines/NBA": 500
},
"tier": "starter",
"resets_at": "2026-04-01T00:00:00+00:00"
}
| Tier | Monthly Cap | Price |
|---|---|---|
| Free | 500 | $0 |
| Starter | 30,000 | $49/mo |
| Pro | 100,000 | $149/mo |
| Enterprise | Unlimited | Custom |
Model Quality & Analytics
Endpoints for evaluating model quality, tracking closing line value (CLV), and monitoring data venues.
GET /v1/model/performance
Available model reports can include Brier score, ROC-AUC, ECE, accuracy and conformal tables. Inspect trained_date and n_test_games with each report; an example value is not a current all-sport performance guarantee.
| Param | Type | Description |
|---|---|---|
sport | string | Filter to a single sport. Optional. |
curl -H "X-API-Key: sk_live_..." "https://zenhodl.net/v1/model/performance?sport=NBA"
{
"sports": [{
"sport": "NBA",
"brier_score": 0.139,
"roc_auc": 0.890,
"ece": 0.106,
"accuracy": 0.803,
"model_type": "Split-Phase",
"trained_date": "2026-03-24T15:52:54Z",
"n_test_games": 90,
"conformal_table": [{"tf_lo": 0.0, "tf_hi": 0.1, "width": 0.029, "n_samples": 35672}, ...]
}]
}
GET /v1/model/clv
Closing-line observations for recorded entries. Pregame CLV can be a useful market-relative diagnostic; terminal in-play CLV can encode the outcome and is not, by itself, proof of edge or profitability. Starter+ tier.
| Param | Type | Description |
|---|---|---|
sport | string | Filter by sport. Optional. |
days | int | Lookback window in days. Default: 7 |
curl -H "X-API-Key: sk_live_..." "https://zenhodl.net/v1/model/clv?sport=NBA&days=30"
{
"total_signals": 142,
"avg_clv_c": 3.8,
"clv_beat_pct": 61.2,
"by_sport": [{"sport": "NBA", "count": 42, "avg_clv_c": 4.1, "clv_beat_pct": 64.3}],
"by_edge_bucket": [{"bucket": "8-12c", "count": 85, "avg_clv_c": 2.1, "clv_beat_pct": 55.3}]
}
GET /v1/venues
List venue connection and polling status. A connected or polling venue may still have stale, absent or unmatched quotes for a specific game; inspect its quote timestamp before using the price.
curl -H "X-API-Key: sk_live_..." "https://zenhodl.net/v1/venues"
{
"venues": [
{"venue": "polymarket", "status": "connected", "connection_type": "websocket", "markets_active": 7770},
{"venue": "kalshi", "status": "connected", "connection_type": "websocket"},
{"venue": "draftkings", "status": "polling", "connection_type": "rest_polling"}
]
}
GET /v1/snapshots/{sport}/{date}
Recorded intraday win-probability snapshots, with a nominal 30-second write cadence. Available days and rows depend on observed game states; missing rows are not continuous coverage. Use recorded timestamps when studying entry timing or momentum. Pro+ tier.
| Param | Type | Description |
|---|---|---|
sport | path | Sport code (NBA, NHL, etc.) |
date | path | Date in YYYYMMDD format |
curl -H "X-API-Key: sk_live_..." "https://zenhodl.net/v1/snapshots/NBA/20260330"
GET /v1/predictions/batch
Bulk download predictions for a date range as CSV. Max 90 days per request. Pro+ tier.
| Param | Type | Description |
|---|---|---|
start | string | Start date (YYYYMMDD). Required. |
end | string | End date (YYYYMMDD). Required. |
sport | string | Filter by sport. Optional. |
curl -H "X-API-Key: sk_live_..." "https://zenhodl.net/v1/predictions/batch?start=20260301&end=20260330&sport=NBA" -o batch.csv
Webhooks
Register a URL for signed POST notifications when a model-versus-venue price gap matches your threshold. Client polling is unnecessary for delivery; that does not guarantee fresh upstream quotes, every venue update, or an executable fill. Starter+ tier.
POST /v1/webhooks
Register a new webhook. Limits: Starter=1, Pro=3, Enterprise=10.
curl -X POST -H "X-API-Key: sk_live_..." \
-H "Content-Type: application/json" \
-d '{"url": "https://your-server.com/hook", "filters": {"sport": "NBA", "min_edge_c": 10}}' \
"https://zenhodl.net/v1/webhooks"
Each webhook POST includes an X-Webhook-Signature header (HMAC-SHA256) for verification.
GET /v1/webhooks
List your active webhooks.
curl -H "X-API-Key: sk_live_..." "https://zenhodl.net/v1/webhooks"
DELETE /v1/webhooks/{webhook_id}
Deactivate a webhook.
curl -X DELETE -H "X-API-Key: sk_live_..." "https://zenhodl.net/v1/webhooks/abc123"
Watchlists & Preferences
Store user-specific game lists and notification settings. These endpoints are useful when you want durable scanner filters or alert automation tied to an account.
GET /v1/watchlists
List the current user's saved watchlists.
curl -H "X-API-Key: sk_live_..." "https://zenhodl.net/v1/watchlists"
POST /v1/watchlists
Create a saved watchlist with your own filters and thresholds.
curl -X POST -H "X-API-Key: sk_live_..." \
-H "Content-Type: application/json" \
-d '{"name": "High Edge NBA", "sports": ["NBA"], "teams": [], "min_edge_c": 10, "max_entry_c": 78}' \
"https://zenhodl.net/v1/watchlists"
GET /v1/preferences
Read the user's alert and notification preferences.
curl -H "X-API-Key: sk_live_..." "https://zenhodl.net/v1/preferences"
PUT /v1/preferences
Update user notification settings for dashboard alerts and watchlists.
curl -X PUT -H "X-API-Key: sk_live_..." \
-H "Content-Type: application/json" \
-d '{"daily_summary": true, "weekly_summary": true, "alert_threshold_c": 9, "favorite_sports": ["NBA", "NHL"], "timezone": "America/New_York"}' \
"https://zenhodl.net/v1/preferences"
Confidence Intervals
Where a model has a conformal table, prediction responses can include confidence_interval. These are model-specific estimated bands; their coverage depends on the evaluation sample and current data regime. They do not establish a guaranteed win probability or a safe position size.
{
"home_fair_wp": 0.72,
"confidence_interval": {
"lower": 0.68,
"upper": 0.76,
"width": 0.08
}
}
Band widths come from the available model table and game-state bucket. The values above are illustrative; do not assume all sports or later game states have the same width or more certainty.
Free Samples
Two public sample endpoints — no API key needed. Signed-in free accounts get broader delayed access on the authenticated API.
GET /v1/predictions/sample.csv
Download a small CSV sample of fair-vs-market price gaps: up to 3 NBA and NHL signals, each at least 15 minutes old. The file can hold no data rows (a comment line then says so). It contains no trade results or P&L; the audited record is on /results.
curl https://zenhodl.net/v1/predictions/sample.csv -o sample.csv
GET /v1/edges/sample
Up to 3 of the largest fair-vs-market price gaps as JSON, from NBA and NHL games only, delayed at least 15 minutes. The list can be empty; count says how many signals it holds. Create a free account for the live dashboard plus delayed /v1/games and /v1/edges access across all 8 sports.
Units (added 2026-09-25): edge/fair_wp on this endpoint are in cents (0–100, e.g. 78.5 = 78.5% win probability) — unlike the authenticated /v1/edges endpoint above, whose identically-named fair_wp/edge fields are a decimal probability (0.0–1.0, e.g. 0.785). Use edge_decimal/fair_wp_decimal below if you want the same convention as /v1/edges; the response also carries a units object naming every field.
curl https://zenhodl.net/v1/edges/sample
{
"count": 2,
"delay_minutes": 15,
"sports": ["NBA", "NHL"],
"note": "Delayed 15 min (NBA+NHL only). Upgrade for real-time across 8 sports.",
"units": {
"fair_wp": "cents (0-100 scale) ...",
"edge": "cents (0-100 scale) ...",
"fair_wp_decimal": "decimal probability, 0.0-1.0 ...",
"edge_decimal": "decimal probability, 0.0-1.0 ..."
},
"signals": [
{"sport": "NBA", "team": "NY", "edge": 11.2, "fair_wp": 78.5, "edge_decimal": 0.112, "fair_wp_decimal": 0.785, "confidence": "high"},
{"sport": "NHL", "team": "BOS", "edge": 9.1, "fair_wp": 71.3, "edge_decimal": 0.091, "fair_wp_decimal": 0.713, "confidence": "medium"}
]
}
Related sports API developer guides
Ready to get started?
Create a free account for delayed games and fair-vs-market price gaps. Paid plans unlock real-time endpoints.