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.
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.
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
sportIdbody— football | nba | tennis
externalMatchIdbody— Adapter-level match id
matchLabelbody— Human-readable matchup
startsAtbody— ISO kickoff timestamp
marketLabelbody— Market + selection label
decimalOddsbody— Odds taken
modelProbbody— 0..1 model probability
edgePctbody— Edge 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
tokenpath— v1.<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
legsbody— Array 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
tokenpath— pv1.<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
picksbody— Array of StoredPick
Returns
{ upserted: number }
DELETE/api/user/picks
auth
Delete picks by id for the signed-in user.
Params
idsbody— Array 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.
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.