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.

1
Get your API key

Create a free account at /signup (no credit card required), or choose a paid plan at /pricing.

2
Fetch fair vs market prices
curl -H "X-API-Key: sk_live_YOUR_KEY" https://zenhodl.net/v1/edges
3
Read the fair value

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.
Python SDK
PyPI ↗
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.

I want only the games where our fair value differs from a venue's price

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.

I want the games board with score, clock, and market state

Use /v1/games. This is the best endpoint for dashboards and game-level monitoring.

I want richer per-sport prediction objects

Use /v1/predict/{sport}/live or /v1/predict/{sport}/pregame. These are the cleanest prediction endpoints for model consumers. Starter+ tier.

I want push instead of polling

Use /v1/ws/stream for browser/app clients, or /v1/webhooks for server-to-server delivery.

I want saved personal automation

Use /v1/watchlists and /v1/preferences to save filters and alert settings tied to a user account.

Key Concepts

Fair Win Probability (fair_wp)

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.

Market Ask (market_ask)

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

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.

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 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: 300
X-RateLimit-Remaining: 297
X-RateLimit-Tier: pro
Tier Requests/min Monthly Cap WebSocket Sports Delay
Free 10500-All 85 min (games/edges); predict + fair lines Starter+
Starter ($49) 6030,0001All 8Real-time
Pro ($149) 300100,0005All 8Real-time
Enterprise (Custom) 1,000Unlimited20All 8Real-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)."}
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)."

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"
}
FieldDescription
statusok, starting, or degraded
active_gamesNumber of live games being tracked right now
sports_loadedWhich public API sport models loaded successfully; this does not describe live-money bot status
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, WNBA, NCAAMB, NCAAWB, CFB, NFL, NHL, MLB). Optional.
venuestringFilter 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_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)
venue_pricesPer-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_tsUTC 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_secondsAge 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_halfMLB 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.

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

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-cent probability gap (before fees/slippage)
edge_cAdded 2026-09-25: edge in cents (0–100), i.e. edge * 100 — same MODEL basis, matching the min_edge query parameter's unit
confidenceFrom 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_cModel fair probability in cents (0–100); same value as fair_wp
bot_fair_cOur 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_cbot_fair_c − the ask, in cents (before fees/slippage)
bot_lanelive or shadow (shadow sports place no live orders); null for venues the bot never trades
edge_basismodel_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_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://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 above

List 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.

URL: wss://zenhodl.net/v1/ws/stream
Push interval: 5 seconds
Auth: session cookie or WebSocket subprotocol
Connection limits: Per tier (1/5/20)
const 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_tsUTC timestamp (ISO 8601) of THAT venue's own last quote update — independent per venue.
venues.<Venue>.quote_age_secondsAge 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
Free500$0
Starter30,000$49/mo
Pro100,000$149/mo
EnterpriseUnlimitedCustom

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.

ParamTypeDescription
sportstringFilter 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.

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

ParamTypeDescription
sportpathSport code (NBA, NHL, etc.)
datepathDate 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.

ParamTypeDescription
startstringStart date (YYYYMMDD). Required.
endstringEnd date (YYYYMMDD). Required.
sportstringFilter 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.