3ba3dd28f3
PROMOTE-THE-EARNED. Nothing was promoted, because nothing earned it -- not because the bar was held high. Measured on the same bar that refuted hits-v1: own rows only, direction-aligned, paired bootstrap, promote only on a CI excluding zero. arch-v1 n=1741 delta 0.0000 CI[-0.0050,+0.0054] inconclusive contact-v1 n=1055 delta +0.0008 CI[-0.0052,+0.0069] inconclusive proj-v1.1 n=1664 delta -0.0301 CI[-0.0543,-0.0060] reliably WORSE matchup/tb-v1/hits-v1 n=0 genuinely pending (rows dated 08-02+) arch-v1 is the interesting one: it MOVED 76% of rows by 2.5 points on average and resolution is identical to the champion to four decimals, on the moved rows too. That is active movement carrying no information -- a finding, not a pending verdict. These are true prospective holdouts: arch-v1 and contact-v1 wrote p_win at grade time into their own columns before the game. Nothing recomputed. THE 429, read-only. The premise was that we re-pull the full picture every slot and blow the quota. Measured: PropLine is at 5 calls of 3,000/day -- 0.17%. One snapshot is ONE PropLine call per sport, all markets comma-joined. There is no request-pattern problem, so a change-based pull cannot fix it and no tier upgrade is needed. The 429 is odds-api: 478/500 MONTHLY, blocked at 95%. oddsService falls through silently when PropLine returns empty, and the backup's quota gate throws the error -- so an empty slate is indistinguishable from an outage and the message names the wrong provider. Flagged for its own order. Could NOT verify PropLine movement endpoints: docs are auth-gated and the keys are production-only. Not asserted either way. The movement-as-data argument stands on its own merits and should be justified that way, not as a quota fix it isn't. Book-breadth invariant written down: we never discard books. All are kept and shown (DISPLAY_BOOKS = MODEL + REFERENCE + DFS); DFS pick'em is excluded from PRICING only, because a fixed-payout shaded number is not a market price. Verified this is already what bookRoles.js does. Champion byte-identical; every challenger stays wired. 4,159 tests green (332 suites); web build exit 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W1sivYNqY2TS5ftykmHBU9
146 lines
6.9 KiB
Markdown
146 lines
6.9 KiB
Markdown
# THE 429 — DIAGNOSED, AND IT IS NOT WHAT WE THOUGHT
|
||
|
||
**Read-only investigation, 2026-08-03.** Nothing was implemented, no tier bought.
|
||
|
||
> **VERDICT: the change-based pull is NOT the fix, and no tier upgrade is
|
||
> needed. There is no request-pattern problem to solve.** PropLine is running at
|
||
> **5 calls out of 3,000/day — 0.17% of the free allowance.** The 429 comes from a
|
||
> different provider entirely.
|
||
|
||
---
|
||
|
||
## 1. BOOK BREADTH — the invariant, written down (and already true in code)
|
||
|
||
**WE DO NOT DISCARD BOOKS.** All books are KEPT and SHOWN. More breadth is
|
||
strictly good for display, line-shopping and consensus. The **only** selectivity
|
||
is that **DFS pick'em (prizepicks / underdog / sleeper / dabble) does not vote on
|
||
the fair-price CONSENSUS**, because a fixed-payout shaded number is not a market
|
||
price. That is one category exclusion from *pricing*, not a reduction in breadth.
|
||
|
||
Verified in `src/config/bookRoles.js` — this is already exactly what the code
|
||
does, and the invariant is recorded so no future "cleanup" undoes it:
|
||
|
||
```
|
||
DISPLAY_BOOKS = MODEL_BOOKS ∪ REFERENCE_BOOKS ∪ DFS_PLATFORMS ← everything shows
|
||
EXCLUDED_FROM_PRICING = DFS_PLATFORMS ∪ OFFSHORE_OR_INTL ← pricing only
|
||
```
|
||
|
||
The historical real discarding — `ALLOWED_BOOKS` cutting the slate to 5 keys and
|
||
making 64.8% of MLB props invisible — was fixed in Order Zero and must not
|
||
return. No language in this repo should describe book handling as "discarding".
|
||
|
||
## 2. Post-reset status (STEP 1) — the pipeline RECOVERED
|
||
|
||
```
|
||
GET /api/internal/snapshot/status → overdue: false
|
||
mlb 2026-08-03T01:00:24Z gradeCount 235
|
||
wnba 2026-08-03T01:02:58Z gradeCount 139
|
||
```
|
||
|
||
The 14/19/22 UTC slots on 2026-08-02 all failed; the 01:00 UTC slot after the
|
||
reset succeeded. So the failure was time-bounded and self-healing — which already
|
||
argued against a structural request-pattern defect.
|
||
|
||
## 3. What one snapshot actually requests (STEP 2)
|
||
|
||
Measured in `proplineAdapter.fetchRaw`: **one snapshot = ONE PropLine HTTP call
|
||
per sport.** All markets ride the same request, comma-joined (12 markets for MLB).
|
||
There is no per-prop, per-book or per-event fan-out.
|
||
|
||
```
|
||
snapshot 1 call × 2 active sports × 5 slots/day ≈ 10 calls/day
|
||
intraday refresh every 20 min, slate hours only (12h) ≈ 72 calls/day
|
||
public /api/odds cache-aside, 1h TTL bounded, small
|
||
── total order-of-10s/day
|
||
```
|
||
|
||
**MEASURED, not estimated** — `GET /api/internal/quota`, 2026-08-03:
|
||
|
||
| provider | used | limit | period | allowed |
|
||
|---|---|---|---|---|
|
||
| **propline** | **5** | **3,000** | daily | ✅ true |
|
||
| **odds-api** | **478** | **500** | **monthly** | ❌ **false (95.6%)** |
|
||
| oddspapi | 0 | 1,000 | monthly | true |
|
||
| tank01 | 86 | 1,000 | monthly | true |
|
||
|
||
**There is no waste to reclaim.** We are using 0.17% of the free PropLine
|
||
allowance. Re-pulling "the full picture" every slot costs one request; a
|
||
change-based pull would replace one request with one request.
|
||
|
||
## 4. Where the 429 actually comes from
|
||
|
||
`oddsService.getOdds` tries PropLine first, and **falls through silently** when it
|
||
returns null or an empty array. The 429 is thrown *after* that, by the **odds-api
|
||
backup's** quota gate:
|
||
|
||
```js
|
||
const quotaStatus = await quotaTracker.getQuotaStatus('odds-api');
|
||
if (!quotaStatus.allowed) {
|
||
const error = new Error('Odds data temporarily unavailable. Try again later.');
|
||
error.statusCode = 429; // ← THIS is the 429 we saw
|
||
```
|
||
|
||
So the real sequence on 2026-08-02 was:
|
||
|
||
1. PropLine returned **no props** for that moment (it had 2,995 calls remaining —
|
||
this was an empty/failed slate, **not** exhaustion),
|
||
2. control fell through to odds-api,
|
||
3. odds-api sits at **478/500 for the month** and the tracker blocks at ≥95%,
|
||
4. → user-facing `429 Odds data temporarily unavailable`.
|
||
|
||
**The 429 is the dead backup announcing itself.** It is not a PropLine quota
|
||
signal, and tuning PropLine request volume cannot affect it.
|
||
|
||
### The real defect this exposes
|
||
|
||
**An empty PropLine slate is indistinguishable from a provider outage.** A
|
||
legitimately empty board (off-hours, no props posted yet) produces the identical
|
||
429 as a genuine failure, because the fall-through is silent and the error text
|
||
belongs to whichever provider happened to be asked last. That is a
|
||
truth-in-reporting bug in the odds path — the operator cannot tell "no games" from
|
||
"we are broken." Flagged for its own order; **not** fixed here (read-only).
|
||
|
||
## 5. Change-based capture (STEP 3) — NOT VERIFIED, and moot for quota
|
||
|
||
**I could not verify whether PropLine supports `/movement`, odds-history, or a
|
||
`since=` parameter.** PropLine's docs are auth-gated (HTTP 401) and the API keys
|
||
are production-only — not present in the local environment. **I am not going to
|
||
assert an endpoint exists or costs less when I could not test it**; "documented ≠
|
||
verified" cuts both ways.
|
||
|
||
What can be said without testing anything:
|
||
|
||
- **For quota, it is moot.** At 5/3,000 there is nothing to optimise. A
|
||
change-based pull would save at most a handful of calls per day out of an
|
||
allowance we are not close to touching.
|
||
- **The movement-data argument stands on its own, and is the better reason.** A
|
||
change-based capture records line movement as `from → to at time T`, which is
|
||
genuine model input we currently lack: steam detection, true CLV attribution,
|
||
and the market-movement axis. Today `intradayRefreshService.trackHistory`
|
||
reconstructs a coarse version of this by diffing full pulls. If PropLine
|
||
exposes movement natively it is worth building **as a data feature** — and it
|
||
should be justified that way, not as a quota fix it isn't.
|
||
|
||
## 6. VERDICT (STEP 4), with the numbers
|
||
|
||
| question | answer |
|
||
|---|---|
|
||
| Did the pipeline recover post-reset? | **Yes** — `overdue: false`, mlb 235 grades at 01:00Z |
|
||
| Requests per snapshot | **1 per sport**, all markets in one call |
|
||
| Requests/day vs allowance | **~10s/day vs 3,000/day free — 0.17% used** |
|
||
| Is the 429 a request-pattern problem? | **No.** Measured, decisively |
|
||
| Is the 429 a PropLine quota problem? | **No.** 2,995 calls remaining, `allowed: true` |
|
||
| What is the 429? | **odds-api backup at 478/500 monthly, blocked at 95%** |
|
||
| Does change-based pull fix it? | **No** — it cannot; wrong provider, and no volume problem |
|
||
| Is a tier upgrade needed? | **No.** Not for PropLine. Nothing is constrained |
|
||
|
||
**What actually deserves an order**, in priority sequence:
|
||
|
||
1. **The silent fall-through** — an empty PropLine slate must report "no props
|
||
available", never the backup's quota 429 (§4).
|
||
2. **odds-api at 478/500 monthly** — it is a dead backup for the rest of August.
|
||
Either accept it (PropLine is primary and healthy) and stop letting its quota
|
||
gate speak for the whole odds path, or budget it deliberately.
|
||
3. **Movement capture as a DATA feature** — worth scoping on its own merits, with
|
||
PropLine's endpoint support verified first from an environment that has keys.
|