Files
vyndr/BUILD-STATE.md
T

45 KiB
Executable File
Raw Blame History

VYNDR — Build State

Last Updated

2026-06-10

Current Phase

SHIP BUILD v9.0 — Critical site fixes, data-source upgrade, production readiness (Session 9)

Session 9 (2026-06-10) — SHIPPED

World Cup opens tomorrow. This session closed three live-site emergencies (404, OOM cycle, slow FCP), added three new soccer data sources with a priority cascade, two new RapidAPI sports adapters, a real grace-period downgrade middleware, and updated the legal pages.

Phase 0 — critical fixes

  • /pricing 404 → fixed. web/src/app/pricing/page.tsx created; wraps the existing Pricing component on a standalone route so email renewal CTAs (which link to /pricing via web/src/services/email.ts:204) no longer land on 404. Metadata block ships with OG + Twitter tags.
  • Web container OOM cycle → cause identified, fix documented. docker logs on the live host (z2zyki…-032334469519, 44 restarts and climbing) returned FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory. Docker mem limit is unlimited (0) — this is Node's own ~2 GB V8 default. Fix is a Coolify env-var change: NODE_OPTIONS=--max-old-space-size=4096 on the web container. Cannot be applied from this session — listed under the Coolify env requirements at the end of this entry.
  • 7.5s FCP → root cause traced to the OOM cycle. All page routes are static-prerendered; root layout makes no blocking calls. The FCP measurement is dominated by cold-start latency hit during each restart. The NODE_OPTIONS fix is the primary FCP fix too — re-measure after deploy.

Phase 1 — soccer source upgrade

New adapter cascade for soccer (priority order):

  1. api-football.com (PRIMARY)src/services/adapters/apiFootballAdapter.js. 100 req/day soft limit (90, with 10-req safety margin). 6 endpoints: getFixtures, getFixtureLineups, getFixturePlayerStats, getFixtureEvents, getPlayerSeasonStats, getStandings. Auth via x-apisports-key header (NOT RapidAPI). Per-endpoint TTLs match data volatility (fixtures 6h, lineups/playerstats 24h, events 12h).
  2. FootApi via RapidAPI (BACKUP)src/services/adapters/footApiAdapter.js. 50 req/day (soft 45). 4 endpoints: getMatchLineups (28 stat keys), getMatchIncidents (minute + addedTime), getRefereeStatistics (yellow/red per game), getWorldCupSchedule (tournament ID 16).
  3. football-data.org (TERTIARY) — existing Session 7j adapter unchanged.

The soccerFeatureExtractor now cascades through these via a new loadFromCascade() helper. Each load returns a _source tag so debugging is straightforward; meta.sources exposes the attribution per lookup (player, nextMatch, lastFixture, referee). Existing 17 soccer-extractor tests still pass; 7 new cascade tests prove the priority order.

Phase 1 — Tank01 RapidAPI adapters

  • tank01NbaAdapter.js — live NBA box scores, schedule, betting odds. Status-aware TTL: 5-min cache while a game is in-progress, 24-hour cache once it reports Final. Free tier 1,000 req/mo; TTL-bound rather than counter-bound.
  • tank01MlbAdapter.js — live MLB box scores, daily scoreboard, and batter-vs-pitcher (the headline new MLB signal — a batter's historical PA/AB/H/HR/SO line against a specific pitcher). Same status-aware TTL pattern as NBA.

Both Tank01 adapters use the shared RAPID_API_KEY (also used by FootApi). Host overridable via TANK01_NBA_HOST / TANK01_MLB_HOST.

Phase 2 — production readiness

  • Grace-period downgrade middlewaresrc/middleware/gracePeriod.js. Fires at request time on tier-gated routes (/api/scan/parlay, /api/alerts, /api/props/joint-history). Reads req.user.grace_period_until (now selected by requireAuth in src/middleware/auth.js), and on expiry atomically downgrades users.tier and user_profiles.tier to 'free', clears the timestamp, sets subscription_status='expired' on the profile mirror, and rewrites req.user so the route immediately sees the downgrade. Closes the long-standing "cancelled users keep paid access forever" gap. Ordering matters: grace must run AFTER requireAuth and BEFORE scanLimit, because scanLimit reads tier off req.user — a just-expired Desk user would otherwise burn one final unlimited-quota request.
  • TOS updateweb/src/app/terms/page.tsx Subscription Terms switched from NexaPay to Stripe; Acceptable Use now explicitly states "VYNDR does NOT offer API access at any tier" — closes the Session 7h immutable.
  • Privacy updateweb/src/app/privacy/page.tsx Payment Data section switched from NexaPay to Stripe with specifics on what Stripe receives. New "Sub-processors" section explicitly lists Stripe, Supabase, PostHog, Resend.
  • Cookie consent bannerweb/src/components/CookieConsent.tsx, mounted in root layout. Thin bottom bar, SSR-safe (renders nothing until client mount checks localStorage), single-button accept, links to Privacy Policy.
  • Root layout metadata — keywords + description extended to include soccer and World Cup 2026 intelligence terms. OG + Twitter cards already comprehensive from prior sessions. Per-page metadata for /soccer + /scan deferred (those pages are 'use client'; would need server-component wrappers — cosmetic).

Tests added

Suite Tests
tests/unit/apiFootballAdapter.test.js 16
tests/unit/footApiAdapter.test.js 13
tests/unit/soccerFeatureExtractorCascade.test.js 7
tests/unit/tank01NbaAdapter.test.js 12
tests/unit/tank01MlbAdapter.test.js 12
tests/unit/gracePeriod.test.js 7
Session 9 total 67

Quality gates

  • npm test: 1240 / 1240 passing (1173 baseline + 67 new), 97 suites, 0 regressions
  • web/npm run build: clean — /pricing + everything else prerenders, no type errors
  • License audit: only permissive licenses

Coolify env vars (apply on the web container — keys not in repo)

NODE_OPTIONS=--max-old-space-size=4096        # fixes the OOM cycle
API_FOOTBALL_KEY=<from api-sports.io>          # PRIMARY soccer source
FOOTBALL_DATA_API_KEY=<from football-data.org> # TERTIARY soccer source
RAPID_API_KEY=<from RapidAPI marketplace>      # FootApi + Tank01 NBA + Tank01 MLB
FOOTAPI_HOST=footapi7.p.rapidapi.com           # default — override only for mirrors
TANK01_NBA_HOST=tank01-fantasy-stats.p.rapidapi.com
TANK01_MLB_HOST=tank01-mlb-live-in-game-real-time-statistics.p.rapidapi.com

Open items

  • NODE_OPTIONS must be set in Coolify before the next deploy; until then, the web container will keep OOM-looping. This is the single most important production action item.
  • The 2 GB+ heap usage that triggered the OOM suggests a memory leak in the Next.js standalone server. Heap-snapshot investigation deferred — the env-var bump buys headroom but doesn't fix the leak root cause.
  • Per-page OG metadata on /soccer and /scan requires those pages to be refactored to a server-component wrapper pattern. Not blocking.
  • The new adapter cascade improves data quality WHEN API_FOOTBALL_KEY / RAPID_API_KEY are populated and a daily prefetch has run against them. Until then, the cascade silently falls through to football-data.org and static reference data. Updating scripts/soccer-data-prefetch.js to write the new apifootball:* / footapi:* cache keys is a follow-up.

Session 8 (2026-06-10) — SHIPPED

Frontend layer that connects users to the Session 7h7j backend. NexaPay → Stripe cutover on the pricing flow + a /soccer page that exposes the soccer intelligence pipeline.

Files created (frontend)

  • web/src/app/api/odds/soccer/[league]/route.ts — Next.js proxy → Express GET /api/odds/soccer/:league. Validates league against the 9 accepted codes upstream so a typo bounces at the Next boundary.
  • web/src/app/soccer/page.tsx — live soccer odds feed. Hosts SportSelector, fetches /api/odds/soccer/:league, groups props by match → stat type. "Grade" button triggers inline scan via /api/scan (sport: Soccer) and renders the result through SoccerGradeResult. Soccer-only page; switching the selector to another sport bounces to /scan.
  • web/src/app/upgrade/success/page.tsx — Stripe success landing. Reads session_id, refreshes AuthContext so the new tier flips immediately. Does NOT verify against Stripe from the client (no secret key on the browser) — the webhook is the source of truth.
  • web/src/app/upgrade/cancel/page.tsx — Stripe cancel landing.
  • web/src/components/SportSelector.tsx — pill tabs (NBA/WNBA/MLB/ Soccer); Soccer reveals a sub-row of the 9 league codes matching Express's SOCCER_SPORT_KEYS. Emits { sport, league? } via onChange — pure UI, no fetches.
  • web/src/components/SoccerGradeResult.tsx — soccer-themed result card. Parses the engine's reasoning summary into visual chips ( goals/90, 📊 xG, 🎯 penalty taker, 🏹 free-kick taker, corner taker, 🏔️ altitude, 🟨 referee, ⏱️ minutes discount, 🛡️ opponent defense, 🏆 tournament pedigree). Color-coded by tone (positive / caution / warning / neutral). Free-tier responses (carrying tier_gated: true) render the chip row blurred under an upgrade CTA; the structured grade + confidence + edge stay visible. Kept separate from GradeCard so the NBA/MLB/WNBA path is untouched.

Files modified (frontend)

  • web/src/app/api/checkout/route.ts — full rewrite. Was a NexaPay payment-link creator; is now a thin proxy that forwards { tier, founder_code? } + bearer to Express /api/stripe/checkout. Response remap: checkout_urlurl for callsite compat; both fields shipped so either reads cleanly.
  • web/src/app/api/scan/route.ts — accepts Soccer sport in addition to NBA/MLB/WNBA. Soccer stat-type allowlist mirrors the backend VALID_STAT_TYPES (goals, shots_on_target, shots, tackles, cards, corners, saves, goals_conceded, passes, clean_sheet, assists).
  • web/src/components/Pricing.tsx — CTAs converted from <a href> to onClick handlers. Uses useAuth() for the bearer token, POSTs to /api/checkout, window.location.assign to the returned Stripe URL. Loading state on the active tier, inline error banner. Anonymous visitors bounce to /signup?return=/%23pricing. Footnote rewritten from "NexaPay" to "Stripe (test mode while we onboard founders)".
  • web/src/components/Nav.tsx — small BETA tag next to the wordmark. Glitch-styled, monospace, low-opacity green border. Renders on every page that mounts Nav.

Files modified (backend — ONE allowed change)

  • src/services/stripeService.jssuccess_url / cancel_url point at the frontend (NEXT_PUBLIC_SITE_URL with BASE_URL fallback, default http://localhost:3000). Previously the routes pointed at the Express origin which would have 404'd the redirect. New URLs:
    • ${frontendUrl}/upgrade/success?session_id={CHECKOUT_SESSION_ID}
    • ${frontendUrl}/upgrade/cancel All 23 Stripe tests still pass (none asserted on the URL strings).

Files modified (docs)

  • docs/SYSTEM-MANIFEST.md/api/odds/soccer/[league] row in Next.js routes, new section listing the three new Next.js pages, the Session 7h "dual-provider divergence" callout flipped from open-work to complete.
  • BUILD-STATE.md — Session 8 entry.

Honest verification status

Build-verified (passed web/npm run build after every component):

  • All TypeScript types resolve
  • All routes prerender / build correctly (24 pages, 30+ API routes)
  • No ESLint errors

NOT runtime-verified in this session (I have no browser to click through):

  • Actual Stripe checkout redirect end-to-end (test mode card flow)
  • Soccer odds rendering with live data (depends on FOOTBALL_DATA_API_KEY being set in prod and the daily prefetch having run)
  • SoccerGradeResult signal parsing against a real engine response (signal-chip regex tested against the exact phrasing buildSoccerReasoningLines emits in analyzeViaEngine1.js, but not against live engine output)
  • AuthContext.refresh() actually triggering a profile re-read after the Stripe redirect

These are the expected next-session sanity checks once Coolify deploys this build.

Quality gates

  • npm test (backend): 1173 / 1173 passing, 91 suites, 0 regressions from Session 7j baseline
  • web/npm run build: clean — all new routes prerendered, no type errors
  • License audit: only permissive licenses

Session 7j (2026-06-10) — SHIPPED

Permanent soccer sport vertical, launching with FIFA World Cup 2026 (opens June 11). League-agnostic architecture supports WC, EPL, La Liga, Bundesliga, Serie A, Ligue 1, UCL, MLS, Liga MX from the same code paths.

Files created

  • src/data/worldcup2026.js — 16 venues + altitudes + climate, CONCACAF
    • CONMEBOL teams, penalty/corner/free-kick takers (top 25 teams), tournament players (≥3 career WC goals). All frozen. Helpers: isPenaltyTaker, isCornerTaker, isFreeKickTaker, getTournamentHistory, isHomeContinent, getVenue, altitudeImpact.
  • src/services/adapters/footballDataAdapter.js — football-data.org v4 REST adapter. 8/min token bucket (2-req safety margin vs the 10/min upstream cap). Tier-matched Redis TTLs (fixtures 6h, standings 12h, squads 24h, scorers 6h). Stale-while-revalidate fallback when the bucket is drained or the API 5xx's. Returns null when no API key — callers degrade gracefully.
  • src/services/intelligence/soccerFeatureExtractor.js — reads from prefetch-populated Redis cache (NEVER hits external APIs on the user request path). Builds the engine1 feature vector + a soccer overlay (goals_per_90, xG, penalty/corner/FK role, altitude, referee, tournament history, rest_days).
  • poller/soccer.js — league-agnostic fixture poller. WC pulls from the rezarahiminia/worldcup2026 OSS API (no rate limit) and falls back to football-data.org. Other leagues use the adapter directly. Writes soccer:nextmatch:{team} (24h TTL) + soccer:lastfixture:{team} (7d TTL) per fixture. Self-rescheduling: 5-min ticks during live matches, 30-min otherwise. PM2-managed.
  • scripts/soccer-data-prefetch.js — daily batch job. Pulls standings
    • scorers per configured league, computes per-team defensive aggregate (goals_conceded_per_game, defensive_rank_norm on a 0..1 scale that slots into engine1's opp_rank_stat) and per-player per-90 rates. Writes soccer:teamdefense:{league}:{team} and soccer:player:{normalizedName}. --leagues=WC,PL --dry-run flags supported. xG fields left null on Day 1 (soccerdata-Python bridge is a follow-up; engine handles nulls gracefully).
  • tests/unit/worldcup2026.test.js (20 tests)
  • tests/unit/footballDataAdapter.test.js (15 tests)
  • tests/unit/soccerFeatureExtractor.test.js (17 tests)
  • tests/unit/trapDetectionSoccer.test.js (21 tests)
  • tests/unit/computeFeaturesSoccerBranch.test.js (4 tests)
  • tests/unit/analyzeViaEngine1Soccer.test.js (8 tests)
  • tests/unit/soccerPoller.test.js (22 tests)
  • tests/unit/soccerDataPrefetch.test.js (14 tests)
  • tests/integration/oddsSoccer.test.js (6 tests)

Files modified

  • src/utils/oddsNormalizer.jsMARKET_MAP gains 10 soccer market keys (player_goals, player_shots_on_target, etc → goals, shots_on_target, etc). Existing NBA mappings untouched.
  • src/routes/analyze.js, src/routes/scan.jsVALID_STAT_TYPES set extended with 10 soccer stat types. 'assists' is shared with NBA; sport field discriminates downstream.
  • src/routes/odds.js — new GET /api/odds/soccer/:league route. Validates league against SOCCER_SPORT_KEYS (9 leagues), surfaces 405 valid-list hint on miss.
  • src/services/oddsService.jsSPORT_KEYS gains 9 soccer entries mapping soccer_wcsoccer_fifa_world_cup, soccer_eplsoccer_epl, etc. SOCCER_SPORT_KEYS exported as a frozen list.
  • src/services/intelligence/computeFeatures.jssport ∈ {'soccer','football'} dispatches to extractSoccerFeatures. NBA path unchanged.
  • src/services/intelligence/trapDetection.js — six soccer signals (xg_regression, altitude_risk, rotation_risk, minute_discount, referee_card_bias [positive — excluded from composite], strong_defense). getTrapScore branches on input.sport.
  • src/services/intelligence/analyzeViaEngine1.js — soccer reasoning branch (buildSoccerReasoningLines). Uses "matches" not "games", surfaces xG / penalty taker / altitude / referee / minutes / WC pedigree. NBA-specific sentences (back-to-back, injury report) guarded by !isSoccer.
  • poller/ecosystem.config.jspoller-soccer PM2 app added. Same restart policy as box-score pollers; SOCCER_LEAGUES env wired.
  • .env.example — soccer block (FOOTBALL_DATA_API_KEY, SOCCER_LEAGUES, WORLDCUP_API_URL, RAPID_API_KEY).
  • docs/SYSTEM-MANIFEST.md/api/odds/soccer/:league row in §2, Soccer env block in §3, soccer poller in poller-set, four new external API rows in §6, [ARCH-3] soccer-pipeline note in §8.

Quality gates (all green)

  • npm test: 1173 / 1173 passing (1042 baseline + 131 new soccer tests across 9 new suites), 91 suites, 0 failures
  • web/npm run build: clean
  • License audit: only permissive third-party licenses

Session 7i (2026-06-10) — SHIPPED

Stripe checkout + webhook (no new routes — gap-fill on existing)

Pre-audit revealed Session 3.4 already shipped a fuller Stripe integration than this session's spec asked for: route, sig verify, all 4 event handlers with 48h grace, customer create + persist, portal + status endpoints, founder-code system, and usersuser_profiles dual writes. Raw-body middleware was already correctly positioned at src/app.js:52 (before global express.json()).

What this session added on top:

  • tests/integration/stripe.test.js — refactored stripe mock to a singleton handle, then added two route-level tests:
    1. constructEvent throws → route returns 400 with { error: /signature/i }
    2. valid signature → route dispatches to handleWebhookEvent and returns { received: true }
  • tests/unit/stripeService.test.js — added customer.subscription.updated test covering portal-driven plan-change: maps items.data[0].price.id back to a tier via PRICE_MAP, writes to both users + user_profiles, clears grace.
  • docs/SYSTEM-MANIFEST.md — appended a Payments: dual-provider divergence subsection under § 8 Findings → Frontend ↔ Backend contract, documenting that the Next.js /api/checkout still routes to NexaPay while Express Stripe is wired but uncalled by the frontend, with a 4-step cutover punch list for a follow-up session.

Quality gates (all green)

  • npm test: 1042 / 1042 passing (delta +3 from 1039 baseline, 0 regressions)
  • web/npm run build: clean
  • License audit: third-party deps only permissive (MIT/Apache-2.0/BSD/ISC/MPL/BlueOak/CC-BY/0BSD)
  • curl https://api.vyndr.app/api/health{"status":"healthy"}

Session 7h (2026-06-10) — SHIPPED

Stripe (test mode)

Resources created against sk_test_* via direct REST API (Stripe MCP plugin OAuth flow was non-functional in this environment; bypassed by hitting https://api.stripe.com/v1 with the secret key in a single shell subprocess, then shredding the on-disk key file).

  • prod_UgBel9RYTROCxr — VYNDR (metadata.tier=analyst)
    • price_1TgpGxIp1Mec3r2E6Wh6oeaP — $14.99/mo recurring (metadata.tier=analyst)
  • prod_UgBeSBYw2j9oXL — VYNDR Desk (metadata.tier=desk)
    • price_1TgpGyIp1Mec3r2EQq50KKhF — $44.99/mo recurring (metadata.tier=desk)
  • we_1TgpGzIp1Mec3r2ERtDIF2n2 — webhook → https://api.vyndr.app/api/stripe/webhook
    • Subscribed events: checkout.session.completed, customer.subscription.updated, customer.subscription.deleted, invoice.payment_failed
    • Signing secret saved to ~/.stripe-webhook-secret (chmod 600) — read once, paste into Coolify, then shred -u.

Tier infrastructure

  • src/config/tiers.js — frozen access matrix (free / analyst / desk); api_access:false on every tier (non-negotiable consumer-product invariant)
  • src/middleware/scanLimit.js — 24h rolling per-user/IP quota (free=3, analyst=15, desk=∞); 429 + Retry-After + X-Scans-Used/Limit headers on overflow; in-memory LRU with MAX_TRACKED=50_000
  • src/utils/tierGating.js — pure response gating; free tier keeps grade/confidence/edge_pct, redacts reasoning + kill_conditions_triggered; paid tiers pass through
  • Wired into src/routes/scan.js (/parlay after requireAuth) and src/routes/analyze.js (/prop + /batch, gating applied per-result)

SQL (run manually in Supabase SQL Editor)

  • docs/sql/pricing_slots.sql — creates pricing_slots table + RLS + price IDs seeded. Not added to the migrations chain per session policy.

Tests

  • tests/unit/tiers.test.js (10 tests) — frozen matrix, api_access=false invariant, fallback behavior
  • tests/unit/tierGating.test.js (9 tests) — free-tier redaction, paid passthrough, no input mutation
  • tests/unit/scanLimit.test.js (10 tests) — per-tier limits, anonymous IP fallback, independent quotas, desk skip
  • Existing suites adapted for the new middleware: tests/unit/analyzeCache.test.js, tests/integration/analyze.test.js, tests/integration/scan.test.js reset the scan-limit map in beforeEach; the integration suite for /api/analyze mocks applyTierGating as pass-through so engine-shape assertions stay focused on the engine contract (gating has its own suite).

Quality gates (all green)

  • npm test: 1039 / 1039 passing, 82 suites, 0 failures
  • web/npm run build: production build clean, all 24 routes prerendered
  • License audit: only permissive third-party licenses (MIT/Apache-2.0/BSD/ISC/etc.); single UNLICENSED entry is our own vyndr-web workspace

Web Tier v6 (2026-05-18) — SHIPPED

Complete frontend overhaul. 18 pages, 22 API routes. npm run build passes with zero errors.

New pages

  • /dashboard — post-login slate (sport tabs, top grades, tonight's games, most parlayed, recent scans, first-time onboarding)
  • /game/[id] — game preview with spread/total/ML, starting lineups with injury flags, expandable prop list, add-to-parlay
  • /profile — tier status, subscription state, founder badge, cancel-at-period-end flow
  • /intelligence — Desk-tier timeline of evolution/coaching/cascade/ABS/line-movement signals (blurred for non-Desk)
  • /terms, /privacy, /responsible-gambling — branded legal pages with brand voice
  • /scan — full rebuild (sport tabs, real /api/scan with tier gating, parlay tray hook)
  • /login, /signup — wired to Supabase Auth via AuthContext (Google OAuth + email/password + age check)
  • /marketplace — coming-soon waitlist (API access, custom alerts, capsule drop)
  • /ledger, /tracker — design system refresh, accuracy buckets, miss autopsy, quick-slip
  • / — auth-aware: logged-in users redirect to /dashboard; anonymous see marketing

New API routes

  • /api/games/tonight, /api/games/[id], /api/games/[id]/props
  • /api/props/top-graded, /api/props/most-parlayed
  • /api/players/search
  • /api/user/recent-scans
  • /api/intelligence/feed
  • /api/parlay/add-leg, /api/parlay/grade
  • /api/ledger, /api/ledger/accuracy
  • All cached via Supabase odds_cache table (5-min TTL) — never hit Odds API directly

Services + middleware

  • services/odds-cache.ts — Supabase-backed TTL cache for upstream calls (loader + stale-fallback)
  • services/email.ts — Resend wrapper: sendWelcomeEmail, sendPaymentReceipt, sendRenewalReminder
  • middleware/rateLimit.ts — per-tier per-minute scan throttle (5/30/60 free/analyst/desk)
  • services/nexapay.ts — already shipped (createPaymentLink + HMAC webhook verify), now wired to email receipts

Components

  • GradeCard.tsx — premium grade card with tier-gated blur (factors locked for free; alt-lines locked for non-Desk)
  • ParlayContext.tsx + ParlayTray.tsx — cross-page parlay state, slide-up tray, /api/parlay/grade integration
  • BottomTabBar.tsx — mobile-only 5-tab navigation (Home/Scan/Parlay/Ledger/Profile) with parlay badge
  • ShareCard.tsx — canvas-rendered 1200x630 OG share image with grade letter; download + copy-to-clipboard
  • Nav.tsx, Hero.tsx, LivePropsStrip.tsx, Features.tsx, Pricing.tsx, HowItWorks.tsx, FAQ.tsx, Footer.tsx — design system refresh already shipped

PWA + meta

  • public/manifest.json (192/512/maskable icons)
  • public/icons/icon-{192,512,maskable-512}.png, apple-touch-icon.png, favicon.ico, favicon.png
  • public/og-image.png — 1200x630 social share card
  • appleWebApp + manifest + theme-color wired in layout.tsx

Supabase migrations

  • 011_user_profiles_web.sql (already deployed): user_profiles (+RLS+trigger), parlay_leg_frequency (+RPC), scan_history
  • 012_web_caching_waitlist.sql (NEW): odds_cache (TTL cache), waitlist_signups, founder_pricing_seats view, prune_expired_odds_cache() helper

Backend

  • src/app.js — CORS middleware added (localhost dev + vyndr.app + *.vercel.app + FRONTEND_ORIGINS env var)
  • package.json — added cors@2.8.5

Bug fixes

  • Scan page sibling-div JSX bug fixed (rewritten from scratch)
  • Lockfile warning silenced via next.config.ts turbopack.root (already in place)
  • Auth callback rewritten to use Supabase JS session API instead of raw localStorage parse

Environment variables (set in Vercel + Railway)

Vercel (Next.js)

  • NEXT_PUBLIC_SUPABASE_URL — Supabase project URL
  • NEXT_PUBLIC_SUPABASE_ANON_KEY — Supabase anon key
  • SUPABASE_SERVICE_ROLE_KEY — service role (server-only, NEVER expose to client)
  • NEXT_PUBLIC_SITE_URLhttps://vyndr.app
  • BACKEND_URL — Railway URL of Express grading engine
  • NEXT_PUBLIC_API_URL — same as BACKEND_URL (for legacy client fetches)
  • NEXT_PUBLIC_NBA_SERVICE_URL — FastAPI nba_api wrapper URL
  • NEXAPAY_API_KEY — bearer token from NexaPay dashboard
  • NEXAPAY_WEBHOOK_SECRET — HMAC secret from NexaPay dashboard
  • NEXAPAY_API_URL — defaults to https://api.nexapay.one/v1
  • RESEND_API_KEY — from resend.com
  • RESEND_FROM_EMAIL — defaults to VYNDR <grades@vyndr.app>
  • NEXT_PUBLIC_POSTHOG_KEY — PostHog project key (optional)
  • NEXT_PUBLIC_POSTHOG_HOST — defaults to https://us.i.posthog.com

Railway (Express backend)

  • All existing engine vars (Odds API key, Supabase, etc.)
  • FRONTEND_ORIGINS — comma-separated additional CORS origins (optional; defaults cover localhost + vyndr.app + *.vercel.app)

Vercel deployment

  1. Repo root → /home/kev/mastermind/vyndr
  2. Root Directory in Vercel project settings: web
  3. Framework Preset: Next.js (auto-detected)
  4. Build Command: npm run build (default)
  5. Install Command: npm install (default)
  6. Output Directory: .next (default; we use output: 'standalone')
  7. Node version: 20.x or 22.x
  8. Add all env vars from the list above

Railway deployment (backend)

  1. railway.toml already configured in repo root
  2. Connect GitHub → Deploy from main
  3. Set env vars (same as Vercel backend list)
  4. Get URL → set BACKEND_URL in Vercel

NexaPay configuration

  1. Create NexaPay account → get API key + webhook secret
  2. Webhook URL: https://vyndr.app/api/webhook/nexapay
  3. Webhook events to enable: payment.succeeded, payment.failed, payment.refunded, subscription.canceled
  4. Settlement wallet: USDC on Polygon (or your preferred chain)
  5. Set NEXAPAY_* env vars in Vercel

Resend configuration

  1. Create Resend account → verify vyndr.app domain
  2. Add DNS records (SPF, DKIM, DMARC) from Resend dashboard
  3. Create API key → set RESEND_API_KEY in Vercel
  4. Test: trigger a signup, check the welcome email arrives

Supabase Auth setup

  1. Run migrations 011_user_profiles_web.sql and 012_web_caching_waitlist.sql (Supabase SQL editor or CLI)
  2. Auth → Providers → enable Email/Password (default)
  3. Auth → Providers → enable Google: paste client ID/secret from Google Cloud Console
  4. Auth → URL Configuration → Site URL: https://vyndr.app
  5. Auth → URL Configuration → Redirect URLs: https://vyndr.app/auth/callback, http://localhost:3001/auth/callback

What Has Shipped (Backend — Already Live)

Phase 1 — Foundation (COMPLETE)

  • Feature 1.1 — Odds API Integration
  • Feature 1.2 — NBA_API Stats Wrapper (FastAPI microservice)
  • Feature 1.3 — Prop Analysis Engine (6-step grading pipeline)
  • Feature 1.4 — Database Schema (9 tables, RLS, triggers)
  • Feature 1.5 — Bet Submission (3 methods + performance tracking)

Phase 2 — Core Product (COMPLETE)

  • Feature 2.1 — Parlay Scan (correlation detection, monetization)
  • Feature 2.2 — Line Movement + Cascade Detection

Phase 3 — Web MVP (COMPLETE)

  • Feature 3.1 — Landing Page + Blog (Next.js, MDX, VYNDR voice, SEO)
  • Feature 3.2 — Scan UI (leg builder, grade results, upgrade pitch)
  • Feature 3.3 — Bet Tracker (performance dashboard, quick slip, settle flow)
  • Feature 3.4 — Stripe Integration (checkout, webhooks, portal, founder codes)

Also Shipped (Separate Repo)

Mastermind Agency Site

  • /home/kev/mastermind/agency-site/
  • Glitch aesthetic, scan lines, CRT flicker, JetBrains Mono
  • Home, VYNDR case study, Contact pages

Phase 1 Additions — Intelligence Engine (COMPLETE)

  • Addition 1 — Stats endpoints (parlays-graded, public, live props)
  • Addition 2 — Dynamic role profile system (8 roles, Shannon entropy, conditional profiles)
  • Addition 3 — Player selector (placeholder — Cowork handles design)
  • Addition 4 — Parlay probability (phi coefficient, juice-adjusted EV, correlation math)
  • Addition 5 — MLB prop grading (14 stat types, 10 kill conditions, 30 parks, weather API)
  • Addition 6 — Intelligence engine (similarity, evolution/PELT, line discrepancy, alt line, Bayesian, model trainer)
  • Addition 7 — Lineup watch speed (role activation detection framework)
  • Addition 8 — Database additions (7 new tables, migration 003, indexes, RLS)
  • Addition 9 — Design system update (forest green, Hero tagline, live props strip, DemoScan result card)
  • Addition 10 — Accuracy ledger page (/ledger)
  • Addition 11 — Marketplace page (/marketplace, waitlist, honeypot)
  • Addition 12 — ARCHITECTURE.md v1.0
  • Permanent: FOUNDER_NOTE constant (immutable, tested for integrity)
  • Permanent: X-VYNDR-Mission header on all API responses

Also Shipped (Separate Repo)

Mastermind Agency Site

  • /home/kev/mastermind/agency-site/
  • Glitch aesthetic, scan lines, CRT flicker, JetBrains Mono
  • Home, VYNDR case study, Contact pages

Test Summary

  • Node.js: 662 tests passing (unit + integration) — 357 original + 187 ship + 65 supplement + 35 patch + 45 security
  • Python: 27 tests passing
  • Total: 689 tests, all green
  • 8 new test files: shipInfrastructure, shipGradingEngine, shipDataSources, shipResolution, shipSchemeClassifier, supplementSystems, patchIntegration, securityAudit
  • Next.js project builds (pending Vercel deploy)

Active Blockers

  • BLOCKER-003: WSL2 DNS cannot resolve *.supabase.co
  • Migrations 003-010 need manual apply via Supabase SQL Editor

Phase 1 Additions Part 2 (COMPLETE)

  • Addition 13 — Simplified scan selector (sport toggle NBA/MLB, player search, stat dropdown, line pre-fill from Odds API)
  • Addition 14 — PostHog analytics integration (5 events: scan_completed, grade_viewed, upgrade_cta_clicked, prop_shared, alt_line_viewed)
  • Addition 15 — Affiliate database (Migration 004: referral_codes, referral_conversions, affiliate_payouts, wallet_addresses, RLS on all)
  • Addition 16 — Scheme intelligence data layer (schemeClassifier.js: PnR coverage classification DROP/SWITCH/HEDGE/MIXED/UNKNOWN, 8-possession min, 6hr cache, graceful degradation, silent logging to model_predictions_extended)
    • Scheme intelligence: data layer active, user activation pending Day 31

Phase 2 Pending

  • Model learning loop (Feature 4.1 spec exists)
  • Player selector UI completion (Cowork handles design)
  • Full parlay probability UI integration
  • Real-time lineup watch CRON implementation
  • Evolution watch UI on ledger page
  • Pre-registered predictions system activation
  • Physical ledger fulfillment
  • Education library content

Manual Actions Required

  1. Paste SQL migrations 003-010 in Supabase SQL Editor (in order)
  2. Run node scripts/seedRoleProfiles.js after NBA API access configured
  3. Set Stripe env vars (STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, price IDs)
  4. Set NEXT_PUBLIC_POSTHOG_KEY env var for PostHog analytics
  5. Set ODDS_API_KEY env var for Odds API
  6. Set SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY for Python service
  7. Deploy Next.js frontend to Vercel
  8. Start Python service: cd src/services/python && pip install -r requirements.txt && python3 app.py
  9. Set up GitHub Actions crons: lineup monitoring (15min), morning odds (10am ET), pre-game odds (90min), weather (30min), nightly resolution (2am ET)
  10. Run cold_start_boot() on first launch (seeds reporters, loads data files)
  11. SHADOW_MODE=True for first 2 weeks — grades logged but not published to capper

Session Log

Sessions 1-6 — 2026-03-21/22

  • Built all backend: Phase 1 + Phase 2 + Feature 1.5
  • 221 backend tests passing

Session 7 — 2026-03-22

  • Built Feature 3.1: Landing page + blog (Hero, Pricing, Blog/MDX, Auth pages)
  • Built Mastermind Agency Site (glitch aesthetic, 5 pages)
  • Built Features 3.2 + 3.3: Scan UI + Bet Tracker
  • Built Feature 3.4: Stripe Integration (checkout, webhooks, portal, founder codes)
  • ALL FEATURES COMPLETE
  • Total: 237 tests (210 Node.js + 27 Python), all green

Session 8 — 2026-03-28

  • Built all 12 Phase 1 additions in single session
  • 68 new tests (305 total), all green
  • New services: roleProfileEngine, roleStabilityEngine, similarityEngine, evolutionEngine, lineDiscrepancyDetector, altLineScanner, bayesianEngine, modelTrainer, correlationMath, mlbGrader, mlbKillConditions, mlbStatsClient
  • New routes: stats, props, waitlist
  • New frontend: LivePropsStrip, ledger page, marketplace page
  • New constants: founderNote, mlbParks
  • New middleware: mission header
  • Migration 003: 7 new tables with indexes and RLS
  • Python microservice: evolutionEngine.py (Flask/PELT on port 5001)
  • ARCHITECTURE.md v1.0 created

Session 9 — 2026-04-12

  • Built 4 Phase 1 Part 2 additions
  • 52 new tests (357 total), all green
  • New component: SimplifiedSelector (sport toggle, player search, stat dropdown, line pre-fill)
  • PostHog analytics: 5 tracked events, initialized in layout.tsx
  • Migration 004: 4 affiliate tables (referral_codes, referral_conversions, affiliate_payouts, wallet_addresses)
  • New service: schemeClassifier.js (PnR coverage classification, 6hr cache, graceful degradation)
  • Scheme intelligence: data layer active, user activation pending Day 31

Session 10 — 2026-04-13 (SHIP BUILD v5.1)

  • Built complete dual-sport grading engine from vyndr-SHIP.md spec
  • 187 new tests (544 total), all green across 38 test suites
  • Phase 1 — Infrastructure:
    • Flask app.py with blueprints, health check, rate limiting (60/min default, 20/min grade), flask-cors, /api/docs
    • evolutionEngine.py moved to blueprints/evolution.py (structural only — logic unchanged)
    • utils: retry.py, data_warehouse.py (game-day TTL), bayesian.py (per-stat-type weights, skewness, data sufficiency curve), edge_calculator.py (real edge + quarter-Kelly), context_aggregator.py (15 factors), similarity.py (min 0.7), regime_detector.py (disabled <20 games), blind_spot_detector.py (worst 5%), supabase_client.py
    • Data files: park_factors.json (30 parks, lat/lng, roof_status), reporter_database.json (80+ handles), timezone_map.json (30 arenas), grade_thresholds.json, odds_api_config.json
    • requirements.txt with all 15 dependencies
    • Cold start boot sequence with reporter seeding
  • Phase 2 — Data Sources:
    • blueprints/synergy.py (team play types, matchup, tracking, defensive scheme)
    • blueprints/nba_context.py (teammate impact, game script, home/road, rest/travel, matchup pace, foul trouble, B2B stat-specific, positional defense, usage-efficiency, playoff modifiers, NBA sub-scores endpoint)
    • blueprints/lineup_intelligence.py (3-source architecture, reporter trust tiers, tweet parsing, two-stage grading, reporter-line correlation)
    • blueprints/odds_scanner.py (free tier 2 pulls/day, odds warehouse, line movement detection, slate scanner)
    • utils/weather.py (Open-Meteo, continuous 30min, dome detection, regrade triggers)
    • utils/archetypes.py (5 pitcher dimensions, 5 batter dimensions, 6 NBA dimensions — ALL with weight_profiles, batting order, batter approach, pitcher identity, weight blending)
    • schemeClassifier.js enhanced: Synergy-first with regex fallback, backward compatible
  • Phase 3 — Grading Engines:
    • blueprints/mlb.py (14-step pipeline, pitcher/batter profiles, ABS challenge system with player-specific discipline score, TTO decay, platoon-specific opponent quality, lineup protection, day/night, bullpen state, catcher framing)
    • blueprints/image_grade.py (OCR pipeline with low-confidence confirmation)
    • utils/sportsbooks.py (10 books, parlay grading with correlation check, phi coefficient)
    • utils/capper.py (pick numbers, breaking alerts, daily recap, miss autopsy)
  • Phase 4 — Self-Improving Loop:
    • blueprints/resolution.py (nightly job: actuals from nba_api/MLB-StatsAPI, hit/miss, CLV, alignment, joint outcomes, calibration triggers)
    • blueprints/calibration.py (point-biserial weights, global offset, Brier score, blind spots, CLV/alignment reports)
  • Phase 5 — Database + Tests:
    • Migration 005: lineup_scheme_data
    • Migration 006: nba_data_cache, mlb_data_cache, grade_outcomes (ALL ship columns incl discipline_score, CLV, alignment), player_calibrated_weights
    • Migration 007: lineup_updates, reporter_trust (with source_type + starting_trust), odds_warehouse, ship_line_movements, reporter_line_correlation, api_health_log, global_calibration, ship_joint_outcomes
    • 5 new test files covering infrastructure, grading engine, data sources, resolution pipeline, scheme classifier enhancement
  • Key Spec Compliance:
    • Grade thresholds LOCKED (A+ through F)
    • SHADOW_MODE = True (first 2 weeks)
    • Bayesian weights are INITIAL ESTIMATES (marked as such)
    • Abstention check BEFORE data cap
    • Point-biserial bounds 0.05-0.50, global offset ±0.15
    • Real edge with vig + quarter-Kelly
    • Brier + CLV from day one
    • Capper A- and above ONLY
    • ABS is CHALLENGE system (successful challenges don't deplete)
    • Foul trouble widens std, not mean
    • Stat-specific B2B adjustments
    • Matchup-specific pace (home 60/40)
    • Positional defense (tracking > roster position)
    • Usage-efficiency tradeoff (-1.5% TS per +5% usage)
    • Tier limits documented but NOT enforced (gate manually later)
    • Node.js stays Node.js, Python is data/utility layer via HTTP

Session 10c — 2026-04-13 (FINAL INTEGRATION PATCH)

  • Applied 15-item integration patch — wiring + features + infrastructure
  • 35 new tests (644 total), all green across 40 test suites
  • Wiring (items 1-5):
    • Scratch → redistribution → re-grade → alt line scan → alert chain in lineup_intelligence.py
    • Slate scan → alt line auto-scan for A-grades in odds_scanner.py
    • Nightly resolution steps 14-18: coaching update, player-out history, evolution scan, unconventional data collection, monthly validation
    • Migration 009: supplement columns on grade_outcomes (coaching_context, redistribution_context, evolution_flag, alt_line_opportunity, unconventional_factors) + unconventional_factor_data table
    • API docs updated with 7 supplement endpoints
  • Features (items 6-10):
    • MLB lineup shift logic (PA multiplier changes when player scratched)
    • high_leverage_hook_tendency added to MLB coaching schema
    • Evolution persistence check (3 games before public promotion, false positive detection)
    • Unconventional daily data collection + monthly validation functions
    • Alt line ladder mode (ALT_LINE_MODE env var — 'manual' generates probability ladder)
  • Infrastructure (items 11-15):
    • 5 GitHub Actions YAML files: nightly (2am ET), morning odds (10am ET), pre-game (3pm/5pm/6:30pm ET), reporter poll (every 15min), weather (every 30min)
    • scripts/seed_historical.py — one-time historical data seeder (NBA 2024-25 + MLB 2024)
    • railway.toml (Flask service, port 5001, health check)
    • web/vercel.json (Next.js deployment)
    • MLB coaching helper functions for historical seeding
  • Product is DEPLOYMENT-READY

Session 10d — 2026-04-13 (SECURITY AUDIT)

  • Applied 19-item security hardening pass — Ryan Montgomery panel reviewed
  • 45 new tests (689 total), all green across 41 test suites
  • Authentication (items 1, 8, 11):
    • utils/auth.py: require_auth (JWT with issuer check) + require_service_role (BETONBLK_INTERNAL_KEY)
    • PyJWT added to requirements.txt
    • BETONBLK_INTERNAL_KEY separates cron auth from service key — service key never leaves Railway
  • Input Security (items 3, 10, 13):
    • utils/validation.py: whitelist stat types, sanitize strings (strip SQL/HTML), validate line 0-500, image upload (magic bytes, 10MB max, PNG/JPEG/GIF), parlay legs 2-12
    • OCR rate limit 3/min, max 2 concurrent
    • MAX_CONTENT_LENGTH 1MB globally, 413 JSON response
  • Network Security (items 2, 12):
    • CORS locked to ALLOWED_ORIGINS env var (no more wildcard)
    • Real IP from X-Forwarded-For for rate limiter and security logger
  • Error Handling (item 9):
    • Production returns generic "Internal server error" — no stack traces
    • 404, 405, 413, 429 all return JSON
  • Monitoring (items 4, 5, 6, 15, 17, 18):
    • Security headers: X-Frame-Options DENY, HSTS, CSP, nosniff, XSS protection, Server removed
    • utils/security_logger.py: request logging, rate tracking, SQL injection detection, security_events table
    • utils/env_check.py: startup validation, exits on missing required vars, never logs secrets
    • security-scan.yml: weekly pip-audit + npm audit
    • security.txt: /.well-known/security.txt with contact
    • 90-day security event retention cleanup + weekly security digest (50+ events per IP = action required)
  • Infrastructure (items 7, 14, 16, 19):
    • Migration 010: security_events table with RLS
    • Supabase client timeout guidance, retry with 30s default timeout
    • Source code secret scan test (sk_live_, eyJhbGci, sbp_)
    • .gitignore: .env, .env.local, .env.production, *.pem, *.key, .vercel/

Session 10b — 2026-04-13 (SUPPLEMENT BUILD)

  • Built 5 intelligence supplement systems — ADDITIVE, no existing code modified
  • 65 new tests (609 total), all green across 39 test suites
  • System 1 — Coaching Tendency Database:
    • blueprints/coaching.py (NEW) — per-coach NBA + MLB tendencies, nightly update from game logs, shift detection (15%+ threshold on last 15 vs season baseline)
    • 12 NBA fields (pace, 3PT rate, ISO freq, PnR usage, rotation depth, late-game player, score-state lineups, second-unit patterns, redistribution profile, shot location, timeout tendency)
    • 10 MLB fields (starter hook, quick hook, bullpen philosophy, IBB rate, PH freq, bunts, closer-only, platoon, lineup consistency, challenge aggressiveness)
  • System 2 — Usage Redistribution Engine:
    • blueprints/redistribution.py (NEW) — two-layer calculation (Layer A: minutes redistribution from historical player-out data + coaching rotation depth; Layer B: offensive system change from archetype shifts)
    • Uses coaching database, applies usage-efficiency tradeoff (-1.5% TS per +5% usage)
    • Three tiers: primary (>=0.20 boost, >=0.75 confidence), secondary (>=0.10, >=0.60), tertiary (>=0.05)
    • Auto-grades at 15%+ boost / 0.65+ confidence, formats 60-second absorption alerts
  • System 3 — Alt Line Scanner:
    • Added to existing odds_scanner.py — auto-runs on A-grade props after slate scan
    • Pulls alt lines from odds_warehouse, calculates model probability via Bayesian norm_cdf
    • Real edge with vig on each alt, finds optimal (best EV/dollar)
    • Only recommends if alt edge exceeds standard by 3%+
  • System 4 — Unconventional Data Pipeline:
    • blueprints/unconventional.py (NEW) — validation gate for non-traditional correlates
    • 500 instance minimum, Pearson r > 0.15, Bonferroni-corrected p-value
    • 5 tracked factors: altitude, contract year, referee crew history, travel distance (pre-validated), arena altitude
    • Factors only enter grading engine AFTER passing validation
  • System 5 — Player Evolution Alerting:
    • Added to existing evolution.py — daily scan across multiple metrics simultaneously
    • NBA: usage_rate, assist_rate, three_pa_rate, fg_pct, minutes
    • MLB: k_rate, bb_rate, exit_velocity, hard_hit_pct, fb_velo
    • PLAYER_EVOLUTION_DETECTED when 2+ metrics show concurrent inflection (10%+ change, 15 game minimum)
    • Timestamped records in evolution_detections table, Evolution Watch content formatter
  • Migration 008: coaching_tendencies, player_out_history, evolution_detections, unconventional_validations (all with indexes + RLS)
  • Integration: 3 new blueprints registered in app.py (coaching_bp, redistribution_bp, unconventional_bp), evolution + odds_scanner extended with new endpoints