Close the public model-price leak; wire the read card's price layer

PHASE 0.5 GATE — the three checks, and one correction.

`fairLine` does not exist. Zero hits across src/ and web/src. Option A as
written had no referent, but it resolves better than feared: `fair_odds` is
already a real de-vigged American price on every graded snapshot row, so there
is nothing to derive.

  Gate 1 (is it a price): PASS. fair_odds is American odds from
  impliedProbToAmerican inside devigTwoWay; fair_prob is the probability. Both
  distinct from `line`, the stat threshold.

  Gate 2 (numeric match): PASS, 8/8 exact. Recomputed fair_odds and fair_prob
  independently from the stored raw over/under prices; every value matched the
  stored one to the integer and to 3dp. Same de-vig, same numbers the component
  was proven against.

  Gate 3 (poison independence): PASS, and proven on the quarantined cohort
  itself. devigTwoWay's inputs are (over_odds, under_odds) — market prices
  only, no model term is reachable. The 8 rows recomputed above are all
  wrong_opponent_grade rows, and their fair prices reproduce exactly from the
  market. The poison is in the grade, not the price. Quarantine therefore
  suppresses the MODEL leg only; the fair leg stands, as designed.

THE LEAK WAS REAL AND ALREADY LIVE. GET /api/snapshot/:sport is public and
unauthenticated, and it was serving model_odds, p_win, ev_pct, value and
takeable to anonymous callers on every graded row — 25 of 25 on the live wnba
board. The Session-66 gate on /api/analyze was bypassed entirely by this
endpoint.

The strip covers more than model_odds, because model_odds is not the only way
to read the model price: p_win IS the price in another base, and ev_pct is
INVERTIBLE — ev is a function of p_win and book_odds, and book_odds is public,
so leaving ev behind hands the price over. All five model-derived fields go.
book_odds, fair_odds, fair_prob, overround and devig_method stay on every tier:
the fair leg is never the paywall. Rows that keep a book+fair pair are stamped
model_price_locked so a gated price is never mistaken for a missing one.

Tier comes from resolveTierFromRequest, which reads a bearer token when one is
present and otherwise returns 'free'. It FAILS CLOSED on every error path, so a
resolution failure can only ever withhold the price. The response now varies by
entitlement, so the /:sport handler downgrades Cache-Control to private for
authenticated callers and the browser proxy forwards the bearer token —
otherwise a CDN could hand a paid payload to an anonymous viewer, or every
request would look anonymous and paid users would lose the leg.

READ CARD — a manual scan carries no market. The request is {player, stat,
line, direction}, so the engine has no over/under prices to de-vig and
book_odds/fair_odds are legitimately absent from its response; that is why the
triplet was hidden there. lookupSnapshotPrices recovers them from the
pre-graded snapshot via the same cache-only read this route already performs
for locked odds and team. The join is exact on player + stat + line + side
(fair_odds is side-specific), and returns nothing unless book and fair are BOTH
present — a user-chosen line the board never graded has no market attached, so
the triplet stays hidden rather than borrowing another line's price.

FAIR-LEG ABSENCE, measured before shipping: 636 graded rows, 636 with book,
636 with fair, 0 one-sided. Absence rate 0.0%. The hero number is not a
sometimes-number on current data.

Tests 3556 passed / 291 suites, web build exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCNgGSt5qvcLxaeQqa7Zpj
This commit is contained in:
Kev
2026-07-20 19:47:42 -04:00
parent 16697a4b90
commit fbcb00b7b1
7 changed files with 373 additions and 9 deletions
+72
View File
@@ -0,0 +1,72 @@
'use strict';
const { canAccess } = require('../config/tiers');
/**
* Snapshot tier gating (Session 67) — the model price never leaves the server
* for an unentitled viewer.
*
* WHY THIS EXISTS: `GET /api/snapshot/:sport` is PUBLIC and unauthenticated.
* Session 66 gated `model_odds` on `/api/analyze`, but the snapshot endpoint
* bypassed that gate entirely and served the model price to anonymous callers
* on every graded row. This closes it at the same layer as the CLV gate
* (`routes/ledger.js` columnsFor/stripClv): strip on the way out, never hide
* in the client.
*
* WHAT MUST GO, AND WHY IT IS MORE THAN `model_odds`:
* model_odds the gated number itself.
* p_win model_odds IS `impliedProbToAmerican(p_win)` — shipping p_win
* is shipping the price in a different base.
* ev_pct INVERTIBLE. ev is a function of (p_win, book_odds) and
* book_odds is public, so p_win — and therefore the price — can
* be recovered exactly from it. A gate that leaves ev behind is
* not a gate.
* value a boolean over (ev, takeable); with the band public it leaks a
* takeable bound on ev. Cheap to drop, so drop them.
*
* WHAT STAYS ON EVERY TIER — deliberately:
* book_odds, fair_odds, fair_prob, overround, devig_method.
* These are MARKET facts and the de-vig of market facts. The de-vigged fair
* number is the free-tier hook and the hero of the price layer: THE FAIR LEG
* IS NEVER THE PAYWALL. Only VYNDR's own price gates.
*
* `model_price_locked` is stamped on rows that still carry a book+fair pair so
* the client renders the lock teaser rather than an absent leg — a gated price
* must never be mistaken for a missing one.
*/
// Model-derived fields. Every one of these can reconstruct the model price.
const MODEL_FIELDS = Object.freeze(['model_odds', 'p_win', 'ev_pct', 'value', 'takeable']);
// Market facts + their de-vig. Never stripped, on any tier.
const MARKET_FIELDS = Object.freeze(['book_odds', 'fair_odds', 'fair_prob', 'overround', 'devig_method']);
/** Does this tier receive VYNDR's own price? */
function entitledToModelPrice(tierName) {
return canAccess(tierName, 'model_price');
}
/**
* stripModelPrice(grades, tierName) — returns a NEW array with the
* model-derived fields removed for unentitled tiers. Entitled tiers get the
* rows back untouched (same reference — no needless copying on the hot path).
*/
function stripModelPrice(grades, tierName) {
if (!Array.isArray(grades)) return grades;
if (entitledToModelPrice(tierName)) return grades;
return grades.map((g) => {
if (!g || typeof g !== 'object') return g;
const out = { ...g };
for (const f of MODEL_FIELDS) delete out[f];
// Only flag a LOCK where there is a price story to lock a third leg onto.
if (out.book_odds != null && out.fair_odds != null) out.model_price_locked = true;
return out;
});
}
module.exports = {
MODEL_FIELDS,
MARKET_FIELDS,
entitledToModelPrice,
stripModelPrice,
};