f0c8b4f29b
- gradeSlateService writes grades:{sport} cache (closes content pipeline →
dataLevel full); fire-and-forget from oddsService.recordDownstream, gated
by shouldGradeSlate (off in test, GRADE_SLATE_ON_FETCH override)
- NFL/NHL wired: oddsService SPORT_KEYS/SPORT_MARKETS (correct the-odds-api
keys americanfootball_nfl/icehockey_nhl), proplineAdapter MARKETS, NHL
MARKET_MAP keys to avoid silent-zero
- rate limiting mounted on 8 public cached routers (odds/parlay 30/min,
rest 60/min)
- jsonlLogger writes to temp under test (no more dirtied tracked artifact);
5MB pipeline test given 20s timeout
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
142 lines
7.0 KiB
Markdown
Executable File
142 lines
7.0 KiB
Markdown
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 stats** — `mlbStatsAdapter` → 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 enrichment** — `scheduleService.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'`.
|
|
- **Writer** — `gradeSlateService.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`).
|
|
|
|
## Active Skills
|
|
- vyndr-voice (all user-facing output)
|
|
- prop-analysis (grading methodology)
|
|
- monetization-system (scan-5 pitch, tier conversion)
|