Leaderboard: company account excluded from rankings. Weekly member email: credits unspent, streak, contest rank, tank, line (sponsors by default; memberWeeklyEmail=1 for everyone)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-14 05:26:23 -05:00
parent b0ef3b3a58
commit 6233d112e0
4 changed files with 51 additions and 14 deletions
+43 -9
View File
@@ -229,20 +229,53 @@ async function nudgeTick() {
catch (e) { console.error('nudge', acct.email, e.message); } catch (e) { console.error('nudge', acct.email, e.message); }
if (nudges >= 40) break; // spread the load across ticks if (nudges >= 40) break; // spread the load across ticks
} }
// weekly digest: every sponsor with at least one direct // weekly member email (Marty, 2026-09-14, modelled on the mailer.gold weekly): credits sitting unspent,
// the streak, the contest standings with the member's own rank, the tank, and the line for sponsors
let lbWeek = null; try { lbWeek = X.lb ? await X.lb().view('week') : null; } catch (e) {}
let tankN = 0; try { tankN = X.tank ? (await X.tank.waiting()).length : 0; } catch (e) {}
for (const acct of all) { for (const acct of all) {
if (!acct.email) continue; if (!acct.email) continue;
const lastD = await impl().lastDigest(acct.email); const lastD = await impl().lastDigest(acct.email);
if (now - lastD < 7 * DAY) continue; if (now - lastD < 7 * DAY) continue;
if (now - (acct.lastSeen || acct.created || 0) > 45 * DAY) { await impl().setDigest(acct.email, now); continue; } // gone quiet for six weeks: leave them to the nudges
const view = await coachView(acct.email); const view = await coachView(acct.email);
if (!view.directs.length) { await impl().setDigest(acct.email, now); continue; } // audience: sponsors with directs (as before) unless the admin switched the weekly on for every member
const everyone = X.siteConfig && String(X.siteConfig().memberWeeklyEmail || '0') === '1';
if (!everyone && !view.directs.length) { await impl().setDigest(acct.email, now); continue; }
const name = acct.username ? '@' + acct.username : 'there';
const parts = ['Hi ' + name + ', your week on InstantAdPay:'];
// credits
try {
const ids = [acct.memberId, ...(await accounts.positions(acct.email)).map(p => p.memberId)].filter(Boolean);
const bal = await X.ads.balances(ids, acct.email);
if (bal.available > 0) parts.push('- You have ' + bal.available.toLocaleString() + ' ad credits sitting unspent. That is ' + bal.available.toLocaleString() + ' cents of delivery doing nothing. Campaigns > New campaign, point it at your invite link: https://instantadpay.com/my#campaigns');
else if (bal.inCampaigns > 0) parts.push('- All ' + bal.inCampaigns.toLocaleString() + ' of your credits are working in live campaigns. Good.');
} catch (e) {}
// streak
try {
const st = await X.ads.viewStatus(acct.email);
if (st.streakDay > 1) parts.push('- Your claim streak is on day ' + st.streakDay + '. Today\'s claim pays ' + (st.claimCredits || 0) + ' credits; miss a day and it restarts at 5.');
else parts.push('- Five ads and a claim a day is the free way in: 5, 7, 10, then 25 credits every seventh day in a row: https://instantadpay.com/my#earn');
} catch (e) {}
// contest
if (lbWeek && lbWeek.top) {
const me = lbWeek.top.find(r => r.name === '@' + acct.username) || null;
let mine = me; if (!mine) { try { mine = (await X.lb().view('week', acct.email)).me; } catch (e) {} }
parts.push('- Referral contest this week (' + (lbWeek.prize || 'credits to the top 3') + '): ' + (lbWeek.top.length ? lbWeek.top.slice(0, 3).map(r => r.rank + '. ' + r.name + ' (' + r.sales + ' sold)').join(', ') : 'no sales yet, first sale takes the top spot') + '.'
+ (mine ? ' You are #' + mine.rank + ' with ' + mine.sales + ' sold.' : ' You are not on the board yet; one $20 package bought by someone you sponsor puts you there.') + ' https://instantadpay.com/leaderboard');
}
// tank
if (tankN > 0 && acct.memberId) parts.push('- ' + tankN + ' member' + (tankN === 1 ? ' is' : 's are') + ' waiting for a sponsor in the holding tank. Adopt one from My line: https://instantadpay.com/my#line');
// line (sponsors only)
if (view.directs.length) {
const week = view.directs.filter(d => now - d.joined < 7 * DAY).length; const week = view.directs.filter(d => now - d.joined < 7 * DAY).length;
const lines = view.directs.slice(0, 25).map(d => '- ' + d.name + ': ' + d.label + (d.stalled ? ' (quiet ' + d.quietDays + ' days)' : '') + '. Next: ' + d.next); parts.push('- Your line: ' + view.directs.length + ' direct' + (view.directs.length === 1 ? '' : 's') + ', ' + week + ' joined this week, ' + view.stalled + ' gone quiet.');
const actions = view.directs.filter(d => d.stalled).slice(0, 3).map(d => '- Message ' + d.name + ': "' + d.say.replace('{{name}}', d.name.replace(/^@/, '')) + '"'); const actions = view.directs.filter(d => d.stalled).slice(0, 3).map(d => ' Message ' + d.name + ': "' + d.say.replace('{{name}}', d.name.replace(/^@/, '')) + '"');
const body = 'Your line this week:\n' + week + ' joined in the last 7 days. ' + view.stalled + ' of your ' + view.directs.length + ' directs have gone quiet.\n\n' if (actions.length) parts.push(actions.join('\n'));
+ lines.join('\n') + '\n\n' + (actions.length ? 'Three things to do today:\n' + actions.join('\n') + '\n\n' : '') }
+ 'Open My line to message anyone in one click: https://instantadpay.com/my#line\n\nInstantAdPay'; parts.push('\nTwenty minutes, in order: the set, one message to your line, the tank, one conversation outward. https://instantadpay.com/blog/the-twenty-minute-day\n\nInstantAdPay · https://instantadpay.com/my\nNo income is guaranteed. Credits are advertising, not money.');
try { await mailer.send(acct.email, 'Your line this week: ' + view.stalled + ' to nudge', body); digests += 1; } catch (e) { console.error('digest', acct.email, e.message); } const subject = lbWeek && lbWeek.top && lbWeek.top.length && lbWeek.top[0].name === '@' + acct.username ? 'You are #1 this week on InstantAdPay' : 'Your week on InstantAdPay: credits, streak, contest';
try { await mailer.send(acct.email, subject, parts.join('\n')); digests += 1; } catch (e) { console.error('digest', acct.email, e.message); }
await impl().setDigest(acct.email, now); await impl().setDigest(acct.email, now);
if (digests >= 30) break; if (digests >= 30) break;
} }
@@ -250,7 +283,8 @@ async function nudgeTick() {
return { nudges, digests }; return { nudges, digests };
} }
function init(opts) { let X = {}; // extra refs for the weekly member email (ads, tank, lb getter), 2026-09-14
function init(opts) { X = arguments[0] || {};
DATA_DIR = opts.dataDir; chain = opts.chain; accounts = opts.accounts; mailer = opts.mailer; DATA_DIR = opts.dataDir; chain = opts.chain; accounts = opts.accounts; mailer = opts.mailer;
J.load(); J.load();
} }
+3 -1
View File
@@ -73,6 +73,8 @@ async function computeRaw(period) {
if (s && s.email !== a.email) joins[s.email] = (joins[s.email] || 0) + 1; if (s && s.email !== a.email) joins[s.email] = (joins[s.email] || 0) + 1;
} }
for (const [em, n] of Object.entries(joins)) { const r = rows[em] = rows[em] || { email: em, name: names[em] || em, sales: 0, cents: 0, pol: 0n, buyers: new Set() }; r.joins = n; } for (const [em, n] of Object.entries(joins)) { const r = rows[em] = rows[em] || { email: em, name: names[em] || em, sales: 0, cents: 0, pol: 0n, buyers: new Set() }; r.joins = n; }
// the admin's own account (company placements, tank arrivals) is not a contestant
if (R.adminEmail && rows[R.adminEmail]) delete rows[R.adminEmail];
const out = Object.values(rows).map(r => ({ email: r.email, name: r.name, sales: r.sales, usd: r.cents / 100, pol: Number(r.pol / 10n ** 14n) / 10000, buyers: r.buyers.size, joins: r.joins || 0 })) const out = Object.values(rows).map(r => ({ email: r.email, name: r.name, sales: r.sales, usd: r.cents / 100, pol: Number(r.pol / 10n ** 14n) / 10000, buyers: r.buyers.size, joins: r.joins || 0 }))
.sort((a, b) => b.usd - a.usd || b.sales - a.sales || b.joins - a.joins); .sort((a, b) => b.usd - a.usd || b.sales - a.sales || b.joins - a.joins);
out.forEach((r, i) => { r.rank = i + 1; }); out.forEach((r, i) => { r.rank = i + 1; });
@@ -131,7 +133,7 @@ async function renderPage() {
let h = '<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Leaderboard | InstantAdPay</title><meta name="description" content="' + esc(desc) + '"><link rel="canonical" href="https://instantadpay.com/leaderboard"><meta property="og:title" content="InstantAdPay leaderboard"><meta property="og:description" content="' + esc(desc) + '"><meta property="og:image" content="https://instantadpay.com/banners/iap-hero-1200x630.png"><meta name="twitter:card" content="summary_large_image">' let h = '<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Leaderboard | InstantAdPay</title><meta name="description" content="' + esc(desc) + '"><link rel="canonical" href="https://instantadpay.com/leaderboard"><meta property="og:title" content="InstantAdPay leaderboard"><meta property="og:description" content="' + esc(desc) + '"><meta property="og:image" content="https://instantadpay.com/banners/iap-hero-1200x630.png"><meta name="twitter:card" content="summary_large_image">'
+ '<link rel="icon" type="image/png" href="/logo-icon.png"><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap"><link rel="stylesheet" href="/assets/site.css?v=20260913a">' + '<link rel="icon" type="image/png" href="/logo-icon.png"><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap"><link rel="stylesheet" href="/assets/site.css?v=20260913a">'
+ '<style>.lbw{max-width:860px}.lb{width:100%;border-collapse:collapse;margin:8px 0 26px;font-variant-numeric:tabular-nums}.lb th{text-align:left;color:var(--muted);font-size:11px;letter-spacing:.08em;text-transform:uppercase;padding:8px 10px;border-bottom:1px solid var(--line)}.lb td{padding:10px;border-bottom:1px solid var(--line)}.lb tr:first-child + tr td:first-child{color:#ffd15c;font-weight:800}.prize{border-left:4px solid var(--mint);background:rgba(67,232,195,.07);padding:12px 16px;border-radius:0 12px 12px 0;margin:0 0 18px}.win{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:12px 16px;margin:0 0 10px}</style></head><body><div class="wrap lbw">' + '<style>.lbw{max-width:860px}.lb{width:100%;border-collapse:collapse;margin:8px 0 26px;font-variant-numeric:tabular-nums}.lb th{text-align:left;color:var(--muted);font-size:11px;letter-spacing:.08em;text-transform:uppercase;padding:8px 10px;border-bottom:1px solid var(--line)}.lb td{padding:10px;border-bottom:1px solid var(--line)}.lb tr:first-child + tr td:first-child{color:#ffd15c;font-weight:800}.prize{border-left:4px solid var(--mint);background:rgba(67,232,195,.07);padding:12px 16px;border-radius:0 12px 12px 0;margin:0 0 18px}.win{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:12px 16px;margin:0 0 10px}</style></head><body><div class="wrap lbw">'
+ '<section class="hero" style="padding:56px 0 8px"><p class="eyebrow">Referral contest</p><h1>Leaderboard: <em>who is selling</em>.</h1><p class="lead">Ranked by ad packages sold to other people (your own positions never count). Read from the chain, updated live. Weeks run Monday to Sunday, Central time.</p></section>'; + '<section class="hero" style="padding:56px 0 8px"><p class="eyebrow">Referral contest</p><h1>Leaderboard: <em>who is selling</em>.</h1><p class="lead">Ranked by ad packages sold to other people (your own positions never count, and the company account is not a contestant). Read from the chain, updated live. Weeks run Monday to Sunday, Central time.</p></section>';
h += '<h2 style="font-size:24px;margin:0 0 4px">' + week.label + ' <span class="muted small">' + fmtD(week.start) + ' to Sunday</span></h2>' + (week.prize ? '<div class="prize"><b>Weekly prizes:</b> ' + esc(week.prize) + '</div>' : '') + table(week); h += '<h2 style="font-size:24px;margin:0 0 4px">' + week.label + ' <span class="muted small">' + fmtD(week.start) + ' to Sunday</span></h2>' + (week.prize ? '<div class="prize"><b>Weekly prizes:</b> ' + esc(week.prize) + '</div>' : '') + table(week);
h += '<h2 style="font-size:24px;margin:0 0 4px">' + month.label + '</h2>' + (month.prize ? '<div class="prize"><b>Monthly prizes:</b> ' + esc(month.prize) + '</div>' : '') + table(month); h += '<h2 style="font-size:24px;margin:0 0 4px">' + month.label + '</h2>' + (month.prize ? '<div class="prize"><b>Monthly prizes:</b> ' + esc(month.prize) + '</div>' : '') + table(month);
h += '<h2 style="font-size:24px;margin:0 0 4px">All time</h2>' + table(all); h += '<h2 style="font-size:24px;margin:0 0 4px">All time</h2>' + table(all);
+1 -1
View File
@@ -757,7 +757,7 @@
})); }));
// site settings: key / value rows; booleans as checkboxes, numbers stay numbers // site settings: key / value rows; booleans as checkboxes, numbers stay numbers
const SITE_META = { siteName: 'Site name', tagline: 'Tagline', rehearsal: 'Testnet rehearsal banner', defaultSponsorId: 'Fallback sponsor (member #)', walletConnectProjectId: 'WalletConnect project id', moonpayPublicKey: 'MoonPay public key', telegramBotToken: 'Telegram proof feed: bot token', telegramChatId: 'Telegram proof feed: chat id', telegramTopicId: 'Telegram proof feed: topic id (optional)', telegramEvents: 'Telegram proof feed: events (payouts | payouts+purchases | all)', telegramCtaUrl: 'Telegram proof feed: join link under each post', leaderboardWeeklyPrize: 'Leaderboard: weekly prize text (optional; blank shows the credit ladder)', leaderboardMonthlyPrize: 'Leaderboard: monthly prize text (optional)', leaderboardWeeklyCredits: 'Leaderboard: weekly credits for 1st,2nd,3rd… (e.g. 1000,500,250; blank = none)', leaderboardMonthlyCredits: 'Leaderboard: monthly credits for 1st,2nd,3rd… (e.g. 5000,2500,1000)', leaderboardAnnounceGeneral: 'Leaderboard: announce winners in the main group too (1/0)', telegramEchoChatId: 'Telegram echo (shared payments topic): chat id', telegramEchoTopicId: 'Telegram echo: topic id', telegramEchoEvents: 'Telegram echo: events (payouts | payouts+purchases | all)', legacyCreditsAdvertiser: 'Legacy welcome credits: former advertisers', legacyCreditsEarner: 'Legacy welcome credits: former earners', pnlFixedMonthlyUsd: 'P&L: fixed monthly cost (USD)' }; const SITE_META = { siteName: 'Site name', tagline: 'Tagline', rehearsal: 'Testnet rehearsal banner', defaultSponsorId: 'Fallback sponsor (member #)', walletConnectProjectId: 'WalletConnect project id', moonpayPublicKey: 'MoonPay public key', telegramBotToken: 'Telegram proof feed: bot token', telegramChatId: 'Telegram proof feed: chat id', telegramTopicId: 'Telegram proof feed: topic id (optional)', telegramEvents: 'Telegram proof feed: events (payouts | payouts+purchases | all)', telegramCtaUrl: 'Telegram proof feed: join link under each post', memberWeeklyEmail: 'Weekly member email to everyone active (1) or only sponsors with a line (0)', leaderboardWeeklyPrize: 'Leaderboard: weekly prize text (optional; blank shows the credit ladder)', leaderboardMonthlyPrize: 'Leaderboard: monthly prize text (optional)', leaderboardWeeklyCredits: 'Leaderboard: weekly credits for 1st,2nd,3rd… (e.g. 1000,500,250; blank = none)', leaderboardMonthlyCredits: 'Leaderboard: monthly credits for 1st,2nd,3rd… (e.g. 5000,2500,1000)', leaderboardAnnounceGeneral: 'Leaderboard: announce winners in the main group too (1/0)', telegramEchoChatId: 'Telegram echo (shared payments topic): chat id', telegramEchoTopicId: 'Telegram echo: topic id', telegramEchoEvents: 'Telegram echo: events (payouts | payouts+purchases | all)', legacyCreditsAdvertiser: 'Legacy welcome credits: former advertisers', legacyCreditsEarner: 'Legacy welcome credits: former earners', pnlFixedMonthlyUsd: 'P&L: fixed monthly cost (USD)' };
function drawSite() { function drawSite() {
const wrap = $('siteForm'); const wrap = $('siteForm');
wrap.innerHTML = Object.entries(siteObj).map(([k, v]) => '<div class="kv-row"><span class="k" title="' + esc(k) + '">' + esc(SITE_META[k] || humanize(k)) + '</span>' wrap.innerHTML = Object.entries(siteObj).map(([k, v]) => '<div class="kv-row"><span class="k" title="' + esc(k) + '">' + esc(SITE_META[k] || humanize(k)) + '</span>'
+3 -2
View File
@@ -358,7 +358,7 @@ async function boot() {
setInterval(() => ads.dailySweep().catch(e => console.error('sweep', e.message)), 60 * 60 * 1000); setInterval(() => ads.dailySweep().catch(e => console.error('sweep', e.message)), 60 * 60 * 1000);
setInterval(() => ads.scheduleSweep().catch(e => console.error('schedule sweep', e.message)), 5 * 60 * 1000); // scheduled starts/ends setInterval(() => ads.scheduleSweep().catch(e => console.error('schedule sweep', e.message)), 5 * 60 * 1000); // scheduled starts/ends
// follow-up email sequence: send whatever came due (every 10 min, first pass shortly after boot) // follow-up email sequence: send whatever came due (every 10 min, first pass shortly after boot)
coach.init({ dataDir: DATA_DIR, chain, accounts, mailer }); coach.init({ dataDir: DATA_DIR, chain, accounts, mailer, ads, tank, lb: () => leaderboard, siteConfig });
tank.init({ dataDir: DATA_DIR, chain, accounts, messages, mailer, site: 'https://instantadpay.com' }); tank.init({ dataDir: DATA_DIR, chain, accounts, messages, mailer, site: 'https://instantadpay.com' });
legacy.init({ dataDir: DATA_DIR }); legacy.init({ dataDir: DATA_DIR });
traffic.init({ dataDir: DATA_DIR }); traffic.init({ dataDir: DATA_DIR });
@@ -368,7 +368,7 @@ async function boot() {
loadOpenTokens(); loadOpenTokens();
syndicate.init({ dataDir: DATA_DIR, publicDir: PUBLIC_DIR, uploadsDir: UPLOADS_DIR }); syndicate.init({ dataDir: DATA_DIR, publicDir: PUBLIC_DIR, uploadsDir: UPLOADS_DIR });
releases.init({ dataDir: DATA_DIR }); releases.init({ dataDir: DATA_DIR });
leaderboard.init({ chain, accounts, ads, dataDir: DATA_DIR, siteConfig, pushFeed, leaderboard.init({ chain, accounts, ads, dataDir: DATA_DIR, siteConfig, pushFeed, adminEmail: ADMIN_EMAIL,
notify: async text => { const sc = siteConfig(); if (!sc.telegramBotToken || !sc.telegramEchoChatId) return; await telegramSend(sc.telegramEchoChatId, text, sc.telegramEchoTopicId); if (String(sc.leaderboardAnnounceGeneral || '1') !== '0') await telegramSend(sc.telegramEchoChatId, text, null); } }); notify: async text => { const sc = siteConfig(); if (!sc.telegramBotToken || !sc.telegramEchoChatId) return; await telegramSend(sc.telegramEchoChatId, text, sc.telegramEchoTopicId); if (String(sc.leaderboardAnnounceGeneral || '1') !== '0') await telegramSend(sc.telegramEchoChatId, text, null); } });
setTimeout(() => leaderboard.rolloverTick().catch(e => console.error('leaderboard rollover', e.message)), 90 * 1000); setTimeout(() => leaderboard.rolloverTick().catch(e => console.error('leaderboard rollover', e.message)), 90 * 1000);
setInterval(() => leaderboard.rolloverTick().catch(e => console.error('leaderboard rollover', e.message)), 60 * 60 * 1000); setInterval(() => leaderboard.rolloverTick().catch(e => console.error('leaderboard rollover', e.message)), 60 * 60 * 1000);
@@ -405,6 +405,7 @@ function siteConfig() {
telegramEchoChatId: '', telegramEchoTopicId: '', telegramEchoEvents: 'payouts', // shared cross-program payments topic telegramEchoChatId: '', telegramEchoTopicId: '', telegramEchoEvents: 'payouts', // shared cross-program payments topic
telegramAdminChatId: '', // private chat for admin alerts (sign-up guard bursts); falls back to ADMIN_EMAIL telegramAdminChatId: '', // private chat for admin alerts (sign-up guard bursts); falls back to ADMIN_EMAIL
launchAt: '', // public launch moment, ISO 8601 with offset (e.g. 2026-09-18T19:00:00-05:00): countdown on /launch + dashboard mark launchAt: '', // public launch moment, ISO 8601 with offset (e.g. 2026-09-18T19:00:00-05:00): countdown on /launch + dashboard mark
memberWeeklyEmail: '0', // 1 = the weekly 'Your week on InstantAdPay' email goes to every active member, not only sponsors with a line
leaderboardWeeklyPrize: '', leaderboardMonthlyPrize: '', leaderboardWeeklyCredits: '1000,500,250', leaderboardMonthlyCredits: '5000,2500,1000', leaderboardAnnounceGeneral: '1', // referral contest prizes (text shown on /leaderboard; credits granted to the winner automatically at rollover) leaderboardWeeklyPrize: '', leaderboardMonthlyPrize: '', leaderboardWeeklyCredits: '1000,500,250', leaderboardMonthlyCredits: '5000,2500,1000', leaderboardAnnounceGeneral: '1', // referral contest prizes (text shown on /leaderboard; credits granted to the winner automatically at rollover)
legacyCreditsAdvertiser: 500, legacyCreditsEarner: 150, // welcome-back credits for listed Faucet Wave / Tier One Ads emails arriving via /from/<brand> legacyCreditsAdvertiser: 500, legacyCreditsEarner: 150, // welcome-back credits for listed Faucet Wave / Tier One Ads emails arriving via /from/<brand>
geoTier1: '', // comma-separated ISO country codes; empty = built-in default (US, CA, GB, AU, NZ, IE, DE, FR, NL, SE, NO, DK, FI, CH, AT, BE) geoTier1: '', // comma-separated ISO country codes; empty = built-in default (US, CA, GB, AU, NZ, IE, DE, FR, NL, SE, NO, DK, FI, CH, AT, BE)