diff --git a/server.js b/server.js
index a95f441..ca4f58c 100644
--- a/server.js
+++ b/server.js
@@ -619,6 +619,8 @@ async function sponsorSyncOnEvent(ev) {
const ref = spAcct && spAcct.code ? spAcct.code : String(onchain);
const old = acct.sponsorRef || '';
await accounts.setSponsorRef(acct.email, ref);
+ movedByTx.set(ev.tx, { buyerId: Number(ev.id) || 0, toId: onchain, oldRef: old, at: Date.now() }); // read by telegramOnEvent on the Purchase in the same tx
+ if (movedByTx.size > 200) { const k = movedByTx.keys().next().value; movedByTx.delete(k); }
console.log('referral moved to where the pay landed:', acct.email, JSON.stringify(old), '->', ref, '(#' + onchain + ')');
// Notices, now that it is a fact on the chain (Marty, 2026-09-16): everyone between the stored
// referrer and the paid sponsor who was simply not activated is told they lost this referral for
@@ -638,6 +640,7 @@ async function sponsorSyncOnEvent(ev) {
if (spAcct && lost.length) sponsorGainNudge(spAcct, nameOf(acct), lost.map(nameOf)).catch(() => {});
} catch (e) { console.error('sponsor move notices', e.message); }
}
+const movedByTx = new Map(); // tx -> { buyerId, toId, oldRef } for purchases routed past an unactivated sponsor
async function refAccount(ref) {
const t = String(ref || '').trim().toLowerCase(); if (!t) return null;
let a = await accounts.byCode(t); if (!a) a = await accounts.byUsername(t);
@@ -883,8 +886,41 @@ async function telegramOnEvent(ev) {
if (!line) return null;
return line + ' \u00b7 verify' + (sc.telegramCtaUrl ? '\nJoin free' : '');
};
- if (sc.telegramChatId) { const t = build(String(sc.telegramEvents || 'payouts')); if (t) await telegramSend(sc.telegramChatId, t, sc.telegramTopicId); }
+ // Missed-payout lines (Marty, 2026-09-16): when a share is passed over, say who missed it and who got it.
+ // (a) on-chain pass-up: the TierPaid with hops>0 follows the PassedUp events of the same tx, already indexed.
+ // (b) off-chain walk-up past an unactivated sponsor: recorded by sponsorSyncOnEvent, posted on the Purchase.
+ let missed = null;
+ try {
+ const site = 'https://instantadpay.com/my';
+ if (ev.type === 'TierPaid' && Number(ev.hops) > 0) {
+ 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:\/\//, '') + '';
+ }
+ }
+ if (ev.type === 'Purchase' && movedByTx.has(ev.tx)) {
+ const mv = movedByTx.get(ev.tx); movedByTx.delete(ev.tx);
+ const lostAcct = await refAccount(mv.oldRef);
+ if (lostAcct) {
+ const lostName = lostAcct.username ? '@' + lostAcct.username : lostAcct.email.replace(/@.*/, '') + '@';
+ const nm3 = await accounts.namesForMembers([mv.toId]).catch(() => ({}));
+ missed = '\u{1F62C} Missed sale: ' + lostName + ' had not switched on payouts, so ' + who(ev.buyerId) + "'s $" + Math.round(ev.priceCents / 100) + ' purchase routed past them. That buyer, and every payout from them, now belongs to #' + mv.toId + (nm3[mv.toId] ? ' @' + nm3[mv.toId] : '') + ' for good.'
+ + '\nTwo minutes on the Wallet tab prevents this: ' + site.replace(/^https:\/\//, '') + '';
+ }
+ }
+ } catch (e) { console.error('missed-payout post', e.message); }
+ if (sc.telegramChatId) { const t = build(String(sc.telegramEvents || 'payouts')); if (t) await telegramSend(sc.telegramChatId, t, sc.telegramTopicId); if (missed && String(sc.telegramEvents || 'payouts') !== 'none') await telegramSend(sc.telegramChatId, missed, sc.telegramTopicId).catch(() => {}); }
if (sc.telegramEchoChatId) { const t = build(String(sc.telegramEchoEvents || 'payouts')); if (t) await telegramSend(sc.telegramEchoChatId, '\u{1F7E0} InstantAdPay \u00b7 ' + t, sc.telegramEchoTopicId); }
+ if (missed && sc.telegramEchoChatId && String(sc.telegramEchoEvents || 'payouts') !== 'none') await telegramSend(sc.telegramEchoChatId, '\u{1F7E0} InstantAdPay \u00b7 ' + missed, sc.telegramEchoTopicId).catch(() => {});
}
// holding-tank arrivals -> one digest line in the shared payments topic (Marty, 2026-09-12): who is
// waiting for a sponsor, by username, so builders go adopt them. Runs every 15 min, posts only