// The scoreboard (Oct 3, build 3 of the "perpetual" push; spec: mva-paid-checks-research, section 4). Trust before // money: every thesis gets a number frozen at writing and a date, code decides how it ended, and the desk publishes its // own hit rate in the open, the way Olas Predict publishes accuracy and a Brier score per window. // Harder rules (PO, Oct 3, later the same day; spec: mva-scoreboard-rules-research). The principle: make it harder for // MVA to look good, and let anyone recheck it. This file is served as it runs at /score.ts, with its sha256. // - Two frozen fields per thesis version: p_at_writing (the chance, stated at writing, that the bet plays out by its // date) and resolves_by (the date). Kept in thesis_scores, written once (insert or ignore), never updated: a rewrite // is a new version with its own row. Theses written before build 3 have no stated p: it stays null, "not stated at // writing". Never invented after the fact. Their resolves_by and lines were derived once from their own words, after // writing: they are the older group, shown apart and kept out of the headline numbers. // - The lines code scores against: the target (the bull level for a long, the bear level for a fade, none for a // two-sided trade) and the break line's market cap number when it has one (touched before a date, read on a date, or // held for N days). Lines that overlap (one reading could cross both) are refused at writing (thesis.ts). // - One price source per thesis (the Orbio launchpad or DexScreener), frozen: only its readings can end it. // - Endings, decided by code, never by a model, and each says how it ended (ended_how): // line: two readings in a row past the target (played out) or past the break line (broken); a line read on a date, // or held for N days, uses the reading nearest the end of each day, within WINDOW_S; // own_call: its own check-in said broken (an admission against itself: it counts, it is final; a check-in's "played // out" never counts); // expiry: resolves_by passed with neither, and every day had a reading; // no_reading: a day it needed had no reading (it is never called expired then). // The first ending wins; when one reading confirms both lines, the break line wins, for every thesis. Once decided, // the ending is frozen. A replaced version keeps being scored against its own frozen lines until its own date. // - Scoring: hit rate = played out / (played out + broken); beside it, played out / every ending; Brier only over // theses with a stated p (played out = 1, everything else = 0); median days to resolve; "too early" under SCORE.tooFew. // - Rule changes are dated (RULE_CHANGES); a corrected ending is a dated public entry (ENTRIES), never a silent edit. import { createHash } from 'node:crypto'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { db, nowS } from './db.js'; import { str } from './net.js'; import { SCORE } from './limits.js'; import { leanOf } from './thesispage.js'; import { readingsOf, mcapOf, type Reading } from './market.js'; import { datesIn } from './guard.js'; import { esc, plain, usd } from './page.js'; import { fmtDate } from './dates.js'; import { SITE_URL } from './links.js'; db.exec(`create table if not exists thesis_scores ( thesis_id integer primary key, slug text, symbol text, token text, written_at integer, p_at_writing real, p_note text, resolves_by integer, resolves_note text, lines text, hash text, source text, frozen_at integer, ending text, ended_at integer, ended_by text, ended_reading text ); create index if not exists thesis_scores_slug on thesis_scores(slug);`); // Oct 3, harder rules: the price source each thesis is scored on, and how each ending came about. for (const col of ['price_source text', 'ended_how text']) { try { db.exec(`alter table thesis_scores add column ${col}`); } catch {} } export type Side = 'above' | 'below'; export type Line = { side: Side; usd: number; when: 'touch' | 'on_date' | 'for_days'; date: string | null; days: number | null }; // if_both is gone (Oct 3): older frozen rows may still carry it; it is ignored. export type Lines = { target: { side: Side; usd: number; from: 'bull' | 'bear' } | null; break: Line | null; events: boolean; note: string | null }; export type EndingKind = 'played_out' | 'broken' | 'expired' | 'no_reading'; export type EndedHow = 'line' | 'own_call' | 'expiry' | 'no_reading'; export type Ending = { ending: EndingKind; how: EndedHow; at: number; by: string; reading: Reading | null }; const DAY = 86400; // The nearest reading to a moment counts only within this (a line read on a date, a daily close). export const WINDOW_S = 2 * 3600; // A touch needs this many readings in a row past the line (one thin print can't end a thesis). export const IN_A_ROW = 2; const dayOf = (t: number) => new Date(t * 1000).toISOString().slice(0, 10); const endOfDay = (d: string) => Math.floor(Date.parse(`${d}T23:59:59Z`) / 1000); const nextDay = (d: string) => dayOf(endOfDay(d) + 1); const isDay = (d: unknown) => /^\d{4}-\d{2}-\d{2}$/.test(str(d)) && Number.isFinite(Date.parse(`${str(d)}T00:00:00Z`)); const tsOf = (x: unknown) => { const t = Date.parse(str(x)); return Number.isFinite(t) ? Math.floor(t / 1000) : 0; }; const hm = (t: number) => `${new Date(t * 1000).toISOString().slice(0, 16).replace('T', ' ')} UTC`; // The two sources a thesis can be scored on. Anything else (a check-in's number, its own pool record) never ends one. export const sourceKey = (src: unknown): 'Orbio launchpad' | 'DexScreener' | null => /launchpad/i.test(str(src)) ? 'Orbio launchpad' : /dexscreener/i.test(str(src)) ? 'DexScreener' : null; // The source for a thesis at writing: where today's number for its token comes from (market.ts keeps one per token). export function sourceAtWriting(token: unknown): 'Orbio launchpad' | 'DexScreener' | null { const m = mcapOf(str(token)); return m ? sourceKey(m.src) : null; } // For a frozen row without one (the older theses, or a write with no reading yet): the best-ranked source among its // token's readings since writing (the launchpad record over DexScreener, as market.ts ranks them), set once. function sourceFromReadings(token: unknown, from: number): 'Orbio launchpad' | 'DexScreener' | null { const srcs = readingsOf(str(token), from, 1e12).map(r => sourceKey(r.src)).filter(Boolean); return srcs.includes('Orbio launchpad') ? 'Orbio launchpad' : srcs.includes('DexScreener') ? 'DexScreener' : null; } // The break lines of the theses written before build 3, read once from their own words (Oct 3, ~13:30 UTC, desk.json). // Only the market cap part; whatever else a line says (an event, a ratio, fees) is `events: true`: code can't read it, // so only its own check-in can break it there. CASINO: its break line names $35K, the same as its bear level: the two // overlap (see ENTRIES). export const EXISTING_LINES: Record & { why: string }> = { ordesk: { break: { side: 'above', usd: 50_000, when: 'for_days', date: '2026-10-31', days: 3 }, events: true, why: 'stays above $50K for 3 days before 31 Oct (the token-use part is an event)' }, ai: { break: { side: 'above', usd: 210_000_000, when: 'touch', date: '2026-10-31', days: null }, events: true, why: 'above $210M by 31 Oct (the vault total is an event)' }, axon: { break: { side: 'above', usd: 200_000, when: 'on_date', date: '2026-10-10', days: null }, events: false, why: 'above $200K on 10 Oct' }, casino: { break: { side: 'below', usd: 35_000, when: 'touch', date: null, days: null }, events: true, why: 'below $35K before a payout post; the same level as its bear case, and the break line wins (the payout by 16 Oct is an event)' }, umine: { break: { side: 'above', usd: 78_000, when: 'on_date', date: '2026-10-09', days: null }, events: true, why: 'launchpad cap above $78K on 9 Oct (daily requests are an event)' }, priors: { events: true, why: 'an on-chain deploy before 31 Dec: an event' }, orbiobook: { events: true, why: 'the claimed-agents count on 10 Oct: an event' }, trenches: { events: true, why: 'a burn transfer before 17 Oct: an event' }, si: { events: true, why: 'liquidity against market cap on 17 Oct: a ratio code keeps no history of' }, mva: { events: true, why: 'cap above $264K and 7-day volume above $50K on 9 Oct: both at once, and code keeps no volume history per thesis' }, otis: { events: true, why: 'above $267K only before an auction date is posted, or an OTIS transfer out: events' }, tank: { events: true, why: 'no live staking by 5 Oct: an event' }, yashmode: { events: true, why: 'its share of ORBIO\'s market cap: a ratio' }, hunch: { events: true, why: 'HunchPay funding events by 16 Oct: an event' }, moonlet: { events: true, why: 'fees on the launchpad record and early holders selling: events' }, orbio: { events: true, why: 'the CREDIT price and supply: not ORBIO\'s market cap' }, errand: { events: true, why: 'completed missions and their posters, and cap above $800K on 9 Oct: all at once' }, }; // The target from the thesis's own levels and lean: a long plays out at its bull level, a fade at its bear level; a // two-sided trade names no single target. export function targetOf(doc: any, prefer?: 'bull' | 'bear' | 'none' | null): Lines['target'] { const bull = Number(doc?.scenarios?.bull?.market_cap_usd ?? doc?.bull?.mcap), bear = Number(doc?.scenarios?.bear?.market_cap_usd ?? doc?.bear?.mcap); const lean = leanOf(doc), want = prefer === 'none' ? null : prefer ?? (lean === 'long' ? 'bull' : lean === 'fade' ? 'bear' : null); if (want === 'bull' && bull > 0) return { side: 'above', usd: bull, from: 'bull' }; if (want === 'bear' && bear > 0) return { side: 'below', usd: bear, from: 'bear' }; return null; } // A break line as the write states it (resolution.break_market_cap), checked; null if it isn't a clean one. export function lineOf(x: any): Line | null { if (!x || typeof x !== 'object') return null; const side = str(x.side).toLowerCase(), v = Number(x.usd), when = str(x.when).toLowerCase(); if (side !== 'above' && side !== 'below') return null; if (!(v > 0 && v < 1e13)) return null; const w = when === 'on_date' || when === 'for_days' ? when : 'touch'; const date = isDay(x.date) ? str(x.date) : null, days = Math.round(Number(x.days)); if (w === 'on_date' && !date) return null; if (w === 'for_days' && !(days >= 1 && days <= 60)) return null; return { side: side as Side, usd: v, when: w, date, days: w === 'for_days' ? days : null }; } // The lines for one thesis version: what its write stated (doc.resolution), else what its own break line says // (EXISTING_LINES), else the target alone with the break line marked as one code can't read. export function linesOf(doc: any, slug: string): { lines: Lines; stated: boolean } { const r = doc?.resolution; if (r && typeof r === 'object' && ['bull', 'bear', 'none'].includes(str(r.target))) { const brk = lineOf(r.break_market_cap); return { lines: { target: targetOf(doc, str(r.target) as any), break: brk, events: !brk || r.break_has_events !== false, note: null }, stated: true }; } const e = EXISTING_LINES[slug]; return { lines: { target: targetOf(doc), break: e?.break ?? null, events: e?.events ?? true, note: e?.why ?? 'its break line has no market cap number code can read' }, stated: false }; } const cross = (l: { side: Side; usd: number }, v: number) => (l.side === 'above' ? v >= l.usd : v <= l.usd); const lineWords = (l: { side: Side; usd: number }) => `market cap ${l.side} ${usd(l.usd)}`; // Lines that can't be scored fairly, or null. Refused at writing (thesis.ts); shown on the older theses that have it. // Overlap: one reading can cross both the target and the break line (the same side, or past each other). With the // market cap at writing: a target already reached, or a touch break line already crossed. export function linesProblem(l: Pick, atWriting?: number | null): string | null { const t = l.target, b = l.break; if (t && b && (t.side === b.side || (t.side === 'above' ? b.usd >= t.usd : b.usd <= t.usd))) return `its target (${lineWords(t)}) and its break line (${lineWords(b)}) overlap: one reading can cross both`; const at = Number(atWriting) || 0; if (at > 0 && t && cross(t, at)) return `its target (${lineWords(t)}) is already reached at writing (${usd(at)})`; if (at > 0 && b && b.when === 'touch' && cross(b, at)) return `its break line (${lineWords(b)}) is already crossed at writing (${usd(at)})`; return null; } // resolves_by, derived from its own dates: the latest date in what it waits for and in its break line; with none, the // written date plus SCORE.tradeDays or SCORE.holdDays. Never before the day after writing. export function resolvesByOf(doc: any, written: number): { at: number; note: string } { // datesIn reads "31 Oct" against a year window; centred well after writing so Oct-Dec dates land in the right year. const days = [...(Array.isArray(doc?.waiting_for) ? doc.waiting_for.map((w: any) => str(w?.date)) : []), ...datesIn(str(doc?.breaks_if || doc?.wrong_if), written + 300 * DAY).map(d => d.day)] .filter(isDay).filter(d => endOfDay(d) >= written + DAY); if (days.length) { const d = days.sort().pop()!; return { at: endOfDay(d), note: `its latest date (${d})` }; } const n = doc?.kind === 'hold' ? SCORE.holdDays : SCORE.tradeDays; return { at: endOfDay(dayOf(written + n * DAY)), note: `no date in it: ${n} days for a ${doc?.kind === 'hold' ? 'hold' : 'trade'}` }; } // A stated p, checked: a number between 0.01 and 0.99 (0 to 100 read as percent); anything else is not a statement. export function pOf(x: unknown): number | null { if (x == null || x === '') return null; let p = Number(x); if (!Number.isFinite(p)) return null; if (p > 1 && p <= 100) p = p / 100; return p >= 0.01 && p <= 0.99 ? Math.round(p * 1000) / 1000 : null; } const LEGACY_SOURCE = 'derived after writing (Oct 3, build 3)'; export const isLegacy = (source: unknown) => /derived after writing/.test(str(source)); // Freeze one version: once, never updated. stated: written with this build's write prompt (doc.p_at_writing present // as a field at writing), else derived after writing. export function freeze(row: any, now = nowS()): boolean { if (!row?.id || !row.doc || !row.slug) return false; const had = db.prepare('select slug from thesis_scores where thesis_id=?').get(row.id) as any; if (had && had.slug === row.slug) return false; if (had) db.prepare('delete from thesis_scores where thesis_id=?').run(row.id); // a row id used again after a delete (tests): not this thesis's let doc: any; try { doc = JSON.parse(row.doc); } catch { return false; } const written = tsOf(doc.written_at) || Number(row.at) || now; const atWriting = Object.prototype.hasOwnProperty.call(doc, 'p_at_writing'); // this build's write: the field is there, set or null const p = atWriting ? pOf(doc.p_at_writing) : null; const pNote = p != null ? 'stated at writing' : atWriting ? 'not stated at writing (the write left it out)' : 'not stated at writing'; const own = atWriting && isDay(doc.resolves_by) && endOfDay(doc.resolves_by) >= written + DAY && endOfDay(doc.resolves_by) <= written + 366 * DAY; const rb = own ? { at: endOfDay(doc.resolves_by), note: 'stated at writing' } : resolvesByOf(doc, written); const { lines, stated } = linesOf(doc, String(row.slug)); const priceSource = atWriting ? sourceAtWriting(row.token) : null; // the older ones get theirs from their readings, once const frozen = { slug: row.slug, symbol: row.symbol, written_at: new Date(written * 1000).toISOString(), bet: str(doc.bet || doc.headline), breaks_if: str(doc.breaks_if || doc.wrong_if), bull: doc.scenarios?.bull?.market_cap_usd ?? null, bear: doc.scenarios?.bear?.market_cap_usd ?? null, p_at_writing: p, resolves_by: dayOf(rb.at), lines, ...(priceSource ? { price_source: priceSource } : {}) }; const hash = createHash('sha256').update(JSON.stringify(frozen)).digest('hex'); db.prepare('insert or ignore into thesis_scores(thesis_id,slug,symbol,token,written_at,p_at_writing,p_note,resolves_by,resolves_note,lines,hash,source,frozen_at,price_source) values(?,?,?,?,?,?,?,?,?,?,?,?,?,?)') .run(row.id, row.slug, row.symbol ?? null, str(row.token).toLowerCase() || null, written, p, pNote, rb.at, rb.note, JSON.stringify(lines), hash, atWriting && stated ? 'at writing' : atWriting ? 'at writing (lines read from its break line)' : LEGACY_SOURCE, now, priceSource); return true; } // Every version not yet frozen (cheap: insert or ignore). Live rows, and replaced ones written with a stated p. // Versions replaced before build 3 were never frozen: they are not scored. export function freezeAll(now = nowS()): number { let n = 0; for (const r of db.prepare("select id, at, slug, symbol, token, doc, status from theses where doc is not null and slug is not null and not exists (select 1 from thesis_scores s where s.thesis_id = theses.id and s.slug = theses.slug)").all() as any[]) { if (r.status === 'replaced' && !/"p_at_writing"/.test(String(r.doc))) continue; try { if (freeze(r, now)) n++; } catch (e) { console.error('[score] freeze:', (e as Error).message); } } return n; } // The reading nearest a moment, within WINDOW_S; null if none. function nearest(rs: Reading[], at: number): Reading | null { let best: Reading | null = null; for (const r of rs) if (Math.abs(r.at - at) <= WINDOW_S && (!best || Math.abs(r.at - at) < Math.abs(best.at - at))) best = r; return best; } // The first IN_A_ROW readings in a row past a line, the last one by `until`. function inARow(l: { side: Side; usd: number }, rs: Reading[], until: number): Reading[] | null { for (let i = 0; i + IN_A_ROW - 1 < rs.length; i++) { const run = rs.slice(i, i + IN_A_ROW); if (run[run.length - 1].at > until) return null; if (run.every(r => cross(l, r.usd))) return run; } return null; } const runText = (run: Reading[]) => `${run.map(r => `${usd(r.usd)} at ${hm(r.at)}`).join(', then ')} (${run[run.length - 1].src})`; export type OwnCall = { at: number; why: string; usd?: number | null; src?: string | null }; export type ResolveOpts = { written: number; resolvesBy: number; now: number; from?: number; source?: string | null; ownCall?: OwnCall | null }; // How one version ended, from timed readings (pure). readings: market caps with time and source (only `source`'s count // when it is set); ownCall: its own check-in that said broken; from: when code started watching it (its freeze), for // the days that need a reading. export function resolve(lines: Lines, readings: Reading[], o: ResolveOpts): Ending | null { const end = Math.min(o.resolvesBy, o.now); const src = o.source ?? null, srcName = src ?? 'its source'; const all = readings.filter(r => r.usd > 0 && r.at >= o.written && r.at <= Math.min(o.resolvesBy + WINDOW_S, o.now) && (!src || sourceKey(r.src) === src)).sort((a, b) => a.at - b.at); const rs = all.filter(r => r.at <= end); const ready = (d: string) => o.now >= endOfDay(d) + WINDOW_S; // that day's window has closed // Days from when code started watching to `to` with no reading at all. const watchFrom = Math.max(o.written, o.from ?? 0) + WINDOW_S; const missingDays = (to: number) => { const have = new Set(rs.map(r => dayOf(r.at))), out: string[] = []; if (watchFrom > to) return out; for (let d = dayOf(watchFrom); d <= dayOf(to); d = nextDay(d)) if (!have.has(d)) out.push(d); return out; }; const daysText = (ds: string[]) => `${ds.length} day${ds.length === 1 ? '' : 's'} without a reading from ${srcName} (${ds.slice(0, 5).join(', ')}${ds.length > 5 ? ', …' : ''})`; type Ev = { at: number; kind: EndingKind; how: EndedHow; by: string; reading: Reading | null }; const evs: Ev[] = []; const t = lines.target, b = lines.break; if (t) { const run = inARow(t, rs, end); if (run) evs.push({ at: run[run.length - 1].at, kind: 'played_out', how: 'line', by: `${IN_A_ROW} readings in a row reached its ${t.from} level (${usd(t.usd)}): ${runText(run)}`, reading: run[run.length - 1] }); } if (b) { const what = `its break line (${lineWords(b)}${b.when === 'on_date' ? ` on ${b.date}` : b.when === 'for_days' ? ` for ${b.days} days${b.date ? ` before ${b.date}` : ''}` : b.date ? ` by ${b.date}` : ''})`; if (b.when === 'touch') { const run = inARow(b, rs, b.date ? Math.min(end, endOfDay(b.date)) : end); if (run) evs.push({ at: run[run.length - 1].at, kind: 'broken', how: 'line', by: `${IN_A_ROW} readings in a row crossed ${what}: ${runText(run)}`, reading: run[run.length - 1] }); } else if (b.when === 'on_date' && b.date && endOfDay(b.date) >= o.written && endOfDay(b.date) <= o.resolvesBy && ready(b.date)) { const r = nearest(all, endOfDay(b.date)); if (r && cross(b, r.usd)) evs.push({ at: r.at, kind: 'broken', how: 'line', by: `the reading nearest the end of ${b.date} crossed ${what}: ${usd(r.usd)} at ${hm(r.at)} (${r.src})`, reading: r }); else if (!r) evs.push({ at: endOfDay(b.date), kind: 'no_reading', how: 'no_reading', by: `no reading from ${srcName} within ${WINDOW_S / 3600} h of the end of ${b.date}, the day ${what} is read`, reading: null }); } else if (b.when === 'for_days' && b.days) { // Daily closes: the reading nearest the end of each UTC day. A day with none, while the line is being held, can't // be told apart from a break: it ends there, unresolved. let run = 0; for (let d = dayOf(o.written); endOfDay(d) <= o.resolvesBy && (!b.date || d <= b.date) && ready(d); d = nextDay(d)) { const r = nearest(all, endOfDay(d)); if (!r) { if (run > 0) { evs.push({ at: endOfDay(d), kind: 'no_reading', how: 'no_reading', by: `no reading from ${srcName} near the end of ${d}, day ${run + 1} of ${b.days} while ${what} was being held`, reading: null }); break; } continue; } run = cross(b, r.usd) ? run + 1 : 0; if (run >= b.days) { evs.push({ at: r.at, kind: 'broken', how: 'line', by: `${b.days} daily closes in a row crossed ${what}, the last ${usd(r.usd)} at ${hm(r.at)} (${r.src})`, reading: r }); break; } } } } const oc = o.ownCall; if (oc && oc.at >= o.written && oc.at <= end) evs.push({ at: oc.at, kind: 'broken', how: 'own_call', by: `its own check-in called it broken: ${oc.why}`.slice(0, 400), reading: Number(oc.usd) > 0 ? { at: oc.at, usd: Number(oc.usd), src: `its check-in${oc.src ? ` (${oc.src})` : ''}` } : null }); if (evs.length) { const first = Math.min(...evs.map(e => e.at)), at = evs.filter(e => e.at === first); const pick = at.find(e => e.kind === 'broken') ?? at.find(e => e.kind === 'no_reading') ?? at[0]; // A play-out after a day with no reading, when a touch could have broken it unseen that day: unresolved. if (pick.kind === 'played_out' && b?.when === 'touch') { const miss = missingDays(pick.at); if (miss.length) return { ending: 'no_reading', how: 'no_reading', at: endOfDay(miss[0]), by: `it reached its target later, but ${daysText(miss)} came first, and its break line could have been crossed unseen`, reading: null }; } const both = at.some(e => e.kind === 'played_out') && pick.kind === 'broken' && pick.how === 'line'; return { ending: pick.kind, how: pick.how, at: pick.at, by: both ? `${pick.by}; the same readings also reached its target, and the break line wins (one rule for every thesis)` : pick.by, reading: pick.reading }; } if (o.now > o.resolvesBy + WINDOW_S) { if (t || b) { const miss = missingDays(o.resolvesBy); if (miss.length) return { ending: 'no_reading', how: 'no_reading', at: o.resolvesBy, by: `resolves_by (${dayOf(o.resolvesBy)}) passed with neither line crossed, but with ${daysText(miss)}: it can't be called expired`, reading: null }; } return { ending: 'expired', how: 'expiry', at: o.resolvesBy, by: `resolves_by (${dayOf(o.resolvesBy)}) passed with neither line crossed`, reading: null }; } return null; } // An ending stored before ended_how existed: from its words. const howOf = (ending: string, by: string): EndedHow => ending === 'expired' ? 'expiry' : ending === 'no_reading' ? 'no_reading' : /^its own check-in/.test(by) ? 'own_call' : 'line'; // ---------- the rules, their changes, and the public entries ---------- // Every change to the rules, dated. New entries go at the end; none is ever edited or removed. export const RULE_CHANGES: { date: string; text: string }[] = [ { date: '2026-10-03', text: 'The scoreboard starts (about 14:00 UTC). Each thesis gets a probability and a date frozen at writing, and code decides how it ended from timed market cap readings. The 17 theses written before then get lines read from their own words, after writing.' }, { date: '2026-10-03', text: 'Harder rules, the same day. Its own "broken" call counts as broken, shown apart from a line crossed. When one reading reaches both lines, the break line wins, for every thesis (a thesis no longer picks). A rewrite doesn\'t erase a loss: the replaced version is still scored on its own lines. One price source per thesis. A day without a reading ends as "unresolved: no reading", never "expired". A touch needs 2 readings in a row past the line. Every ending says how it ended. The 17 older theses are kept out of the headline numbers. A hit rate that counts expired theses sits next to the first one. Lines that overlap are refused at writing. No ending decided before this change was changed: MOONLET was the only one, and it reads the same under the new rules.' }, ]; // Public entries: notes on one thesis's score, and corrections. A correction is the only way an ending changes; the // first ending stays on record next to it. export type Entry = { date: string; slug: string; thesis_id: number; kind: 'note' | 'correction'; text: string; ending?: { status: EndingKind; how: EndedHow } }; export const ENTRIES: Entry[] = [ { date: '2026-10-03', slug: 'casino', thesis_id: 15, kind: 'note', text: 'Its target (its bear level, market cap below $35K) and its break line (market cap below $35K before a payout post) overlap: a reading that reaches one crosses the other, and the break line wins. So code can end it broken, expired or unresolved, never played out. Its lean was changed after writing (06:51 UTC), before its lines were frozen. It is scored as it was frozen. A fix comes only as a new version with lines that don\'t overlap; this version stays scored as it stands.' }, ]; const RULES: Record = { p_at_writing: 'the chance, stated when the thesis was written, that it plays out by resolves_by; frozen, never edited; theses written before 3 Oct 2026 (~14:00 UTC) have none: "not stated at writing", never filled in after', resolves_by: 'a date; stated at writing, or for older theses derived once from their own dates (the latest date in what they wait for and their break line; with none, 30 days for a trade, 90 for a hold)', price_source: 'one per thesis, frozen: the source of its market cap at writing (the Orbio launchpad record for launchpad tokens, DexScreener for the rest); only that source\'s readings can end it. Older theses: the best-ranked source among their readings, set once', played_out: `${IN_A_ROW} readings in a row reach its target (the bull level for a long, the bear level for a fade; a two-sided trade names none) before any other ending`, broken_line: `${IN_A_ROW} readings in a row cross the market cap number in its break line (touched before a date); or, for a line read on a date, the reading nearest the end of that day (within ${WINDOW_S / 3600} h); or, for a line held N days, N daily closes in a row (each the reading nearest the end of its day, within ${WINDOW_S / 3600} h)`, broken_own_call: 'its own check-in said broken; the call names what happened, with the number, and keeps the market cap at that moment; it is final; a check-in\'s "played out" never counts', expired: 'resolves_by passed with neither, and every day had a reading: counted apart in the hit rate, in the hit rate counting expired, and as 0 in the Brier score', no_reading: `a day it needed had no reading from its source: the date its break line is read (none within ${WINDOW_S / 3600} h of the end of that day), a day of a held line, a day before a play-out when a touch could have broken it unseen, or any day before its date passed. Shown as "unresolved: no reading", never as expired; counted like expired`, tie: 'the first ending wins; when the same readings reach both the target and the break line, the break line wins, for every thesis', overlap: 'lines where one reading could cross both, a target already reached at writing, or a touch break line already crossed at writing are refused at writing', hit_rate: 'played out / (played out + broken)', hit_rate_all: 'played out / every ending (expired and unresolved included)', brier: 'mean of (p_at_writing - outcome)^2 over ended theses with a stated p; outcome 1 = played out, 0 = anything else; 0 is perfect, 0.25 is a coin', readings: 'market caps code reads anyway (the Orbio launchpad list, DexScreener), at most one per token every 10 minutes, with source and read time; decided by code, never by a model', versions: 'a rewrite is a new version with its own frozen row; the replaced version keeps being scored against its own frozen lines until its own resolves_by. Versions replaced before 3 Oct (~14:00 UTC) were never frozen and are not scored', older: 'the theses written before 3 Oct (~14:00 UTC) had their lines filled in after writing; they are shown in their own group and kept out of the headline numbers', corrections: 'a wrong ending is fixed only by a dated public entry here; the first ending stays on record next to it. Rule changes are dated and listed', }; // The scorer itself, as it runs: served at /score.ts with its sha256, so anyone can read the rules in code. let SOURCE: string | null = null; export function scorerSource(): string { if (SOURCE == null) { try { SOURCE = readFileSync(fileURLToPath(import.meta.url), 'utf8'); } catch { SOURCE = ''; } } return SOURCE; } export const scorer = () => { const s = scorerSource(); return { url: `${SITE_URL}/score.ts`, sha256: s ? createHash('sha256').update(s).digest('hex') : null }; }; // ---------- the scoreboard ---------- export type Status = 'open' | EndingKind; export type ThesisScore = { thesis_id: number; slug: string; symbol: string; written_at: string; p_at_writing: number | null; p_note: string; resolves_by: string; resolves_note: string; target: string | null; break_line: string | null; break_has_events: boolean; lines_note: string | null; lines_problem: string | null; price_source: string | null; source: string; older: boolean; hash: string; status: Status; ended_how: EndedHow | null; ended_at: string | null; ended_by: string | null; reading: { usd: number; read_at: string; source: string } | null; days_to_resolve: number | null; current: boolean; corrected: { was: Status; was_how: EndedHow | null; on: string } | null; entries: { date: string; kind: string; text: string }[] }; export type Window = { window: string; resolved: number; played_out: number; broken: number; broken_line: number; broken_own_call: number; expired: number; no_reading: number; hit_rate: number | null; hit_rate_all: number | null; brier: number | null; brier_n: number; avg_p: number | null; median_days: number | null; too_early: boolean }; export type Scoreboard = { format: 'mva-scores/2'; updated_at: string; open: number; windows: Window[]; older: { note: string; theses: number; open: number; window: Window }; calibration: { bucket: string; n: number; avg_p: number; actual: number }[]; text: string; scorer: { url: string; sha256: string | null }; rules: Record; rule_changes: typeof RULE_CHANGES; entries: Entry[]; theses: ThesisScore[] }; const median = (a: number[]) => { if (!a.length) return null; const s = [...a].sort((x, y) => x - y), m = Math.floor(s.length / 2); return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2; }; const r3 = (n: number) => Math.round(n * 1000) / 1000; const ENDED = ['played_out', 'broken', 'expired', 'no_reading']; export function windowOf(name: string, rows: Pick[]): Window { const done = rows.filter(r => ENDED.includes(r.status)); const n = (f: (r: typeof done[number]) => boolean) => done.filter(f).length; const po = n(r => r.status === 'played_out'), br = n(r => r.status === 'broken'), ex = n(r => r.status === 'expired'), nr = n(r => r.status === 'no_reading'); const own = n(r => r.status === 'broken' && r.ended_how === 'own_call'); const withP = done.filter(r => r.p_at_writing != null); const brier = withP.length ? r3(withP.reduce((a, r) => a + (r.p_at_writing! - (r.status === 'played_out' ? 1 : 0)) ** 2, 0) / withP.length) : null; return { window: name, resolved: done.length, played_out: po, broken: br, broken_line: br - own, broken_own_call: own, expired: ex, no_reading: nr, hit_rate: po + br ? r3(po / (po + br)) : null, hit_rate_all: done.length ? r3(po / done.length) : null, brier, brier_n: withP.length, avg_p: withP.length ? r3(withP.reduce((a, r) => a + r.p_at_writing!, 0) / withP.length) : null, median_days: (() => { const m = median(done.map(r => r.days_to_resolve!).filter(x => x != null)); return m == null ? null : Math.round(m * 10) / 10; })(), too_early: done.length < SCORE.tooFew }; } const lineText = (l: Line) => `${lineWords(l)}${l.when === 'on_date' ? ` on ${l.date}` : l.when === 'for_days' ? ` for ${l.days} days${l.date ? ` before ${l.date}` : ''}` : l.date ? ` by ${l.date}` : ''}`; let memo: { at: number; sb: Scoreboard } | null = null; export function scoreboard(now = nowS()): Scoreboard { freezeAll(now); const rows = db.prepare('select s.*, t.status t_status, t.doc t_doc from thesis_scores s join theses t on t.id = s.thesis_id and t.slug = s.slug order by s.written_at, s.thesis_id').all() as any[]; const theses: ThesisScore[] = []; for (const r of rows) { let doc: any = {}; try { doc = JSON.parse(r.t_doc ?? '{}') ?? {}; } catch {} const lines: Lines = JSON.parse(r.lines); const written = Number(r.written_at), resolvesBy = Number(r.resolves_by); let src: string | null = r.price_source ?? null; if (!src) { src = sourceFromReadings(r.token, written); if (src) db.prepare('update thesis_scores set price_source=? where thesis_id=? and price_source is null').run(src, r.thesis_id); } const checkins = (Array.isArray(doc.checkins) ? doc.checkins : []).filter((c: any) => tsOf(c?.at)).sort((a: any, b: any) => tsOf(a.at) - tsOf(b.at)); let end: Ending | null = r.ending ? { ending: r.ending, how: (r.ended_how as EndedHow) || howOf(r.ending, str(r.ended_by)), at: Number(r.ended_at), by: str(r.ended_by), reading: r.ended_reading ? JSON.parse(r.ended_reading) : null } : null; if (!end) { const ck = checkins.find((c: any) => str(c?.state) === 'broken'); end = resolve(lines, readingsOf(r.token, written, Math.min(resolvesBy + WINDOW_S, now)), { written, resolvesBy, now, from: Number(r.frozen_at) || written, source: src, ownCall: ck ? { at: tsOf(ck.at), why: str(ck.why), usd: Number(ck.market_cap_usd) || null, src: str(ck.market_cap_src) || null } : null }); // Frozen once decided (an expiry is final too). if (end) db.prepare('update thesis_scores set ending=?, ended_at=?, ended_by=?, ended_reading=?, ended_how=? where thesis_id=? and ending is null').run(end.ending, end.at, end.by, end.reading ? JSON.stringify(end.reading) : null, end.how, r.thesis_id); } // An own call stored before the market cap was kept with it: the check-in's own number, at that moment. if (end?.how === 'own_call' && !end.reading) { const c = checkins.find((x: any) => tsOf(x.at) === end!.at); if (Number(c?.market_cap_usd) > 0) end.reading = { at: end.at, usd: Number(c.market_cap_usd), src: 'its check-in' }; } // An own call's words, whole (Oct 3 review: the stored text was cut at 400 characters, "…and market cap."): the // check-in's own text from the doc. What was decided (the ending, its time, its reading) is unchanged. if (end?.how === 'own_call') { const c = checkins.find((x: any) => tsOf(x.at) === end!.at), head = /^its own check-in[^:]*:\s*/.exec(end.by)?.[0]; if (c?.why && head && str(c.why).length > end.by.length - head.length && str(c.why).startsWith(end.by.slice(head.length, head.length + 60))) end = { ...end, by: `${head}${str(c.why)}` }; } const entries = ENTRIES.filter(e => e.thesis_id === Number(r.thesis_id) && e.slug === r.slug); const fix = [...entries].reverse().find(e => e.kind === 'correction' && e.ending); let status: Status = end ? end.ending : 'open', how: EndedHow | null = end?.how ?? null, corrected: ThesisScore['corrected'] = null; if (fix?.ending) { corrected = { was: status, was_how: how, on: fix.date }; status = fix.ending.status; how = fix.ending.how; } theses.push({ thesis_id: Number(r.thesis_id), slug: r.slug, symbol: r.symbol, written_at: new Date(written * 1000).toISOString(), p_at_writing: r.p_at_writing == null ? null : Number(r.p_at_writing), p_note: r.p_note, resolves_by: dayOf(resolvesBy), resolves_note: r.resolves_note, target: lines.target ? `${lineWords(lines.target)} (its ${lines.target.from} level)` : null, break_line: lines.break ? lineText(lines.break) : null, break_has_events: lines.events, lines_note: lines.note, lines_problem: linesProblem(lines, Number(doc.market_cap_at_writing_usd) || null), price_source: src, source: r.source, older: isLegacy(r.source), hash: r.hash, status, ended_how: how, ended_at: end ? new Date(end.at * 1000).toISOString() : null, ended_by: end?.by ?? null, reading: end?.reading ? { usd: Math.round(end.reading.usd), read_at: new Date(end.reading.at * 1000).toISOString(), source: end.reading.src } : null, days_to_resolve: end ? Math.round((end.at - written) / DAY * 10) / 10 : null, current: r.t_status !== 'replaced', corrected, entries: entries.map(e => ({ date: e.date, kind: e.kind, text: e.text })) }); } const head = theses.filter(t => !t.older), old = theses.filter(t => t.older); const all = windowOf('all', head), d30 = windowOf('30d', head.filter(t => t.ended_at && tsOf(t.ended_at) > now - 30 * DAY)); const olderW = windowOf('older', old); const withP = head.filter(t => t.p_at_writing != null && ENDED.includes(t.status)); const calibration = [[0, 0.2], [0.2, 0.4], [0.4, 0.6], [0.6, 0.8], [0.8, 1.01]].map(([lo, hi]) => { const b = withP.filter(t => t.p_at_writing! >= lo && t.p_at_writing! < hi); return { bucket: `${Math.round(lo * 100)}-${Math.min(100, Math.round(hi * 100))}%`, n: b.length, avg_p: b.length ? r3(b.reduce((a, t) => a + t.p_at_writing!, 0) / b.length) : 0, actual: b.length ? r3(b.filter(t => t.status === 'played_out').length / b.length) : 0 }; }).filter(x => x.n > 0); const open = head.filter(t => t.status === 'open').length, olderOpen = old.filter(t => t.status === 'open').length; const older = { note: `The ${old.length} theses written before 3 Oct 2026 (~14:00 UTC). Their lines were filled in after writing, so they are kept out of the headline numbers.`, theses: old.length, open: olderOpen, window: olderW }; const sb: Scoreboard = { format: 'mva-scores/2', updated_at: new Date(now * 1000).toISOString(), open, windows: [all, d30], older, calibration, text: scoreText(all, open, head, { window: olderW, theses: old.length, open: olderOpen }), scorer: scorer(), rules: RULES, rule_changes: RULE_CHANGES, entries: ENTRIES, theses }; memo = { at: now, sb }; return sb; } // The same board for a page drawn right after it (the thesis pages, drawn with the desk): reused within a minute. export function scoreboardNow(now = nowS()): Scoreboard { return memo && Math.abs(now - memo.at) < 60 ? memo.sb : scoreboard(now); } const endingsText = (w: Window) => `${w.played_out} played out, ${w.broken} broken (${w.broken_line} by a line, ${w.broken_own_call} by its own call), ${w.expired} expired, ${w.no_reading} unresolved for lack of a reading`; export function scoreText(w: Window, open: number, rows: Pick[], older?: { window: Window; theses: number; open: number }): string { const withP = rows.filter(t => t.p_at_writing != null).length; const hit = w.hit_rate == null ? 'no hit rate yet' : `hit rate ${Math.round(w.hit_rate * 100)}% (${w.played_out} of ${w.played_out + w.broken}), ${Math.round(w.hit_rate_all! * 100)}% counting every ending (${w.played_out} of ${w.resolved})`; const head = `Theses written with their lines: n=${w.resolved} ended${w.too_early ? ', too early' : ''}: ${endingsText(w)}; ${hit}; ${open} open.${w.brier != null ? ` Brier ${w.brier.toFixed(2)} over ${w.brier_n}.` : ` No Brier score yet: ${withP ? `${withP} thesis${withP === 1 ? '' : 'es'} with a stated probability, none ended` : 'no thesis written with a stated probability yet'}.`}`; if (!older || !older.theses) return head; const o = older.window; return `${head} The ${older.theses} older theses, lines filled in after writing and kept out of these numbers: ${o.resolved} ended (${endingsText(o)}); ${older.open} open.`; } // ---------- on the pages ---------- export const SCORE_CSS = `.sb{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin:0 0 12px}.sb.five{grid-template-columns:repeat(5,minmax(0,1fr))}.sb div{background:var(--soft);border-radius:10px;padding:14px;min-width:0} .sb b{display:block;font:600 24px/1.15 ui-monospace,"SF Mono",Menlo,monospace}.sb span{display:block;font:12px/1.35 ui-monospace,"SF Mono",Menlo,monospace;color:var(--dim);margin-top:4px} .sbtxt{font:13px/1.6 ui-monospace,"SF Mono",Menlo,monospace;color:var(--dim);margin:0 0 8px;overflow-wrap:anywhere} .sbh{font:600 14px/1.5 ui-monospace,"SF Mono",Menlo,monospace;margin:16px 0 8px} .sbl{list-style:none;padding:0;margin:8px 0 0}.sbl li{font:13px/1.55 ui-monospace,"SF Mono",Menlo,monospace;padding:8px 0;border-top:1px solid var(--line);margin:0;overflow-wrap:anywhere}.sbl .st{font-weight:600} details.sbd summary{cursor:pointer;font:14px/1.5 ui-monospace,"SF Mono",Menlo,monospace} @media (max-width:760px){.sb,.sb.five{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.sb div{padding:10px}.sb b{font-size:20px}}`; export function statusWords(t: Pick): string { if (t.status === 'broken') return t.ended_how === 'own_call' ? 'broken (own call)' : 'broken (line crossed)'; return ({ open: 'open', played_out: 'played out', expired: 'expired', no_reading: 'unresolved: no reading' } as Record)[t.status] ?? t.status; } export function thesisScoreLine(t: ThesisScore): string { const p = t.p_at_writing != null ? `p ${t.p_at_writing.toFixed(2)} at writing` : esc(t.p_note); const end = t.status === 'open' ? `open until ${esc(fmtDate(t.resolves_by, 'date'))}` : `${statusWords(t)} ${esc(fmtDate(t.ended_at!, 'date'))}${t.reading ? ` at ${usd(t.reading.usd)} (${esc(t.reading.source)})` : ''}`; const lines = [t.target ? `plays out at ${esc(t.target)}` : 'no single target (two-sided)', t.break_line ? `breaks at ${esc(t.break_line)}` : 'breaks only on events', t.break_has_events && t.break_line ? 'plus events' : '', t.price_source ? `read on ${esc(t.price_source)}` : ''].filter(Boolean).join(' · '); const notes = [t.current ? '' : 'an earlier version, still scored on its own lines', t.lines_problem ? esc(t.lines_problem) : '', t.corrected ? `corrected ${esc(fmtDate(t.corrected.on, 'date'))}, was ${statusWords({ status: t.corrected.was, ended_how: t.corrected.was_how })}` : ''].filter(Boolean).join(' · '); return `
  • $${esc(t.symbol)} ${end} · ${p}
    ${lines}${notes ? `
    ${notes}` : ''}
  • `; } const cap = (x: string) => x ? x[0].toUpperCase() + x.slice(1) : x; const tile = (v: string, label: string) => `
    ${esc(v)}${esc(label)}
    `; const pct = (x: number | null) => (x == null ? '–' : `${Math.round(x * 100)}%`); const splitTiles = (w: Window) => `
    ${tile(String(w.played_out), 'played out')}${tile(String(w.broken_line), 'broken (line crossed)')}${tile(String(w.broken_own_call), 'broken (own call)')}${tile(String(w.expired), 'expired')}${tile(String(w.no_reading), 'unresolved: no reading')}
    `; export function rulesHtml(sb: Scoreboard): string { return `
    How it is scored, and every change to the rules
      ${Object.entries(sb.rules).map(([k, v]) => `
    • ${esc(k.replace(/_/g, ' '))} · ${esc(v)}
    • `).join('')}

    Changes to the rules

      ${sb.rule_changes.map(c => `
    • ${esc(fmtDate(c.date, 'date'))} ${esc(c.text)}
    • `).join('')}

    Corrections and notes

      ${sb.entries.length ? sb.entries.map(e => `
    • ${esc(fmtDate(e.date, 'date'))} · $${esc(e.slug.toUpperCase())} · ${esc(e.kind)} ${esc(e.text)}
    • `).join('') : '
    • None yet.
    • '}

    The scorer runs as published in score.ts${sb.scorer.sha256 ? ` (sha256 ${esc(sb.scorer.sha256.slice(0, 16))}…)` : ''}. Every number is in scoreboard.json.

    `; } export function scoreboardHtml(sb: Scoreboard, o: { list?: boolean; heading?: string } = {}): string { const w = sb.windows[0], ow = sb.older.window; const head = sb.theses.filter(t => !t.older), old = sb.theses.filter(t => t.older); const list = (ts: ThesisScore[]) => ts.length ? `
      ${ts.map(thesisScoreLine).join('')}
    ` : '

    None yet.

    '; return `${esc(o.heading ?? 'Scoreboard')}

    Theses written with their lines stated, from 3 Oct 2026.

    ${tile(String(w.resolved), 'ended')}${tile(pct(w.hit_rate), w.too_early ? 'hit rate (too early)' : 'hit rate')}${tile(pct(w.hit_rate_all), 'hit rate counting expired')}${tile(String(sb.open), 'open')}
    ${splitTiles(w)}

    ${esc(sb.older.note)} ${ow.resolved} ended, ${sb.older.open} open.

    ${splitTiles(ow)}

    ${esc(sb.text)} Endings are decided by code from timed market cap readings against each thesis's own lines.

    ${o.list === false ? '' : `
    Each thesis: its date, its lines, how it ended

    Written with their lines

    ${list(head)}

    Older theses (lines filled in after writing)

    ${list(old)}
    `} ${rulesHtml(sb)}`; } // On a thesis page: how this version (and any earlier version still scored) stands on the scoreboard. export function thesisScoreHtml(slug: string, now = nowS()): string { let sb: Scoreboard; try { sb = scoreboardNow(now); } catch { return ''; } const ts = sb.theses.filter(t => t.slug === slug).sort((a, b) => Number(b.current) - Number(a.current) || tsOf(b.written_at) - tsOf(a.written_at)); if (!ts.length) return ''; const one = (t: ThesisScore) => { const end = t.status === 'open' ? `Open. It ends by ${esc(fmtDate(t.resolves_by, 'date'))} at the latest.` : `${esc(cap(statusWords(t)))}, ${esc(fmtDate(t.ended_at!, 'full'))}.`; const items = [ t.ended_by ? `${plain(cap(t.ended_by).replace(/[.\s]+$/, ''))}.` : '', t.reading ? `${t.ended_how === 'own_call' ? 'The market cap at the call was' : 'The reading that ended it was'} ${usd(t.reading.usd)} (${esc(t.reading.source)}, ${esc(fmtDate(t.reading.read_at, 'full'))}).` : '', t.ended_how === 'own_call' ? 'Its own call against itself counts as broken, and it is final.' : '', `${t.target ? `Plays out at ${esc(t.target)}` : 'No single target (two-sided)'}. ${t.break_line ? `Breaks at ${esc(t.break_line)}${t.break_has_events ? ', plus events' : ''}` : 'Breaks only on events (its own call)'}.`, t.price_source ? `Scored on ${esc(t.price_source)} readings only.` : 'No price source yet: it is set from its first reading.', `${t.p_at_writing != null ? `p ${t.p_at_writing.toFixed(2)} at writing` : esc(cap(t.p_note))}; resolves by ${esc(fmtDate(t.resolves_by, 'date'))}.`, t.older ? 'An older thesis: its lines were filled in after writing, so it is kept out of the headline numbers.' : '', t.lines_problem ? `${esc(cap(t.lines_problem))}.` : '', t.corrected ? `Corrected ${esc(fmtDate(t.corrected.on, 'date'))}; it first ended ${esc(statusWords({ status: t.corrected.was, ended_how: t.corrected.was_how }))}.` : '', ...t.entries.map(e => `${esc(fmtDate(e.date, 'date'))} · a public ${esc(e.kind)} ${esc(e.text)}`), t.older ? `Frozen after writing, on 3 Oct, as hash ${esc(t.hash.slice(0, 16))}…` : `Frozen at writing as hash ${esc(t.hash.slice(0, 16))}…`, ].filter(Boolean); return `${t.current ? '' : `

    An earlier version, written ${esc(fmtDate(t.written_at, 'date'))}, still scored on its own lines

    `}

    ${end}

      ${items.map(x => `
    • ${x}
    • `).join('')}
    `; }; return `

    On the scoreboard

    ${ts.map(one).join('')}

    The rules and every number are in scoreboard.json. The scorer runs as published in score.ts.

    `; }