Order Zero Phase 1: keyed read-only PropLine verification endpoint

Adds GET /api/internal/propline-verify (internal-key gated, read-only) so
Phase 1 can run WHERE THE KEY LIVES. Touches no cache, no ledger, no
grade; the live adapter and the live ruler are untouched. Breadth reuses
proplineAdapter.fetchRaw -- the exact live request -- so what it measures
is what the pipeline actually receives.

Reports per sport (never pooled): books/prop from the feed vs after our
own ALLOWED_BOOKS, props made INVISIBLE by that filter, reference-book
presence, DFS presence reported separately, and consensus eligibility.

Consensus eligibility is deliberately strict: >=2 REFERENCE books posting
BOTH sides at the SAME line. A one-sided quote cannot be de-vigged, and
two books at different lines are not the same market -- counting either
would overstate how much of the slate can carry a real ruler.

Probes the documented-but-unverified endpoints (/sports, /context,
/odds/closing, /movement, /results, /exports/resolved-props for four sport
keys) and classifies works/partial/no, with 403 = tier-gated and 200-but-
empty = partial rather than works.

Key safety is the other locked property: the key goes via axios params,
never string-interpolated, and every emitted string passes scrubKeys()
which removes the literal key AND any surviving apiKey= query value. A
test asserts a thrown transport error carrying the key cannot escape.

13 unit tests, hermetic (no network, no key).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
This commit is contained in:
Kev
2026-07-31 23:34:03 -04:00
parent 293367917c
commit 2071b79456
3 changed files with 488 additions and 0 deletions
+28
View File
@@ -528,4 +528,32 @@ router.post('/outcomes/:sport', async (req, res) => {
}
});
/**
* GET /api/internal/propline-verify (Order Zero, Phase 1)
*
* KEYED, READ-ONLY verification — runs where the PropLine key lives. Reports
* per-sport book breadth (feed vs after our own allow-list), exchange reality,
* consensus eligibility, and works/partial/no for the documented-but-unverified
* endpoints. Touches no cache, no ledger, no grade; the live adapter and the
* live ruler are untouched.
*
* Costs ~1 PropLine call per sport plus one per probe. Never returns the key —
* every string it emits passes through `scrubKeys`.
*
* ?sports=mlb,wnba (default: mlb,wnba)
*/
router.get('/propline-verify', async (req, res) => {
try {
const sports = String(req.query.sports || 'mlb,wnba')
.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean).slice(0, 6);
const { verify } = require('../services/proplineVerify');
const out = await verify({ sports });
res.set('Cache-Control', 'no-store');
return res.json({ ok: true, ...out });
} catch (err) {
const { __internals } = require('../services/proplineVerify');
return res.status(500).json({ ok: false, error: __internals.scrubKeys(err && err.message) });
}
});
module.exports = router;