Proof feed: one message per purchase, and it says InstantAdPay

Marty, reading the feed on 25 September: "I see the level 1 payout, I see
a level 2 payout, but I don't see who purchased something and what they
bought." And in the main Crypto Team Build group the same lines carried no
product name at all, so nobody knew what they were from.

feedgroup.js composes one message from every event of a purchase
transaction: the buyer and the package on top, the level payouts under it
with their dollar equivalents, then who was passed over and why. It is
triggered on AdminPaid, the last event the contract emits for a purchase,
which is how the missed-payout notice already knows the whole transaction
is indexed. Tier payouts and purchases are no longer posted one line at a
time. In payouts mode a purchase that paid no member (root or unsponsored
buyer) stays silent, as before. Every post now opens with the InstantAdPay
header in the main group as well as the payments topic.

Composed against three real transactions from the live index before
shipping: three tiers, a double pass-up, and a root purchase. QA member
walk: 0 bugs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-25 07:02:59 -05:00
parent 742c271469
commit d2a3f162d0
2 changed files with 56 additions and 6 deletions
+39
View File
@@ -0,0 +1,39 @@
// One proof-feed message per purchase (Marty, 2026-09-25).
//
// The feed used to post each tier payout on its own line and, in payouts mode, never said who
// bought what. Marty: "I see the level 1 payout, I see a level 2 payout, but I don't see who
// purchased something and what they bought." So: the buyer and the package first, then the
// level payouts under it, then any pass-ups, as ONE message. It composes from every event of
// the transaction, which is why the caller triggers it on AdminPaid, the last event the contract
// emits for a purchase: by then Purchase, TierPaid and PassedUp of that tx are all indexed.
//
// Pure: events in, HTML string out. Tested against real index rows before it shipped.
'use strict';
const PCT = { 1: 50, 2: 20, 3: 10 };
function usd(cents) { return ('$' + (cents / 100).toFixed(2)).replace(/\.00$/, ''); }
// txEvents: every indexed event with this tx. who(id) -> "#12 @name". weiToPol(wei) -> "1.23".
// Returns null when there is no Purchase in the tx (not a package purchase) or when
// `requirePayout` is set and no member was paid (a root or unsponsored buyer: nothing to prove).
function compose(txEvents, who, weiToPol, opts) {
opts = opts || {};
const pur = txEvents.find(e => e.type === 'Purchase');
if (!pur) return null;
const tiers = txEvents.filter(e => e.type === 'TierPaid').sort((a, b) => (a.tier || 0) - (b.tier || 0) || (a.li || 0) - (b.li || 0));
if (opts.requirePayout && !tiers.length) return null;
const lines = ['\u{1F9FE} <b>' + who(pur.buyerId) + '</b> bought a <b>' + usd(pur.priceCents) + ' package</b>'];
for (const t of tiers) {
const pct = PCT[t.tier];
const about = pct ? ' (' + usd(Math.round(pur.priceCents * pct / 100)) + ')' : '';
lines.push('\u{1F4B8} Level ' + t.tier + ': <b>' + weiToPol(t.amountWei) + ' POL</b>' + about + ' → ' + who(t.recipientId)
+ (t.hops > 0 ? ' <i>(passed up ' + t.hops + ')</i>' : ''));
}
if (!tiers.length) lines.push('<i>No member payout on this one: the buyer has nobody above them to pay.</i>');
const passed = txEvents.filter(e => e.type === 'PassedUp');
for (const p of passed) if (p.skippedId) lines.push('⏭ Level ' + (p.tier || '?') + ' skipped ' + who(p.skippedId) + ' (not yet qualified)');
return lines.join('\n');
}
module.exports = { compose };
+17 -6
View File
@@ -37,7 +37,8 @@ const releases = require('./releases');
const ledger = require('./ledger');
const videosweep = require('./videosweep');
const sbcheck = require('./sbcheck');
const marks = require('./marks'); // one-time claims shared across running instances (deploy overlap) // Google Safe Browsing screen on advertiser destinations
const marks = require('./marks');
const feedgroup = require('./feedgroup'); // one proof-feed message per purchase: buyer, package, then the tier payouts // one-time claims shared across running instances (deploy overlap) // Google Safe Browsing screen on advertiser destinations
const updates = require('./updates');
const audit = require('./audit'); // counter audit: views vs delivery logs, charges vs shows (Marty, 2026-09-15) // member update emails from Admin > Releases (Marty, 2026-09-14)
const leaderboard = require('./leaderboard');
@@ -1072,11 +1073,19 @@ async function telegramOnEvent(ev) {
const who = id => '#' + id + (names[id] ? ' @' + names[id] : '');
const cc = chain.getConfig();
const tx = (cc.explorer ? cc.explorer.replace(/\/+$/, '') : 'https://polygonscan.com') + '/tx/' + ev.tx;
const build = (mode) => {
const build = async (mode) => {
let line = null;
if (ev.type === 'TierPaid') line = '\u{1F4B8} Level ' + ev.tier + ' payout: <b>' + weiToPol(ev.amountWei) + ' POL</b> \u2192 ' + who(ev.recipientId);
// Tier payouts and the purchase itself are no longer posted one by one: they are composed
// into ONE message on AdminPaid, the last event of every purchase tx, so the feed reads
// "who bought what, then who was paid" (Marty, 2026-09-25). In payouts mode a purchase that
// paid no member (root or unsponsored buyer) stays silent, as before.
if (ev.type === 'AdminPaid') {
const txEvents = chain.recentEvents(1e9).filter(x => x.tx === ev.tx);
const ids = new Set(); txEvents.forEach(x => { for (const k of ['buyerId', 'recipientId', 'skippedId']) if (x[k]) ids.add(x[k]); });
const nm = await accounts.namesForMembers([...ids]).catch(() => ({}));
line = feedgroup.compose(txEvents, id => '#' + id + (nm[id] ? ' @' + nm[id] : ''), weiToPol, { requirePayout: mode === 'payouts' });
}
else if (ev.type === 'BuyerCounted' && mode !== 'payouts') line = '\u2B50 ' + who(ev.sponsorId) + ' now has <b>' + ev.newCount + '</b> qualifying buyer' + (ev.newCount === 1 ? '' : 's') + (ev.newCount === 2 ? ' \u00b7 level 2 open' : ev.newCount === 5 ? ' \u00b7 level 3 open' : '');
else if (ev.type === 'Purchase' && mode !== 'payouts') line = '\u{1F9FE} ' + who(ev.buyerId) + ' bought a $' + Math.round(ev.priceCents / 100) + ' package';
else if (ev.type === 'AwardPaid') line = '\u{1F4B8} Award payout: <b>' + weiToPol(ev.amountWei) + ' POL</b> \u2192 ' + who(ev.toId);
else if (ev.type === 'MemberActivated' && mode === 'all') line = '\u{1F91D} ' + who(ev.id) + ' switched on payouts';
if (!line) return null;
@@ -1112,8 +1121,10 @@ async function telegramOnEvent(ev) {
}
}
} 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} <b>InstantAdPay</b> \u00b7 ' + t, sc.telegramEchoTopicId); }
// the main group used to get bare payout lines with no product name: nobody knew what they were from
const brand = t => '\u{1F7E0} <b>InstantAdPay</b>\n' + t;
if (sc.telegramChatId) { const t = await build(String(sc.telegramEvents || 'payouts')); if (t) await telegramSend(sc.telegramChatId, brand(t), sc.telegramTopicId); if (missed && String(sc.telegramEvents || 'payouts') !== 'none') await telegramSend(sc.telegramChatId, missed, sc.telegramTopicId).catch(() => {}); }
if (sc.telegramEchoChatId) { const t = await build(String(sc.telegramEchoEvents || 'payouts')); if (t) await telegramSend(sc.telegramEchoChatId, brand(t), sc.telegramEchoTopicId); }
if (missed && sc.telegramEchoChatId && String(sc.telegramEchoEvents || 'payouts') !== 'none') await telegramSend(sc.telegramEchoChatId, '\u{1F7E0} <b>InstantAdPay</b> \u00b7 ' + missed, sc.telegramEchoTopicId).catch(() => {});
}
// holding-tank arrivals -> one digest line in the shared payments topic (Marty, 2026-09-12): who is