S1 (a1): feature-promise audit — no claim survives unverified
PROMISE-AUDIT.md: every /pricing claim → verified/built/reworded. BUILT (was vapor): alt line ladder + edge ranking (same-features regrade at shifted lines, Desk-gated at the API), quarter-Kelly (engine quantile P(win) x real captured odds — either missing → no sizing), free-tier kill-condition locked previews. FIXED (was false): analyst 15/day cap vs the Founder 'Unlimited reads' promise → analyst unlimited; every '40+ factors' claim (real count: 22 named features) reworded truthfully in 7 files. VERIFIED: cascade alerts (real, wired), phi correlation, leg history, cross-book comparison, WC soccer, real-time feed. Locked by tests/unit/promiseAudit.test.js. Jest now ignores .claude/worktrees (parallel agents' suites no longer leak into runs). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,7 @@ export default function AboutPage() {
|
||||
<p className="mono" style={{ fontSize: 14.5, color: 'var(--text-1)', lineHeight: 1.7 }}>
|
||||
VYNDR is prop intelligence — a proprietary terminal that reads the context the books hide.
|
||||
Beyond the box score: form, matchup, usage, rest, the cascade when a star sits. Then a single
|
||||
grade, earned across 40+ factors, that tells you whether the number is worth taking.
|
||||
grade, earned across every factor the model weighs, that tells you whether the number is worth taking.
|
||||
</p>
|
||||
<p className="mono" style={{ fontSize: 14.5, color: 'var(--text-1)', lineHeight: 1.7 }}>
|
||||
We are an analytics tool, not a sportsbook. We don't take wagers. We hand you the read and
|
||||
|
||||
@@ -5,7 +5,7 @@ import SectionHead from '@/components/vyndr/SectionHead';
|
||||
import TerminalInput from '@/components/vyndr/TerminalInput';
|
||||
|
||||
const FAQ: { cat: string; q: string; a: string }[] = [
|
||||
{ cat: 'GRADES', q: 'What does a VYNDR grade mean?', a: 'A grade is our confidence that a prop hits, weighed across 40+ factors. A+ is the rarest and strongest; D means stay away. The letter is earned, not opinion.' },
|
||||
{ cat: 'GRADES', q: 'What does a VYNDR grade mean?', a: 'A grade is our confidence that a prop hits, weighed across every factor the model computes for that prop. A+ is the rarest and strongest; D means stay away. The letter is earned, not opinion.' },
|
||||
{ cat: 'GRADES', q: 'What is a kill condition?', a: 'A red flag we detect that can sink a prop regardless of the stats — a blowout risk, a minutes cap, a brutal matchup. Analyst and Desk see them in full.' },
|
||||
{ cat: 'READS', q: 'How many free reads do I get?', a: 'Five reads every calendar month on the Free tier. The counter resets at the start of each month.' },
|
||||
{ cat: 'READS', q: 'What counts as a read?', a: 'Grading one prop. Viewing the same prop twice in a session only counts once.' },
|
||||
|
||||
@@ -44,7 +44,8 @@ interface ScanResponse {
|
||||
confidence?: number;
|
||||
sample_size?: number;
|
||||
factors?: Record<string, string>;
|
||||
alt_lines?: { line: number; grade: string; hit_rate?: number; edge_pct?: number }[];
|
||||
alt_lines?: { line: number; grade: string; hit_rate?: number; edge_pct?: number; base?: boolean }[];
|
||||
kelly?: { pct: number; quarter: number; full: number; odds: string };
|
||||
kill_conditions?: { code: string; reason: string }[];
|
||||
reasoning?: string;
|
||||
historical_hit_rate?: number;
|
||||
@@ -733,6 +734,7 @@ export default function ScanPage() {
|
||||
sample_size: result.sample_size,
|
||||
factors: result.factors,
|
||||
alt_lines: result.alt_lines,
|
||||
kelly: result.kelly,
|
||||
kill_conditions: result.kill_conditions,
|
||||
tier,
|
||||
// Session 44 — forward the engine's intel fields so the grade
|
||||
|
||||
@@ -14,7 +14,7 @@ const PRICES: Record<Cadence, { display: string; sub: string; checkoutParam: str
|
||||
|
||||
const EARLY_FEATURES = [
|
||||
'Unlimited reads',
|
||||
'Full factor analysis (40+ signals)',
|
||||
'Full factor analysis — every signal the model weighs',
|
||||
'Kill conditions surfaced inline',
|
||||
'Cascade alerts when lineups shift',
|
||||
'Parlay leg history with grades',
|
||||
|
||||
@@ -8,7 +8,7 @@ import { markReadComplete } from '@/lib/reads';
|
||||
// Short, plain-English explanations rendered when Explain Like I'm New is on.
|
||||
// Each key maps to one piece of data we surface on this card.
|
||||
const EXPLANATIONS = {
|
||||
grade: "Our overall confidence. A-minus means we estimate about a 76% chance this prop hits, based on 40+ factors.",
|
||||
grade: "Our overall confidence. A-minus means we estimate about a 76% chance this prop hits, based on every factor the model weighs for the prop.",
|
||||
projection: "What our model predicts the player will actually do tonight for this stat.",
|
||||
line: "The number the sportsbook set. The player needs to go over or under it.",
|
||||
overUnder: 'Over = the player needs MORE than the line. Under = LESS.',
|
||||
|
||||
@@ -36,8 +36,8 @@ const TIERS: TierConfig[] = [
|
||||
'Confidence indicator',
|
||||
],
|
||||
locked: [
|
||||
'Factor analysis (blurred)',
|
||||
'Kill conditions (blurred)',
|
||||
'Factor analysis (3-signal preview)',
|
||||
'Kill conditions (locked preview)',
|
||||
'Alt line ladder (locked)',
|
||||
],
|
||||
highlight: false,
|
||||
@@ -54,13 +54,13 @@ const TIERS: TierConfig[] = [
|
||||
cta: 'Unlock Africa Pricing',
|
||||
features: [
|
||||
'10 reads per day',
|
||||
'Full factor analysis (40+ signals)',
|
||||
'Full factor analysis — every signal the model weighs',
|
||||
'Kill conditions surfaced inline',
|
||||
'Grade + reasoning visible',
|
||||
'World Cup soccer intelligence',
|
||||
],
|
||||
locked: [
|
||||
'Cascade alerts (Analyst+)',
|
||||
'Scratch cascade alerts (Analyst+)',
|
||||
'Alt line ladder (Desk only)',
|
||||
],
|
||||
highlight: false,
|
||||
@@ -76,9 +76,9 @@ const TIERS: TierConfig[] = [
|
||||
cta: 'Lock Founder Price',
|
||||
features: [
|
||||
'Unlimited reads',
|
||||
'Full factor analysis (40+ signals)',
|
||||
'Full factor analysis — every signal the model weighs',
|
||||
'Kill conditions surfaced inline',
|
||||
'Cascade alerts when lineups shift',
|
||||
'Scratch cascade alerts when players are pulled',
|
||||
'Parlay leg history with grades',
|
||||
'Sportsbook deep links',
|
||||
],
|
||||
|
||||
@@ -24,7 +24,9 @@ export interface GradeResultData {
|
||||
signals: string[];
|
||||
killConditions?: string[];
|
||||
books: Array<{ name: string; line: number; odds: string; best?: boolean }>;
|
||||
altLadder?: Array<{ line: number; grade: string }>;
|
||||
altLadder?: Array<{ line: number; grade: string; edge?: number | null; base?: boolean }>;
|
||||
// Session 62 (A1-S1) — quarter-Kelly sizing (Desk only; real p × real odds).
|
||||
kelly?: { pct: number; quarter: number; odds: string };
|
||||
// Session 42 — Player Intelligence additions (all optional; self-hide).
|
||||
archetypeBlend?: Array<{ archetype: string; weight: number }>;
|
||||
propDNA?: { reliable: string[]; volatile: string[] };
|
||||
@@ -183,7 +185,7 @@ export default function GradeResultCard({
|
||||
{/* 5. SIGNAL BREAKDOWN */}
|
||||
{d.signals.length > 0 && (
|
||||
<div style={{ padding: '16px 20px' }}>
|
||||
<SectionHead style={{ marginBottom: 12 }}>SIGNAL BREAKDOWN · {d.signals.length} OF 40+ FACTORS</SectionHead>
|
||||
<SectionHead style={{ marginBottom: 12 }}>SIGNAL BREAKDOWN · {d.signals.length} FACTORS SHOWN</SectionHead>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 9 }}>
|
||||
{d.signals.map((s, i) => (
|
||||
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 11 }}>
|
||||
@@ -266,12 +268,23 @@ export default function GradeResultCard({
|
||||
</SectionHead>
|
||||
<div style={{ display: 'flex', gap: 7, flexWrap: 'wrap' }}>
|
||||
{d.altLadder!.map((a, i) => (
|
||||
<div key={i} style={{ flex: '1 1 0', minWidth: 64, textAlign: 'center', padding: '9px 6px', background: 'var(--bg-2)', border: '1px solid var(--border)', borderRadius: 6 }}>
|
||||
<div className="mono" style={{ fontSize: 12, color: 'var(--text-1)', marginBottom: 5 }}>{a.line}</div>
|
||||
<div key={i} style={{ flex: '1 1 0', minWidth: 64, textAlign: 'center', padding: '9px 6px', background: 'var(--bg-2)', border: a.base ? '1px solid var(--border-hi)' : '1px solid var(--border)', borderRadius: 6 }}>
|
||||
<div className="mono" style={{ fontSize: 12, color: a.base ? 'var(--text-0)' : 'var(--text-1)', marginBottom: 5 }}>{a.line}{a.base ? ' •' : ''}</div>
|
||||
<div className="mono" style={{ fontSize: 17, fontWeight: 800, color: gradeColor(a.grade) }}>{a.grade}</div>
|
||||
{a.edge != null && (
|
||||
<div className="mono" style={{ fontSize: 10, color: Number(a.edge) >= 0 ? 'var(--g-a)' : 'var(--miss)', marginTop: 3 }}>{Number(a.edge) >= 0 ? '+' : ''}{a.edge}%</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* Quarter-Kelly (Desk) — real probability × the real captured odds. */}
|
||||
{d.kelly && (
|
||||
<div className="mono" style={{ marginTop: 10, fontSize: 12, color: 'var(--text-1)' }}>
|
||||
<span className="label" style={{ fontSize: 9.5, marginRight: 8 }}>QTR-KELLY</span>
|
||||
<span style={{ color: 'var(--g-a)', fontWeight: 700 }}>{d.kelly.pct}% of bankroll</span>
|
||||
<span style={{ color: 'var(--text-2)' }}> · at {d.kelly.odds}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ export default function ProcessingGrade({ data, replayKey = 0, onShare, onAddToP
|
||||
</div>
|
||||
<div style={{ marginLeft: 'auto', textAlign: 'right' }}>
|
||||
<div className="mono" style={{ fontSize: 30, fontWeight: 800, color: 'var(--g-ap)', textShadow: '0 0 16px rgba(0,255,184,.5)' }}>{pct}%</div>
|
||||
<div className="mono" style={{ fontSize: 9.5, letterSpacing: '0.12em', color: 'rgba(232,255,244,.5)' }}>WEIGHING 40+ FACTORS</div>
|
||||
<div className="mono" style={{ fontSize: 9.5, letterSpacing: '0.12em', color: 'rgba(232,255,244,.5)' }}>WEIGHING THE FACTORS</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -64,8 +64,14 @@ function mapScanToGradeResult(input = {}) {
|
||||
// richer blurred-paywall treatment returns in Phase G (Session 38).
|
||||
const allSignals = toSignals(input.factors);
|
||||
const signals = tier === 'free' ? allSignals.slice(0, 3) : allSignals;
|
||||
// Session 62 (A1-S1) — the pricing page promises free users a LOCKED
|
||||
// PREVIEW of kill conditions (the count + codes exist server-side via
|
||||
// tierGating), not their absence. Free sees "⚠ TRAP — upgrade to see
|
||||
// details"; paid sees the reasons.
|
||||
const killConditions = tier === 'free'
|
||||
? []
|
||||
? (input.kill_conditions || [])
|
||||
.map((k) => (k && k.code ? `${k.code} — upgrade to see details` : ''))
|
||||
.filter(Boolean)
|
||||
: (input.kill_conditions || []).map((k) => (typeof k === 'string' ? k : (k && k.reason) || '')).filter(Boolean);
|
||||
|
||||
return {
|
||||
@@ -89,8 +95,10 @@ function mapScanToGradeResult(input = {}) {
|
||||
killConditions,
|
||||
books: Array.isArray(input.books) ? input.books : [],
|
||||
altLadder: includeAlt && Array.isArray(input.alt_lines)
|
||||
? input.alt_lines.map((a) => ({ line: a.line, grade: a.grade }))
|
||||
? input.alt_lines.map((a) => ({ line: a.line, grade: a.grade, edge: a.edge_pct, base: a.base }))
|
||||
: [],
|
||||
// Session 62 (A1-S1) — quarter-Kelly (Desk; API already strips below).
|
||||
kelly: includeAlt && input.kelly && input.kelly.pct != null ? input.kelly : undefined,
|
||||
// Session 42 — Player Intelligence additions. All OPTIONAL + self-hiding:
|
||||
// they only render once the engine supplies them (Session 43 data pipeline).
|
||||
...buildIntelFields(input),
|
||||
|
||||
Reference in New Issue
Block a user