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>
This commit is contained in:
Kev
2026-07-15 18:07:14 -04:00
parent 4cd933d83e
commit 2d413cfe1e
8 changed files with 191 additions and 61 deletions
+24
View File
@@ -42,6 +42,30 @@ router.get('/quota', async (req, res) => {
}
});
/**
* POST /api/internal/quota/test-alert (quota guard)
*
* Test-fires the quota pager end-to-end through the REAL opsNotify path so
* "does the 80% alert actually deliver?" is verifiable on demand (the reason
* the 500/500 drain went unnoticed was a counting blind spot, not delivery —
* this proves the delivery leg). Sends one ntfy to vyndr-pipeline-kev2026.
* Does NOT touch the real counter. Body: { pct? } (default 0.85).
*/
router.post('/quota/test-alert', async (req, res) => {
try {
const notify = require('../utils/opsNotify').notify;
const pct = Number.isFinite(Number(req.body && req.body.pct)) ? Number(req.body.pct) : 0.85;
const out = await notify(
`TEST — odds-api quota alert delivery check (${Math.round(pct * 100)}%). If you can read this, the quota pager path works.`,
{ title: 'VYNDR quota (test)', priority: 'high', tags: ['warning', 'chart_decreasing'] },
);
return res.json({ ok: true, delivery: out });
} catch (err) {
const message = err && err.message ? err.message : String(err);
return res.status(500).json({ ok: false, error: message });
}
});
/**
* POST /api/internal/prefetch/tank01
*