675 lines
41 KiB
Markdown
675 lines
41 KiB
Markdown
# VYNDR — System Manifest
|
||
|
||
Source of truth for every interface in the codebase as of Session 7c
|
||
(958 tests across 73 suites, zero regressions). When this drifts from
|
||
reality, update this file FIRST.
|
||
|
||
---
|
||
|
||
## 1. Architecture overview
|
||
|
||
```
|
||
┌──────────────────┐
|
||
│ Next.js (3000) │ ─── public web app + app/api/* proxies
|
||
└────────┬─────────┘
|
||
│ BACKEND_URL
|
||
▼
|
||
n8n cron ── POST ─► ┌──────────────────┐ ─► Supabase (RLS-enforced reads,
|
||
PM2 pollers ──────► │ Express (3001) │ service-role writes from API)
|
||
Coolify healthcheck │ │ ─► Redis (cache + locks)
|
||
└────────┬─────────┘
|
||
│
|
||
▼
|
||
┌───────────────────────────────────┐
|
||
│ External adapters (configured()- │
|
||
│ gated, rate-limited, breakered) │
|
||
│ SharpAPI · OddsPapi · PropOdds · │
|
||
│ ParlayAPI · CFBD · OpenRouter · │
|
||
│ ESPN · MLB Stats API · Pinnacle │
|
||
└───────────────────────────────────┘
|
||
```
|
||
|
||
**Unified grading path (closed by Session 7g):**
|
||
|
||
- `routes/scan/parlay` and `routes/analyze/{prop,batch}` →
|
||
`analyzeViaEngine1` (`computeFeatures` → `engine1` → `gradeAdapter`).
|
||
- `routes/grading/pipeline` (scheduled) → `gradingOrchestrator` →
|
||
`featureCache` + `trapDetection` + `consistencyScore` +
|
||
`probabilityEstimator` → `engine1` → (A/B-tier only) `engine2`.
|
||
|
||
The book-adapter chain (`UnifiedOddsProvider` + 9 legacy adapters +
|
||
`services/{rateLimiter,circuitBreaker}.js`) remains live but only for
|
||
the DATA REFRESH path at `/api/pipeline/refresh` — a separate concern
|
||
from grading. ARCH-1 retired the legacy GRADING path (`propAnalyzer`,
|
||
`grader.js`) in Session 7g.
|
||
|
||
---
|
||
|
||
## 2. Express API endpoints
|
||
|
||
Mounted in `src/app.js`. Auth column meanings:
|
||
|
||
- **public** — no auth middleware
|
||
- **user** — `requireAuth` (Supabase JWT bearer)
|
||
- **internal** — `requireInternal` (X-VYNDR-Internal-Key + loopback IP)
|
||
- **pipeline** — `requirePipelineSecret` (X-Pipeline-Secret)
|
||
- **stripe-sig** — `stripe.webhooks.constructEvent` signature
|
||
|
||
| Method | Path | Auth | Body limit | Handler |
|
||
| ------ | ------------------------------------- | ----------- | ---------- | ------------------------------------ |
|
||
| GET | /api/health | public | n/a | `app.js` (inline) |
|
||
| GET | /api/odds/nba | public | 10mb | `routes/odds.js` |
|
||
| GET | /api/odds/ncaab | public | 10mb | `routes/odds.js` |
|
||
| GET | /api/odds/soccer/:league | public | 10mb | `routes/odds.js` (Session 7j) |
|
||
| POST | /api/analyze/prop | public + 10/min IP | 10mb | `routes/analyze.js` (cached 60s) |
|
||
| POST | /api/analyze/batch | public + 10/min IP | 10mb | `routes/analyze.js` (cached 60s) |
|
||
| POST | /api/scan/parlay | user | 10mb | `routes/scan.js` |
|
||
| GET | /api/movements | public | 10mb | `routes/movements.js` |
|
||
| GET | /api/alerts | user | 10mb | `routes/alerts.js` |
|
||
| PATCH | /api/alerts/:id/read | user | 10mb | `routes/alerts.js` |
|
||
| POST | /api/bets/quickslip | user | 10mb | `routes/bets.js` |
|
||
| POST | /api/bets/screenshot | user | 10mb | `routes/bets.js` |
|
||
| POST | /api/bets/screenshot/confirm | user | 10mb | `routes/bets.js` |
|
||
| POST | /api/bets/sync | user | 10mb | `routes/bets.js` |
|
||
| PATCH | /api/bets/:id/settle | user | 10mb | `routes/bets.js` |
|
||
| GET | /api/bets | user | 10mb | `routes/bets.js` |
|
||
| GET | /api/bets/performance | user | 10mb | `routes/bets.js` |
|
||
| POST | /api/stripe/checkout | user | 10mb | `routes/stripe.js` |
|
||
| POST | /api/stripe/webhook | stripe-sig | raw bytes | `routes/stripe.js` |
|
||
| POST | /api/stripe/portal | user | 10mb | `routes/stripe.js` |
|
||
| GET | /api/stripe/status | user | 10mb | `routes/stripe.js` |
|
||
| GET | /api/stats/parlays-graded | public | 10mb | `routes/stats.js` |
|
||
| GET | /api/stats/public | public | 10mb | `routes/stats.js` |
|
||
| GET | /api/stats/live | public | 10mb | `routes/stats.js` |
|
||
| GET | /api/props/joint-history | user | 10mb | `routes/props.js` |
|
||
| POST | /api/waitlist | public | 10mb | `routes/waitlist.js` |
|
||
| POST | /api/pipeline/refresh | pipeline | 10mb | `routes/pipeline.js` |
|
||
| GET | /api/pipeline/status | public | 10mb | `routes/pipeline.js` |
|
||
| POST | /api/share-card | public | 10mb | `routes/shareCard.js` |
|
||
| POST | /api/push/subscribe | user | 10mb | `routes/push.js` |
|
||
| DELETE | /api/push/unsubscribe | user | 10mb | `routes/push.js` |
|
||
| POST | /api/grading/resolve | internal | **10mb** | `routes/grading.js` |
|
||
| POST | /api/grading/pipeline | internal | 10mb | `routes/grading.js` (Session 10: off-host n8n callers; empty body iterates active sports) |
|
||
| POST | /api/grading/correct | internal | 256kb | `routes/corrections.js` |
|
||
| GET | /api/widget | public | 10mb | `routes/widget.js` |
|
||
| OPTIONS| /api/widget | public | n/a | `routes/widget.js` |
|
||
|
||
### Next.js API routes (frontend `app/api/*`)
|
||
|
||
These are proxies or thin wrappers; they hit Express via `BACKEND_URL`
|
||
or the Python service via `NEXT_PUBLIC_NBA_SERVICE_URL`.
|
||
|
||
- `/api/checkout` (POST/GET) — Stripe checkout proxy (Session 8 cutover — was NexaPay)
|
||
- `/api/games/[id]` and `/api/games/tonight` — list / detail
|
||
- `/api/games/[id]/props` — props for a game
|
||
- `/api/intelligence/feed` — homepage live signals
|
||
- `/api/ledger`, `/api/ledger/accuracy` — Ledger feed
|
||
- `/api/odds/soccer/[league]` — soccer odds proxy → Express `/api/odds/soccer/:league` (Session 8)
|
||
- `/api/parlay/add-leg`, `/api/parlay/grade` — proxy to `/api/scan/parlay`
|
||
- `/api/players/search` — proxy to Python `/players/search`
|
||
- `/api/props/live`, `/api/props/most-parlayed`, `/api/props/top-graded`
|
||
- `/api/scan` — bare scan endpoint (Session 8 — accepts `Soccer` sport in addition to NBA/MLB/WNBA)
|
||
- `/api/stats/parlays-graded`, `/api/stats/public` — proxy
|
||
- `/api/user/profile`, `/api/user/scans`, `/api/user/recent-scans`
|
||
- `/api/waitlist` — proxy
|
||
- `/api/webhook/nexapay` — NexaPay webhook (legacy — Stripe cutover Session 8; webhook still listening for any in-flight NexaPay events)
|
||
|
||
### Next.js pages (Session 8 additions)
|
||
|
||
- `/soccer` — live soccer odds feed + inline prop grading. Hosts `SportSelector` + per-league match cards, scans selected props through `/api/scan` → Express `/api/analyze/prop` with `sport: 'Soccer'`. Results render in `SoccerGradeResult` (parses the engine's reasoning summary into visual signal chips: ⚽ goals/90, 📊 xG, 🏔️ altitude, 🟨 referee, 🎯 penalty taker, 🏆 WC pedigree). Free tier gets a blurred preview + upgrade CTA.
|
||
- `/upgrade/success` — Stripe checkout success landing. Reads `session_id` query param, refreshes the AuthContext so the new tier flips immediately. Stripe webhook is the source of truth; this page does not verify the session against Stripe (no secret key on the client).
|
||
- `/upgrade/cancel` — Stripe checkout cancel landing. No judgment, links back to `/#pricing` and `/scan`.
|
||
|
||
---
|
||
|
||
## 3. Environment variables
|
||
|
||
Source: `grep -rn 'process\.env\.' src/ poller/ scripts/ web/src/`.
|
||
✓ = documented in `.env.example`. ⚠ = used but optional (default falls
|
||
back). Updated this session in Section 1 of Session 7c.
|
||
|
||
### Core
|
||
| Var | Required | Default | Used By | Doc? |
|
||
| ------------------------- | -------- | -------------- | ------------------------------------ | ---- |
|
||
| `NODE_ENV` | no | (none) | various | ✓ |
|
||
| `PORT` | no | `3001` | `src/server.js` | implicit |
|
||
| `BASE_URL` | yes | `http://localhost:3001` | `stripeService`, `widget` | ✓ |
|
||
| `FRONTEND_ORIGINS` | yes | (none) | `app.js` (CORS) | ✓ |
|
||
| `VYNDR_INTERNAL_KEY` | yes | (none) | `middleware/internalAuth` (S10), `routes/grading`, `routes/corrections` | ✓ |
|
||
| `SENTRY_DSN` | no | (none) | `utils/sentry` (S10) — graceful skip when unset | ✓ S10 |
|
||
| `NEXT_PUBLIC_SENTRY_DSN` | no | (none) | `web/components/SentryInit` (S10) — same DSN, browser-side | ✓ S10 |
|
||
|
||
### Supabase
|
||
| Var | Required | Used By | Doc? |
|
||
| -------------------------------- | -------- | -------------------------------- | ---- |
|
||
| `SUPABASE_URL` | yes | both clients | ✓ |
|
||
| `SUPABASE_ANON_KEY` | yes | `getSupabaseClient` | ✓ |
|
||
| `SUPABASE_SERVICE_ROLE_KEY` | yes | `getSupabaseServiceClient` | ✓ |
|
||
| `SUPABASE_SERVICE_KEY` (legacy) | no | fallback only (7b) | ✓ commented |
|
||
| `SUPABASE_DB_URL` | yes | `scripts/backup.sh`, `migrations`| ✓ |
|
||
| `SUPABASE_DB_PASSWORD` | no | legacy backup form | ✓ commented |
|
||
| `NEXT_PUBLIC_SUPABASE_URL` | yes | Next.js client | ✓ |
|
||
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | yes | Next.js client | ✓ |
|
||
| `NEXT_PUBLIC_SITE_URL` | yes | Next.js metadata | ✓ |
|
||
|
||
### Web ↔ Backend
|
||
| Var | Required | Default | Used By | Doc? |
|
||
| ---------------------------- | -------- | ------------------------ | -------------------------------- | ---- |
|
||
| `NEXT_PUBLIC_API_URL` | yes | `http://localhost:3001` | `web/src/lib/api.ts` | ✓ (added 7c) |
|
||
| `NEXT_PUBLIC_NBA_SERVICE_URL`| yes | `http://localhost:8000` | `web/api/players/search` | ✓ (added 7c) |
|
||
| `BACKEND_URL` | yes | `http://localhost:3001` | `web/services/odds-cache.ts` | ✓ (added 7c) |
|
||
|
||
### Stripe + Payments
|
||
| Var | Doc? |
|
||
| ---------------------------- | ---- |
|
||
| `STRIPE_SECRET_KEY` | ✓ |
|
||
| `STRIPE_WEBHOOK_SECRET` | ✓ |
|
||
| `STRIPE_PRICE_ANALYST` | ✓ |
|
||
| `STRIPE_PRICE_ANALYST_FOUNDER` | ✓ |
|
||
| `STRIPE_PRICE_DESK` | ✓ |
|
||
| `STRIPE_PRICE_DESK_FOUNDER` | ✓ |
|
||
| `FOUNDER_CODES` | ✓ |
|
||
| `FOUNDER_CODE_EXPIRY` | ✓ |
|
||
| `NEXAPAY_API_URL` | ✓ (added 7c) |
|
||
| `NEXAPAY_API_KEY` | ✓ (added 7c) |
|
||
| `NEXAPAY_WEBHOOK_SECRET` | ✓ (added 7c) |
|
||
|
||
### Push
|
||
| Var | Doc? |
|
||
| ---------------------------- | ---- |
|
||
| `VAPID_PUBLIC_KEY` | ✓ |
|
||
| `VAPID_PRIVATE_KEY` | ✓ |
|
||
| `VAPID_SUBJECT` | ✓ |
|
||
| `NEXT_PUBLIC_VAPID_PUBLIC_KEY`| ✓ |
|
||
|
||
### Odds adapters
|
||
| Var | Doc? |
|
||
| ------------------------- | ---- |
|
||
| `SHARPAPI_KEY` | ✓ |
|
||
| `SHARPAPI_BASE_URL` | ✓ |
|
||
| `ODDSPAPI_KEY` | ✓ |
|
||
| `ODDSPAPI_BASE_URL` | ✓ |
|
||
| `PARLAYAPI_KEY` | ✓ |
|
||
| `PARLAYAPI_BASE_URL` | ✓ |
|
||
| `PARLAYAPI_PULL_RATE_MS` | ✓ |
|
||
| `PROPODDS_KEY` | ✓ |
|
||
| `PROPODDS_BASE_URL` | ✓ |
|
||
| `CFBD_KEY` | ✓ |
|
||
| `CFBD_BASE_URL` | ✓ |
|
||
| `PINNACLE_API_KEY` | ✓ commented (legacy) |
|
||
| `PINNACLE_API_BASE` | ✓ commented (legacy) |
|
||
| `ODDS_API_KEY` | ✓ commented (legacy) |
|
||
|
||
### Soccer / World Cup 2026 (Session 7j + 9)
|
||
| Var | Required | Default | Used By | Doc? |
|
||
| ------------------------- | -------- | ------------------------------------------------ | ------------------------------------------------------- | ---- |
|
||
| `FOOTBALL_DATA_API_KEY` | no | (none) | `footballDataAdapter` (TERTIARY) | ✓ |
|
||
| `API_FOOTBALL_KEY` | no | (none) | `apiFootballAdapter` (PRIMARY, Session 9) | ✓ S9 |
|
||
| `SOCCER_LEAGUES` | no | `WC` | `poller/soccer.js`, `soccer-data-prefetch` | ✓ |
|
||
| `WORLDCUP_API_URL` | no | `https://worldcup2026-api.up.railway.app/api/...` | `poller/soccer.js` | ✓ |
|
||
| `RAPID_API_KEY` | no | (none) | `footApiAdapter` (BACKUP), `tank01NbaAdapter`, `tank01MlbAdapter` | ✓ S9 |
|
||
| `FOOTAPI_HOST` | no | `footapi7.p.rapidapi.com` | `footApiAdapter` | ✓ S9 |
|
||
| `TANK01_NBA_HOST` | no | `tank01-fantasy-stats.p.rapidapi.com` | `tank01NbaAdapter` | ✓ S9 |
|
||
| `TANK01_MLB_HOST` | no | `tank01-mlb-live-in-game-real-time-statistics.p.rapidapi.com` | `tank01MlbAdapter` | ✓ S9 |
|
||
|
||
Container runtime (Session 9 finding):
|
||
- `NODE_OPTIONS=--max-old-space-size=4096` — set on the web container
|
||
in Coolify. Without it, Next.js's V8 default ceiling (~2 GB) is hit
|
||
in production and the container OOM-loops (44 restarts observed on
|
||
the live host before the fix was identified).
|
||
|
||
### Engine 2
|
||
| Var | Doc? |
|
||
| ---------------------------- | ---- |
|
||
| `OPENROUTER_API_KEY` | ✓ |
|
||
| `OPENROUTER_PRIMARY_MODEL` | ✓ |
|
||
| `OPENROUTER_FALLBACK_MODEL` | ✓ |
|
||
| `OPENROUTER_BASE_URL` | ✓ |
|
||
| `OPENROUTER_REFERER` | ✓ |
|
||
| `OPENROUTER_TITLE` | ✓ |
|
||
| `ENGINE2_BATCH_SIZE` | ✓ |
|
||
| `ENGINE2_ENABLED` | ✓ |
|
||
|
||
### Redis + Python
|
||
| Var | Doc? |
|
||
| ------------------------- | ---- |
|
||
| `REDIS_URL` | ✓ |
|
||
| `PYTHON_SERVICE_URL` | ✓ |
|
||
| `NBA_SERVICE_URL` | ✓ commented (legacy alias) |
|
||
| `NBA_STATS_URL` | ✓ commented (legacy alias) |
|
||
| `EVOLUTION_SERVICE_URL` | ✓ commented (legacy) |
|
||
| `VYNDR_PYTHON` | ✓ commented (legacy alias) |
|
||
|
||
### Distribution + Analytics
|
||
| Var | Doc? |
|
||
| ------------------------- | ---- |
|
||
| `TELEGRAM_BOT_TOKEN` | ✓ |
|
||
| `TELEGRAM_CHANNEL_ID` | ✓ |
|
||
| `DISCORD_WEBHOOK_DAILY` | ✓ |
|
||
| `DISCORD_WEBHOOK_RESULTS` | ✓ |
|
||
| `DISCORD_WEBHOOK_ALERTS` | ✓ |
|
||
| `DISCORD_WEBHOOK_RARE` | ✓ |
|
||
| `RESEND_API_KEY` | ✓ (added 7c) |
|
||
| `RESEND_FROM_EMAIL` | ✓ (added 7c) |
|
||
| `NEXT_PUBLIC_POSTHOG_KEY` | ✓ (added 7c) |
|
||
| `NEXT_PUBLIC_POSTHOG_HOST`| ✓ (added 7c) |
|
||
|
||
### Poller-set (per-process via PM2)
|
||
| Var | Default | Doc? |
|
||
| ----------------- | ------- | ---- |
|
||
| `SPORT` | (none) | ✓ commented |
|
||
| `POLL_INTERVAL` | `60000` | ✓ commented |
|
||
| `BUFFER_MS` | `30000` | ✓ commented |
|
||
| `VYNDR_API_URL` | `http://localhost:3001` | ✓ commented |
|
||
| `OFF_HOURS_POLL_MS` | hardcoded 5min | not env |
|
||
|
||
PM2 ecosystem (Session 7j) — four poller processes per container:
|
||
- `poller-nba`, `poller-wnba`, `poller-mlb` (box-score resolution path via `poller/poller.js`)
|
||
- `poller-soccer` (fixture indexing via `poller/soccer.js` — different
|
||
data sources and cache shape; honors `SOCCER_LEAGUES` env)
|
||
|
||
### Backup + Ops
|
||
| Var | Doc? |
|
||
| ------------------------- | ---- |
|
||
| `NTFY_PORT` | ✓ |
|
||
| `NTFY_TOPIC` | ✓ |
|
||
| `BACKUP_RETENTION_DAYS` | ✓ |
|
||
| `PIPELINE_SECRET` | ✓ commented |
|
||
| `PUBLIC_SHARE_CARD_BASE` | ✓ commented |
|
||
| `API_BASE_URL` | ✓ commented (alias of BASE_URL) |
|
||
| `ENGINE` | ✓ commented (legacy) |
|
||
|
||
---
|
||
|
||
## 4. Supabase tables
|
||
|
||
Source: `grep -rn "\.from('[a-z_]*')" src/ poller/`.
|
||
|
||
### Tables created by VYNDR migrations
|
||
| Table | Migration | Read by | Written by |
|
||
| ---------------------- | --------- | ------------------------------------------------------ | --------------------------------------- |
|
||
| `user_profiles` | 011, 015 | AuthContext, stripeService mirror, accuracyTracker | handle_new_user, stripeService |
|
||
| `parlay_leg_frequency` | 011 | parlay analytics | scan flow |
|
||
| `scan_history` | 011 | user scans listing | scanRoutes |
|
||
| `odds_cache` | 014 | oddsService | oddsService, n8n pipeline |
|
||
| `line_history` | 014 | lineMovementService | lineMovementService |
|
||
| `cascade_alerts` | 014 | cascadeService, intelligence feed | cascadeService |
|
||
| `player_stats_cache` | 014 | propAnalyzer | propAnalyzer |
|
||
| `grade_history` | 014/16/18 | Ledger, resolution route, weight adjuster, accuracy | orchestrator, resolution, engine2, corrections |
|
||
| `prop_correlations` | 014 | correlationEngine | correlationEngine |
|
||
| `player_id_map` | 014→016 | populate-player-ids, oddsPapiAdapter | populate-player-ids |
|
||
| `push_subscriptions` | 015 | webPush | push routes, webPush prune |
|
||
| `closing_lines` | 016 | clvTracker, oddsPapiAdapter | oddsPapiAdapter.batchCapture |
|
||
| `resolution_results` | 016 | clvTracker, weightAdjuster, corrections | resolution route |
|
||
| `historical_props` | 017 | trap historical signal (future) | pull-parlayapi-history |
|
||
| `line_snapshots` | 017 | lineMovement intel | sharpApiAdapter (fire-and-forget) |
|
||
| `ref_profiles` | 017 | refSignals | scrape-sports-reference |
|
||
| `coach_profiles` | 017 | coachSignals | scrape-sports-reference, seed file |
|
||
| `game_ref_assignments` | 017 | refSignals | ops manual entry |
|
||
| `engine1_weights` | 018 | weightAdjuster | weightAdjuster |
|
||
| `accuracy_tracking` | 018 | accuracyTracker, Ledger accuracy page | accuracyTracker |
|
||
| `user_notifications` | 013 | notifications route | notification senders |
|
||
|
||
### Tables present in DB but NOT in VYNDR migrations (BetonBLK era)
|
||
`bets`, `daily_scan`, `discrepancy_reliability_scores`, `joint_outcomes`,
|
||
`lineup_role_profiles`, `model_predictions_extended`, `outcomes`,
|
||
`performance`, `picks`, `player_role_activations`,
|
||
`player_role_profiles`, `prediction_registry`, `scan_sessions`.
|
||
|
||
VYNDR code reads/writes only the subset still in use:
|
||
- `bets` — `routes/bets.js`, `betService.js`
|
||
- `joint_outcomes` — `routes/props.js` (joint-history)
|
||
- `model_predictions_extended` — `propAnalyzer`
|
||
- `performance` — `performanceService`
|
||
- `picks` — `parlayScanService`
|
||
- `scan_sessions` — `routes/scan.js`
|
||
|
||
### Shared tables (BetonBLK + VYNDR)
|
||
- `users` — both eras
|
||
- `waitlist` / `waitlist_signups` — created in 012 (latter is canonical)
|
||
|
||
---
|
||
|
||
## 5. Redis keys
|
||
|
||
Source: `grep -rn "cacheSet\|cacheGet\|redis\.set"`.
|
||
|
||
| Key pattern | TTL | Set by | Read by |
|
||
| ---------------------------------------- | ----------- | --------------------------------- | ----------------------------- |
|
||
| `odds:{sport}:{gameId}:player_props` | 60s (300s stale) | sharpApiAdapter | sharpApiAdapter, orchestrator |
|
||
| `odds:{sport}:{gameId}:game` | 60s | sharpApiAdapter | sharpApiAdapter |
|
||
| `parlayapi:hist:{sport}:{p}:{stat}:{n}` | 24h | parlayApiAdapter | trapDetection (future) |
|
||
| `parlayapi:close:{sport}:{date}` | 24h | parlayApiAdapter | pull script |
|
||
| `parlayapi:checkpoint:{sport}:{season}` | 30d | pull-parlayapi-history | pull-parlayapi-history |
|
||
| `propodds:{sport}:{game}:{player}:{stat}`| 90s (300s stale) | propOddsAdapter | propOddsAdapter |
|
||
| `cfbd:teamstats:{team}:{year}` | 6h | cfbdAdapter | cfbdAdapter |
|
||
| `cfbd:usage:{player}:{team}:{year}` | 6h | cfbdAdapter | cfbdAdapter |
|
||
| `cfbd:talent:{team}:{year}` | 6h | cfbdAdapter | cfbdAdapter |
|
||
| `cfbd:lines:{team}:{year}` | 6h | cfbdAdapter | cfbdAdapter |
|
||
| `gamelogs:{sport}:{player}:{n}` | 4h | gameLogService | featureCache, consistency |
|
||
| `team_stats:{sport}:{teamAbbr}` | 24h | teamStatsCache | featureCache, engine1 |
|
||
| `features:{sport}:{playerId}:{stat}:{game}` | 2m | featureCache | orchestrator |
|
||
| `injuries:{sport}:{teamId}` | 2h | injuryParser | featureCache |
|
||
| `game:{gameId}:status` | 36h | poller | poller |
|
||
| `game:{gameId}:resolution_lock` | 5min (NX) | poller | poller (dedupe) |
|
||
| `game:{gameId}:live_status` | 1h | poller | frontend badges (future) |
|
||
| `poller:{sport}:heartbeat` | 3min | poller | ops monitoring |
|
||
| `vyndr:line_baseline:{...}` | 24h | lineMovementService | lineMovementService |
|
||
| (cascadeService player-set key) | 24h | cascadeService | cascadeService |
|
||
| (oddsService legacy game cache) | 5min | oddsService | routes/odds |
|
||
| (schemeClassifier cache) | varies | schemeClassifier | schemeClassifier |
|
||
|
||
---
|
||
|
||
## 6. External API dependencies
|
||
|
||
| Service | Adapter | Env vars | Rate budget | Used by |
|
||
| -------------- | ------------------------------------ | ------------------------------------- | ----------------- | ------------------------ |
|
||
| SharpAPI | `sharpApiAdapter.js` | `SHARPAPI_KEY`, `SHARPAPI_BASE_URL` | 10/min | orchestrator |
|
||
| OddsPapi | `oddsPapiAdapter.js` | `ODDSPAPI_KEY`, `ODDSPAPI_BASE_URL` | 5/min | poller (tip-off CLV) |
|
||
| ParlayAPI | `parlayApiAdapter.js` | `PARLAYAPI_KEY` | 5/min | pull script |
|
||
| PropOdds | `propOddsAdapter.js` | `PROPODDS_KEY` | 3/min | trap consensus |
|
||
| CFBD | `cfbdAdapter.js` | `CFBD_KEY` | 10/min | college features |
|
||
| OpenRouter | `openRouterAdapter.js` | `OPENROUTER_API_KEY` + model env | 20/min, 1000/day | engine2 |
|
||
| ESPN scoreboard| `poller`, `teamStatsCache`, `injuryParser` | none | 2/min (limiter) | poller, refresh job |
|
||
| MLB Stats API | `poller` (gamePk feed) | none | 2/min | poller (mlb final) |
|
||
| Pinnacle (legacy) | `PinnacleAdapter.js` | `PINNACLE_API_KEY`, `PINNACLE_API_BASE` | tunable | UnifiedOddsProvider |
|
||
| DraftKings (legacy) | `DraftKingsAdapter.js` | none | tunable | UnifiedOddsProvider |
|
||
| FanDuel/BetMGM/Caesars/PrizePicks/Covers/Rotowire (legacy) | each `*Adapter.js` | none | tunable | UnifiedOddsProvider |
|
||
| Sports-Reference HTML | `scripts/scrape-sports-reference.js` | `REF_HTML_FILE`, `COACH_HTML_FILE` (optional) | 1 req / 5s | scraper |
|
||
| Resend (email) | `web/src/services/email.ts` | `RESEND_API_KEY`, `RESEND_FROM_EMAIL` | n/a | transactional email |
|
||
| NexaPay | `web/src/services/nexapay.ts` | `NEXAPAY_*` | n/a | checkout fallback |
|
||
| PostHog | `web/src/lib/analytics.ts` | `NEXT_PUBLIC_POSTHOG_KEY/HOST` | n/a | browser analytics |
|
||
| football-data.org | `footballDataAdapter.js` | `FOOTBALL_DATA_API_KEY` | 10/min (8 enforced) | poller-soccer, prefetch (TERTIARY) |
|
||
| api-football.com | `apiFootballAdapter.js` | `API_FOOTBALL_KEY` | 100/day (soft 90) | soccer cascade (PRIMARY, Session 9) |
|
||
| FootApi (RapidAPI) | `footApiAdapter.js` | `RAPID_API_KEY`, `FOOTAPI_HOST` | 50/day (soft 45) | soccer cascade (BACKUP, Session 9) |
|
||
| Tank01 NBA (RapidAPI) | `tank01NbaAdapter.js` | `RAPID_API_KEY`, `TANK01_NBA_HOST` | 1000/mo (TTL bound) | live NBA box scores (Session 9) |
|
||
| Tank01 MLB (RapidAPI) | `tank01MlbAdapter.js` | `RAPID_API_KEY`, `TANK01_MLB_HOST` | 1000/mo (TTL bound) | live MLB box + batter-vs-pitcher (Session 9) |
|
||
| Stripe | `services/stripeService.js` | `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`, `STRIPE_PRICE_*` | n/a | checkout + webhook |
|
||
| The Odds API | `services/oddsService.js` | `ODDS_API_KEY` | quota tracked | per-sport odds endpoints |
|
||
| worldcup2026 OSS | `poller/soccer.js` | `WORLDCUP_API_URL` | none (free) | WC fixture poll |
|
||
|
||
---
|
||
|
||
## 7. File dependency graph (entry points)
|
||
|
||
### Express entry: `src/server.js`
|
||
```
|
||
server.js
|
||
└─ app.js
|
||
├─ routes/{odds,analyze,scan,movements,alerts,bets,stripe,stats,
|
||
│ props,waitlist,pipeline,shareCard,push,grading,corrections,widget}
|
||
└─ middleware/{auth, mission}
|
||
|
||
routes/grading.js (POST /pipeline)
|
||
└─ services/intelligence/gradingOrchestrator
|
||
├─ adapters/sharpApiAdapter
|
||
├─ services/intelligence/featureCache
|
||
│ ├─ teamStatsCache · refSignals · coachSignals
|
||
│ ├─ injuryParser · lineMovement · gameLogService
|
||
│ └─ lineupSignals
|
||
├─ services/intelligence/trapDetection
|
||
│ └─ lineMovement
|
||
├─ services/intelligence/consistencyScore
|
||
├─ services/intelligence/probabilityEstimator
|
||
├─ services/intelligence/engine1
|
||
└─ services/intelligence/engine2
|
||
└─ adapters/openRouterAdapter
|
||
|
||
routes/grading.js (POST /resolve)
|
||
└─ services/training/jsonlLogger
|
||
└─ services/distribution/{webPush, telegram, discord}
|
||
└─ services/intelligence/{clvTracker, accuracyTracker, weightAdjuster}
|
||
|
||
routes/scan.js (POST /parlay — legacy path, still wired)
|
||
└─ services/parlayScanService
|
||
└─ services/propAnalyzer
|
||
└─ services/grader ← legacy grading (see ARCH-1)
|
||
└─ services/UnifiedOddsProvider
|
||
└─ adapters/{ESPN,Pinnacle,DraftKings,FanDuel,BetMGM,
|
||
Caesars,PrizePicks,Covers,Rotowire}
|
||
└─ services/processing/{LineShoppingEngine,
|
||
MiddlesDetector, EVCalculator}
|
||
```
|
||
|
||
### Poller entry: `poller/poller.js`
|
||
```
|
||
poller.js
|
||
├─ src/config/sports
|
||
├─ src/utils/redis (cacheGet/cacheSet/lock)
|
||
├─ src/utils/rateLimiter (espnLimiter, mlbLimiter)
|
||
└─ src/services/adapters/oddsPapiAdapter (tip-off CLV capture)
|
||
```
|
||
|
||
No circular imports detected.
|
||
|
||
---
|
||
|
||
## 8. Findings
|
||
|
||
### ARCH — Architecture
|
||
|
||
- **[ARCH-1] Dual grading paths.** Severity: Medium. Status:
|
||
**FIXED in Session 7g** — all grading routes (/api/analyze/prop,
|
||
/api/analyze/batch, /api/scan/parlay) now grade through engine1 via
|
||
the unified `analyzeViaEngine1` helper. /api/bets/* never used the
|
||
legacy path. Dead files removed: `src/services/propAnalyzer.js`,
|
||
`src/services/grader.js`, `tests/unit/grader.test.js` (-10 tests).
|
||
|
||
UnifiedOddsProvider + the 9 legacy book adapters + the legacy
|
||
rateLimiter/circuitBreaker stay live — they're still consumed by
|
||
`/api/pipeline/refresh` (the data refresh path, a separate
|
||
concern). When that route eventually migrates off them, those
|
||
files can also retire.
|
||
|
||
Migration story for the record:
|
||
Session 7c — audit catalogued ARCH-1.
|
||
Session 7d — adapter scoped + DEPRECATED banner on grader.js.
|
||
Session 7e — adapter built + tested (gradeAdapter.js, 26 tests).
|
||
Session 7f — computeFeatures (8 tests) + analyzeViaEngine1
|
||
(7 tests) + /api/scan/parlay migrated.
|
||
/api/analyze escape-hatched (integration test
|
||
asserted legacy-engine values).
|
||
Session 7g — analyze test mocks rotated to drive the new path;
|
||
/api/analyze migrated; dead code removed.
|
||
|
||
Adapter, computeFeaturesForProp, and analyzeViaEngine1 helpers all
|
||
shipped and tested. Future migration can flip the analyze route any
|
||
time the integration test mocks are updated to drive the new
|
||
upstream chain.
|
||
|
||
ORIGINAL Session 7e ARCH-1 narrative below for history.
|
||
|
||
**PARTIAL (carried from 7e)** — Step 1 of the migration (the adapter)
|
||
is complete and tested. Steps 2-6 partially executed.
|
||
Step 1 ✓ — `src/utils/gradeAdapter.js` translates engine1 output
|
||
into the legacy shape `DemoScan` reads. 26 unit
|
||
tests covering grade collapse, confidence math,
|
||
kill-condition mapping, reasoning fallback, partial
|
||
input safety.
|
||
Step 2-6 deferred — legacy `analyzeProp` and `engine1.gradeProp()`
|
||
have incompatible INPUT contracts: the legacy
|
||
analyzer takes a raw prop and self-fetches data
|
||
(odds, stats, opponent, spread, kill conditions);
|
||
engine1 takes a pre-computed feature vector and is
|
||
a pure grading function. To rewire any route to
|
||
engine1 we'd first need to extract an
|
||
orchestrator-lite preprocessor — that's the kind
|
||
of architectural change 7c/7d's no-restructure
|
||
rule blocks. The reasoning.summary string-level
|
||
parity is also currently lost (concrete numbers
|
||
in legacy vs abstract factor labels in engine1).
|
||
Migration roadmap (now actionable for the next session):
|
||
(a) Extract `gradingOrchestrator.gradeProp()` into a standalone
|
||
`computeFeaturesForProp({player, stat_type, line, direction})`
|
||
that lifts the player-roster + feature-fetch + trap-detect +
|
||
consistency-score logic into a reusable callable.
|
||
(b) Wrap engine1 + gradeAdapter behind a thin
|
||
`analyzeViaEngine1(prop)` helper.
|
||
(c) Rewire one route at a time — `/api/analyze/prop` first
|
||
(lowest risk: single prop, public, well-tested), then
|
||
`/api/analyze/batch`, then `/api/scan/parlay`, then
|
||
`/api/bets/*`.
|
||
(d) Remove `grader.js` + `propAnalyzer.js` + the legacy book
|
||
adapters only after every consumer has migrated and a soak
|
||
period.
|
||
Session 7d's DEPRECATED banner on `src/services/grader.js` stays
|
||
put.
|
||
|
||
- **[ARCH-2] Two circuit-breaker / rate-limiter modules.** Severity:
|
||
Low. Status: **DOCUMENTED in Session 7e.** Both legacy modules now
|
||
carry banners listing their three callers:
|
||
- `src/services/UnifiedOddsProvider.js`
|
||
- `src/services/adapters/ESPNAdapter.js`
|
||
- `src/services/adapters/PinnacleAdapter.js`
|
||
Full removal blocks on ARCH-1 Step 6 — the legacy book adapters
|
||
retire together with the legacy grading path.
|
||
|
||
- **[ARCH-3] Soccer pipeline added as a parallel branch.** Severity:
|
||
Info. Status: **SHIPPED in Session 7j.** Soccer routes off
|
||
`computeFeaturesForProp` to `soccerFeatureExtractor` when
|
||
`sport ∈ {'soccer','football'}`; trap detection branches on the same
|
||
in `getTrapScore`; reasoning branches in `buildConcreteReasoning`.
|
||
Engine1 is sport-agnostic (passes unknown feature keys through).
|
||
Data flow: `poller/soccer.js` writes per-team `nextmatch` /
|
||
`lastfixture` pointers; `scripts/soccer-data-prefetch.js` writes
|
||
per-player + per-team-defense aggregates. The feature extractor
|
||
reads ONLY from cache — no external HTTP on the user request path.
|
||
Day-1 gap: xG fields (`xg_per_90`, `xg_delta`) are null until the
|
||
soccerdata-Python bridge ships; engine handles the nulls gracefully.
|
||
|
||
### SEC — Security
|
||
|
||
- **[SEC-1] `/api/analyze/batch` has no auth or rate limit.** Severity:
|
||
High. Status: **FIXED in Session 7d.** Added IP-keyed rate limit
|
||
(`src/middleware/rateLimit.js`, 10 req/min) and mounted it on the
|
||
`/api/analyze` router via `router.use(analyzeLimit)` — covers both
|
||
`/prop` and `/batch`. 7 unit tests + 1 behavioral test verify the
|
||
429 path.
|
||
|
||
- **[SEC-2] `routes/shareCard.js` leaks `err.message` via `detail:`.**
|
||
Severity: Medium. Status: **FIXED in Session 7d.** Lines 185 and
|
||
206 now log to `console.error('[VYNDR] shareCard ... failed:',
|
||
err?.message)` (ops-only) and return generic error verbs to public
|
||
callers. The validation-errors `detail:` on line 166 is intentional
|
||
user-facing input feedback, retained.
|
||
|
||
- **[SEC-3] Several `console.log` calls in production code.** Severity:
|
||
Low. All have `[VYNDR]` / `[redis]` / `[poller-X]` prefixes and
|
||
represent OPERATIONAL status (server starting, poller tick summary),
|
||
not debug. Per project convention `console.log` is acceptable for
|
||
operational events. Documenting so future audits don't re-flag.
|
||
|
||
### DEAD — Dead code
|
||
|
||
- **[DEAD-1] No truly orphaned files.** Every adapter in
|
||
`src/services/adapters/` has at least one consumer (legacy ones via
|
||
`UnifiedOddsProvider`, new ones via orchestrator/poller). Removing
|
||
any would break either `/api/scan` or `/api/grading/pipeline`.
|
||
|
||
- **[DEAD-2] `grader.js` is LIVE.** Imported by `propAnalyzer.js` →
|
||
`parlayScanService.js` → `routes/scan.js`. Kept until ARCH-1 is
|
||
resolved.
|
||
|
||
### DUP — Duplicates
|
||
|
||
- **[DUP-1] `normalizeName()` exists twice.** Severity: Low. Status:
|
||
**FIXED in Session 7e.** Consolidated into `src/utils/normalize.js`
|
||
with a `keepDigits` option (default false). `trapDetection.js`
|
||
imports the default; `scripts/populate-player-ids.js` wraps with
|
||
`keepDigits: true` via `normalizeRosterName`. 9 unit tests cover
|
||
accent strip, suffix removal, digit options, idempotency, null
|
||
input.
|
||
|
||
- **[DUP-2] `oddsToImplied` etc. only live in `src/utils/odds.js`.**
|
||
Confirmed not duplicated despite the `oddsNormalizer.js` neighbor —
|
||
the two files serve different purposes (math primitives vs book-level
|
||
prop shape).
|
||
|
||
### LEGACY — Documentation / naming
|
||
|
||
- **[LEG-1] BetonBLK references.** Severity: Informational. Confined to
|
||
`BUILD-STATE.md` historical notes, the `FOUNDER_CODES` allowlist
|
||
(intentional grandfathering — documented in stripeService comment),
|
||
and tests that assert the grandfathered code still validates. No
|
||
action.
|
||
|
||
### TODO
|
||
|
||
- **[TODO-1] PrizePicks / DraftKings / Pinnacle adapter TODOs.**
|
||
Severity: Low. Tracked in `specs/data-pipeline-books.md`. Each has a
|
||
comment line referencing the spec. Keep until the corresponding
|
||
feature ships.
|
||
|
||
### PERF — Performance
|
||
|
||
- **[PERF-1] `/api/analyze/*` lacks Redis cache.** Severity: Medium.
|
||
Status: **FIXED in Session 7d.** Added `cachedAnalyze()` wrapper in
|
||
`src/routes/analyze.js` with key
|
||
`analyze:{sport}:{player}:{stat}:{line}:{direction}` and a 60s TTL.
|
||
Response payload gains `_cache: 'HIT'|'MISS'` for observability.
|
||
3 unit tests verify HIT/MISS, independent keys, and case folding on
|
||
player name.
|
||
|
||
- **[PERF-2] `routes/scan.js` resolves parlays one prop at a time.**
|
||
Severity: Medium. Status: **FIXED in Session 7d.** Replaced the
|
||
sequential `for (leg of legs) await analyzeProp(leg)` with
|
||
`Promise.allSettled(legs.map(analyzeProp))` in
|
||
`src/services/parlayScanService.js`. The picks-table writes also
|
||
switched from N sequential inserts to a single batched insert. A
|
||
behavioral test asserts a 6-leg parlay completes in well under
|
||
6 × delay wall-clock.
|
||
|
||
- **[PERF-3] Bundle analyzer not installed.** Severity: Low. Status:
|
||
**FIXED in Session 7d.** `@next/bundle-analyzer@^16.2.9` added as a
|
||
devDependency and wired into `web/next.config.ts`. Inert by default;
|
||
emits `web/.next/analyze/{client,edge,nodejs}.html` when
|
||
`ANALYZE=true npm run build` runs.
|
||
|
||
### Frontend ↔ Backend contract
|
||
|
||
All frontend API paths discovered are either:
|
||
- Direct Express paths confirmed in `src/app.js` (e.g. `/api/scan/parlay`,
|
||
`/api/waitlist`), or
|
||
- Next.js `app/api/*/route.ts` proxies that internally fetch
|
||
`${BACKEND_URL}/...`.
|
||
|
||
**No broken contracts found** — every fetched path maps to an existing
|
||
handler. Spot-checked: `/api/players/search` (Next → Python),
|
||
`/api/scan` (Next → Express), `/api/intelligence/feed` (Next direct DB).
|
||
|
||
#### Payments: Stripe cutover (Session 8 — COMPLETE)
|
||
|
||
The dual-provider divergence flagged in 7h is closed:
|
||
|
||
1. ✅ `web/src/app/api/checkout/route.ts` now forwards to
|
||
`${BACKEND_URL}/api/stripe/checkout` with the user's bearer token.
|
||
The route remaps `{ checkout_url, session_id }` → `{ url, … }` so
|
||
the existing client field shape still works.
|
||
2. ✅ `Pricing.tsx` CTAs were converted from `<a href>` to onClick
|
||
handlers that POST to `/api/checkout` and `window.location.assign`
|
||
the returned Stripe URL. Loading state during redirect; error
|
||
surfaced inline.
|
||
3. ✅ `/upgrade/success?session_id=…` and `/upgrade/cancel` pages
|
||
shipped. Express `stripeService.js` updated to point `success_url`
|
||
and `cancel_url` at the new frontend pages via `NEXT_PUBLIC_SITE_URL`
|
||
(the only backend file touched in Session 8).
|
||
4. NexaPay is still wired but no UI calls it. Disposition (remove vs
|
||
keep as fallback) is a follow-up call — leaving it in place doesn't
|
||
cost anything and gives the team a fallback if Stripe goes down
|
||
during the World Cup window.
|
||
|
||
---
|
||
|
||
## 9. How to update this manifest
|
||
|
||
When you add a route, table, env var, or external API:
|
||
|
||
1. Add the row to the appropriate section above.
|
||
2. Re-run the discovery greps from this session's prompt to confirm
|
||
nothing else changed implicitly.
|
||
3. If you remove a table or env var, mark it `[REMOVED Session NX]`
|
||
instead of deleting the row outright — keeps audit trail.
|
||
|
||
Last updated: Session 7c.
|