Files
vyndr/CLAUDE.md
T
builtbykev a74b5dd1ed Session 33: Design system Phase A+B — tokens, fonts, glitch CSS, shared components (1792 tests)
VYNDR 2.0 design-system conversion, foundation only (pages/mobile/systems are
Sessions 34+). Frontend-only; zero backend changes.

Phase A: §2 token set + token-wired typography + Inter/JetBrains Mono fonts +
§4 glitch keyframes (ported verbatim from the prototype's vyndr.css) + scanline
texture + §10 a11y data-* layer in globals.css. Legacy alias block kept.

Phase B: web/src/lib/vyndrTokens.js (CommonJS helpers) + 9 shared components
under web/src/components/vyndr/ (Wordmark, GradeBadge, SportBadge, TerminalInput,
SectionHead, VBtn, Card, Sparkline, Ticker).

74 new design-system tests. Backend 1718 -> 1792, 141 suites, zero regressions.
Web build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 22:58:57 -04:00

9.0 KiB
Executable File

VYNDR — Claude Code Project Context

What This Is

Sports betting intelligence SaaS. Real software product. Three tiers: Free (5 scans), Analyst ($19.99 / $14.99 founder), Desk ($49.99 / $34.99 founder).

Tech Stack

  • Backend: Node.js / Express
  • Database: Supabase (PostgreSQL)
  • Frontend: React Native (built in Cursor)
  • Data: The Odds API ($30/mo), nba_api (free, Python wrapper)
  • Caching: Redis — 15min for odds, 24hr for season averages, 1hr for recent games
  • Payments: Stripe

Critical Rules — Non-Negotiable

1. NO CODE WITHOUT A SPEC

Every feature requires a spec file in specs/ before any code is written. Spec must include: endpoints, data shapes, acceptance criteria, test plan. Get approval before building.

2. WSL2 HEREDOC RULE

WSL2 corrupts heredoc for files over 10 lines. ALWAYS use Python file-writing: python3 with triple-quoted strings. This applies to every file creation operation.

3. 5 QUALITY GATES (all must pass before any feature is marked complete)

  1. Unit tests pass
  2. Integration tests pass
  3. Acceptance criteria met (from spec)
  4. PR description written
  5. CLAUDE.md updated if anything new learned

4. BUILD-STATE.md

Update after every session. What shipped, what's next, any blockers.

5. BLOCKERS.md

If you hit something you cannot resolve: log it. Don't guess. Don't skip.

Folder Structure

vyndr/
├── src/
│   ├── routes/        # Express route handlers
│   ├── models/        # Supabase data models
│   ├── services/      # Business logic (prop analysis, odds normalization)
│   ├── middleware/     # Auth, rate limiting, scan counting
│   └── utils/         # Helpers, formatters, validators
├── tests/             # Unit + integration tests
├── docs/              # API docs, architecture notes
├── specs/             # Feature specs (write BEFORE code)
├── build-briefs/      # Session summaries
├── CLAUDE.md          # This file
├── ROADMAP.md         # Feature roadmap with phases
├── BUILD-STATE.md     # Current build status
├── BLOCKERS.md        # Unresolved blockers
└── DECISIONS.md       # Architecture decisions log

All-Day Intelligence Layer (Session 23)

Free/cheap content that keeps the platform alive when odds-api props are empty. NONE of these spend odds-api credits:

  • /api/schedule/:sport — cache-aside ESPN scoreboard (scheduleService), self-heals on cache miss. Per-game hasOdds/hasGameLines flags peek at other caches without fetching.
  • /api/gamelines/:sport — Tank01 book-by-book lines (RAPID_API_KEY quota).
  • /api/streaks/:sport + /api/hotlist/:sport — PURE engines (streaksService, hotListService) computed from cached game logs. NO API calls. Logs loaded by rosterLogs.js (prefetch blob, else Redis SCAN over gamelogs:{sport}:{player}:{count}). Empty roster = valid empty state.
  • ?stat= filters narrow streaks/hotlist; categories in config/statFilters.js (mirror web/src/config/statFilters.ts). Discovery: /api/stats/filters/:sport.
  • Dead providers: set status: 'dead' in config/providers.js to drop a provider from fallback chains + configured list (ParlayAPI host is dead).

Provider Strategy (Session 30)

Player props now have abundance, not rationing.

  • Player props — PRIMARY: PropLine (proplineAdapter, 3 keys PROPLINE_API_KEY_1/2/3, 3,000 req/day FREE, rotates per-key; registry propline priority 1). BACKUP: The Odds API (ODDS_API_KEY, 500/month, priority 2, conserve). getOdds() tries PropLine first when keys present, falls back to odds-api; the response + cache carry a provider field. PropLine is The-Odds-API-compatible → reuses utils/oddsNormalizer. MLB market keys (batter_hits, pitcher_strikeouts, …) were added to MARKET_MAP — without them MLB props normalize to zero.
  • MLB statsmlbStatsAdapter → statsapi.mlb.com. FREE, no auth, unlimited. Game logs, season averages, BvP, probable pitchers. Does NOT use the gateway (no quota). Registry mlb-stats (noAuth: true).
  • Game enrichmentscheduleService.getGameSummary(sport, eventId) → ESPN summary (injuries, ESPN Bet odds, ATS, leaders, box score). Free.
  • Game-level odds — Tank01 (unchanged). Tank01 PLAYER PROPS = empty, do not wire.

Grades Content Pipeline (Session 32)

Closes the content pipeline: contentTemplateService reads a grades:{sport} cache "when present" but nothing wrote it, so slate/POTD never reached dataLevel: 'full'.

  • WritergradeSlateService.gradeAndCacheSlate(sport, props, opts) dedupes props to unique player+stat+line (cap 25, concurrency 5), grades BOTH sides via analyzeViaEngine1 (engine1 is direction-aware — keeps the higher-confidence side), sorts by confidence desc, writes grades:{sport} = { grades, updated_at, source } (TTL 2h). The legacy grade shape already matches normalizeGrade — no remap needed.
  • Trigger — fire-and-forget inside oddsService.recordDownstream (runs on a fresh odds fetch / cache MISS, NOT on cache hits). Does NOT hold the odds HTTP response. Gated by shouldGradeSlate(): ON by default, OFF when NODE_ENV==='test' (its feature-compute fan-out would pollute call-count assertions), override with GRADE_SLATE_ON_FETCH=1/0. 0 is the operator kill-switch if the per-prop feature-compute cost needs shedding.

NFL/NHL Props (Session 32)

NFL + NHL wired end-to-end. the-odds-api keys are americanfootball_nfl and icehockey_nhl (full-name prefix like basketball_nba) — NOT football_nfl/hockey_nhl (those are PropLine's keys in proplineAdapter). oddsService.SPORT_KEYS + SPORT_MARKETS carry both; proplineAdapter.MARKETS filled. NHL keys (player_shots_on_goal, goalie_saves) added to MARKET_MAP so NHL props don't silently normalize to zero in-season.

Public Route Rate Limiting (Session 32)

middleware/rateLimit (createRateLimit, in-memory per-IP, independent bucket per call site) now mounts via router.use at the top of every public cached router: odds + parlay = 30/min, schedule/gamelines/streaks/hotlist/ content/lines/books = 60/min. /api/analyze keeps its own 10/min.

Frontend ↔ Backend Wiring (Session 25 — non-obvious)

A new Express route under /api/* is NOT reachable from the browser until a matching Next.js proxy route exists at web/src/app/api/.../route.ts that forwards to ${BACKEND_URL}/api/.... The browser hits the Next origin, not Express directly. This bit us: schedule/gamelines/streaks/hotlist endpoints worked on Express but 404'd in the UI for two sessions. When adding a backend endpoint the frontend calls, ALWAYS add the proxy too (pattern: web/src/app/api/odds/nba/route.ts).

Tank01 betting-odds real shape: sportsbooks are TOP-LEVEL keys on each game object ({ awayTeam, homeTeam, bet365:{...} }), not a sportsBooks array. Filter NON_BOOK_KEYS to extract books (see gameLines.js).

VYNDR 2.0 Design System (Session 33 — Phase A+B)

Multi-session frontend conversion of the claude.ai/design "VYNDR 2.0" handoff (NOT a backend change). Foundation shipped; pages/mobile/systems are Sessions 34+. Source of truth = the prototype's vyndr.css + VYNDR_HANDOFF.md.

  • Tokens live in web/src/app/globals.css :root. The NEW canonical set is the short names: grades --g-ap/--g-a/--g-b/--g-c/--g-d, sports --s-nba/--s-mlb/--s-wnba/--s-soccer, --amber, --live/--hit/--miss, --scan-op (0.04), --glitch (1), --sans (Inter), --mono (JetBrains Mono). The legacy alias block (--grade-a, --nba, --accent, …) is KEPT — do not delete it until every consumer is migrated.
  • Two hard brand rules: (1) JetBrains Mono (var(--mono) / .mono) for ALL data — odds, %, timestamps, book names, stat lines, grades; Inter (var(--sans)) for everything else. (2) Glitch animations apply ONLY to chrome (wordmark, headers-on-hover, dividers, loaders, living layer). DATA NEVER GLITCHES.
  • Glitch keyframes (§4) are appended to globals.css after the legacy block — later-wins where names collide, so the appended VYNDR-2.0 definitions are authoritative. Entrance keyframes floor at the visible state (fade-in from opacity .6) so a paused frame is never invisible.
  • Shared components: @/components/vyndr/* (Wordmark, GradeBadge, SportBadge, TerminalInput, SectionHead, VBtn, Card, Sparkline, Ticker), helpers in web/src/lib/vyndrTokens.js. The NEW Wordmark is .wm markup at @/components/vyndr/Wordmark; the legacy @/components/Wordmark (.wordmark) is still used by Nav until pages convert. vyndrTokens.js is CommonJS so it's importable by .tsx (allowJs) AND requireable by the plain-JS Jest suite — keep helper logic there so it stays genuinely unit-testable.
  • a11y layer (§10): <html data-contrast|data-text|data-cb|data-font| data-motion> overrides in globals.css. Wiring the toggles to these attrs is Phase G (Session 38).

Active Skills

  • vyndr-voice (all user-facing output)
  • prop-analysis (grading methodology)
  • monetization-system (scan-5 pitch, tier conversion)