← Docs

NuroPicks API Reference

Last updated: 2026-04-19

Status: scaffold. Endpoint bodies verified from source 2026-04-19. Requests/responses partially inferred where handlers return NextResponse.json without a typed return. Anything that could not be confirmed from source is marked TBD: verify.

This document covers:

  • Next.js web API routes under web/src/app/api/
  • Discord bot slash commands under src/bot/commands/
  • Webhook endpoints (Whop, NOWPayments) and their signature scheme
  • Known rate limits (both web and bot)
  • Auth + session model

It does not cover the draft-room Socket.IO protocol (see services/draft-room/) or Python worker HTTP endpoints (internal, cron-only).

Auth and session

The web app currently runs in a hybrid model (Discord bot is still the primary user surface):

  • Session cookies: nuropicks_session (opaque id) + nuropicks_discord_id (fallback). Set by the Discord OAuth handshake. web/src/lib/userAuth.ts exposes resolveUserIdentity(), requireUserPage(), requireUserApi(req).
  • Header auth: Some routes (notably POST /api/fantasy/deposit) accept x-discord-id + x-nuropicks-auth for bot-to-web calls.
  • Admin auth: web/src/lib/adminAuth.ts accepts two signals, in priority:
    1. nuropicks_session cookie whose user has users.is_admin = TRUE (fallback: ADMIN_DISCORD_IDS env allowlist).
    2. x-admin-token header matching ADMIN_REVIEW_TOKEN (legacy, preserved for /admin/fraud and ops curl).
  • Webhook auth: HMAC signature per provider. See Webhooks.

Discord bot identifies users by Discord user id only (no separate session). Every command reads interaction.user.id and looks up the profile via src/bot/store.js.

The bot and web share the same Postgres users table. The users.is_admin column referenced in adminAuth.ts is not yet shipped at the schema level. TBD: verify column lands before admin-via-cookie is relied on in prod.

Rate limits

Web (web/src/lib/rateLimit.ts)

In-memory sliding-window bucket keyed by ${key}. checkRateLimit(key, limit, windowMs) returns { ok: true } or { ok: false, retryAfter }. Swap-ready for Redis.

Known per-route limits (sampled from source):

Route Key Limit Window
POST /api/apply apply:${ip} 3 10 min
POST /api/fantasy/deposit deposit:${discordId} 5 10 min
POST /api/fantasy/trade-advisor trade-advisor:${userId or ip} 5 1 min
POST /api/fantasy/waitlist waitlist:${ip} 1 60 s
POST /api/referrals/track see code - not documented here
Other rate-limited routes see code - not documented here

Bot (src/bot/utils/rateLimit.js)

Per-user + per-bucket sliding window. Buckets:

Bucket Limit Window Used by
bet 10 60 s /bet
predict 20 60 s /predict
casino 30 60 s /play, /crash, /war, /lottery
give 5 60 s /give
duel 10 60 s /duel
fantasy_write 8 60 s fantasy create/join/trade/waiver/dispute
fantasy_read 30 60 s fantasy read commands
rg_control 15 60 s /fantasy-rg, /self-exclude
default 60 60 s unclassified

Bot commands currently do not share a centralized rate-limit registry beyond this util; per-command cooldown choice lives in each command file.

External dependency limits

  • The Odds API: free tier 500 req/month (documented in .env.example). Bot workers cache aggressively; exact quota enforcement is worker-side and not yet instrumented. TBD: verify quota tracking.
  • Discord Gateway: discord.js v14 default rate-limit handling. No custom overrides.
  • NOWPayments: no documented per-merchant limit; IPN webhook is idempotent via idempotency_key on ledger_entries.

Web API routes

All routes live under web/src/app/api/ and use Next.js App Router route.ts / route.tsx handlers. Runtime is nodejs with dynamic = "force-dynamic" unless noted.

Public routes

GET /api/health

Liveness + build identity.

Response

{
  "ok": true,
  "service": "nuropicks-web",
  "commit": "abc1234",
  "env": "production",
  "region": "iad1",
  "ts": "2026-04-19T20:00:00.000Z"
}

Headers: Cache-Control: no-store.

POST /api/apply

Capper marketplace application. Public. Rate-limited 3/10min per IP.

Body

{
  "handle": "string",
  "email": "string",
  "specialty": "string",
  "trackRecordUrl": "string",
  "pickCount": "string",
  "startedAt": "string",
  "bankrollStrategy": "string",
  "notes": "string"
}

Required: handle, email, specialty. Email is regex-validated.

Response: { ok: true } or { ok: false, error: string } with 400/429.

Side effect: posts application summary to CAPPER_APPLY_WEBHOOK_URL (Discord relay).

POST /api/fantasy/waitlist

Landing page email capture. Public. Rate-limited 1/60s per IP.

Body

{ "email": "string", "state": "string (optional 2-letter)", "biggest_frustration": "string (optional, <=2000)" }

Duplicate email returns 200 with friendly message (no enumeration).

Response: { ok: true, message: string }. Fire-and-forget Resend confirmation email on new insert.

Other public routes

Shapes partially inferred; see route files for authoritative bodies.

Route Purpose
GET /api/fantasy/waitlist/stats Public waitlist counter for landing page
GET /api/fantasy/geo-check Is caller's geo allowed for paid fantasy entry
POST /api/fingerprint Browser fingerprint telemetry for collusion detection (returns server-side token)
GET /api/fingerprint Fingerprint handshake/health
GET /api/fantasy/mock-draft/leaderboard Public top-N mock draft scores (query: limit, default 25)

Account (session) routes

Require a valid nuropicks_session cookie. All return 401 if unauthenticated.

GET /api/account/profile

Returns the signed-in user's profile.

Response: { discordId, displayName, username, avatarUrl, email, homeState, timeZone, kycTier, tier, sessionId } (from UserIdentity in lib/userAuth.ts).

PUT /api/account/profile

Update displayName, homeState, timeZone, email. Non-admin users cannot change tier or kycTier.

Body: Partial<{ displayName, email, homeState, timeZone }>.

Response: { ok: true, user: UserIdentity }.

GET /api/account/close + POST /api/account/close

GET returns the pre-close preview (bankroll, open bets, escrow). POST executes the close (soft-delete + self-exclusion).

Response (POST): { ok: true, closedAt: string } (inferred).

GET /api/account/notifications + PUT /api/account/notifications

Notification preferences (email/push/Discord DM). PUT body is a NotificationPrefs struct from lib/notificationPrefs.ts. TBD: verify full shape.

Fantasy (session) routes

All require an identified caller (session cookie or x-discord-id header).

POST /api/fantasy/deposit

Create a league buy-in invoice. Rate-limited 5/10min per user.

Body

{ "leagueId": 123, "amountCents": 2500, "method": "crypto" }

Response

{ "ok": true, "invoiceUrl": "https://...", "escrowId": 456 }

Errors: 401 unauth, 402 RG-blocked (DepositBlockedByRGError), 409 withdrawals halted, 422 validation.

POST /api/fantasy/payout

Request league payout (after season settle / dispute close). TBD: verify exact body - likely { escrowId, walletAddress, currency }.

POST /api/fantasy/ipn

NOTE: This is a webhook endpoint, not user-callable. See Webhooks.

POST /api/fantasy/trade-advisor

Accepts a TradeProposal, returns a Claude-backed TradeAnalysis. Rate-limited 5/min per user-or-ip. Reads + writes to trade_analyses for idempotent replays.

Body: TradeProposal from src/services/ai/types.ts (team rosters + proposed swap).

Response: TradeAnalysis (fairness score + rationale + flags).

Other fantasy advisors (all POST unless noted)

Shapes partially inferred; see route files for authoritative bodies.

Route Purpose
POST /api/fantasy/dispute-advisor Claude-backed dispute triage for commissioners
POST /api/fantasy/lineup-optimizer Optimal lineup given roster + projections
POST /api/fantasy/start-sit Claude start/sit for a specific player pair
POST /api/fantasy/prop-alignment Mock-draft prop alignment score
GET /api/fantasy/waiver-wire Ranked waiver adds/drops for a team

Mock-draft + commissioner + draft-token

Commissioner routes require the caller to be the league commissioner; mock-draft and draft-token routes require a session. Body/response shapes inferred; see route files.

Route Purpose
POST /api/fantasy/mock-draft/start Start a new mock draft session
POST /api/fantasy/mock-draft/[draftId]/pick Submit a pick
GET /api/fantasy/mock-draft/[draftId]/summary Draft recap + grades
POST /api/fantasy/mock-draft/track Telemetry for abandon-rate analytics
GET /api/fantasy/commissioner/[leagueId] League ops summary
PUT /api/fantasy/commissioner/[leagueId] Update league settings
POST /api/fantasy/commissioner/[leagueId]/remind Fire lineup reminders
POST /api/fantasy/commissioner/[leagueId]/broadcast DM all league members
POST /api/fantasy/commissioner/disputes/[disputeId]/resolve Close a dispute
POST /api/fantasy/draft/[draftId]/token Short-lived JWT for draft-room Socket.IO handshake

KYC, notifications, referrals, creators

Shapes partially inferred; see route files for authoritative bodies.

Route Purpose Auth
POST /api/kyc/submit Tier 1/2 KYC submission, enqueues kyc_review session
POST /api/kyc/submit-w9 W9 for US payout recipients session
GET /api/notifications List notifications (query: unreadOnly, limit) session
PATCH /api/notifications Mark read / dismissed (body: { ids, action }) session
POST /api/notifications/subscribe-push Register Web Push subscription session
DELETE /api/notifications/subscribe-push Unregister Web Push subscription session
POST /api/referrals/track Record referral click (body: { code, source? }) public
GET /api/referrals/track Pixel/beacon version, returns 1x1 gif or 204 public
GET /api/referrals/stats Stats for a referral code (query: code) public
GET /api/referrals/card OG share card PNG via ImageResponse public
POST /api/creators/[creatorId]/subscribe Subscribe to a capper feed session
GET /api/creators/content/[contentId] Fetch capper content (paywall applied server-side) session

Admin routes

All admin routes require requireAdminApi(req) to pass. Non-admin returns 403 JSON.

Method + Path Purpose
GET /api/admin/overview Dashboard KPIs
GET /api/admin/revenue Revenue breakdown
GET /api/admin/alerts Open alert queue
POST /api/admin/alerts/[alertId] Acknowledge / dismiss alert
GET /api/admin/users Paginated user list
GET /api/admin/users/[discordId] User detail
POST /api/admin/users/[discordId]/actions Admin action on user (suspend / ban / promote / adjust-balance)
GET /api/admin/leagues Paginated league list
GET /api/admin/leagues/[leagueId] League detail
POST /api/admin/leagues/[leagueId]/actions Admin action on league
GET /api/admin/creators Capper marketplace admin
POST /api/admin/creators/[capperId]/actions Approve / suspend / promote capper
GET /api/admin/disputes Open disputes
POST /api/admin/disputes/[disputeId] Resolve dispute
POST /api/admin/fraud/[flagId] Act on collusion flag (suspend / ban / clear / escalate)
GET /api/admin/escrow Escrow ledger summary
GET /api/admin/escrow/payouts Pending payouts queue
POST /api/admin/escrow/payouts/approve Approve a payout batch
POST /api/admin/escrow/reconcile Trigger manual reconcile job
GET /api/admin/rg-audit Responsible-gambling alerts
GET /api/admin/waitlist Waitlist rows
POST /api/admin/waitlist/invite Send an invite email
GET /api/admin/export/csv CSV export (users / picks / escrow)

Admin actions generally accept { action, note? }. Response is typically { ok: true, updated: {...} } or { ok: false, error }. See individual route files for exact shape.

Webhooks

All webhook endpoints reject unsigned or invalid-signature requests.

POST /api/webhooks/whop

Whop subscription lifecycle. Signature header: X-Whop-Signature (HMAC-SHA256 of raw body with WHOP_WEBHOOK_SECRET). Events: membership.created, membership.expired, membership.canceled, membership.renewed.

Response: { ok: true } on success, 401 bad sig, 501 not configured.

Idempotency: currently logs + relays to Discord. Tier upsert into DB is TODO(launch).

Also supports GET /api/webhooks/whop for Whop dashboard health-check.

POST /api/webhooks/nowpayments

NOWPayments IPN for crypto subscription payments. Signature header: x-nowpayments-sig (HMAC-SHA512 of canonical-sorted JSON body with NOWPAYMENTS_IPN_SECRET). Statuses: waiting | confirming | confirmed | finished | failed | refunded | expired.

Response: { ok: true } (always 200 after sig verify to prevent retry storms), 401 bad sig, 501 not configured.

Idempotency: downstream credit/upgrade logic is a stub; full idempotency lands with real-money flow at launch.

Also supports GET /api/webhooks/nowpayments for dashboard health-check.

POST /api/fantasy/ipn

NOWPayments IPN for fantasy money flow (league buy-ins). Same signature scheme as above but always returns 200 after signature verification; internal failures surface via reconciliation job. confirmDeposit is idempotent via deterministic idempotency_key on ledger_entries, so duplicate IPNs are no-ops.

The live retail and fantasy IPN endpoints are separate because the domain models diverge (subscription tier vs league escrow); do not merge.

Discord bot slash commands

Commands live in src/bot/commands/. Fantasy subcommands live in src/bot/commands/fantasy/. All commands auto-register via readdirSync walk in src/bot/index.js.

Tiers: free (default), pro, elite, admin. Tier is read from getProfile(userId).tier.

Picks and analysis

Command Purpose Params Tier
/picks Today's AI picks (with why explainer per pick) sport? (nfl/nba/mlb/nhl) free
/odds Compare odds across sportsbooks sport?, game? free
/predict Full AI prediction with reasoning sport, game pro
/bet Place a tracked virtual pick sport, bet, odds, stake free
/mybets View bet history limit? free
/settle Settle a tracked bet bet_id, result admin
/parlay Build and analyze parlays legs free
/potd Pick of the Day with Quarter-Kelly stake none free
/compare Compare two picks side-by-side pick_a, pick_b free

Casino + tools

Casino commands (/play game=coinflip/dice/slots/blackjack, /crash, /war, /lottery) each award +5 XP per action; all gated by the casino rate-limit bucket (30/min).

Command Purpose Params Tier
/bankroll Unit size / Kelly / EV calculators subcommands: unit, kelly, ev free
/convert Odds format converter value, from, to free
/glossary 207 betting terms lookup term? free
/schedule Today's game schedule sport? free
/remind Set a lineup / game reminder when, what free
/accounthealth Book-limit risk assessment none pro

Social

Command Purpose Params Tier
/give Send credits to a member user, amount (1-100000) free
/refer Show referral link / claim code code? free
/top All-time rankings (5 categories) category? free
/leaderboard Composite ROI x log(vol) x CLV ranking limit? free
/serverstats Server dashboard none free
/challenge Head-to-head pick competition user, sport, game free
/duel 1v1 prediction duel (rate-limited 10/min) user, stake free
/poll Community prediction polls question, options free
/tip Tip a capper user, amount free

Account

Command Purpose Params Tier
/daily Claim daily credits + streak bonus (+25 XP) none free
/balance Check credits, XP, level none free
/profile Full stats, badges, tier, Level N/100 none free
/streak Current win streak none free
/report Weekly betting report card none free
/self-exclude Responsible-gambling lockout 7/30/90/365 days duration free
/selfexclude Alias of /self-exclude duration free
/inventory Shop items owned none free
/shop Virtual shop (cosmetics, boosts) item? free
/badges List badges earned none free
/reset Reset profile stats none free
/upgrade Upgrade tier (opens Whop link) none free
/funnel Upgrade funnel analytics entry none free

Education + onboarding + draft

Command Purpose Params Tier
/quiz Interactive quizzes (13 modules, 65 Qs) module? free
/gettingstarted Bot tour for new users none free
/about What is NuroPicks none free
/help Full command list none free
/setup Post rules, roles, welcome content none admin
/feedback Submit feedback message free
/draft NFL draft tracker / pick grading pick?, team? free

Capper follows

Writes through to the capper_follows table (db/schema.sql). Follow set is per Discord user, unique on (discord_id, capper_id). The marketplace + 50/50 rev-share framework lives in docs/CAPPER_AGREEMENT.md.

Command Purpose Params Tier
/follow-capper Subscribe to a verified capper (validates against cappers table, idempotent on re-follow) capper (handle) free
/unfollow-capper Drop a capper from your follow list capper (handle) free
/my-cappers Show your current follows with record + CLV + ROI one-liner per capper none free

Example:

/follow-capper capper:glizzysharp
/my-cappers
/unfollow-capper capper:glizzysharp

Fantasy (subcommand group under /fantasy-*)

These are separate top-level commands prefixed fantasy- because Discord limits subcommands per root.

Command Purpose Tier
/fantasy-create-league Create a paid fantasy league free
/fantasy-join Join a league by code free
/fantasy-leagues List your leagues free
/fantasy-channel-setup Bind league to a Discord channel free
/fantasy-schedule-draft Schedule draft start commissioner
/fantasy-draft Join live draft room free
/fantasy-rookie-draft Rookie-draft side game free
/fantasy-nba-create-league NBA variant free
/fantasy-roster View your roster free
/fantasy-standings League standings free
/fantasy-lineup-check Validate current lineup free
/fantasy-start-sit Claude start/sit advisor free
/fantasy-trade Propose trade (uses trade-advisor) free
/fantasy-waiver Submit waiver claim free
/fantasy-waiver-wire Ranked waiver recommendations free
/fantasy-dispute Open dispute free
/fantasy-payouts Payout status free
/fantasy-refer Fantasy-specific referral free
/fantasy-rg Responsible-gambling controls (looser rate limit, see rg_control bucket) free
/fantasy-creator-publish Capper publishes to subscribers capper
/fantasy-creator-subscribe Subscribe to a capper free

Commissioner-gated commands (fantasy-schedule-draft, dispute-resolve, etc.) check league.commissioner_discord_id === interaction.user.id at command entry.

Example usage

/picks sport:nfl
/bet sport:nba bet:Warriors -4.5 odds:-110 stake:100
/bankroll kelly bankroll:1000 odds:-110 winprob:58
/give user:@mar amount:500
/fantasy-trade league:2026-best-ball proposed-to:@glizzy give:ceedee-lamb get:amon-ra

Unknowns / TBD

  • POST /api/fantasy/payout exact body + response not verified from source in this scaffold pass.
  • Notification preference struct fields not enumerated here - see lib/notificationPrefs.ts for authoritative shape.
  • users.is_admin column does not yet exist in schema; admin-via-cookie path falls back to ADMIN_DISCORD_IDS env allowlist until the column lands.
  • Odds API per-worker quota is not instrumented centrally; see workers for per-call caching logic.
  • The draft-room Socket.IO protocol is not documented here; see services/draft-room/README.md and src/services/draft/ for the event-sourced actor-per-draft model.
  • Command tier enforcement is inconsistent: most commands check interaction.user.id or getProfile(userId).tier inline rather than via middleware. A centralized tier-gate helper is flagged as a P2 cleanup task elsewhere.
  • /settle is gated by a staff-role check inside the command, not a global admin middleware. TBD: migrate to shared requireAdmin helper once it exists on the bot side.

See also

21+ only. Not financial advice. 1-800-GAMBLER.

api - NuroPicks Docs