Files
vyndr/docs/SYSTEM-MANIFEST.md
T

44 KiB
Raw Blame History

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 (computeFeaturesengine1gradeAdapter).
  • routes/grading/pipeline (scheduled) → gradingOrchestratorfeatureCache + trapDetection + consistencyScore + probabilityEstimatorengine1 → (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
  • userrequireAuth (Supabase JWT bearer)
  • internalrequireInternal (X-VYNDR-Internal-Key + loopback IP)
  • pipelinerequirePipelineSecret (X-Pipeline-Secret)
  • stripe-sigstripe.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).

Pricing tiers (Session 12 — Africa tier added; Session 13 — geo-gated)

Var Required Default Used By Doc?
STRIPE_PRICE_AFRICA no (none) web/components/Pricing, stripeService (post-DB-CHECK migration) ✓ S12
Cloudflare CF-IPCountry n/a (none) middleware.tsx-vyndr-countryuseRegion() ✓ S13

Blocker: the existing migrations (001 + 011) declare tier IN ('free','analyst','desk') as a CHECK constraint on users.tier and user_profiles.tier. The africa tier value will VIOLATE that constraint until the constraint is updated. The frontend Pricing page shows the tier now (UX), but the click handler short-circuits to an honest "coming soon" message rather than triggering checkout. Required follow-up: manual SQL to drop + re-add the CHECK across both tables including 'africa' (cannot be done in this session per the no-migration rule).

Session 13 — Africa tier visibility is now driven by real IP geo (Cloudflare CF-IPCountry header), not by locale. The middleware copies CF-IPCountry to x-vyndr-country; the root layout reads it into LocaleProvider; useRegion() exposes inAfrica: boolean. The Pricing component filters the Africa tier out of the render entirely when inAfrica === false. Empty header (traffic bypassing Cloudflare) degrades closed.

Internationalization (Session 12)

Var / file Required Default Used By Doc?
NEXT_LOCALE cookie no en web/middleware.ts, switcher ✓ S12
web/src/lib/locales.ts n/a n/a locale registry (10 languages) ✓ S12
web/src/locales/{code}.json n/a n/a per-locale translation dictionaries ✓ S12

Locale-detection priority (left wins): URL prefix (reserved for future [locale]/ segment) → NEXT_LOCALE cookie → Accept-Language header → 'en'. Resolved locale rides on the request via the x-vyndr-locale header; server components read it through next/headers, client components consume it via LocaleContext.

Side effect: every Next.js page is now ƒ (Dynamic) instead of ○ (Static) because the root layout reads request headers. If FCP regresses meaningfully, the fix is to move locale resolution client-side (cookie-only, no headers() in layout) — that re-enables static prerendering at the cost of a brief English flash on first paint for non-English users.

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:

  • betsroutes/bets.js, betService.js
  • joint_outcomesroutes/props.js (joint-history)
  • model_predictions_extendedpropAnalyzer
  • performanceperformanceService
  • picksparlayScanService
  • scan_sessionsroutes/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.jsparlayScanService.jsroutes/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.