IDENTITY (CLAUDE.md top + MASTER-PLAN header). VYNDR is a PREDICTIVE MODEL:
it projects what a player will DO and picks accurately. Market edge is a
BYPRODUCT of a good prediction, never the success criterion. Success =
the forecast is honest about its own confidence AND still ranks --
calibration and resolution, both. No edge/CLV term belongs in a pass/fail
gate; they are diagnostics we report, not thresholds a model must clear.
A model tuned to beat a closing line has been fitted to the market instead
of to the game.
Per-sport doctrine (Phillips 2022, classify by what players DO not by
position): each sport is its own model -- own variables, archetypes,
conditions, calibration, honest ceiling. Shared across sports: ONLY the
Bayesian inference math.
Truth Law: no fabricated data; honest-absent over invented; label
limitations in-band; provisional stays provisional until re-run;
documented is not verified.
PHASE 2 -- AGGREGATOR WIDENING (live). normalizeProps now emits every
DISPLAY book instead of 5 of 18. Before this we discarded 13 books of our
own accord and 64.8% of the MLB slate was invisible to users. Every prop
carries book_role (both/takeable/reference/dfs/offshore) so the display
layer can say WHAT a price is -- a fixed-payout DFS number and a two-way
sportsbook price are not interchangeable objects. Unknown books are still
dropped.
PHASE 3 -- MODEL GATE (the model does not move). bookRoles splits
MODEL_BOOKS (the legacy allow-list, character for character) from
DISPLAY_BOOKS. Both model paths re-filter before they pick a line:
gradeSlateService.dedupeProps (before first-row-wins AND before the limit)
and intradayRefreshService.indexOddsProps (which RE-GRADES at the current
line -- without the gate, widening would have silently moved locked lines
onto books the model has never been calibrated against). A test asserts
the graded set is byte-identical through the widening.
CURRENT_RULER_VERSION stays v1_first_book. The gate lifts only when the
MLB calibration is re-run on the consensus ruler and v2 is promoted.
HONEST FRAMING, recorded in the plan: this is an AGGREGATOR win and it
does NOT fix the model. WNBA still abstains -- a model problem, not a
coverage problem; it is better covered than MLB. MLB isotonic still
provisional. The consensus is MARKET, not SHARP: pinnacle, matchbook and
polymarket are 0% on both sports, so no sharp anchor exists in our feed.
Two superseded tests updated to stronger properties rather than deleted:
roleOf now names the KIND of book, and the normalizer test asserts the
display set widens WHILE the model set does not.
Gates: 4,027 tests / 322 suites green; next build exit 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
My first delta run modelled the incumbent as first-row-wins over the RAW
feed and reported that an EXCLUDED book was "the market" on 69% of MLB
prop-lines, with prizepicks alone at 47%. That is WRONG and I caught it
before it went anywhere.
normalizeProps applies ALLOWED_BOOKS BEFORE gradeSlateService.dedupeProps
runs, so DFS books never reach the incumbent. The allow-list, for all the
coverage it costs, does keep DFS out of the ruler.
incumbentFairProb now takes the allow-list (defaulting to the live
ALLOWED_BOOKS) and reproduces the real chain. Two tests lock it, including
that a prop with no admitted book has NO incumbent -- it is never graded
at all, which is the real loss and is already measured as invisible_props.
Overstating the incumbent's badness would have been as dishonest as
understating it, and more persuasive.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
CHALLENGER-FIRST. The live ruler is byte-identical: CURRENT_RULER_VERSION
is still v1_first_book, nothing here writes a cache, a grade or a ledger
row, and no live code path calls consensusRuler yet.
bookRoles.js splits one allow-list into three, because it was answering
two different questions -- "can we show this?" and "can we price against
this?" -- with the same list, which is what bent the ruler.
TAKEABLE the user can actually bet here (drives best price / shopping)
REFERENCE may price the fair-prob ruler; never surfaced as a place to bet
EXCLUDED DFS pick'em + offshore, permanently barred from all pricing
Two deliberate calls, both evidence-based:
- The six PropLine-phantom books (caesars/fanatics/bet365/hardrockbet/
pointsbet/thescore) are KEPT despite the order saying remove. They
returned zero PropLine quotes, but PropLine is not our only provider and
the odds-api backup path may carry them. A book that never appears is
never matched, which costs nothing; deleting them risks silently
dropping real books on the backup with no upside. Recorded in
PHANTOM_ON_PROPLINE rather than enacted as a deletion.
- REFERENCE = exchanges + pinnacle + bovada + the four US majors, chosen
off the measured coverage curve rather than theory. exchange_only is
cleanest (order-book, ~zero vig) but covers 14.3% of MLB and 5.6% of
WNBA; adding the US majors gives 28.1% / 46.3%. pinnacle, matchbook and
polymarket measured 0% on both sports and add nothing. The honest
limitation is recorded in the config: this is a MARKET consensus, not a
SHARP one.
consensusRuler.js: median de-vigged fair_prob across >=2 reference books
posting BOTH sides at the SAME line. Median so one stale exchange cannot
drag it. Different lines are never averaged, one-sided quotes never rule,
and n<2 falls back to single-book LABELLED as such with the v1 stamp --
never silently mixed, because a column holding both is two rulers wearing
one name.
The challenger delta runs over the live feed and reports incumbent_book_
roles, which is the real headline: the incumbent is literally first-row-
wins, so it reports what KIND of book has been acting as "the market".
DFS pick'em has the highest coverage in the feed, so a DFS book can be it.
18 ruler tests + 37 total in the two new suites. Full suite 4021 passed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
/sports and /markets/resolution-summary carry no per-prop data and no
credentials, and the shape summary alone cannot answer the question they
exist to answer -- whether PropLine actually GRADES the sports we cannot
settle. A shape is not a number. Both bodies are scrubbed on the way out.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
Two corrections to the first pass, both of which would have produced a
false positive.
1) A non-empty body is NOT proof of access. PropLine's free tier returns
the full STRUCTURE of tier-gated endpoints with values stripped plus an
upgrade_url -- and the first pass classified /odds/closing and /movement
as "works" on structure alone. detectRedaction() now counts actual
prices and downgrades works -> partial when a body advertises an upgrade
or carries outcomes with zero prices. Same class as the harness that
returned a silent false, inverted.
2) One hard-coded reference set forces a yes/no on a question that is
really a curve. reference_policy_curve reports strict eligibility
(>=2 books, both sides, same line) under exchange_only /
exchange_plus_sharp / exchange_plus_us / takeable_only, so the ruler
decision is made on coverage-vs-quality rather than on a guess. DFS is
absent from every policy by construction and a test asserts it.
Also probes /markets/resolution-summary: /exports/resolved-props being 403
tells us we cannot PULL settlements; resolution-summary tells us whether
they EXIST to be bought. Different questions.
19 unit tests, still hermetic.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
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