' + String(text).replace(/&/g, '&').replace(//g, '>').replace(/(https:\/\/[^\s]+)/g, '$1').split('\n\n').join('
').replace(/\n/g, '
') + '
' + l.replace(/&/g, '&').replace(/$1') + '
').join(''); - await messages.deliver(1, ADMIN_EMAIL || 'house@instantadpay.com', [sp.email], subject, html, 'notice'); - } catch (e) {} -} -// Partner-code credits arrive as a drip (Marty, 2026-09-19): a fifth on the day the code is -// redeemed, then a fifth on each day the member finishes their daily ad set, five days in all, -// nothing after 30 days. Same headline number for the partner to promote; paid only to people -// who show up. Member-funded codes charge the funder one installment at a time. -async function payPromo(email, day) { - return promos.payDue(email, day, async (r, amount, n) => { - if (r.funder && r.funder !== email) { - const ok = await ads.spendEarned(r.funder, amount, { log: { kind: 'promo', note: 'Partner code ' + r.code + ' funded for ' + email + ' (day ' + n + ' of ' + promos.STEPS + ')' } }); - if (!ok) { console.log('promo funder short', r.code, r.funder); return false; } - } - await ads.addEarned(email, amount, { log: { kind: 'promo', note: 'Partner code ' + r.code + ': ' + amount + ' of ' + r.credits + ' (day ' + n + ' of ' + promos.STEPS + ')' } }); - return true; - }); -} -// anti-fraud admin alert (Telegram admin chat, else email): who, which flags, and whether the sign-up was blocked -// Send as a specific bot, for operational alerts that should arrive from Marty's own Hermes bot -// rather than from the member-facing InstantAdPay one. -function tgSendAs(token, chatId, text, threadId) { - return new Promise(resolve => { - const body = JSON.stringify(Object.assign({ chat_id: String(chatId), text, parse_mode: 'HTML', disable_web_page_preview: true }, - threadId ? { message_thread_id: Number(threadId) } : {})); - const rq = https.request({ hostname: 'api.telegram.org', path: '/bot' + token + '/sendMessage', method: 'POST', timeout: 12000, - headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } }, - r => { r.resume(); resolve(r.statusCode === 200); }); - rq.on('error', () => resolve(false)); - rq.on('timeout', () => { rq.destroy(); resolve(false); }); - rq.end(body); - }); -} -function fraudAlert(email, fc, spAcct, blocked) { - try { - const sc = siteConfig(); - const text = (blocked ? '\u26D4 InstantAdPay sign-up BLOCKED: ' : '\u{1F6A9} InstantAdPay sign-up flagged: ') + String(email).replace(/^(.{2}).*(@.*)$/, '$1***$2') - + ' [' + (fc.flags || []).join(', ') + ']' + (fc.ip ? ' ip ' + fc.ip : '') + (spAcct ? ' sponsor ' + (spAcct.username ? '@' + spAcct.username : spAcct.email) : '') + '. Admin > Members > Duplicate signals.'; - if (sc.telegramBotToken && sc.telegramAdminChatId) telegramSend(sc.telegramAdminChatId, text).catch(() => {}); - else if (ADMIN_EMAIL && mailer.hasKey()) mailer.send(ADMIN_EMAIL, 'InstantAdPay: sign-up ' + (blocked ? 'blocked' : 'flagged'), text).catch(() => {}); - } catch (e) {} -} -const sponsorRoutedLast = new Map(); // buyer email -> ts (one alert per buyer per hour) -function sponsorRoutedAlert(who, spd, routed, skipped) { - const k = String(who || '?'); if (Date.now() - (sponsorRoutedLast.get(k) || 0) < 3600000) return; sponsorRoutedLast.set(k, Date.now()); - const text = '\u2934\uFE0F InstantAdPay heads-up: ' + k.replace(/^(.{2}).*(@.*)$/, '$1***$2') + ' is at checkout. Their sponsor ' + (spd.name || '?') - + ' has not switched on payouts, so if this purchase goes through it pays ' + routed.to + ' instead.' + (skipped.length > 1 ? ' Also skipped: ' + skipped.map(x => x.name).join(', ') + '.' : '') - + ' Nothing has been paid yet; the on-chain result posts to Telegram when the sale settles.'; - const sc = siteConfig(); - if (sc.telegramBotToken && sc.telegramAdminChatId) telegramSend(sc.telegramAdminChatId, text).catch(() => {}); - else if (ADMIN_EMAIL && mailer.hasKey()) mailer.send(ADMIN_EMAIL, 'InstantAdPay heads-up: a checkout will route past an unactivated sponsor', text).catch(() => {}); - // the skipped sponsor and the one who gains are told by sponsorSyncOnEvent, once the position exists on the chain -} -const sponsorAlertLast = new Map(); // email -> ts (one alert per member per hour) -function sponsorBlockedAlert(who, r) { - const k = String(who || '?'); if (Date.now() - (sponsorAlertLast.get(k) || 0) < 3600000) return; sponsorAlertLast.set(k, Date.now()); - const text = '\u26A0\uFE0F InstantAdPay: purchase held for ' + k.replace(/^(.{2}).*(@.*)$/, '$1***$2') + '. Their sponsor ' + (r.name || r.tok || '?') + ' could not be resolved (' + r.reason + '), so the buy was blocked instead of crediting the company. ' + (r.reason === 'notActivated' ? 'The sponsor needs to switch on payouts.' : r.reason === 'rpc' ? 'Chain lookup failed; they can retry.' : 'Check the sponsor field in Admin > Members.'); - const sc = siteConfig(); - if (sc.telegramBotToken && sc.telegramAdminChatId) telegramSend(sc.telegramAdminChatId, text).catch(() => {}); - else if (ADMIN_EMAIL && mailer.hasKey()) mailer.send(ADMIN_EMAIL, 'InstantAdPay: purchase held, sponsor unresolved', text).catch(() => {}); - // and the sponsor themselves (Marty, 2026-09-15): a buy is waiting on them, once an hour at most - if (r.reason === 'notActivated') sponsorHoldNudge(String(r.tok || ''), k).catch(() => {}); -} -const sponsorNudgeLast = new Map(); // sponsor email -> ts -async function sponsorHoldNudge(tok, buyerEmail, routedTo) { - const t = String(tok || '').trim().toLowerCase(); if (!t) return; - let sp = t.includes('@') ? await accounts.byEmail(t) : null; - if (!sp) sp = await accounts.byCode(t); if (!sp) sp = await accounts.byUsername(t); - if (!sp || !sp.email) return; - if (Date.now() - (sponsorNudgeLast.get(sp.email) || 0) < 3600000) return; sponsorNudgeLast.set(sp.email, Date.now()); - const buyer = await accounts.byEmail(buyerEmail); const bn = buyer && buyer.username ? '@' + buyer.username : 'One of your referrals'; - const step = !sp.address ? 'link a wallet and switch on payouts' : 'switch on payouts'; - const subject = routedTo - ? 'You lost ' + bn + ' to ' + routedTo + ' (payouts were not switched on)' - : bn + ' is trying to buy. ' + (sp.address ? 'Switch on payouts' : 'Link your wallet') + ' so it pays you'; - const lines = routedTo ? [ - 'You just lost a sale. ' + bn + ' is buying an ad package on InstantAdPay right now, and because payouts are not switched on for your account, the contract cannot pay you. The purchase is paying ' + routedTo + ' instead.', - 'This is permanent. The contract locks a buyer to the sponsor who was paid at their first purchase, so ' + bn + ' now belongs to ' + routedTo + ', has moved out of your line, and will never pay you from that wallet. Nothing can bring that referral back.', - 'The next one does not have to go the same way. Open the Wallet tab and ' + step + '. One free signature to link, one small transaction to switch on. Two minutes, and every purchase in your line from then on pays you the moment it happens.', - 'Wallet tab: https://instantadpay.com/my#wallet' - ] : [ - bn + ' just tried to buy an ad package on InstantAdPay, and the purchase is on hold because payouts are not switched on for your account yet. The contract pays your share in the same transaction, but only to a wallet that is switched on, so the site paused the buy instead of sending your 50 percent to someone else.', - 'To fix it: open the Wallet tab and ' + step + '. One free signature to link, one small transaction to switch on. Takes two minutes, and every purchase in your line from then on pays you the moment it happens.', - 'Until then their purchase waits, and they have been told why.', - 'Wallet tab: https://instantadpay.com/my#wallet' - ]; - const text = lines.join('\n\n'); - if (mailer.hasKey()) mailer.send(sp.email, subject, text + '\n\nInstantAdPay').catch(() => {}); - try { - const html = lines.map(l => '' + l.replace(/&/g, '&').replace(/$1') + '
').join(''); - await messages.deliver(1, ADMIN_EMAIL || 'house@instantadpay.com', [sp.email], subject, html, 'alert'); - } catch (e) {} -} -// The moment someone joins through a code, nudge its owner to activate. -// Email a member's sponsor the moment they get a new referral (free OR paid). -// Resolves the sponsor from the join token by member id, share code, or username, -// and notifies EVERY sponsor — activated or not (an active sponsor still wants to -// know their team grew). A referral is on the line from signup; it only counts -// toward qualification once it makes a $20+ purchase. -async function notifyNewReferral(ref, newAcct) { - try { - if (!mailer.hasKey()) return; - const t = String(ref || '').trim().toLowerCase(); - if (!t) return; - let owner = null; - if (/^\d+$/.test(t)) { try { owner = await accounts.byMemberId(Number(t)); } catch (e) {} } - if (!owner) { try { owner = await accounts.byCode(t); } catch (e) {} } - if (!owner) { try { owner = await accounts.byUsername(t); } catch (e) {} } - if (!owner || !owner.email) return; - const who = newAcct && newAcct.username ? '@' + newAcct.username : 'A new member'; - let body = who + ' just joined InstantAdPay through your link — they are on your team from today.\n\n' - + 'They count toward your qualification once they make a $20+ purchase.\n\n'; - if (!owner.address) body += 'Make sure payouts are switched on (one free wallet step) so you never miss a commission — ' - + 'the contract locks each buyer to their sponsor at their first purchase.\n\n'; - body += 'See your team: https://instantadpay.com/my\n\nInstantAdPay'; - mailer.send(owner.email, 'You have a new referral on InstantAdPay', body) - .catch(e => console.error('referral notify failed', e.message)); - } catch (e) {} -} -// welcome email on a new account: onboarding steps + who their sponsor is -// how a member is named in emails/alerts: @username, a linked position named after its owner, else member #id -async function memberLabel(id) { - try { - let a = await accounts.byMemberId(id); - if (!a) { // brand-new buyer: the account learns its member id on its next page load, so read the - // wallet from the chain and match it now (Jim's purchase mailed as "member #25", 2026-09-13) - try { const m = await chain.member(id); if (m && m.account) { a = await accounts.byAddress(String(m.account).toLowerCase()); if (a && !a.memberId) accounts.setMemberId(a.email, id).catch(() => {}); } } catch (e) {} - } - if (a && a.username) return '@' + a.username; - const pos = await accounts.positionByMember(id); - if (pos && pos.email) { const o = await accounts.byEmail(pos.email); if (o && o.username) return '@' + o.username + ' (extra position #' + id + ')'; } - } catch (e) {} - return 'member #' + id; -} -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 (!ev) return; - const notify = async (memberId, subject, body) => { - if (!memberId || !mailer.hasKey()) 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(() => {}); - }; - // 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, kind) => { - if (!memberId) return; - try { - const a = await accounts.byMemberId(memberId); - if (!a || !a.email) return; - const html = '' + String(text).replace(/&/g, '&').replace(//g, '>') - .replace(/(https:\/\/[^\s]+)/g, '$1').split('\n\n').join('
').replace(/\n/g, '
') + '
' + esc(sub.charAt(0).toUpperCase() + sub.slice(1)) + '. On InstantAdPay every ad package that sells pays the sponsor in the same transaction, straight to their wallet, on-chain.
' - + 'Join @' + esc(un) + '\u2019s line freeAdvertising with a performance referral program. Not an investment; no income is guaranteed.
' + String(text).replace(/&/g, '&').replace(//g, '>').replace(/(https:\/\/[^\s]+)/g, '$1').split('\n\n').join('
').replace(/\n/g, '
') + '
' + l.replace(/&/g, '&').replace(/$1') + '
').join(''); + await messages.deliver(1, ADMIN_EMAIL || 'house@instantadpay.com', [sp.email], subject, html, 'notice'); + } catch (e) {} +} +// Partner-code credits arrive as a drip (Marty, 2026-09-19): a fifth on the day the code is +// redeemed, then a fifth on each day the member finishes their daily ad set, five days in all, +// nothing after 30 days. Same headline number for the partner to promote; paid only to people +// who show up. Member-funded codes charge the funder one installment at a time. +async function payPromo(email, day) { + return promos.payDue(email, day, async (r, amount, n) => { + if (r.funder && r.funder !== email) { + const ok = await ads.spendEarned(r.funder, amount, { log: { kind: 'promo', note: 'Partner code ' + r.code + ' funded for ' + email + ' (day ' + n + ' of ' + promos.STEPS + ')' } }); + if (!ok) { console.log('promo funder short', r.code, r.funder); return false; } + } + await ads.addEarned(email, amount, { log: { kind: 'promo', note: 'Partner code ' + r.code + ': ' + amount + ' of ' + r.credits + ' (day ' + n + ' of ' + promos.STEPS + ')' } }); + return true; + }); +} +// anti-fraud admin alert (Telegram admin chat, else email): who, which flags, and whether the sign-up was blocked +// Send as a specific bot, for operational alerts that should arrive from Marty's own Hermes bot +// rather than from the member-facing InstantAdPay one. +function tgSendAs(token, chatId, text, threadId) { + return new Promise(resolve => { + const body = JSON.stringify(Object.assign({ chat_id: String(chatId), text, parse_mode: 'HTML', disable_web_page_preview: true }, + threadId ? { message_thread_id: Number(threadId) } : {})); + const rq = https.request({ hostname: 'api.telegram.org', path: '/bot' + token + '/sendMessage', method: 'POST', timeout: 12000, + headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } }, + r => { r.resume(); resolve(r.statusCode === 200); }); + rq.on('error', () => resolve(false)); + rq.on('timeout', () => { rq.destroy(); resolve(false); }); + rq.end(body); + }); +} +function fraudAlert(email, fc, spAcct, blocked) { + try { + const sc = siteConfig(); + const text = (blocked ? '\u26D4 InstantAdPay sign-up BLOCKED: ' : '\u{1F6A9} InstantAdPay sign-up flagged: ') + String(email).replace(/^(.{2}).*(@.*)$/, '$1***$2') + + ' [' + (fc.flags || []).join(', ') + ']' + (fc.ip ? ' ip ' + fc.ip : '') + (spAcct ? ' sponsor ' + (spAcct.username ? '@' + spAcct.username : spAcct.email) : '') + '. Admin > Members > Duplicate signals.'; + if (sc.telegramBotToken && sc.telegramAdminChatId) telegramSend(sc.telegramAdminChatId, text).catch(() => {}); + else if (ADMIN_EMAIL && mailer.hasKey()) mailer.send(ADMIN_EMAIL, 'InstantAdPay: sign-up ' + (blocked ? 'blocked' : 'flagged'), text).catch(() => {}); + } catch (e) {} +} +const sponsorRoutedLast = new Map(); // buyer email -> ts (one alert per buyer per hour) +function sponsorRoutedAlert(who, spd, routed, skipped) { + const k = String(who || '?'); if (Date.now() - (sponsorRoutedLast.get(k) || 0) < 3600000) return; sponsorRoutedLast.set(k, Date.now()); + const text = '\u2934\uFE0F InstantAdPay heads-up: ' + k.replace(/^(.{2}).*(@.*)$/, '$1***$2') + ' is at checkout. Their sponsor ' + (spd.name || '?') + + ' has not switched on payouts, so if this purchase goes through it pays ' + routed.to + ' instead.' + (skipped.length > 1 ? ' Also skipped: ' + skipped.map(x => x.name).join(', ') + '.' : '') + + ' Nothing has been paid yet; the on-chain result posts to Telegram when the sale settles.'; + const sc = siteConfig(); + if (sc.telegramBotToken && sc.telegramAdminChatId) telegramSend(sc.telegramAdminChatId, text).catch(() => {}); + else if (ADMIN_EMAIL && mailer.hasKey()) mailer.send(ADMIN_EMAIL, 'InstantAdPay heads-up: a checkout will route past an unactivated sponsor', text).catch(() => {}); + // the skipped sponsor and the one who gains are told by sponsorSyncOnEvent, once the position exists on the chain +} +const sponsorAlertLast = new Map(); // email -> ts (one alert per member per hour) +function sponsorBlockedAlert(who, r) { + const k = String(who || '?'); if (Date.now() - (sponsorAlertLast.get(k) || 0) < 3600000) return; sponsorAlertLast.set(k, Date.now()); + const text = '\u26A0\uFE0F InstantAdPay: purchase held for ' + k.replace(/^(.{2}).*(@.*)$/, '$1***$2') + '. Their sponsor ' + (r.name || r.tok || '?') + ' could not be resolved (' + r.reason + '), so the buy was blocked instead of crediting the company. ' + (r.reason === 'notActivated' ? 'The sponsor needs to switch on payouts.' : r.reason === 'rpc' ? 'Chain lookup failed; they can retry.' : 'Check the sponsor field in Admin > Members.'); + const sc = siteConfig(); + if (sc.telegramBotToken && sc.telegramAdminChatId) telegramSend(sc.telegramAdminChatId, text).catch(() => {}); + else if (ADMIN_EMAIL && mailer.hasKey()) mailer.send(ADMIN_EMAIL, 'InstantAdPay: purchase held, sponsor unresolved', text).catch(() => {}); + // and the sponsor themselves (Marty, 2026-09-15): a buy is waiting on them, once an hour at most + if (r.reason === 'notActivated') sponsorHoldNudge(String(r.tok || ''), k).catch(() => {}); +} +const sponsorNudgeLast = new Map(); // sponsor email -> ts +async function sponsorHoldNudge(tok, buyerEmail, routedTo) { + const t = String(tok || '').trim().toLowerCase(); if (!t) return; + let sp = t.includes('@') ? await accounts.byEmail(t) : null; + if (!sp) sp = await accounts.byCode(t); if (!sp) sp = await accounts.byUsername(t); + if (!sp || !sp.email) return; + if (Date.now() - (sponsorNudgeLast.get(sp.email) || 0) < 3600000) return; sponsorNudgeLast.set(sp.email, Date.now()); + const buyer = await accounts.byEmail(buyerEmail); const bn = buyer && buyer.username ? '@' + buyer.username : 'One of your referrals'; + const step = !sp.address ? 'link a wallet and switch on payouts' : 'switch on payouts'; + const subject = routedTo + ? 'You lost ' + bn + ' to ' + routedTo + ' (payouts were not switched on)' + : bn + ' is trying to buy. ' + (sp.address ? 'Switch on payouts' : 'Link your wallet') + ' so it pays you'; + const lines = routedTo ? [ + 'You just lost a sale. ' + bn + ' is buying an ad package on InstantAdPay right now, and because payouts are not switched on for your account, the contract cannot pay you. The purchase is paying ' + routedTo + ' instead.', + 'This is permanent. The contract locks a buyer to the sponsor who was paid at their first purchase, so ' + bn + ' now belongs to ' + routedTo + ', has moved out of your line, and will never pay you from that wallet. Nothing can bring that referral back.', + 'The next one does not have to go the same way. Open the Wallet tab and ' + step + '. One free signature to link, one small transaction to switch on. Two minutes, and every purchase in your line from then on pays you the moment it happens.', + 'Wallet tab: https://instantadpay.com/my#wallet' + ] : [ + bn + ' just tried to buy an ad package on InstantAdPay, and the purchase is on hold because payouts are not switched on for your account yet. The contract pays your share in the same transaction, but only to a wallet that is switched on, so the site paused the buy instead of sending your 50 percent to someone else.', + 'To fix it: open the Wallet tab and ' + step + '. One free signature to link, one small transaction to switch on. Takes two minutes, and every purchase in your line from then on pays you the moment it happens.', + 'Until then their purchase waits, and they have been told why.', + 'Wallet tab: https://instantadpay.com/my#wallet' + ]; + const text = lines.join('\n\n'); + if (mailer.hasKey()) mailer.send(sp.email, subject, text + '\n\nInstantAdPay').catch(() => {}); + try { + const html = lines.map(l => '' + l.replace(/&/g, '&').replace(/$1') + '
').join(''); + await messages.deliver(1, ADMIN_EMAIL || 'house@instantadpay.com', [sp.email], subject, html, 'alert'); + } catch (e) {} +} +// The moment someone joins through a code, nudge its owner to activate. +// Email a member's sponsor the moment they get a new referral (free OR paid). +// Resolves the sponsor from the join token by member id, share code, or username, +// and notifies EVERY sponsor — activated or not (an active sponsor still wants to +// know their team grew). A referral is on the line from signup; it only counts +// toward qualification once it makes a $20+ purchase. +async function notifyNewReferral(ref, newAcct) { + try { + if (!mailer.hasKey()) return; + const t = String(ref || '').trim().toLowerCase(); + if (!t) return; + let owner = null; + if (/^\d+$/.test(t)) { try { owner = await accounts.byMemberId(Number(t)); } catch (e) {} } + if (!owner) { try { owner = await accounts.byCode(t); } catch (e) {} } + if (!owner) { try { owner = await accounts.byUsername(t); } catch (e) {} } + if (!owner || !owner.email) return; + const who = newAcct && newAcct.username ? '@' + newAcct.username : 'A new member'; + let body = who + ' just joined InstantAdPay through your link — they are on your team from today.\n\n' + + 'They count toward your qualification once they make a $20+ purchase.\n\n'; + if (!owner.address) body += 'Make sure payouts are switched on (one free wallet step) so you never miss a commission — ' + + 'the contract locks each buyer to their sponsor at their first purchase.\n\n'; + body += 'See your team: https://instantadpay.com/my\n\nInstantAdPay'; + mailer.send(owner.email, 'You have a new referral on InstantAdPay', body) + .catch(e => console.error('referral notify failed', e.message)); + } catch (e) {} +} +// welcome email on a new account: onboarding steps + who their sponsor is +// how a member is named in emails/alerts: @username, a linked position named after its owner, else member #id +async function memberLabel(id) { + try { + let a = await accounts.byMemberId(id); + if (!a) { // brand-new buyer: the account learns its member id on its next page load, so read the + // wallet from the chain and match it now (Jim's purchase mailed as "member #25", 2026-09-13) + try { const m = await chain.member(id); if (m && m.account) { a = await accounts.byAddress(String(m.account).toLowerCase()); if (a && !a.memberId) accounts.setMemberId(a.email, id).catch(() => {}); } } catch (e) {} + } + if (a && a.username) return '@' + a.username; + const pos = await accounts.positionByMember(id); + if (pos && pos.email) { const o = await accounts.byEmail(pos.email); if (o && o.username) return '@' + o.username + ' (extra position #' + id + ')'; } + } catch (e) {} + return 'member #' + id; +} +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 (!ev) return; + const notify = async (memberId, subject, body) => { + if (!memberId || !mailer.hasKey()) 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(() => {}); + }; + // 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, kind) => { + if (!memberId) return; + try { + const a = await accounts.byMemberId(memberId); + if (!a || !a.email) return; + const html = '' + String(text).replace(/&/g, '&').replace(//g, '>') + .replace(/(https:\/\/[^\s]+)/g, '$1').split('\n\n').join('
').replace(/\n/g, '
') + '
' + esc(sub.charAt(0).toUpperCase() + sub.slice(1)) + '. On InstantAdPay every ad package that sells pays the sponsor in the same transaction, straight to their wallet, on-chain.
' + + 'Join @' + esc(un) + '\u2019s line freeAdvertising with a performance referral program. Not an investment; no income is guaranteed.