Commit Graph

2 Commits

Author SHA1 Message Date
builtbykev 2d413cfe1e Quota guard: close the silent odds-api drain + reserve floor for MLB
Diagnosis (why 500/500 went unpaged): the only regular odds-api burner was
futuresService, which called axios DIRECTLY — bypassing the gateway, so it
never hit recordCall (the ONE place the WARN/BLOCK pager fires) and never
respected the 95% block. It only syncFromHeaders, which updated the counter's
number SILENTLY. oddsService (which does go through the gateway) only touches
odds-api when PropLine fails, so recordCall for odds-api effectively never ran.
Result: the counter could reach 100% with neither pager firing.

Fixes (a silent drain is now impossible, not just guarded):
- futuresService routes through gateway.fetch('odds-api', …) → counted, blocked
  at 95%, and reserve-gated. Closes the raw-axios bypass.
- Reserve floor in the gateway: a DISCRETIONARY call (futures/soccer) passes
  reserve=ODDS_API_RESERVE (default 50) and is refused while remaining <= reserve.
  The ESSENTIAL MLB prop-backup passes no reserve and may spend to the 95% block.
  → a futures/soccer drain can NEVER starve MLB's backup path.
- quotaTracker.syncFromHeaders (the AUTHORITATIVE number) now fires the same
  once-per-period WARN/BLOCK alert on a crossing — extracted fireThresholdAlert
  shared with recordCall. The header-only drain now pages.
- POST /api/internal/quota/test-alert (internal-key) test-fires the pager
  end-to-end so ntfy delivery is verifiable on demand.

Also (reality-corrected cadence): WNBA restored to the full grid. 2026-07-15
had two AFTERNOON WNBA games finished before the 22 UTC slot — 14 UTC (10am ET)
is the only slot early enough for a 1pm ET game's props, and on PropLine the
extra slots cost a rounding error. Soccer stays the only trimmed sport (the
real odds-api discipline). Assumption corrected by observed data.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 18:07:14 -04:00
builtbykev 4cd933d83e Job 1: per-sport snapshot cadence (config, not baseball's rhythm for all)
Every ACTIVE sport was graded at all five MLB slots (14/19/22/1/3 UTC). Sports
post lines on different clocks, so that inheritance was wasteful both ways:
WNBA props aren't posted at 14:00 UTC (10am ET) → that slot always graded 0
(the audit's "wnba:0"); soccer odds come from the 500/MONTH odds-api key, so
five slots/day is a third of the budget for 1-2 matches.

New src/config/sportCadence.js is the single source of truth (config-over-
constants). Mapped from reality + quota headroom (PropLine 9k/day abundant,
odds-api 500/mo scarce):
  mlb    14/19/22/1/3  intraday   (full grid — games+props all day)
  nba    14/19/22/1/3  intraday   (in-season fits; off-season self-skips empty)
  wnba   19/22/1       intraday   (afternoon→evening ET; drops the 14/3 waste)
  soccer 14/19         NO intraday (WC live; 2 lean odds-api reads, key-protected)

The scheduler still fires at HOURS_UTC and the missed-cron watchdog still
references MLB (which runs every grid hour) — each slot now grades only
sportsForHour(h), and only intradaySports() get the 20-min refresh. Every
sport's hours are kept a subset of the firing grid (a boot-time guard + a test
warn if that's ever violated). Retune a sport by editing one table row.

Adaptive, not constant: near-zero when a sport is quiet, protecting the scarce
odds-api quota from being drained by noon.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 16:17:55 -04:00