API Documentation

Base URL: https://api.zenhodl.net

Quickstart

Get your first edge signal in 30 seconds.

1
Get your API key

Sign up at /pricing — 7-day free trial, no credit card required for free tier.

2
Fetch live edges
curl -H "X-API-Key: sk_live_YOUR_KEY" https://api.zenhodl.net/v1/edges
3
Use the edge

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.
Python quickstart
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

Fair Win Probability (fair_wp)

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.

Market Ask (market_ask)

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

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.

Settlement

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: 300
X-RateLimit-Remaining: 297
X-RateLimit-Tier: pro
Tier Requests/min WebSocket Sports Delay
Free10-NBA, NHL15 min
Starter ($49)601All 9Real-time
Pro ($149)3005All 9Real-time
Enterprise ($299)1,00020All 9Real-time

Errors

All errors return JSON with a detail field:

{"detail": "Rate limit exceeded (300 req/min for pro tier)."}
CodeMeaningExample
401Missing or invalid API key"Invalid or inactive API key."
402Payment required"Payment not completed."
403Tier too low / dashboard-only key"Your plan includes dashboard access only."
404Resource not found"No predictions for 20260101."
429Rate 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"
}
FieldDescription
statusok or starting
active_gamesNumber of live games being tracked right now
sports_loadedWhich sport models are loaded and active
last_espn_pollLast 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.

ParamTypeDescription
sportstringFilter 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_wpModel's fair probability for home team (0.0–1.0)
home_market_askCurrent Polymarket ask price (0.0–1.0). null if no market match
home_edgefair_wp - market_ask. Positive = underpriced. 0.07 = 7 cents edge
model_confidencehigh (edge >12c), medium (6–12c), low (<6c)
is_score_changetrue if a score change happened in the last 30 seconds (prices may lag)
statuslive, scheduled, final
seconds_remainingTotal 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.

ParamTypeDescription
sportstringFilter by sport. Optional.
min_edgefloatMinimum 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

teamTeam name the signal is for
sidehome or away
fair_wpModel's fair probability (0.0–1.0)
market_askPolymarket ask price (0.0–1.0)
edgefair_wp − market_ask. 0.11 = 11 cents expected value
confidencehigh (≥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,confidence
NCAAMB,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 above

Run a WP model backtest with custom strategy parameters against our 25M+ row dataset. Test different edge thresholds, periods, and fee assumptions.

FieldTypeDefaultDescription
sportstringNBASport to backtest (NBA, NCAAMB, CFB, NFL, etc.)
min_edge_cfloat8.0Minimum edge in cents to enter a trade
max_edge_cfloat50.0Maximum edge (filter out suspicious outliers)
min_fair_wp_cfloat65.0Only trade if model confidence ≥ this
max_entry_cfloat78.0Don't buy above this price
min_entry_cfloat35.0Don't buy below this price
min_periodint2Earliest game period to enter
max_per_gameint3Max entries per game (both sides combined)
seasonslistallFilter by season, e.g. ["2024-25"]
taker_fee_cfloat2.0Platform taker fee per trade (cents)
slippage_cfloat1.0Expected 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.

URL: wss://api.zenhodl.net/v1/ws/stream?api_key=sk_live_...
Push interval: 5 seconds
Auth: api_key query param
Connection limits: Per tier (1/5/20)
const 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.