From 844ab96f214648e988f58f32e2c6da13b173e85b Mon Sep 17 00:00:00 2001 From: Kev Date: Fri, 31 Jul 2026 06:40:43 -0400 Subject: [PATCH] =?UTF-8?q?report:=20tier=20structure=20pull=20=E2=80=94?= =?UTF-8?q?=20the=20declared=20free-tier=20gate=20is=20unenforced=20on=20t?= =?UTF-8?q?he=20board?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Read-only. Nothing changed. FREE TIER, EXACTLY: - Board /api/snapshot: NO count limit. The only gate is stripModelPrice(grades, tier) at routes/snapshot.js:106-107 — no slice, no volume branch. Live anonymous right now: MLB 5, WNBA 25 = the full board. The "3 scans/day" cap rations the SCAN path only. - Grade letter: fully visible on every tier (grade_visible: true). Anon also receives confidence, edge_pct and VYNDR's own projection. - Edge fields: correctly stripped. p_win/ev_pct/model_odds/value/takeable are ALL absent from the anonymous payload, with model_price_locked stamped so the card shows a lock teaser rather than an absent leg. This half works as designed. THE HEADLINE — the two paths disagree on reasoning: - Scan REDACTS it: tierGating.js lockReasoning + lockKillConditions + tier_gated + upgrade hint, driven by free.reasoning_visible = false. - Board SERVES IT IN FULL: snapshotGating MODEL_FIELDS is [model_odds, p_win, ev_pct, value, takeable] — reasoning is not in the list. Verified live anonymously: full reasoning.summary plus a kill condition WITH its reason. Intent: config/tiers.js declares free: { reasoning_visible: false } with the comment "blurred — frontend renders tier-locked". One of the two paths does not enforce the product's own declared line, so the evidence reads as oversight rather than funnel — a funnel would be declared in config, not contradicted by it. Flagged with the counterweight: board reasoning is good marketing and the data layer is already free, so closing it is a monetization tightening (Kev's call), not a fabrication fix. FREE DATA IS A REAL AGGREGATOR, not just a limited graded view: schedule, per-book lines, player stats, streaks, hot lists, team hubs, public record — all public and uncapped (probed live). PAID (config/tiers.js, checkout.js:4): analyst $14.99 / desk $44.99. Analyst is unlimited reads; Desk differentiates on capability (alt ladder, Kelly, portfolio, engine2). africa tier is defined but activation is blocked on a DB CHECK constraint. api_access is false on every tier. book_odds/fair_odds deliberately pass through on all tiers — the de-vigged fair number is the hook and is never the paywall; only model_odds gates. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc --- specs/tier-structure-pull.md | 119 +++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 specs/tier-structure-pull.md diff --git a/specs/tier-structure-pull.md b/specs/tier-structure-pull.md new file mode 100644 index 0000000..a1a45ab --- /dev/null +++ b/specs/tier-structure-pull.md @@ -0,0 +1,119 @@ +# VYNDR — TIER STRUCTURE PULL (report-only, 2026-07-31) +Real numbers from code + live anonymous prod probes. Nothing changed. + +--- + +## PHASE 1 — THE FREE TIER, EXACTLY + +### 1. How many grades does free/anonymous see? +| path | limit | enforced where | +|---|---|---| +| **Board `/api/snapshot/:sport`** | **NO LIMIT — the entire slate** | `routes/snapshot.js:106-107` — the only gate is `stripModelPrice(grades, tier)`; there is **no count cap, no slice, no tier branch on volume** | +| **Scan `/api/scan`** | **3 per day** (`scans_per_day: 3`) | `config/tiers.js` free block; anonymous callers fall back to the free bucket, IP-keyed | + +**Live, anonymous, right now: MLB 5 grades and WNBA 25 grades returned — the full board.** +So "5 free scans" rations the SCAN path only; the BOARD is unlimited and ungated on volume. + +### 2. Does free see the GRADE LETTER? — **YES, in full.** `grade_visible: true` on every tier. +Anon payload carries `grade: "B"`, `confidence: 63`, `edge_pct: 10.8`, and **`projection: 5.8` +(VYNDR's own model number)**. + +### 3. Does free get REASONING? — **THE TWO PATHS DISAGREE. This is the headline.** +| path | reasoning | evidence | +|---|---|---| +| **Scan** | **REDACTED** | `utils/tierGating.js` — `if (!canAccess(tier,'reasoning_visible'))` → `lockReasoning()` + `lockKillConditions()` + `tier_gated: true` + upgrade hint | +| **Board** | **SERVED IN FULL** | `utils/snapshotGating.js MODEL_FIELDS` = `['model_odds','p_win','ev_pct','value','takeable']` — **`reasoning` is not in the list** | + +**Live anonymous board payload:** `reasoning.summary` = *"Natasha Cloud is averaging 5.8 assists +over his last 5 games…"* plus **1 kill condition WITH its reason**. The scan path locks exactly +this content; the board hands it over. + +### 4. Edge fields — **CORRECTLY STRIPPED.** `p_win`, `ev_pct`, `model_odds`, `value`, `takeable` +are **ALL absent** from the anonymous payload, and `model_price_locked: true` is stamped so the +card renders a lock teaser rather than an absent leg. **This half of the gate works exactly as +designed.** + +--- + +## PHASE 2 — THE FREE DATA EXPERIENCE + +Every one of these is **public, no auth, 200 live**: `schedule/:sport` · `streaks/:sport` · +`hotlist/:sport` · `stats/player/:name` · `books/:sport` (per-book lines) · `gamelines/:sport` · +`team/:abbr` · `accuracy` · `ticker`. + +**So there IS a real free data-aggregator surface, not merely a limited view of the graded +product**: schedules, per-book line shopping, player season/last-10 stats, archetypes, streaks, +hot lists, team hubs and the public model record are all free and uncapped. The graded product +sits on top of that as the paid layer — except that, per Phase 1.3, the board currently gives +away the *explanation* layer too. + +--- + +## PHASE 3 — THE PAID TIERS (as coded) + +`config/tiers.js` — four tiers (`africa` is defined but **activation is blocked on a DB CHECK +constraint**, so it is not live): + +| capability | free | africa (inactive) | analyst | desk | +|---|---|---|---|---| +| scans/day | **3** | 10 | **∞** | **∞** | +| grade_visible | ✅ | ✅ | ✅ | ✅ | +| reasoning_visible | **❌** | ✅ | ✅ | ✅ | +| kill_conditions_detail | ❌ (count only) | ✅ | ✅ | ✅ | +| alerts | ❌ | ❌ | ✅ | ✅ | +| clv_badge | ❌ | — | ✅ | ✅ | +| model_price (our own price) | ❌ | — | ✅ | ✅ | +| alt_line_ladder | ❌ | ❌ | ❌ | ✅ | +| kelly_sizing | ❌ | ❌ | ❌ | ✅ | +| portfolio / engine2 (LLM) | ❌ | ❌ | ❌ | ✅ | +| stat_dashboard | ✅ | ✅ | ✅ | ✅ | +| api_access | ❌ | ❌ | ❌ | ❌ (nobody) | + +**Prices as coded** (`web/src/lib/checkout.js:4`): **analyst `$14.99` · desk `$44.99`**. +Analyst is unlimited reads by design — Desk differentiates on **capability** (alt ladder, Kelly, +portfolio, engine2), not on rationing reads. + +**A deliberate, documented carve-out:** `book_odds` / `fair_odds` pass through on **every** tier — +*"the de-vigged fair number is the free-tier hook, and the fair leg is NEVER the paywall; only our +own price gates."* Only `model_odds` is withheld. + +--- + +## PHASE 4 — THE HONEST PICTURE + +### 8. Deliberate funnel, or oversight? — **THE CONFIG SAYS OVERSIGHT.** +`config/tiers.js` states the intent in one line: **`free: { reasoning_visible: false }`**, with the +comment *"blurred — frontend renders tier-locked."* The scan path implements exactly that. The +board path simply **never got the same treatment** — `MODEL_FIELDS` was written (Session 67) to +close the *model-price* leak and lists only the five price-derived fields. + +The evidence points one way: **the product has declared that free must not see reasoning, and one +of two paths does not enforce it.** I would not call that a funnel decision — a funnel would be +declared in config, not contradicted by it. (It is also not a *new* leak: it has been serving this +way, and the earlier honesty passes did not catch it because they hunted fabricated content, not +under-gated real content.) + +**Worth weighing before "fixing" it:** the board reasoning is genuinely good marketing, and the +line-shopping/stats layer is already free. Closing it is a monetization *tightening*, which is a +different decision from closing a fabrication. That is Kev's call, not a bug fix. + +### 9. WHAT IS GIVEN AWAY vs SOLD TODAY + +| | FREE gets today | PAID adds | +|---|---|---| +| **Grades** | **Whole board, uncapped** + letter + confidence + edge_pct + **VYNDR's projection** | — (nothing on volume) | +| **Explanation** | **Full reasoning + kill-condition reasons ON THE BOARD**; redacted on scan | Reasoning on the **scan** path; kill-condition detail | +| **Prices** | book + **fair (de-vigged)** — deliberate hook | **model_odds** (our price), CLV badge | +| **Edge math** | none (`p_win`/`ev_pct`/`value`/`takeable` all stripped) | the edge fields | +| **Scans** | 3/day | ∞ | +| **Desk-only** | — | alt ladder, Kelly, portfolio, engine2 | +| **Raw data** | schedule, per-book lines, player stats, streaks, hot lists, team hubs, public record — **all uncapped** | — | + +**One-line summary:** free already receives the entire graded board, the model's projection, and — +on the board only — the model's full explanation; paid buys the **price/edge math**, unlimited +scans, and Desk capabilities. **The gate that is declared but not enforced is board reasoning.** + +## TAGS +VERIFIED: every number above (config file/line + live anonymous probes). CANNOT DETERMINE: +`/api/odds/:sport` returned no status on probe (timeout/limiter) — not conclusive either way. +BLOCKED: none. Nothing changed.