35 KiB
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/parlayandroutes/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.constructEventsignature
| 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 |
| 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 |
| 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) — NexaPay checkout/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/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/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
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) | routes/grading, corrections |
✓ |
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) |
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 |
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.jsjoint_outcomes—routes/props.js(joint-history)model_predictions_extended—propAnalyzerperformance—performanceServicepicks—parlayScanServicescan_sessions—routes/scan.js
Shared tables (BetonBLK + VYNDR)
users— both eraswaitlist/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 |
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
analyzeViaEngine1helper. /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.jstranslates engine1 output into the legacy shapeDemoScanreads. 26 unit tests covering grade collapse, confidence math, kill-condition mapping, reasoning fallback, partial input safety. Step 2-6 deferred — legacyanalyzePropandengine1.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) ExtractgradingOrchestrator.gradeProp()into a standalonecomputeFeaturesForProp({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 thinanalyzeViaEngine1(prop)helper. (c) Rewire one route at a time —/api/analyze/propfirst (lowest risk: single prop, public, well-tested), then/api/analyze/batch, then/api/scan/parlay, then/api/bets/*. (d) Removegrader.js+propAnalyzer.js+ the legacy book adapters only after every consumer has migrated and a soak period. Session 7d's DEPRECATED banner onsrc/services/grader.jsstays 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.jssrc/services/adapters/ESPNAdapter.jssrc/services/adapters/PinnacleAdapter.jsFull removal blocks on ARCH-1 Step 6 — the legacy book adapters retire together with the legacy grading path.
SEC — Security
-
[SEC-1]
/api/analyze/batchhas 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/analyzerouter viarouter.use(analyzeLimit)— covers both/propand/batch. 7 unit tests + 1 behavioral test verify the 429 path. -
[SEC-2]
routes/shareCard.jsleakserr.messageviadetail:. Severity: Medium. Status: FIXED in Session 7d. Lines 185 and 206 now log toconsole.error('[VYNDR] shareCard ... failed:', err?.message)(ops-only) and return generic error verbs to public callers. The validation-errorsdetail:on line 166 is intentional user-facing input feedback, retained. -
[SEC-3] Several
console.logcalls 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 conventionconsole.logis 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 viaUnifiedOddsProvider, new ones via orchestrator/poller). Removing any would break either/api/scanor/api/grading/pipeline. -
[DEAD-2]
grader.jsis LIVE. Imported bypropAnalyzer.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 intosrc/utils/normalize.jswith akeepDigitsoption (default false).trapDetection.jsimports the default;scripts/populate-player-ids.jswraps withkeepDigits: truevianormalizeRosterName. 9 unit tests cover accent strip, suffix removal, digit options, idempotency, null input. -
[DUP-2]
oddsToImpliedetc. only live insrc/utils/odds.js. Confirmed not duplicated despite theoddsNormalizer.jsneighbor — 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.mdhistorical notes, theFOUNDER_CODESallowlist (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. AddedcachedAnalyze()wrapper insrc/routes/analyze.jswith keyanalyze:{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.jsresolves parlays one prop at a time. Severity: Medium. Status: FIXED in Session 7d. Replaced the sequentialfor (leg of legs) await analyzeProp(leg)withPromise.allSettled(legs.map(analyzeProp))insrc/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.9added as a devDependency and wired intoweb/next.config.ts. Inert by default; emitsweb/.next/analyze/{client,edge,nodejs}.htmlwhenANALYZE=true npm run buildruns.
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.tsproxies 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: dual-provider divergence (Session 7h)
The frontend /api/checkout (Next.js) creates NexaPay payment
links and is what web/src/components/Pricing.tsx CTAs currently hit.
The Express POST /api/stripe/checkout (Stripe Checkout Sessions) is
fully wired, tested in test mode against real Stripe resources
(products + prices + webhook all created), and ready for traffic —
but no frontend caller invokes it yet. Cutover work for a follow-up
session:
- Replace
web/src/app/api/checkout/route.tsbody to fetch${BACKEND_URL}/api/stripe/checkoutwith the user's bearer token instead of calling NexaPay'screatePaymentLink. - Wire
Pricing.tsxCTAs through that same Next.js route (response shape is already{ url, ... }-compatible; Express returns{ checkout_url, session_id }, so the proxy needs to remapcheckout_url → url). - Add
/upgrade/success?session_id=...and/upgrade/cancelpages. Current Stripesuccess_urlpoints at/scan?upgraded=trueandcancel_urlat/#pricing— those work but a confirmation page reads better. - Decide on NexaPay: keep as fallback, remove, or feature-flag.
9. How to update this manifest
When you add a route, table, env var, or external API:
- Add the row to the appropriate section above.
- Re-run the discovery greps from this session's prompt to confirm nothing else changed implicitly.
- 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.