Payment and missed-payment notices also land in the on-site inbox (login pop-up + Messages card), with who bought, share in POL and dollars, and the transaction link

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-15 10:44:48 -05:00
parent d6d771d93e
commit 56aa7418ad
2 changed files with 40 additions and 21 deletions
+1 -1
View File
@@ -85,7 +85,7 @@ FACTS:
- PROMO TOOLKIT BY BADGE + AI COPY ENGINE (2026-09-14): Promo tools > AI Copy Engine shows the ladder: Free (links, posts, swipes, banners, wall, objections, shorts, badge pages), Spark (payouts on: one-tap campaign templates aimed at the member's link, printable handout with their QR at /handout/<username>), Surge (first qualifying buyer; the AI Copy Engine is live: posts, DMs, follow-ups, objection replies, emails, story posts, team broadcasts in the member's name with their link, honesty rules built in; 20 free generations a month), Circuit (60 free; Video Maker renders every promo video and short with the member's own end card and QR, hosted for download; split tester compares join angles by views, joins, buyers), Nexus (150 free; Leader Ops: three-level team triage with stalled flags and one-click nudges, AI-drafted team broadcasts, credit grants from the leader's earned pool to anyone in their line, co-branded join page showing the sponsor's bio, and the member's own partner code: welcome credits funded from the leader's pool at each redemption, plus a partner kit page /partners?ref=<username>&promo=CODE). After the free allowance each generation costs 10 ad credits from the earned pool. Credits are advertising, never money. - PROMO TOOLKIT BY BADGE + AI COPY ENGINE (2026-09-14): Promo tools > AI Copy Engine shows the ladder: Free (links, posts, swipes, banners, wall, objections, shorts, badge pages), Spark (payouts on: one-tap campaign templates aimed at the member's link, printable handout with their QR at /handout/<username>), Surge (first qualifying buyer; the AI Copy Engine is live: posts, DMs, follow-ups, objection replies, emails, story posts, team broadcasts in the member's name with their link, honesty rules built in; 20 free generations a month), Circuit (60 free; Video Maker renders every promo video and short with the member's own end card and QR, hosted for download; split tester compares join angles by views, joins, buyers), Nexus (150 free; Leader Ops: three-level team triage with stalled flags and one-click nudges, AI-drafted team broadcasts, credit grants from the leader's earned pool to anyone in their line, co-branded join page showing the sponsor's bio, and the member's own partner code: welcome credits funded from the leader's pool at each redemption, plus a partner kit page /partners?ref=<username>&promo=CODE). After the free allowance each generation costs 10 ad credits from the earned pool. Credits are advertising, never money.
- MEMBER UPDATE EMAILS (2026-09-14, admin only): Admin > Releases > Email an update to members: pick release notes, add an intro, choose an audience (newsletter opt-ins, opt-ins active in the last 30 days, or everyone including those who declined), preview, send a test to the admin, then send; plain text from no-reply@instantadpay.com with an opt-out link; the log shows sent/opted out/failed. - MEMBER UPDATE EMAILS (2026-09-14, admin only): Admin > Releases > Email an update to members: pick release notes, add an intro, choose an audience (newsletter opt-ins, opt-ins active in the last 30 days, or everyone including those who declined), preview, send a test to the admin, then send; plain text from no-reply@instantadpay.com with an opt-out link; the log shows sent/opted out/failed.
- LINKED POSITIONS AND BADGES (2026-09-14): qualifying buyers on a member's linked positions count toward their achievement badges (Spark/Surge/Circuit/Nexus) and light their chip on their sponsor's line, and a badge once earned never regresses. The contract still pays levels 2 and 3 to each position based on that position's OWN qualifying buyers, so the Qualifying buyers tile shows the main position's count with the linked positions' count underneath. - LINKED POSITIONS AND BADGES (2026-09-14): qualifying buyers on a member's linked positions count toward their achievement badges (Spark/Surge/Circuit/Nexus) and light their chip on their sponsor's line, and a badge once earned never regresses. The contract still pays levels 2 and 3 to each position based on that position's OWN qualifying buyers, so the Qualifying buyers tile shows the main position's count with the linked positions' count underneath.
- MISSED-PAYOUT EMAIL (2026-09-15): when a level-2 or level-3 share passes a member by because that level is not open on their account, the member gets an email the same minute: who bought, the POL and dollar amount they missed, how many qualifying buyers they have versus the 2 or 5 needed, and the two ways to close the gap (bring buyers, or Qualified Start). Qualified members whose wallet rejected a transfer get a different email telling them to link a regular wallet. - PAYMENT + MISSED-PAYMENT NOTICES (2026-09-15): every payout that lands (who bought, level, share in POL and dollars, transaction link) and every missed payout is delivered BOTH by email and as an on-site inbox message from the company account, shown in the login pop-up and the Messages card, so it is waiting when the member signs in. MISSED-PAYOUT detail: when a level-2 or level-3 share passes a member by because that level is not open on their account, the member gets an email the same minute: who bought, the POL and dollar amount they missed, how many qualifying buyers they have versus the 2 or 5 needed, and the two ways to close the gap (bring buyers, or Qualified Start). Qualified members whose wallet rejected a transfer get a different email telling them to link a regular wallet.
- LAUNCH WEEK SWIPES (2026-09-15): the founding-week checklist page (/launch) ends with four promoter emails members send to their OWN lists, one a day toward Monday's opening, with the member's invite link and the FOUNDER code (500 credits for anyone who joins before Mon 2026-09-21 9 AM Central) filled in, each with a Copy button. These are swipes for members, not emails the site sends. - LAUNCH WEEK SWIPES (2026-09-15): the founding-week checklist page (/launch) ends with four promoter emails members send to their OWN lists, one a day toward Monday's opening, with the member's invite link and the FOUNDER code (500 credits for anyone who joins before Mon 2026-09-21 9 AM Central) filled in, each with a Copy button. These are swipes for members, not emails the site sends.
- LOGIN ADS CHARGE ONLY ON DAYS SHOWN (2026-09-15): the login ad daily fee (100 credits) is charged only for days the ad was actually shown at least once; with many login ads sharing the sign-ins, an ad that was not picked that day pays nothing. Featured links now count views (one per viewer per hour) as well as clicks. - LOGIN ADS CHARGE ONLY ON DAYS SHOWN (2026-09-15): the login ad daily fee (100 credits) is charged only for days the ad was actually shown at least once; with many login ads sharing the sign-ins, an ad that was not picked that day pays nothing. Featured links now count views (one per viewer per hour) as well as clicks.
- SPONSOR HOLD (2026-09-14): if a member's account names a sponsor that cannot be paid right now (sponsor has not switched on payouts, or the chain lookup failed), the first purchase and the payouts activation are held with a message instead of silently crediting the company. Fix: the sponsor switches on payouts, or the member retries; the admin gets a Telegram alert. - SPONSOR HOLD (2026-09-14): if a member's account names a sponsor that cannot be paid right now (sponsor has not switched on payouts, or the chain lookup failed), the first purchase and the payouts activation are held with a message instead of silently crediting the company. Fix: the sponsor switches on payouts, or the member retries; the admin gets a Telegram alert.
+39 -20
View File
@@ -599,12 +599,32 @@ async function sendWelcome(email, ref) {
// on-chain event emails: a payout received, or a payout that passed you by // on-chain event emails: a payout received, or a payout that passed you by
const weiToPol = w => { try { return (Number(BigInt(w) / (10n ** 14n)) / 10000).toString(); } catch (e) { return '?'; } }; const weiToPol = w => { try { return (Number(BigInt(w) / (10n ** 14n)) / 10000).toString(); } catch (e) { return '?'; } };
async function emailOnEvent(ev) { async function emailOnEvent(ev) {
if (!mailer.hasKey() || !ev) return; if (!ev) return;
const notify = async (memberId, subject, body) => { const notify = async (memberId, subject, body) => {
if (!memberId) return; if (!memberId || !mailer.hasKey()) return;
const a = await accounts.byMemberId(memberId); const a = await accounts.byMemberId(memberId);
if (a && a.email) mailer.send(a.email, subject, body + '\n\nSee it on the live ledger: https://instantadpay.com/ledger\n\nInstantAdPay').catch(() => {}); if (a && a.email) mailer.send(a.email, subject, body + '\n\nSee it on the live ledger: https://instantadpay.com/ledger\n\nInstantAdPay').catch(() => {});
}; };
// Marty (2026-09-15): payment and missed-payment notices also land in the on-site inbox, so
// they are waiting (login modal + Messages card) whether or not the email was read. Sent as
// the company account (#1), the same sender the credit-return notes used. Plain text in, HTML out.
const inboxNote = async (memberId, subject, text) => {
if (!memberId) return;
try {
const a = await accounts.byMemberId(memberId);
if (!a || !a.email) return;
const html = '<p>' + String(text).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/(https:\/\/[^\s]+)/g, '<a href="$1" target="_blank" rel="noopener">$1</a>').split('\n\n').join('</p><p>').replace(/\n/g, '<br>') + '</p>';
await messages.deliver(1, ADMIN_EMAIL || 'house@instantadpay.com', [a.email], subject, html);
} catch (e) {}
};
const tell = async (memberId, subject, text) => { await notify(memberId, subject, text); await inboxNote(memberId, subject, text); };
const PCT = { 1: 50, 2: 20, 3: 10 };
// the Purchase event of the same tx is already indexed (it precedes every payout log), so the
// dollar side of any share is that purchase's price times the tier percentage
const purchaseOf = tx => { try { return chain.recentEvents(600).find(e => e.tx === tx && e.type === 'Purchase'); } catch (e) { return null; } };
const usdShare = (pur, pct) => pur ? (' (about ' + ('$' + (pur.priceCents * pct / 10000).toFixed(2)).replace(/\.00$/, '') + ')') : '';
const txUrlOf = tx => { const cc = chain.getConfig(); return (cc.explorer ? cc.explorer.replace(/\/+$/, '') : 'https://polygonscan.com') + '/tx/' + tx; };
if (ev.type === 'Purchase') { if (ev.type === 'Purchase') {
const cc = chain.getConfig(); const cc = chain.getConfig();
const txUrl = (cc.explorer ? cc.explorer.replace(/\/+$/, '') : 'https://polygonscan.com') + '/tx/' + ev.tx; const txUrl = (cc.explorer ? cc.explorer.replace(/\/+$/, '') : 'https://polygonscan.com') + '/tx/' + ev.tx;
@@ -635,36 +655,35 @@ async function emailOnEvent(ev) {
} }
} catch (e) {} } catch (e) {}
} }
else if (ev.type === 'TierPaid') await notify(ev.recipientId, 'You just got paid on InstantAdPay', 'A level-' + ev.tier + ' payout of ' + weiToPol(ev.amountWei) + ' POL just landed in your wallet.'); else if (ev.type === 'TierPaid') {
else if (ev.type === 'AwardPaid') await notify(ev.toId, 'You just got paid on InstantAdPay', weiToPol(ev.amountWei) + ' POL just landed in your wallet.'); const pct = PCT[ev.tier] || 0; const pur = purchaseOf(ev.tx);
let buyer = 'a member on your level ' + ev.tier; try { buyer = await memberLabel(ev.buyerId); } catch (e) {}
const pol = weiToPol(ev.amountWei);
await tell(ev.recipientId, 'You just got paid ' + pol + ' POL on InstantAdPay',
buyer + ' just bought an ad package on your level ' + ev.tier + (pur ? ' ($' + (pur.priceCents / 100).toFixed(2).replace(/\.00$/, '') + ')' : '') + '. Your ' + pct + ' percent share, ' + pol + ' POL' + usdShare(pur, pct) + ', landed in your wallet in the same transaction' + (ev.hops ? ', passed up to you because someone between you was not qualified for this level' : '') + '.\n\n' +
'Transaction: ' + txUrlOf(ev.tx) + '\n\nYour dashboard: https://instantadpay.com/my');
}
else if (ev.type === 'AwardPaid') await tell(ev.toId, 'You just got paid ' + weiToPol(ev.amountWei) + ' POL on InstantAdPay', weiToPol(ev.amountWei) + ' POL just landed in your wallet.\n\nTransaction: ' + txUrlOf(ev.tx) + '\n\nYour dashboard: https://instantadpay.com/my');
else if (ev.type === 'PassedUp' && ev.reason === 'send-failed') { else if (ev.type === 'PassedUp' && ev.reason === 'send-failed') {
// the member WAS qualified but their wallet rejected the POL (usually a smart-contract // the member WAS qualified but their wallet rejected the POL (usually a smart-contract
// wallet that needs more than the capped gas): tell them and the admin, loudly // wallet that needs more than the capped gas): tell them and the admin, loudly
await notify(ev.skippedId, 'Your wallet rejected a payout on InstantAdPay', 'A level-' + ev.tier + ' payout tried to reach your linked wallet and the wallet refused the transfer, so it passed to the next qualified member. This happens with some smart-contract wallets. Link a regular wallet address (MetaMask, SafePal, Phantom) on the Wallet tab so the next payout lands.'); await tell(ev.skippedId, 'Your wallet rejected a payout on InstantAdPay', 'A level-' + ev.tier + ' payout tried to reach your linked wallet and the wallet refused the transfer, so it passed to the next qualified member. This happens with some smart-contract wallets. Link a regular wallet address (MetaMask, SafePal, Phantom) on the Wallet tab so the next payout lands.\n\nTransaction: ' + txUrlOf(ev.tx));
if (ADMIN_EMAIL) mailer.send(ADMIN_EMAIL, 'InstantAdPay: payout send-failed for member #' + ev.skippedId, 'A level-' + ev.tier + ' payout to member #' + ev.skippedId + ' failed at the wallet (send-failed) and passed up. Tx: ' + ev.tx + '\n\nThe member has been emailed to link a regular wallet.').catch(() => {}); if (ADMIN_EMAIL) mailer.send(ADMIN_EMAIL, 'InstantAdPay: payout send-failed for member #' + ev.skippedId, 'A level-' + ev.tier + ' payout to member #' + ev.skippedId + ' failed at the wallet (send-failed) and passed up. Tx: ' + ev.tx + '\n\nThe member has been emailed to link a regular wallet.').catch(() => {});
} }
else if (ev.type === 'PassedUp') { else if (ev.type === 'PassedUp') {
// Marty (2026-09-15): a member who missed a payout because they were not qualified must be told // Marty (2026-09-15): a member who missed a payout because they were not qualified must be told
// exactly what they missed. The Purchase event of the same tx is already indexed (it precedes // exactly what they missed: who bought, the share in POL and dollars, and how to close the gap.
// the PassedUp log), so the amount is that purchase's share for the tier: 50 / 20 / 10 percent. const pct = PCT[ev.tier] || 0; const pur = purchaseOf(ev.tx);
const PCT = { 1: 50, 2: 20, 3: 10 }; const pct = PCT[ev.tier] || 0; let pol = '', buyer = 'a member on your level ' + ev.tier;
let pol = '', usd = '', buyer = 'a member on your level ' + ev.tier; try { if (pur) pol = weiToPol((BigInt(pur.paidWei) * BigInt(pct) / 100n).toString()); buyer = await memberLabel(ev.buyerId); } catch (e) {}
try {
const pur = chain.recentEvents(600).find(e => e.tx === ev.tx && e.type === 'Purchase');
if (pur) {
pol = weiToPol((BigInt(pur.paidWei) * BigInt(pct) / 100n).toString());
usd = ('$' + (pur.priceCents * pct / 10000).toFixed(2)).replace(/\.00$/, '');
}
buyer = await memberLabel(ev.buyerId);
} catch (e) {}
let bc = 0; try { bc = (await chain.member(ev.skippedId)).buyerCount || 0; } catch (e) {} let bc = 0; try { bc = (await chain.member(ev.skippedId)).buyerCount || 0; } catch (e) {}
const need = ev.tier === 3 ? 5 : 2, short = Math.max(0, need - bc); const need = ev.tier === 3 ? 5 : 2, short = Math.max(0, need - bc);
const what = pol ? pol + ' POL (about ' + usd + ')' : 'a level-' + ev.tier + ' payout'; const what = pol ? pol + ' POL' + usdShare(pur, pct) : 'a level-' + ev.tier + ' payout';
await notify(ev.skippedId, 'You missed ' + (pol ? pol + ' POL' : 'a payout') + ' on InstantAdPay', await tell(ev.skippedId, 'You missed ' + (pol ? pol + ' POL' : 'a payout') + ' on InstantAdPay',
buyer + ' just bought an ad package on your level ' + ev.tier + '. Your share was ' + what + ', and it passed you by because level ' + ev.tier + ' is not open on your account yet. The contract paid it to the next qualified person above you.\n\n' + buyer + ' just bought an ad package on your level ' + ev.tier + '. Your share was ' + what + ', and it passed you by because level ' + ev.tier + ' is not open on your account yet. The contract paid it to the next qualified person above you.\n\n' +
'Level ' + ev.tier + ' opens at ' + need + ' qualifying buyers (people you referred who bought a $20 or larger package). You have ' + bc + (short ? ', so you are ' + short + ' buyer' + (short === 1 ? '' : 's') + ' away.' : '.') + '\n\n' + 'Level ' + ev.tier + ' opens at ' + need + ' qualifying buyers (people you referred who bought a $20 or larger package). You have ' + bc + (short ? ', so you are ' + short + ' buyer' + (short === 1 ? '' : 's') + ' away.' : '.') + '\n\n' +
'Two ways to close the gap: bring ' + (short || 1) + ' more buyer' + (short === 1 ? '' : 's') + ' from your My line page, or use Qualified Start under Buy packages to be your own buyer today. Every package on level ' + ev.tier + ' pays you ' + pct + ' percent once it is open, and the next one is coming whether you are ready or not.\n\n' + 'Two ways to close the gap: bring ' + (short || 1) + ' more buyer' + (short === 1 ? '' : 's') + ' from your My line page, or use Qualified Start under Buy packages to be your own buyer today. Every package on level ' + ev.tier + ' pays you ' + pct + ' percent once it is open, and the next one is coming whether you are ready or not.\n\n' +
'Your dashboard: https://instantadpay.com/my'); 'Transaction: ' + txUrlOf(ev.tx) + '\n\nYour dashboard: https://instantadpay.com/my');
} }
} }
// payment-proof Telegram feed (same pattern as the RM Circle proof channel): one compact // payment-proof Telegram feed (same pattern as the RM Circle proof channel): one compact