3da1b4242c
Feature 1.2: Python FastAPI microservice wrapping nba_api - GET /stats/season-avg, /stats/last-n, /stats/splits, /players/search - Redis caching (24hr/1hr/6hr/7day), 0.6s rate limiting, PRA derived stat - 27 Python tests passing Feature 1.4: Complete Supabase database schema - 6 tables: users, picks, scan_sessions, bets, outcomes, performance - RLS enabled on all tables with auth.uid() policies - 3 triggers: auto-create user, updated_at, scan count reset - 37 schema validation tests passing - Migration SQL ready, pending manual apply (WSL2 DNS blocker) Total: 92 tests (65 Node.js + 27 Python), all passing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
5.2 KiB
Executable File
5.2 KiB
Executable File
BetonBLK — Architecture Decisions
Format
Each decision follows this structure:
- DECISION-[NNN]: [Title]
- Date: [YYYY-MM-DD]
- Context: [Why this decision was needed]
- Decision: [What was decided]
- Alternatives considered: [What else was on the table]
- Consequences: [What this means going forward]
Decisions
DECISION-001: Odds API Raw Response Format (Feature 1.1)
- Date: 2026-03-21
- Context: Needed to verify actual Odds API response shape before writing normalizer. Made live test calls to
/v4/sports/basketball_nba/eventsand/v4/sports/basketball_nba/events/{id}/odds.
Raw response structure (verified):
Event level:
- id: "a1158df1a3a21def58491807df167c6a"
- home_team: "Washington Wizards" (FULL NAME, not abbreviation)
- away_team: "Oklahoma City Thunder"
- commence_time: "2026-03-21T21:10:00Z" (ISO 8601 UTC)
Bookmaker level (nested under event.bookmakers[]):
- key: "fanduel" | "draftkings" | "betmgm"
- title: "FanDuel" | "DraftKings" (human-readable)
Market level (nested under bookmaker.markets[]):
- key: "player_points" (matches our expected market keys)
- last_update: "2026-03-21T12:17:04Z" (ISO 8601 UTC)
Outcome level (nested under market.outcomes[]):
- name: "Over" | "Under"
- description: "Shai Gilgeous-Alexander" (FULL PLAYER NAME)
- price: -110 (American odds, integer)
- point: 28.5 (the line)
Key findings:
- Team names are full names ("Washington Wizards"), NOT 3-letter abbreviations. We need a mapping table.
- Player names are in
descriptionfield, full names. - Over/Under for the same player+line appear as separate outcome objects. Must pair them.
- The API does NOT tell us which team a player belongs to. We only know home_team/away_team for the event. Player-to-team assignment requires roster data (Feature 1.2).
marketsparam accepts comma-separated values — can fetch all 8 prop markets in one API call per event.
Quota headers (verified):
-
x-requests-used: cumulative credits used this month -
x-requests-remaining: credits left -
x-requests-last: credits consumed by this specific call (was 1) -
Decision:
- Build a static NBA team name → 3-letter abbreviation mapping in utils.
- Normalizer must pair Over/Under outcomes by player name + point value.
- For Feature 1.1, set
teamto the full team name from the event. Player-to-team resolution deferred to Feature 1.2 integration. - Fetch all markets in a single call per event to conserve credits.
- Use on-demand fetching only (not polling) — fetch from API only when a user request hits and cache is cold.
-
Alternatives considered:
- Could skip team abbreviations entirely — rejected because downstream features (Prop Engine, UI) need short team identifiers.
- Could try to resolve player→team via external lookup now — rejected because Feature 1.2 will provide this natively.
-
Consequences:
- Need
src/utils/teamMap.jswith full name → abbreviation mapping. - Normalizer groups Over+Under outcomes by
description+point. - Credit budget: ~1 credit per event per refresh. With 15-min cache + on-demand only, budget stays within 500/month for typical usage.
- Need
DECISION-002: Credit Conservation Strategy (Feature 1.1)
- Date: 2026-03-21
- Context: Starter plan = 500 credits/month. Player props require per-event API calls (sport-level endpoint only supports main markets). ~10 NBA games/day.
- Decision: On-demand fetching only. Never poll. Cache aggressively at 15-min TTL. Batch all markets into one call per event. For a full NBA slate, one refresh = ~10 credits. At 15-min cache, even heavy usage stays under budget.
- Alternatives considered: Background polling every 15 min — rejected, would burn ~480 credits per game day.
- Consequences: First request after cache expires will be slower (live API call). Acceptable tradeoff for free tier.
DECISION-003: Python Microservice for nba_api (Feature 1.2)
- Date: 2026-03-21
- Context: nba_api is a Python library. Backend is Node.js/Express. Need a bridge.
- Decision: FastAPI microservice. Node calls it via internal HTTP. Python stays idiomatic, caching strategy (24hr season averages, 1hr recent games) lives in the Python service with its own Redis connection.
- Alternatives considered:
- Python child process (Node spawns python3, parses stdout) — rejected: cold start on every call, awkward error handling.
- Pre-fetch cron (Python writes to Redis on schedule, Node reads) — rejected: more moving parts, stale data risk.
- Consequences: Two processes to run (Node + Python). Need a startup script or docker-compose. Internal port convention: Node on 3000, Python on 8000.
DECISION-004: Supabase Auth + RLS (Feature 1.4)
- Date: 2026-03-21
- Context: Need auth provider for user system. Supabase already in stack.
- Decision: Supabase Auth. RLS enabled at project level. Our
userstable extendsauth.usersvia FK onid. All tables use RLS policies scoped toauth.uid(). - Alternatives considered:
- Clerk — better DX but adds a vendor, costs more at scale.
- Auth-agnostic (own users table, plug in later) — rejected: delays RLS setup, more migration work later.
- Consequences: All table access goes through RLS. Service role key bypasses RLS for admin/backend operations. Anon key used for client-side auth flows.