diff --git a/snapshot.js b/snapshot.js
index 0c95317..b85e770 100644
--- a/snapshot.js
+++ b/snapshot.js
@@ -81,6 +81,7 @@ async function post() {
// every 10 minutes: post once a day at or after snapshotHourUtc (a restart never double-posts)
async function tick() {
+ try { await milestoneTick(); } catch (e) { console.error('milestone', e.message); }
const sc = X.siteConfig();
if (String(sc.snapshotEnabled || '1') !== '1' || !sc.telegramBotToken) return;
const hour = Number(sc.snapshotHourUtc == null || sc.snapshotHourUtc === '' ? 14 : sc.snapshotHourUtc);
@@ -92,4 +93,39 @@ async function tick() {
async function preview() { const sc = X.siteConfig(); return compose(await gather(), sc.telegramCtaUrl); }
-module.exports = { init, gather, compose, post, tick, preview };
+// ---- membership milestones (Marty, 2026-09-19): one celebratory post per mark, the moment it is crossed
+const MILESTONES = [500, 1000, 2500, 5000, 10000];
+function composeMilestone(mark, s, cta) {
+ const L = [];
+ L.push('\u{1F389} ' + n(mark) + ' members on InstantAdPay.');
+ L.push(n(mark) + ' people have joined, and here is what that looks like right now: ' + n(s.wallets) + ' wallets linked, ' + n(s.payoutsOn) + ' with payouts switched on, ' + n(s.lifeBuys) + ' purchases, and ' + pol(s.lifePaidWei) + ' POL paid to members in the same transactions the purchases came from. Every one of those payments is on the public ledger.');
+ L.push('Thank you to every member who brought someone in. That is the whole engine, and it is working.');
+ L.push('The next ' + n(mark) + ' start today.');
+ L.push('Live ledger' + (cta ? ' \u00b7 Join free' : ''));
+ return L.join('\n\n');
+}
+async function memberCount() {
+ if (!(X.db && X.db.enabled())) return 0;
+ const r = await X.db.q('SELECT COUNT(*) n FROM accounts'); return Number((r[0] || {}).n || 0);
+}
+// runs from tick(): announce the highest newly crossed mark, once, whatever snapshotEnabled says
+async function milestoneTick() {
+ const sc = X.siteConfig(); if (!sc.telegramBotToken) return null;
+ const st = state(); const done = st.milestones || {};
+ const count = await memberCount();
+ const due = MILESTONES.filter(m => count >= m && !done[m]);
+ if (!due.length) return null;
+ const mark = due[due.length - 1];
+ const text = composeMilestone(mark, await gather(), sc.telegramCtaUrl);
+ const targets = String(sc.snapshotTargets || 'feed,echo').split(',').map(x => x.trim());
+ let sent = 0;
+ if (targets.includes('feed') && sc.telegramChatId) { if (await X.send(sc.telegramChatId, text, sc.telegramTopicId)) sent++; }
+ if (targets.includes('echo') && sc.telegramEchoChatId) { if (await X.send(sc.telegramEchoChatId, text, sc.telegramEchoTopicId)) sent++; }
+ for (const m of due) done[m] = Date.now(); // lower marks crossed in the same jump are marked too, so they never post late
+ st.milestones = done; setState(st);
+ console.log('milestone posted', mark, 'members', count, 'sent', sent);
+ return { ok: true, mark, count, sent, text };
+}
+async function previewMilestone(mark) { const sc = X.siteConfig(); return composeMilestone(Number(mark) || 500, await gather(), sc.telegramCtaUrl); }
+
+module.exports = { init, gather, compose, post, tick, preview, milestoneTick, previewMilestone, memberCount, MILESTONES };