Email notifications (welcome + payout + passed-up) and net->network label

- Welcome email on new account: onboarding steps + who their sponsor is.
- On-chain event emails: a payout received (TierPaid/AwardPaid) and a payout
  that passed you by unqualified (PassedUp), wired into the chain event stream.
- Campaign stats: "+N net" relabeled to "+N network" (clearer than "net").

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-07 08:15:28 -05:00
parent 19b7770a4d
commit a8968041dc
3 changed files with 33 additions and 4 deletions
+31 -2
View File
@@ -121,7 +121,7 @@ async function frameCheck(url) {
}
async function boot() {
await db.init({ dataDir: DATA_DIR }); // no-op without DATABASE_URL (JSON mode)
chain.init({ onEvent: ev => attachNames([ev]).then(a => pushFeed(a[0])).catch(() => pushFeed(ev)) });
chain.init({ onEvent: ev => { attachNames([ev]).then(a => pushFeed(a[0])).catch(() => pushFeed(ev)); emailOnEvent(ev).catch(() => {}); } });
auth.init({ dataDir: DATA_DIR, chain, isProd: IS_PROD, site: 'instantadpay.com' });
accounts.init({ dataDir: DATA_DIR });
ads.init({ dataDir: DATA_DIR, chain });
@@ -245,6 +245,35 @@ async function nudgeReferrer(ref) {
+ 'https://instantadpay.com/my\n\nInstantAdPay').catch(e => console.error('nudge failed', e.message));
} catch (e) {}
}
// welcome email on a new account: onboarding steps + who their sponsor is
async function sendWelcome(email, ref) {
try {
if (!mailer.hasKey()) return;
const spon = await accounts.sponsorOf(email);
const who = spon ? (spon.username ? '@' + spon.username : 'member #' + (spon.memberId || 0)) : '';
const sponsorLine = who ? ('You joined through ' + who + ', your sponsor. They are there to help you get started, and you can message them anytime from your dashboard.\n\n') : '';
mailer.send(email, 'Welcome to InstantAdPay',
'Your free InstantAdPay account is ready.\n\n' + sponsorLine
+ 'Getting started:\n'
+ '1. Pick your username and fill out your profile.\n'
+ '2. Grab your invite link and start sharing to build your line.\n'
+ '3. Explore the ad packages when you are ready. Every payout settles on-chain, straight to your wallet.\n\n'
+ 'Sign in anytime: https://instantadpay.com/my\n\nInstantAdPay').catch(() => {});
} catch (e) {}
}
// 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 '?'; } };
async function emailOnEvent(ev) {
if (!mailer.hasKey() || !ev) return;
const notify = async (memberId, subject, body) => {
if (!memberId) return;
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 (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 === 'AwardPaid') await notify(ev.toId, 'You just got paid on InstantAdPay', weiToPol(ev.amountWei) + ' POL just landed in your wallet.');
else if (ev.type === 'PassedUp') await notify(ev.skippedId, 'A payout passed you by on InstantAdPay', 'A level-' + ev.tier + ' payout passed you by because you were not qualified yet. Get qualified so you catch the next one.');
}
// ---- live feed (SSE) ----
const feedClients = new Set();
@@ -389,7 +418,7 @@ const server = http.createServer(async (req, res) => {
const ref = parseCookies(req)['iap.sponsor'] || '';
const r = await accounts.ensure(e, ref); // first touch wins; existing accounts unchanged
if (r.error) return json(res, 400, r);
if (r.created) nudgeReferrer(ref).catch(() => {});
if (r.created) { nudgeReferrer(ref).catch(() => {}); sendWelcome(e, ref).catch(() => {}); }
let memberId = 0;
if (r.account.address) { try { memberId = await chain.memberIdByAccount(r.account.address); } catch (err) {} }
const token = await auth.mintSession({ email: r.account.email, address: r.account.address, memberId });