# VYNDR — Build State ## Last Updated 2026-06-10 ## Current Phase SHIP BUILD v13.0 — The Slate (browse-first dashboard) + OAuth providers + Africa geo (Session 13) ## Session 13 (2026-06-11) — SHIPPED ### Phase 1 — Africa geo-restriction via CF-IPCountry The Session 12 Africa tier was visible to anyone on a Swahili locale (too narrow: most African users browse in English/French; too broad: Swahili speakers anywhere got the discount). Session 13 swaps the locale proxy for real Cloudflare IP geolocation. - **`web/middleware.ts`** — reads `cf-ipcountry` (uppercase), stamps `x-vyndr-country` on the request alongside the locale header. Empty string when traffic bypasses Cloudflare (local dev). - **`web/src/lib/locales.ts`** — `AFRICAN_COUNTRIES` set covering all 54 sovereign African nations (NG/KE/ZA/GH + sub-Saharan + MENA overlap). `isAfricanCountry(code)` is case-insensitive and degrades closed on empty/null inputs. - **`LocaleContext`** — extended with `country`/`inAfrica` fields; new `useRegion()` hook for components that gate by geography. - **`Pricing.tsx`** — `inAfrica === false` filters the Africa tier out of the render entirely. `inAfrica === true` puts it first. Locale-based reorder removed. - **Pricing grid CSS** — desktop column count now tracks the visible tier count via a `--pricing-cols` CSS custom property on the grid root (3 outside Africa, 4 inside). Sidesteps a styled-jsx limitation with attribute selectors inside `:global()`. ### Phase 2 — OAuth: Google + Apple + X - **`AuthContext`** — added generic `signInWithProvider(provider)` alongside the legacy `signInWithGoogle()` (kept as an alias so existing callers don't break). Translates Supabase OAuth errors into a flat `{ error: string }` so the UI can surface a friendly inline message when a provider isn't configured. - **`login/page.tsx` + `signup/page.tsx`** — both pages now render three OAuth buttons (Google, Apple, X). The `handleOAuth` helper routes to `signInWithProvider` and shows an inline error when the provider isn't configured ("apple login isn't available yet. Use email or another method."). - **External configuration required** (operator action, not code): - Supabase Auth → Providers → Apple: needs an Apple Developer Service ID + private key - Supabase Auth → Providers → Twitter: needs an X Developer OAuth 2.0 client - Google should already work — if it doesn't, verify Supabase Auth → URL Configuration → Site URL = https://vyndr.app and Redirect URLs include `https://vyndr.app/**`, and that the Google Cloud Console OAuth consent screen has the Supabase callback URL in Authorized redirect URIs. ### Phase 3 — The Slate (browse-first dashboard) Generalizes the Session 8 `/soccer` page pattern across every sport. - **`web/src/components/PropRow.tsx`** — single-prop UI with three states (ungraded/grading/graded). Pure presentational — parent owns the API call so there's one shared rate-limited grading queue. Free-tier expansion shows blurred reasoning + Unlock CTA; paid tier shows full reasoning + kill conditions. Exports `propRowKey()` for stable Map keys. - **`web/src/components/GameCard.tsx`** — game header + expandable prop list. Sport emoji prefix (🏀 NBA/WNBA, ⚾ MLB, ⚽ soccer), sport-accented left border, formatted local game time, `+ N more` expander when props > defaultVisible. - **`web/src/components/Slate.tsx`** — the orchestrator. Sport tabs (ALL / NBA / WNBA / MLB / Soccer), sticky search input, group-by-game pipeline, `gradedProps` Map, single-flight grading queue (`gradingKey`). `Promise.allSettled` fan-out for the ALL tab so a single sport failing doesn't blank the slate. `FETCH_URLS` is null-aware — sports without an odds proxy yet (WNBA, MLB) render a bottom-of-page "endpoint not configured yet" note rather than spamming 404s. - **Search filter + manual-scan fallback** — sticky search filters game cards by team name and prop rows by player/stat. Empty result shows a CTA linking to `/scan?q=` so users land on a partially-filled scan form. - **`/dashboard`** — `` mounted as the lead surface above the existing Top Graded / Most Parlayed / Recent Reads sections. Those sections stay as supplementary intelligence layers — not removed. - **`Nav.tsx`** — "Scan" link removed from primary nav. The Slate is the scan surface; `/scan` stays reachable from the slate's empty-state CTA. ### Tests added | Suite | Tests | |----------------------------------------|-------| | `tests/unit/africaCountries.test.js` | 6 | | **Session 13 total** | **6** | ### Quality gates - `npm test`: **1311 / 1311 passing** (1305 + 6 new), 102 suites, 0 regressions - `web/npm run build`: clean — Slate page + components prerender - License audit: third-party deps remain permissive ### Honest gaps (documented, not bugs) - I could not visually verify The Slate in a browser. Build/type correctness is confirmed; "renders correctly with live odds data" needs a deploy smoke test. - Google/Apple/X OAuth: button wiring is complete. Whether the buttons actually authenticate depends on external dashboard configuration (Supabase + Google Cloud Console + Apple Developer + X Developer Portal). Apple and X are guaranteed to show the "isn't available yet" inline error until configured. - WNBA + MLB don't have `/api/odds/*` proxies on the Next.js side yet. The Slate degrades cleanly (footer note), but those tabs return empty until the proxies exist. Session-14 work. - Africa tier still can't be SOLD even when geo gates open it — the Stripe price + the DB CHECK migration remain outstanding from Session 12. ### Coolify env (Session 13 additions) None. CF-IPCountry is set by Cloudflare automatically; no env-var change required. --- ## Session 12 (2026-06-11) — SHIPPED ### FIX 1 — i18n infrastructure (10 languages, cookie-based) Honest scope decision: skipped the full `[locale]/` URL-prefix refactor (would have touched all 24+ pages). Went cookie-based + header-stamping middleware instead — same UX, much smaller blast radius. URL-prefix routing can layer on later without breaking anything. - **`web/src/lib/locales.ts`** — locale registry. 10 locales: en (source), es, fr, pt, ar (RTL), sw, hi, ja, ko, zh. `LOCALE_META` carries native names + dir + region. `AFRICA_LOCALES = {sw}` used by the pricing reorder logic. - **`web/src/middleware.ts`** — locale resolver. Priority: URL prefix → `NEXT_LOCALE` cookie → `Accept-Language` parsing → default 'en'. Stamps `x-vyndr-locale` on the request so server components can read it via `next/headers`. - **`web/src/locales/{en,es,fr,pt,ar,sw,hi,ja,ko,zh}.json`** — 10 translation dictionaries, each ~17 keys covering nav, slate, grade, pricing, sports, auth, common, cookie. Every file declares its `_meta.review_status`: `en` is `source`, the other 9 are `translated_unreviewed`. Sports terminology is locale-correct (Fútbol/Football/サッカー/كرة القدم/Soka, etc.). - **`web/src/lib/i18n.ts`** — synchronous server-side loader (`getTranslations(locale) → {t, locale, dir}`) plus `getServerTranslations()` which reads the middleware-stamped header. English fallback per key, falls to the key string itself when missing on both. `{name}` interpolation supported. - **`web/src/contexts/LocaleContext.tsx`** — client provider + `useT()` / `useLocale()` hooks. Mounted in the root layout above every other provider. - **RTL** — `` set in root layout when locale is Arabic. `globals.css` flips nav direction and isolates monospace blocks (numbers stay LTR — financial data convention). - **`LocaleSwitcher.tsx`** — compact mono dropdown with native language names. Sets the cookie, reloads the page. Mounted in Nav for both authenticated and anonymous states. - **Wired into**: Nav (5 links + login button), CookieConsent (message + accept + privacy link), Pricing (CTAs translate per tier). High-impact components first; longer-tail strings remain English with `t('key')` calls scheduled for a follow-up. ### FIX 2 — Africa tier ($4.99/mo) - **`src/config/tiers.js`** — adds the `africa` tier between free and analyst: 10 scans/day, reasoning_visible:true, kill_conditions_detail:true, alerts:false, api_access:false. Frozen. - **Scan-limit middleware** — no change needed. `scanLimit()` reads via `getScanLimit()`, which now resolves 'africa' to 10. - **`web/src/components/Pricing.tsx`** — adds the VYNDR Africa card. The pricing-grid CSS unfolds from 2-up (tablet) to 4-up (≥1100px desktop). When the user's locale is Swahili (a proxy for African markets — IP-based geolocation deferred to a future session), the Africa tier renders FIRST. - **Honest UX gap**: Africa-tier checkout short-circuits to an inline "coming soon" message instead of triggering Stripe. Two reasons: (a) the backend `/api/stripe/checkout` route validates tier against `['analyst','desk']` and the spec forbids backend edits this session; (b) `STRIPE_PRICE_AFRICA` is unset and the Stripe product hasn't been created in the dashboard yet. - **DB CHECK constraint blocker**: migrations 001 + 011 declare `tier IN ('free','analyst','desk')`. The webhook will 23514 (check_violation) if it tries to write `africa` until the constraint is extended. Documented in `tiers.js` header + in SYSTEM-MANIFEST. Out of scope this session per the no-migration rule. - **`.env.example`** — `STRIPE_PRICE_AFRICA=price_...` placeholder with explanatory comment. ### Tests added (Session 12) | Suite | Tests | |------------------------------------|-------| | `tests/unit/i18n.test.js` | 14 | | `tests/unit/tiers.test.js` (extended) | +5 | | **Total new** | **19** | ### Quality gates - `npm test`: **1305 / 1305 passing** (1286 + 19), 101 suites, 0 regressions - `web/npm run build`: clean. NOTE — every page is now `ƒ Dynamic` rather than `○ Static` because the root layout reads request headers (`next/headers`) for locale resolution. This is the expected cost of SSR i18n. If FCP regresses, the fallback is client-side cookie reads (brief English flash on first paint, but static prerender returns). - License audit: third-party deps remain permissive (no new licenses introduced — translation files are JSON in our own repo). ### Open items / follow-ups 1. **DB CHECK constraint** must be updated before the Africa tier can actually be assigned to users. Manual SQL: ``` ALTER TABLE users DROP CONSTRAINT users_tier_check; ALTER TABLE users ADD CONSTRAINT users_tier_check CHECK (tier IN ('free','africa','analyst','desk')); -- same for user_profiles ``` 2. **Stripe product** for VYNDR Africa not created. Manual step: create the product + price in the Stripe dashboard, set `STRIPE_PRICE_AFRICA` in Coolify, then extend the backend checkout route's validation list. 3. **Translation review** — only `en` is `source` quality. The other 9 locales are `translated_unreviewed`. Native-speaker review recommended for Arabic, Chinese, Korean, Japanese, Hindi before public launch. 4. **Browser geolocation** — Africa tier currently sorts first only for Swahili-locale users. IP-based detection (NG/KE/ZA/GH/etc.) would catch English-speaking African users; deferred to a session with proper geo middleware (Cloudflare headers, etc.). 5. **Per-page meta translations** — page `` and OG tags are still English. Adding per-locale metadata requires the `[locale]/` segment refactor, deferred. ### Coolify env (Session 12 additions) ``` # Already required: NEXT_LOCALE # No env — set as a per-user cookie by the switcher. # New, optional: STRIPE_PRICE_AFRICA=price_... # Once you create the Stripe product ``` --- ## Session 10 (2026-06-10) — SHIPPED ### FIX 1 — Internal auth refactor + /pipeline off-host support Pre-audit revealed the spec's premise was wrong: `/api/grading/pipeline` and `/api/grading/resolve` ALREADY EXISTED with `requireInternal` middleware inline in each route file. The actual n8n bug was a header- name mismatch (n8n sends `x-internal-key`, code read `X-VYNDR-Internal-Key`) PLUS a hard loopback-IP check that blocks any caller from a separate container. - **`src/middleware/internalAuth.js`** (new) — centralized middleware. Accepts BOTH `x-internal-key` (Session 10 short form, n8n) AND `X-VYNDR-Internal-Key` (legacy, poller + existing tests). Timing-safe string compare. `loopbackOnly` is now an OPT-IN flag (default off). - **`src/routes/grading.js`** — replaced inline `requireInternal` with the centralized middleware. `/resolve` uses `{loopbackOnly: true}` (poller from localhost). `/pipeline` uses the off-host variant (n8n from a separate container). `__helpers.requireInternal` kept exported for the existing test suite — backwards compatible. - **`src/routes/corrections.js`** — same refactor; `/correct` stays loopback-only (morning sweep is co-located). - **`/api/grading/pipeline`** body shape — empty body now iterates `nba/wnba/mlb` (n8n's "Morning Ops" workflow case). Single-sport body still works and returns the legacy summary object so existing per-sport tests continue to pass. ### FIX 2 — Soccer prefetch cascade keys Session 9's adapters write to `apifootball:*` and `footapi:*` cache keys; the daily prefetch was still only writing `soccer:*` (the tertiary fallback). The cascade in `soccerFeatureExtractor` never hit PRIMARY because nothing populated those keys. - **`scripts/soccer-data-prefetch.js`** — new `enrichFromApiFootball()` walks finished WC fixtures via `apiFootballAdapter.getFixtures` + `getFixturePlayerStats`, aggregates per-player season stats across matches (minutes, goals, assists, shots, tackles, cards, rating), collapses to per-90 rates, and writes `apifootball:player_by_name:{normalizedName}` (24h TTL). Hard-capped at `--max-players=80` per run. - **CLI flags added** — `--source=api-football|footapi|football-data|all` (default `all`), `--max-players=N`, `--season=N`. Existing `--leagues` and `--dry-run` flags unchanged. - **`enrichRefereesFromFootApi()`** — best-effort referee enrichment. Writes `footapi:referee_by_name:{name}` (7d TTL). - **Behavior preserved** — legacy `soccer:player:*` writes still happen when `football-data` source is selected (and it's the default in `all` mode). The cascade resolves at PRIMARY when api-football data is available, TERTIARY otherwise. - **Boot guard relaxed** — previously bailed when `FOOTBALL_DATA_API_KEY` was unset; now bails only when EVERY source is unavailable. The script can run on api-football alone. ### FIX 3 — Sentry error tracking - **`src/utils/sentry.js`** (new) — graceful no-op when `SENTRY_DSN` is unset (every Sentry surface becomes a noop). Initialized at the top of `src/app.js` BEFORE express is required. - **`Sentry.setupExpressErrorHandler(app)`** mounted AFTER all routes in `app.js` — catches uncaught route errors automatically. - **PII scrubbing** — `beforeSend` strips `user.ip_address`, `user.email`, `request.cookies`, `request.headers.authorization`, `request.headers.cookie`, and BOTH internal-key headers. Bearer tokens never reach Sentry. - **Sampling** — 10% traces, 100% errors. Free-tier friendly. - **Frontend** — manual init via `web/src/components/SentryInit.tsx` (client component, mounted in root layout). Lazy `import('@sentry/nextjs')` fires on mount only if `NEXT_PUBLIC_SENTRY_DSN` is set. Avoids the `withSentryConfig` plugin which conflicts with standalone output mode (per Session 10 spec note). ### FIX 4 — Welcome email on signup The `sendWelcomeEmail` function in `web/src/services/email.ts` already existed; nobody called it. - **Copy updated** — 5/month → 3/day, NexaPay → Stripe founder pricing ($14.99/mo locked for life), added the soccer/World Cup mention per Session 10 spec. Both HTML and plain-text variants. - **`web/src/app/api/welcome-email/route.ts`** (new) — POST endpoint, bearer-auth required. Reads Supabase `user_metadata` via the service-role admin client, checks `welcome_email_sent`, sends if absent, sets the flag. Idempotent — re-trigger is a cheap noop. **No migration needed** — `user_metadata` is the Supabase auth user's existing JSONB scratchpad. - **Trigger** — `web/src/app/welcome/page.tsx` fires the POST once on mount via `useRef` guard. Server-side idempotency keeps it safe across refreshes too. - **Graceful failure** — if `RESEND_API_KEY` is unset, send returns `{ ok: false }` but the flag is still set (manual operator override if a batch needs re-sending). ### Tests added | Suite | Tests | |--------------------------------------------------------|-------| | `tests/unit/internalAuth.test.js` | 15 | | `tests/unit/soccerDataPrefetchCascade.test.js` | 20 | | `tests/unit/sentry.test.js` | 10 | | Existing suites (pipeline, resolution, prefetch) re-verified | 0 new | | **Session 10 total** | **45+** | ### Quality gates - `npm test`: **1286 / 1286 passing** (1240 + 46 new), 100 suites, 0 regressions - `web/npm run build`: clean — Sentry mount + `/api/welcome-email` prerender - License audit: only permissive licenses (Sentry adds nothing exotic) ### Env vars to set in Coolify ``` # Already required from prior sessions: VYNDR_INTERNAL_KEY=<existing — header is now x-internal-key OR X-VYNDR-Internal-Key> RESEND_API_KEY=<existing> RESEND_FROM_EMAIL=<existing, defaults to "VYNDR <grades@vyndr.app>"> # New in Session 10 (all optional — wrappers degrade gracefully): SENTRY_DSN=<from sentry.io project settings> NEXT_PUBLIC_SENTRY_DSN=<same DSN — needs the NEXT_PUBLIC_ prefix to reach browser bundle> ``` ### Open items - Soccer prefetch hasn't run against live api-football yet — first cron tick after deploy will populate the cascade. Until then, the feature extractor resolves at tertiary (football-data). - Sentry's frontend manual-init pattern means errors before the React tree mounts (e.g. SSR errors) bypass Sentry. The backend handler catches Express-side errors; for browser-side SSR errors we'd need `instrumentation.ts`, deferred. - Welcome email idempotency relies on Supabase `user_metadata`. If a user signs in via SSO and never lands on `/welcome`, they don't get the email. Acceptable Day-1 — track via PostHog if it becomes a real conversion gap. --- ## 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 middleware** — `src/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 update** — `web/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 update** — `web/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 banner** — `web/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 7h–7j 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_url` → `url` 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.js` — `success_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.js` — `MARKET_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.js` — `VALID_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.js` — `SPORT_KEYS` gains 9 soccer entries mapping `soccer_wc` → `soccer_fifa_world_cup`, `soccer_epl` → `soccer_epl`, etc. `SOCCER_SPORT_KEYS` exported as a frozen list. - `src/services/intelligence/computeFeatures.js` — `sport ∈ {'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.js` — `poller-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 `users` ↔ `user_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_URL` — `https://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