785 lines
45 KiB
Markdown
Executable File
785 lines
45 KiB
Markdown
Executable File
# 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 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
|