diff --git a/daysnap.js b/daysnap.js
new file mode 100644
index 0000000..cdb16a3
--- /dev/null
+++ b/daysnap.js
@@ -0,0 +1,233 @@
+// The day page (Marty, 2026-09-23): everything that happened across the WHOLE contract on one
+// Central calendar day, at a dated address that keeps working forever.
+//
+// Why it exists: the daily Telegram snapshot outgrew Telegram. Past 1,000 members the post hit
+// the 4,096-character limit and was rejected outright for two mornings, and capping the lists to
+// fit meant deciding what to throw away. Manson caught the cost of that: the level-up list was
+// ordered by time, so the cap hid 2 Culmen and 13 Fabrica behind 24 routine Ascensus upgrades.
+// Now the post carries the headline and the climbs, and everything else lives here with no cap.
+//
+// Today is computed live from the chain index. A past day is read from a file written once when
+// that day closed, because the index only keeps the most recent few hundred payouts (about four
+// days at current volume) and an older day could never be rebuilt from it.
+'use strict';
+const fs = require('fs');
+const path = require('path');
+
+let DATA_DIR = null;
+const LEVELS = ['Scintilla', 'Ascensus', 'Fabrica', 'Culmen', 'Apex', 'Fastigium', 'Vertex', 'Corona'];
+const lname = n => (n >= 1 && n <= 8) ? LEVELS[n - 1] : 'L' + n;
+
+function init(opts) { DATA_DIR = opts.dataDir; try { fs.mkdirSync(path.join(DATA_DIR, 'days'), { recursive: true }); } catch (e) {} }
+const indexFile = () => path.join(DATA_DIR, 'chain-index.json');
+const dayFile = key => path.join(DATA_DIR, 'days', key + '.json');
+
+// Central calendar day, the same key the rest of the estate uses
+const dayKey = ts => new Date(ts === undefined ? Date.now() : ts).toLocaleDateString('en-CA', { timeZone: 'America/Chicago' });
+const isKey = k => /^\d{4}-\d{2}-\d{2}$/.test(String(k || ''));
+// midnight-to-midnight in Central, found by bisection so it is right on both clock-change days
+function bounds(key) {
+ const probe = Date.parse(key + 'T12:00:00Z');
+ let lo = probe - 36 * 3600000, hi = probe;
+ while (hi - lo > 1000) { const mid = Math.floor((lo + hi) / 2); if (dayKey(mid) < key) lo = mid; else hi = mid; }
+ const start = hi;
+ lo = start; hi = start + 36 * 3600000;
+ while (hi - lo > 1000) { const mid = Math.floor((lo + hi) / 2); if (dayKey(mid) <= key) lo = mid; else hi = mid; }
+ return { start, end: hi };
+}
+
+function readIndex() { try { return JSON.parse(fs.readFileSync(indexFile(), 'utf8')); } catch (e) { return null; } }
+
+// everything that happened on one Central day, company-wide: no team or org filter anywhere
+function compute(key) {
+ const idx = readIndex();
+ if (!idx) return null;
+ const { start, end } = bounds(key);
+ const s = Math.floor(start / 1000), e = Math.floor(end / 1000);
+ const members = idx.members || {};
+ const all = idx.payouts || [];
+
+ const joins = Object.keys(members)
+ .map(i => ({ id: Number(i), m: members[i] }))
+ .filter(x => { const t = Number(x.m.joinedAt || 0); return t >= s && t < e; })
+ .sort((a, b) => Number(a.m.joinedAt) - Number(b.m.joinedAt))
+ .map(x => ({ id: x.id, referrerId: Number(x.m.referrerId || 0) || null, tier: x.m.tier || null, level: Number(x.m.level || 0) || null }));
+
+ const pays = all.filter(p => { const t = Number(p.ts || 0); return t >= s && t < e; })
+ .map(p => ({ toId: Number(p.toId || 0), fromId: Number(p.fromId || 0), kind: p.kind || null, level: Number(p.level || 0) || null,
+ pol: Number(p.pol || 0), tx: p.tx || null, ts: Number(p.ts || 0), upgrade: p.upgrade || null }))
+ .sort((a, b) => b.pol - a.pol);
+
+ const climbSeen = new Map();
+ for (const p of pays) if (p.upgrade) climbSeen.set(p.upgrade.id + ':' + p.upgrade.newLevel, { id: Number(p.upgrade.id), lvl: Number(p.upgrade.newLevel), ts: p.ts });
+ const climbs = [...climbSeen.values()].sort((a, b) => b.lvl - a.lvl || a.ts - b.ts);
+
+ const recruited = {};
+ for (const j of joins) if (j.referrerId) (recruited[j.referrerId] = recruited[j.referrerId] || []).push(j.id);
+ const recruiters = Object.keys(recruited).map(r => ({
+ id: Number(r), brought: recruited[r], directs: Number((members[r] || {}).directCount || 0),
+ })).sort((a, b) => b.brought.length - a.brought.length || a.id - b.id);
+
+ const polMoved = pays.reduce((n, p) => n + p.pol, 0);
+ // the index keeps a rolling payout window; if a day fills it the totals are a floor, not the whole day
+ const clipped = pays.length >= all.length && all.length > 0;
+
+ return {
+ key, computedAt: Date.now(), clipped,
+ totals: { joins: joins.length, climbs: climbs.length, payouts: pays.length, polMoved: Math.round(polMoved * 1e6) / 1e6 },
+ contract: { members: Object.keys(members).length, lifetimePol: Number((idx.totals || {}).pol || 0), lifetimePayouts: Number((idx.totals || {}).count || 0) },
+ climbs, joins, recruiters, payouts: pays,
+ biggest: pays[0] || null,
+ };
+}
+
+// a finished day is written once and read from disk forever after
+function get(key) {
+ if (!isKey(key)) return null;
+ const today = dayKey();
+ if (key !== today) {
+ try { return JSON.parse(fs.readFileSync(dayFile(key), 'utf8')); } catch (e) {}
+ if (key > today) return null;
+ }
+ const d = compute(key);
+ if (d && key !== today) save(d);
+ return d;
+}
+function save(d) { try { fs.writeFileSync(dayFile(d.key), JSON.stringify(d)); } catch (e) {} }
+function stored() { try { return fs.readdirSync(path.join(DATA_DIR, 'days')).filter(f => f.endsWith('.json')).map(f => f.slice(0, -5)).sort().reverse(); } catch (e) { return []; } }
+
+// hourly: close out any finished day that has not been written yet, so the archive never depends
+// on someone opening the page before the payout window rolls past it
+function tick() {
+ const today = dayKey();
+ for (let back = 1; back <= 3; back++) {
+ const k = dayKey(Date.now() - back * 86400000);
+ if (k === today) continue;
+ if (fs.existsSync(dayFile(k))) continue;
+ const d = compute(k);
+ if (d && d.totals.payouts + d.totals.joins > 0) { save(d); console.log('daysnap: stored ' + k); }
+ }
+}
+
+// ---- the page ---------------------------------------------------------------------------------
+const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
+const n0 = v => Number(v || 0).toLocaleString('en-US', { maximumFractionDigits: 0 });
+const n2 = v => Number(v || 0).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
+const SCAN = 'https://polygonscan.com/tx/';
+const pretty = key => { const [y, m, d] = key.split('-').map(Number); return new Date(Date.UTC(y, m - 1, d)).toLocaleDateString('en-US', { timeZone: 'UTC', weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' }); };
+
+function render(key, opts) {
+ const o = opts || {};
+ const d = get(key);
+ const today = dayKey();
+ const prev = dayKey(bounds(key).start - 3600000);
+ const nextKey = dayKey(bounds(key).end + 3600000);
+ const title = 'The Circle on ' + pretty(key);
+ if (!d) return { status: 404, html: shell(title, '
No figures for that date yet.
', key, prev, null) };
+
+ const usd = Number(o.polUsd) || 0;
+ const dol = p => usd ? ' (~$' + n0(p * usd) + ')' : '';
+ const high = d.climbs.filter(c => c.lvl >= 3);
+ const routine = d.climbs.length - high.length;
+
+ const head = ''
+ + tile(n0(d.totals.joins), 'new members')
+ + tile(n0(d.totals.climbs), 'upgrades')
+ + tile(n0(d.totals.polMoved) + ' POL', 'moved on chain' + (usd ? ' · ~$' + n0(d.totals.polMoved * usd) : ''))
+ + tile(n0(d.contract.members), 'on the contract')
+ + '
';
+
+ // grouped by rung, highest first: the ladder is the story, and a two-column table of the same
+ // thing wastes half the page and buries a Culmen among two dozen Ascensus rows
+ const byLevel = new Map();
+ for (const c of d.climbs) { if (!byLevel.has(c.lvl)) byLevel.set(c.lvl, []); byLevel.get(c.lvl).push(c.id); }
+ const climbRows = d.climbs.length
+ ? [...byLevel.keys()].sort((a, b) => b - a).map(lv => ''
+ + '
' + esc(lname(lv)) + ' ' + byLevel.get(lv).length + '
'
+ + '
' + byLevel.get(lv).map(i => '#' + i + '').join('') + '
').join('')
+ : 'No upgrades on this day.
';
+
+ const joinRows = d.joins.length
+ ? '| Position | Invited by | Entry |
'
+ + d.joins.map(j => '| #' + j.id + ' | ' + (j.referrerId ? '#' + j.referrerId : '—') + ' | ' + esc(j.tier === 2 || j.tier === 'Premium' ? 'Premium' : 'Standard') + ' |
').join('')
+ + '
'
+ : 'No new members on this day.
';
+
+ const recRows = d.recruiters.length
+ ? '| Position | Brought in | Who | Directs |
'
+ + d.recruiters.map(r => '| #' + r.id + ' | ' + r.brought.length + ' | '
+ + r.brought.map(x => '#' + x).join(' ') + ' | ' + (r.directs >= 2 ? 'qualified' : r.directs + '/2') + ' |
').join('')
+ + '
'
+ : 'Nobody added a personal referral on this day.
';
+
+ const payRows = d.payouts.length
+ ? '| POL | To | From | Why | Proof |
'
+ + d.payouts.map(p => '| ' + n2(p.pol) + '' + dol(p.pol) + ' | #' + p.toId + ' | '
+ + (p.fromId ? '#' + p.fromId : '—') + ' | ' + esc(p.upgrade ? 'upgrade to ' + lname(p.upgrade.newLevel) : (p.kind || 'payment'))
+ + ' | ' + (p.tx ? 'Polygonscan ↗' : '—') + ' |
').join('')
+ + '
'
+ : 'No payments on this day.
';
+
+ const body = head
+ + (high.length ? 'Highest climb of the day: ' + esc(lname(high[0].lvl)) + ', reached by '
+ + high.filter(c => c.lvl === high[0].lvl).map(c => '#' + c.id).join(', ') + '.
' : '')
+ + (d.biggest ? 'Biggest single payment: ' + n2(d.biggest.pol) + ' POL' + dol(d.biggest.pol)
+ + ', caught by #' + d.biggest.toId + ''
+ + (d.biggest.tx ? ' · Polygonscan ↗' : '') + '.
' : '')
+ + (d.clipped ? 'These payment totals are a floor: the index window filled for this day.
' : '')
+ + sec('Every climb', (high.length ? high.length + ' above Ascensus' : '') + (routine ? (high.length ? ' · ' : '') + routine + ' reached Ascensus' : ''), climbRows)
+ + sec('Every payment', d.totals.payouts + ' on chain, each one verifiable', payRows)
+ + sec('Every new member', d.totals.joins + ' joined', joinRows)
+ + sec('Recruiters', d.recruiters.length + ' added someone', recRows);
+
+ return { status: 200, html: shell(title, body, key, prev, nextKey <= today && nextKey !== key ? nextKey : null) };
+}
+const tile = (v, l) => '';
+const sec = (h, sub, inner) => '' + esc(h) + (sub ? ' ' + esc(sub) + '' : '') + '
'
+ + (/^