# 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/events` and `/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:** 1. Team names are full names ("Washington Wizards"), NOT 3-letter abbreviations. We need a mapping table. 2. Player names are in `description` field, full names. 3. Over/Under for the same player+line appear as separate outcome objects. Must pair them. 4. 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). 5. `markets` param 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: 1. Build a static NBA team name → 3-letter abbreviation mapping in utils. 2. Normalizer must pair Over/Under outcomes by player name + point value. 3. For Feature 1.1, set `team` to the full team name from the event. Player-to-team resolution deferred to Feature 1.2 integration. 4. Fetch all markets in a single call per event to conserve credits. 5. 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.js` with 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. ### 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.