Skip to content
Developer reference

API

15 public14 auth2 cron

Every endpoint the app exposes. public endpoints need no auth (Supabase RLS enforces read-only access). auth endpoints require a Supabase session cookie (signed in via /signin). cron endpoints require the CRON_SECRET bearer token — used by Vercel Cron.

31 / 31 endpoints

Public (15)

GET/api/version
public

Build + deploy metadata. Surfaces commit info from Vercel env vars, null locally. Useful for Sentry release tags, diffing staging vs prod, spotting stale caches.

Returns
{ name, version, commitSha, commitRef, commitMessage, deploymentId, environment, generatedAt }
GET/api/counts
public

Aggregate counters for public dashboards: DB row counts + share totals in one round trip. 60s CDN cache + SWR so growth shows up within a minute without hammering the DB.

Returns
{ generatedAt, db, shares: { pick, parlay, total } }
GET/api/status
public

Cheap health check for uptime monitors. Default returns { ok, generatedAt, demoMode, integrations } with no DB or external calls. HEAD supported for even lighter pings. Pass ?include=shares,db (comma-separated) to append optional sections.

Params
  • includequeryComma-separated extensions. Supported: 'shares' (share-counter totals), 'db' (analyses/matches/outcomes/ratings row counts).
Returns
{ ok: true, generatedAt, demoMode, integrations, shares?, db? }
GET/api/brief
public

Curated daily brief — top +EV edges, upcoming 24h, recent graded, streaks.

Params
  • sportqueryfootball | nba | tennis — optional filter
Returns
DailyBrief JSON
GET/api/feed.xml
public

RSS 2.0 feed of current +EV edges ≥ 4%. Subscribable in any reader.

Params
  • sportqueryOptional sport filter
Returns
application/rss+xml
GET/api/analyses/[sport]/[matchId]
public

Full cached analysis for a single match — probabilities, edges, props, news, line history, retrospective.

Params
  • sportpathfootball | nba | tennis
  • matchIdpathThe sport's external match identifier
Returns
{ analysis, fresh, lineHistory, retrospective }
GET/api/matches/[sport]
public

Upcoming matches for a sport — sourced from the sport's free data provider.

Params
  • sportpathfootball | nba | tennis
  • leaguequeryOptional league filter
  • limitqueryOptional result cap (default 40)
Returns
{ matches: MatchMeta[] }
POST/api/analyze
public

Generate or serve a cached analysis for a match. Rate-limited per match + per IP.

Params
  • sportIdbodyfootball | nba | tennis
  • matchIdbodyexternal match id
  • forcebodybool — regenerate (respects rate limit)
Returns
{ cached: boolean, analysis, demoMode }
POST/api/picks/grade
public

Grade a batch of saved picks against recorded outcomes. Runs every market through gradeProp().

Returns
{ picks: [{ id, result, pnl }] }
POST/api/picks/share
public

Generate a signed shareable link for a single pick. Uses PICK_SHARE_SECRET; 503 when unconfigured. Rate-limited 10/IP/10min.

Params
  • sportIdbodyfootball | nba | tennis
  • externalMatchIdbodyAdapter-level match id
  • matchLabelbodyHuman-readable matchup
  • startsAtbodyISO kickoff timestamp
  • marketLabelbodyMarket + selection label
  • decimalOddsbodyOdds taken
  • modelProbbody0..1 model probability
  • edgePctbodyEdge at save time
Returns
{ token, shareUrl }
GET/api/picks/shared/[token]
public

Decode a signed pick token into its SharedPickPayload JSON. 400 { reason } on tamper/format errors.

Params
  • tokenpathv1.<body>.<sig> format
Returns
{ payload: SharedPickPayload }
POST/api/parlays/share
public

Generate a signed shareable link for a 2–6 leg parlay. Computes joint prob + combined odds + edge and freezes them into the token. Same rate limiter as pick share.

Params
  • legsbodyArray of leg objects (sportId, externalMatchId, matchLabel, marketLabel, decimalOdds, modelProb)
Returns
{ token, shareUrl }
GET/api/parlays/shared/[token]
public

Decode a signed parlay token into its SharedParlayPayload JSON. 400 { reason } on tamper/format errors.

Params
  • tokenpathpv1.<body>.<sig> format
Returns
{ payload: SharedParlayPayload }

Auth required (14)

GET/api/user/picks
auth

Return all cloud-stored picks for the signed-in user. Anonymous callers get 401.

Returns
{ picks: StoredPick[] }
POST/api/user/picks
auth

Upsert a batch of picks (≤5000) for the signed-in user.

Params
  • picksbodyArray of StoredPick
Returns
{ upserted: number }
DELETE/api/user/picks
auth

Delete picks by id for the signed-in user.

Params
  • idsbodyArray of pick ids (≤1000)
Returns
{ deleted: number }
GET/api/user/picks/export
auth

Download all cloud-stored picks for the signed-in user as CSV (Content-Disposition: attachment). Same schema as the in-app CSV export.

Returns
text/csv
GET/api/user/watchlist
auth

Cloud-synced watchlist for the signed-in user.

Returns
{ refs: WatchRef[] }
POST/api/user/watchlist
auth

Upsert watchlist refs (≤500) for the signed-in user.

Returns
{ upserted: number }
DELETE/api/user/watchlist
auth

Delete watchlist refs by key for the signed-in user.

Returns
{ deleted: number }
GET/api/user/watchlist/export
auth

Download the signed-in user's cloud-stored watchlist as a JSON backup envelope (version 1). Triggers a file download.

Returns
{ version: 1, exportedAt, refs: WatchRef[] }
GET/api/user/prefs
auth

Read the signed-in user's notification + staking prefs.

Returns
{ prefs: NotificationPrefs | null, updatedAt }
PUT/api/user/prefs
auth

Replace the signed-in user's prefs.

Returns
{ ok: true }
GET/api/user/profile
auth

Read the signed-in user's profile (display name, digest opt-in, last digest sent).

Returns
{ profile: UserProfile }
PATCH/api/user/profile
auth

Patch profile fields. Currently supports email_digest_opt_in + display_name.

Returns
{ ok: true }
GET/api/user/export
auth

Download the signed-in user's full state (picks + watchlist + prefs + profile) as one JSON envelope. Single-call 'give me everything' export.

Returns
{ version: 1, exportedAt, user, picks, watchlist, prefs, profile }
DELETE/api/user/account
auth

Self-serve account deletion. Cascades to every user_* row via FK. Irreversible — client should redirect to / and wipe localStorage after.

Returns
{ ok: true }

Cron only (2)

GET/api/cron/ingest-outcomes
cron

Trigger outcome ingestion + facts write-back. Protected by CRON_SECRET.

Returns
{ ok: true, reports: [{ sport, scanned, graded, skipped, errors }] }
GET/api/cron/digest-weekly
cron

Send the weekly email digest to opted-in users via Resend. Guarded by CRON_SECRET and gated on RESEND_API_KEY + EMAIL_FROM.

Returns
{ sent, failed, total, weekLabel, results[] }
All endpoints are demo-friendly: if the relevant API key isn't set, they return sensible empty-state responses instead of erroring. See /about for the model methodology.