diff --git a/missed.js b/missed.js
new file mode 100644
index 0000000..f2c1eda
--- /dev/null
+++ b/missed.js
@@ -0,0 +1,53 @@
+// The missed-payout notice, whole chain in one message (Marty, 2026-09-19).
+//
+// A purchase pays three tiers up the sponsor line. When a tier's rightful recipient is not
+// qualified, the contract passes that share up until it finds someone who is, or hands it to
+// the company. Every hop is an on-chain event: PassedUp (who was skipped, and why) and TierPaid
+// (who finally got it, and how many hops up). Until now the Telegram notice reported one tier
+// per message; this composes ONE message per purchase that shows every tier's walk, every
+// person passed, and what it added up to. The point is that the people passed over see, in
+// numbers, exactly what qualifying would have paid them.
+//
+// Pure: takes the events of one transaction plus a name resolver; returns the text or null.
+'use strict';
+
+const SHARE = { 1: 0.5, 2: 0.2, 3: 0.1 }; // tier shares of the purchase, for a tier nobody caught
+const NEED = { 2: 2, 3: 5 }; // qualifying buyers a tier requires
+
+function fmtPol(wei) {
+ try { return (Number(BigInt(String(wei)) / 10n ** 14n) / 1e4).toFixed(4); } catch (e) { return '0'; }
+}
+
+// txEvents: every indexed event sharing the tx. who(id) -> '#id @name'. had(id, tier) -> qualifying
+// buyers that member had at the time (a number) or null when unknown.
+function composeMissed(txEvents, who, had, site) {
+ const buy = txEvents.find(e => e.type === 'Purchase');
+ const skips = txEvents.filter(e => e.type === 'PassedUp');
+ if (!buy || !skips.length) return null;
+ const price = '$' + Math.round((buy.priceCents || 0) / 100);
+ const lines = ['\u{1F62C} Missed payouts on ' + who(buy.buyerId) + "'s " + price + ' purchase:'];
+ const passed = new Set(); let walked = 0;
+ for (const tier of [2, 3]) {
+ const sk = skips.filter(s => Number(s.tier) === tier);
+ if (!sk.length) continue;
+ const paid = txEvents.find(e => e.type === 'TierPaid' && Number(e.tier) === tier);
+ const pol = paid ? Number(fmtPol(paid.amountWei)) : Number(fmtPol(BigInt(String(buy.paidWei || 0)) * BigInt(Math.round(SHARE[tier] * 100)) / 100n));
+ walked += pol;
+ const hops = sk.map(s => {
+ passed.add(s.skippedId);
+ let why;
+ if (s.reason === 'unqualified') { const n = had(s.skippedId, tier); why = (n == null ? 'not qualified' : n + ' of ' + NEED[tier] + ' qualifying buyers'); }
+ else if (s.reason === 'send-failed') why = 'wallet refused the payment';
+ else why = String(s.reason || 'passed');
+ return who(s.skippedId) + ' (' + why + ')';
+ });
+ const end = paid ? 'paid to ' + who(paid.recipientId) : 'nobody above was qualified, so the company kept it';
+ lines.push('Level ' + tier + ' · ' + pol.toFixed(4) + ' POL: ' + hops.join(' → ') + ' → ' + end);
+ }
+ const n = passed.size;
+ lines.push('' + walked.toFixed(4) + ' POL walked past ' + n + ' ' + (n === 1 ? 'person' : 'people') + ' on one ' + price + ' sale.');
+ lines.push('Qualify before the next one: ' + site.replace(/^https:\/\//, '') + '');
+ return lines.join('\n');
+}
+
+module.exports = { composeMissed, fmtPol };
diff --git a/server.js b/server.js
index 2024c24..56ccbd3 100644
--- a/server.js
+++ b/server.js
@@ -28,7 +28,8 @@ const coach = require('./coach'); // coaching view, nudges, digest, prospects,
const tank = require('./tank'); // holding tank: unsponsored free members, adoptions, pay-it-forward
const legacy = require('./legacy'); // Faucet Wave / Tier One Ads bridge: welcome-back credits for listed emails
const traffic = require('./traffic'); // public page views by referring domain (admin Traffic tab)
-const promos = require('./promos'); // partner promo codes -> free ad credits (link ?promo=CODE or the dashboard box)
+const promos = require('./promos');
+const missedNotice = require('./missed'); // the whole-chain missed-payout notice // partner promo codes -> free ad credits (link ?promo=CODE or the dashboard box)
const blog = require('./blog');
const adminMember = require('./adminmember');
const syndicate = require('./syndicate');
@@ -952,19 +953,17 @@ async function telegramOnEvent(ev) {
let missed = null;
try {
const site = 'https://instantadpay.com/my';
- if (ev.type === 'TierPaid' && Number(ev.hops) > 0) {
+ // (a) on-chain pass-ups: AdminPaid is the last event of every purchase tx, so by then every
+ // PassedUp and TierPaid of the tx is indexed; one message shows the whole chain (missed.js).
+ if (ev.type === 'AdminPaid') {
const all = chain.recentEvents(1e9);
- const sk = all.filter(x => x.type === 'PassedUp' && x.tx === ev.tx && x.tier === ev.tier);
- if (sk.length) {
- const nm2 = await accounts.namesForMembers(sk.map(x => x.skippedId)).catch(() => ({}));
- const need = ev.tier === 2 ? 2 : 5;
- const parts = sk.map(x => {
- const had = all.filter(y => y.type === 'BuyerCounted' && y.sponsorId === x.skippedId && y.block < ev.block).length;
- const why = x.reason === 'unqualified' ? 'not qualified for level ' + ev.tier + ' yet: ' + had + ' of ' + need + ' qualifying buyers' : x.reason === 'send-failed' ? 'the wallet refused the payment' : String(x.reason || '');
- return '#' + x.skippedId + (nm2[x.skippedId] ? ' @' + nm2[x.skippedId] : '') + ' (' + why + ')';
- });
- missed = '\u{1F62C} Missed payout: ' + parts.join(' and ') + ' passed over on ' + who(ev.buyerId) + "'s purchase. " + weiToPol(ev.amountWei) + ' POL went to ' + who(ev.recipientId) + ' instead.'
- + '\nQualify before the next one: ' + site.replace(/^https:\/\//, '') + '';
+ const txEvents = all.filter(x => x.tx === ev.tx);
+ if (txEvents.some(x => x.type === 'PassedUp')) {
+ const ids = new Set(); txEvents.forEach(x => { for (const k of ['buyerId', 'skippedId', 'recipientId']) if (x[k]) ids.add(x[k]); });
+ const nm2 = await accounts.namesForMembers([...ids]).catch(() => ({}));
+ const who2 = id => '#' + id + (nm2[id] ? ' @' + nm2[id] : '');
+ const had = (id, tier) => all.filter(y => y.type === 'BuyerCounted' && y.sponsorId === id && y.block < ev.block).length;
+ missed = missedNotice.composeMissed(txEvents, who2, had, site);
}
}
if (ev.type === 'Purchase' && movedByTx.has(ev.tx)) {