Leaderboard + referral contest: /leaderboard (week/month/all, read from chain, own positions excluded), Overview card with own rank, weekly and monthly winners recorded at rollover with credit ladders granted automatically and announced to Telegram
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -152,7 +152,7 @@ function rss(posts) {
|
||||
return '<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"><channel><title>InstantAdPay blog</title><link>' + SITE + '/blog</link><description>Coaching and teaching articles from Marty Bostick.</description>' + items + '</channel></rss>';
|
||||
}
|
||||
function sitemap(posts) {
|
||||
const pages = ['/', '/blog', '/whats-new', '/ledger', '/contract', '/plays', '/wallets', '/earning', '/partners'];
|
||||
const pages = ['/', '/blog', '/whats-new', '/leaderboard', '/ledger', '/contract', '/plays', '/wallets', '/earning', '/partners'];
|
||||
const u = pages.map(p => '<url><loc>' + SITE + p + '</loc><changefreq>weekly</changefreq></url>').join('')
|
||||
+ posts.map(p => '<url><loc>' + SITE + '/blog/' + p.slug + '</loc><lastmod>' + new Date(p.updated).toISOString().slice(0, 10) + '</lastmod><changefreq>monthly</changefreq></url>').join('');
|
||||
return '<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' + u + '</urlset>';
|
||||
|
||||
@@ -82,6 +82,7 @@ FACTS:
|
||||
- DAILY CLAIM STREAK: finishing the daily ad set and claiming pays 5 credits on day 1, 7 on day 2, 10 from day 3, and 25 on every 7th consecutive day; miss a day and it restarts. After the set, verified visits (up to 20 a day, 1 credit each) keep earning, and the credits are meant to be spent on a campaign.
|
||||
- BLOG (public, instantadpay.com/blog): Marty's coaching and teaching articles on building a line, advertising that pays, and daily habits; each article has its own page and can be shared; RSS at /blog/feed.xml. Members who want to write their own articles: not offered today.
|
||||
- ACHIEVEMENT BADGES ON TELEGRAM (2026-09-13): when a member unlocks Spark/Surge/Circuit/Nexus, their personalised badge image (username on the ribbon) is posted automatically to the team's Telegram payments topic and the main group, once per badge; members cannot trigger posts themselves (the 'Post to Telegram' button is admin-only); 'Share' opens a picker (X, Facebook, Telegram, WhatsApp, LinkedIn, Massifly (copies the post and opens the feed composer), the phone's own share menu, copy link, save image) that shares the member's public badge page instantadpay.com/b/<username>/<badge>, which shows the badge and their join link.
|
||||
- LEADERBOARD + REFERRAL CONTEST (2026-09-14): instantadpay.com/leaderboard ranks members by ad packages sold to people they directly sponsor (dollar value, read from the chain; a member's own linked positions and second accounts never count). Periods: this week (Monday to Sunday, Central time), this month, all time. Weekly and monthly winners are recorded automatically at rollover, announced in Telegram, and the credit prizes set by the admin (a ladder: 1st 1,000 / 2nd 500 / 3rd 250 weekly, 5,000 / 2,500 / 1,000 monthly by default) are granted automatically to those positions; the prize text is shown on the page and on the Overview's Leaderboard card, which also shows the member's own rank. Prizes are credits or packages, never cash.
|
||||
- WHAT'S NEW / ROADMAP (2026-09-14): instantadpay.com/whats-new lists release notes (what shipped, dated, tagged new/improved/fixed) and the roadmap (planned / building, with rough ETAs). The Overview has a "What's new" card with the latest three notes and what is being built; a dot marks notes since the member's last look. Written by the admin in Admin > Releases.
|
||||
- HOLDING TANK ALERTS: when new members land in the tank, a note at the top of every member's Overview names them (usernames) and a post goes to the team's Telegram payments topic; adopt from My line > Holding tank (your own $20 package required).
|
||||
- LEGACY WELCOME (former Faucet Wave / Tier One Ads members): they join through instantadpay.com/from/faucetwave or instantadpay.com/from/tieroneads and, if their email is on the legacy list, welcome-back credits are added automatically at signup (former advertisers 500, former earners 150; once per person; credits, not POL). They land in the holding tank like any member who joins without a sponsor.
|
||||
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
// Leaderboard + weekly/monthly referral contest (Marty, 2026-09-14).
|
||||
// Sales credit = tier-1 payouts on-chain: the direct sponsor of every package sold. A member's own linked
|
||||
// positions and second accounts do not count for them (only sales to other people's accounts). Periods run
|
||||
// on Central time: week = Monday 00:00 to Sunday 23:59, month = calendar month. Winners are recorded at
|
||||
// rollover (checked hourly), announced to Telegram, and credit prizes are granted automatically.
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let R = null; // { chain, accounts, ads, dataDir, siteConfig, notify(text), pushFeed(ev) }
|
||||
const TZ = 'America/Chicago';
|
||||
let cache = { at: 0, rows: null };
|
||||
const FILE = () => path.join(R.dataDir, 'leaderboard-winners.json');
|
||||
function init(refs) { R = refs; }
|
||||
|
||||
// Central-time helpers (no tz lib): shift by the zone offset at that instant
|
||||
function ctParts(ts) {
|
||||
const s = new Date(ts).toLocaleString('en-US', { timeZone: TZ, hour12: false, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', weekday: 'short' });
|
||||
const m = /(\w{3}), (\d{2})\/(\d{2})\/(\d{4}), (\d{2}):(\d{2})/.exec(s);
|
||||
return { wd: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].indexOf(m[1]), y: +m[4], mo: +m[2], d: +m[3], h: +m[5] % 24, mi: +m[6] };
|
||||
}
|
||||
function ctMidnight(ts) { // the instant of 00:00 Central on the Central date of ts
|
||||
const p = ctParts(ts); const guess = Date.UTC(p.y, p.mo - 1, p.d, 5, 0, 0); // CDT = UTC-5; CST = UTC-6
|
||||
const q = ctParts(guess); return (q.h === 0 && q.d === p.d) ? guess : guess + 3600000;
|
||||
}
|
||||
function weekStart(ts) { const p = ctParts(ts); const mid = ctMidnight(ts); const back = (p.wd + 6) % 7; return mid - back * 86400000; }
|
||||
function monthStart(ts) { const p = ctParts(ts); return ctMidnight(Date.UTC(p.y, p.mo - 1, 1, 12)); }
|
||||
function periodBounds(period, ts) {
|
||||
const now = ts || Date.now();
|
||||
if (period === 'week') return { start: weekStart(now), end: now, label: 'This week' };
|
||||
if (period === 'lastweek') { const s = weekStart(now); return { start: weekStart(s - 1), end: s, label: 'Last week' }; }
|
||||
if (period === 'month') return { start: monthStart(now), end: now, label: 'This month' };
|
||||
if (period === 'lastmonth') { const s = monthStart(now); return { start: monthStart(s - 1), end: s, label: 'Last month' }; }
|
||||
return { start: 0, end: now, label: 'All time' };
|
||||
}
|
||||
|
||||
// member id -> account email (main ids + linked positions), refreshed every minute
|
||||
let idMap = { at: 0, map: {}, names: {} };
|
||||
async function memberMap() {
|
||||
if (Date.now() - idMap.at < 60000) return idMap;
|
||||
const map = {}, names = {};
|
||||
const list = await R.accounts.listAll(5000);
|
||||
for (const a of list) {
|
||||
if (a.memberId) { map[a.memberId] = a.email; names[a.email] = a.username ? '@' + a.username : 'member #' + a.memberId; }
|
||||
else names[a.email] = a.username ? '@' + a.username : a.email.replace(/@.*/, '') + '@';
|
||||
try { for (const p of await R.accounts.positions(a.email)) if (p.memberId) map[p.memberId] = a.email; } catch (e) {}
|
||||
}
|
||||
idMap = { at: Date.now(), map, names }; return idMap;
|
||||
}
|
||||
|
||||
const computeCache = {};
|
||||
async function compute(period) {
|
||||
const c = computeCache[period]; if (c && Date.now() - c.at < 30000) return c.val;
|
||||
const val = await computeRaw(period); computeCache[period] = { at: Date.now(), val }; return val;
|
||||
}
|
||||
async function computeRaw(period) {
|
||||
const { start, end, label } = periodBounds(period);
|
||||
const { map, names } = await memberMap();
|
||||
const price = {}; // tx -> cents
|
||||
for (const e of R.chain.recentEvents(1e9)) if (e.type === 'Purchase') price[e.tx + ':' + e.buyerId] = Number(e.priceCents || 0);
|
||||
const rows = {};
|
||||
for (const e of R.chain.recentEvents(1e9)) {
|
||||
if (e.type !== 'TierPaid' || e.tier !== 1 || e.ts < start || e.ts >= end) continue;
|
||||
const sponsor = map[e.recipientId], buyer = map[e.buyerId];
|
||||
if (!sponsor) continue;
|
||||
if (buyer && buyer === sponsor) continue; // own positions never count
|
||||
const r = rows[sponsor] = rows[sponsor] || { email: sponsor, name: names[sponsor], sales: 0, cents: 0, pol: 0n, buyers: new Set() };
|
||||
r.sales += 1; r.cents += price[e.tx + ':' + e.buyerId] || 0; r.pol += BigInt(e.amountWei || 0); if (buyer) r.buyers.add(buyer);
|
||||
}
|
||||
// sign-ups sponsored in the period (site-side), for the secondary column
|
||||
const joins = {};
|
||||
for (const a of await R.accounts.listAll(5000)) {
|
||||
if (!a.sponsorRef || a.created < start || a.created >= end) continue;
|
||||
let s = null; try { s = await R.accounts.sponsorOf(a.email); } catch (e) {}
|
||||
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; }
|
||||
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);
|
||||
out.forEach((r, i) => { r.rank = i + 1; });
|
||||
return { period, label, start, end, rows: out };
|
||||
}
|
||||
async function view(period, meEmail) {
|
||||
const key = period || 'week';
|
||||
const r = await compute(key);
|
||||
const sc = R.siteConfig();
|
||||
const prize = prizeText(key.includes('month') ? 'month' : 'week', sc);
|
||||
const me = meEmail ? r.rows.find(x => x.email === meEmail) : null;
|
||||
return { period: r.period, label: r.label, start: r.start, end: r.end, prize: prize || '', top: r.rows.slice(0, 10).map(pub), me: me ? pub(me) : null, count: r.rows.length,
|
||||
winners: winners().slice(0, 6) };
|
||||
}
|
||||
const pub = r => ({ rank: r.rank, name: r.name, sales: r.sales, usd: r.usd, buyers: r.buyers, joins: r.joins });
|
||||
// "1000,500,250" -> [1000, 500, 250]: credits for 1st, 2nd, 3rd... (Marty: award the top X positions, 2026-09-14)
|
||||
const ladder = v => String(v || '').split(',').map(x => Math.round(Number(x)) || 0).filter(n => n > 0);
|
||||
const ORD = ['1st', '2nd', '3rd', '4th', '5th', '6th', '7th', '8th', '9th', '10th'];
|
||||
const ladderText = l => l.length ? l.map((n, i) => ORD[i] + ' ' + n.toLocaleString()).join(' · ') + ' credits' : '';
|
||||
function prizeText(kind, sc) { const t = kind === 'week' ? sc.leaderboardWeeklyPrize : sc.leaderboardMonthlyPrize; if (t) return t; return ladderText(ladder(kind === 'week' ? sc.leaderboardWeeklyCredits : sc.leaderboardMonthlyCredits)); }
|
||||
function winners() { try { return JSON.parse(fs.readFileSync(FILE(), 'utf8')); } catch (e) { return []; } }
|
||||
|
||||
// rollover: once a completed week/month has no winner recorded, record it, grant credits, announce
|
||||
async function rolloverTick() {
|
||||
const now = Date.now(); const sc = R.siteConfig(); const w = winners(); let changed = false;
|
||||
for (const kind of ['week', 'month']) {
|
||||
const cur = kind === 'week' ? weekStart(now) : monthStart(now);
|
||||
const prevStart = kind === 'week' ? weekStart(cur - 1) : monthStart(cur - 1);
|
||||
if (prevStart < Date.parse('2026-09-08T05:00:00Z')) continue; // contest starts with the week of Sep 8
|
||||
if (w.find(x => x.kind === kind && x.start === prevStart)) continue;
|
||||
const r = await compute(kind === 'week' ? 'lastweek' : 'lastmonth');
|
||||
const lad = ladder(kind === 'week' ? sc.leaderboardWeeklyCredits : sc.leaderboardMonthlyCredits);
|
||||
const top = r.rows.filter(x => x.sales > 0).slice(0, Math.max(3, lad.length));
|
||||
const prize = prizeText(kind, sc);
|
||||
const rec = { kind, start: prevStart, end: cur, prize: prize || '', ladder: lad, top: top.map(pub), granted: [], at: now };
|
||||
w.unshift(rec); changed = true;
|
||||
for (let i = 0; i < lad.length && i < top.length; i++) { try { await R.ads.addEarned(top[i].email, lad[i]); rec.granted.push({ rank: i + 1, name: top[i].name, credits: lad[i] }); } catch (e) {} }
|
||||
if (top[0]) {
|
||||
const when = new Date(prevStart).toLocaleDateString('en-US', { timeZone: TZ, month: 'short', day: 'numeric' });
|
||||
const line = '\u{1F3C6} <b>InstantAdPay</b> · ' + (kind === 'week' ? 'Weekly' : 'Monthly') + ' referral contest (from ' + when + '): <b>' + top[0].name + '</b> wins with ' + top[0].sales + ' package' + (top[0].sales === 1 ? '' : 's') + ' sold ($' + top[0].usd + ')' + (top[1] ? ' · 2nd ' + top[1].name + ' ($' + top[1].usd + ')' : '') + (top[2] ? ' · 3rd ' + top[2].name + ' ($' + top[2].usd + ')' : '') + (rec.granted.length ? '\nCredits awarded: ' + rec.granted.map(g => g.name + ' +' + g.credits.toLocaleString()).join(', ') : (prize ? '\nPrize: ' + prize : '')) + '\ninstantadpay.com/leaderboard';
|
||||
try { await R.notify(line); } catch (e) {}
|
||||
try { R.pushFeed({ type: 'Contest', kind, winner: top[0].name, ts: now }); } catch (e) {}
|
||||
}
|
||||
}
|
||||
if (changed) fs.writeFileSync(FILE(), JSON.stringify(w.slice(0, 60), null, 1));
|
||||
}
|
||||
|
||||
const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
||||
async function renderPage() {
|
||||
const week = await view('week'), month = await view('month'), all = await view('all');
|
||||
const sc = R.siteConfig();
|
||||
const table = v => '<table class="lb"><tr><th>#</th><th>Member</th><th>Packages</th><th>Sales</th><th>Buyers</th><th>New members</th></tr>'
|
||||
+ (v.top.length ? v.top.map(r => '<tr><td>' + r.rank + '</td><td><b>' + esc(r.name) + '</b></td><td>' + r.sales + '</td><td>$' + r.usd.toLocaleString() + '</td><td>' + r.buyers + '</td><td>' + r.joins + '</td></tr>').join('') : '<tr><td colspan="6" class="muted">No sales yet in this period.</td></tr>') + '</table>';
|
||||
const fmtD = ts => new Date(ts).toLocaleDateString('en-US', { timeZone: TZ, month: 'short', day: 'numeric' });
|
||||
const desc = 'Who is selling the most ad packages on InstantAdPay this week and this month. Weekly and monthly referral contest standings, read from the chain.';
|
||||
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">'
|
||||
+ '<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>';
|
||||
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">All time</h2>' + table(all);
|
||||
const w = winners();
|
||||
if (w.length) h += '<h2 style="font-size:24px;margin:0 0 10px">Past winners</h2>' + w.slice(0, 12).map(x => '<div class="win"><b>' + (x.kind === 'week' ? 'Week of ' : 'Month of ') + fmtD(x.start) + '</b>: ' + (x.top[0] ? esc(x.top[0].name) + ' (' + x.top[0].sales + ' sold, $' + x.top[0].usd + ')' : 'no sales') + (x.granted && x.granted.length ? ' · awarded: ' + x.granted.map(g => esc(g.name) + ' +' + g.credits.toLocaleString()).join(', ') : (x.prize ? ' · prize ' + esc(x.prize) : '')) + '</div>').join('');
|
||||
h += '<p class="muted small" style="margin-top:30px">Sales are $ of packages bought by members you directly sponsor. Prizes are advertising credits or packages, never cash. No income is guaranteed.</p>';
|
||||
h += '</div><script src="/assets/common.js?v=20260913a"></script><script src="/assets/blog-page.js?v=20260912a"></script></body></html>';
|
||||
return h;
|
||||
}
|
||||
module.exports = { init, view, compute, renderPage, rolloverTick, winners, periodBounds };
|
||||
@@ -757,7 +757,7 @@
|
||||
}));
|
||||
|
||||
// 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', 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', 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() {
|
||||
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>'
|
||||
|
||||
@@ -53,7 +53,7 @@ window.IAP = (function () {
|
||||
f.style.cssText = 'border-top:1px solid var(--line);margin-top:48px;padding:26px 22px;text-align:center;color:var(--muted);font-size:13px';
|
||||
f.innerHTML = '<div>© ' + new Date().getFullYear() + ' InstantAdPay</div>'
|
||||
+ '<div style="margin-top:8px;display:flex;gap:16px;justify-content:center;flex-wrap:wrap">'
|
||||
+ '<a href="/">How it works</a><a href="/ledger">Live ledger</a><a href="/contract">The contract</a><a href="/blog">Blog</a><a href="/whats-new">What\'s new</a>'
|
||||
+ '<a href="/">How it works</a><a href="/ledger">Live ledger</a><a href="/contract">The contract</a><a href="/blog">Blog</a><a href="/leaderboard">Leaderboard</a><a href="/whats-new">What\'s new</a>'
|
||||
+ '<a href="/terms">Terms</a><a href="/privacy">Privacy</a><a href="/disclaimer">Disclaimer</a></div>';
|
||||
document.body.appendChild(f);
|
||||
}
|
||||
|
||||
+16
-1
@@ -363,6 +363,7 @@
|
||||
// site-wide activity (not about me): joins, tank, adoptions, purchases, payouts, qualifications
|
||||
if (ev.type === 'Joined') { communityToast('👋 ' + ev.name + ' just joined' + (ev.tank ? ' and is waiting for a sponsor in the holding tank' : '')); liveRefresh(); return; }
|
||||
if (ev.type === 'Adopted') { communityToast('🤝 ' + ev.sponsor + ' picked up ' + ev.member + ' from the holding tank'); liveRefresh(); return; }
|
||||
if (ev.type === 'Contest') { communityToast('🏆 ' + ev.winner + ' won the ' + (ev.kind === 'week' ? 'weekly' : 'monthly') + ' referral contest'); return; }
|
||||
if (ev.type === 'Badge') { communityToast('🏆 ' + ev.member + ' unlocked ' + ev.label); return; }
|
||||
if (ev.type === 'Released') { communityToast('🪣 ' + ev.sponsor + ' returned ' + ev.member + ' to the holding tank'); liveRefresh(); return; }
|
||||
const mine = MYID && (ev.recipientId === MYID || ev.toId === MYID || ev.sponsorId === MYID || ev.skippedId === MYID || ev.buyerId === MYID);
|
||||
@@ -457,9 +458,23 @@
|
||||
$('newsCard').addEventListener('click', () => { try { localStorage.setItem('iap.news.seen', r.latest || ''); } catch (e) {} $('newsList').querySelectorAll('span[style*="mint"]').forEach(s => { if (s.textContent === '●') s.remove(); }); }, { once: true });
|
||||
} catch (e) {}
|
||||
}
|
||||
// Leaderboard card: top 5 this week + your own rank + the prize (Marty, 2026-09-14)
|
||||
async function loadLeaderboard() {
|
||||
if (!$('lbCard')) return;
|
||||
try {
|
||||
const r = await (await fetch('/api/leaderboard?period=week')).json();
|
||||
const esc = t => String(t == null ? '' : t).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
||||
let h = r.prize ? '<p style="margin:0 0 8px"><b>Prize this week:</b> ' + esc(r.prize) + '</p>' : '';
|
||||
h += r.top.length ? '<table style="width:100%;border-collapse:collapse;font-variant-numeric:tabular-nums">' + r.top.slice(0, 5).map(x => '<tr><td style="padding:3px 0;width:26px;color:' + (x.rank === 1 ? '#ffd15c' : 'var(--muted)') + '">' + x.rank + '</td><td style="padding:3px 0"><b>' + esc(x.name) + '</b></td><td style="padding:3px 0;text-align:right">' + x.sales + ' sold · $' + x.usd + '</td></tr>').join('') + '</table>'
|
||||
: '<p class="muted" style="margin:0">No packages sold yet this week. First sale takes the top spot.</p>';
|
||||
h += r.me ? '<p style="margin:8px 0 0">You: <b>#' + r.me.rank + '</b> · ' + r.me.sales + ' sold · $' + r.me.usd + '</p>' : '<p class="muted" style="margin:8px 0 0">You: no sales yet this week. Sales are $20+ packages bought by people you sponsor.</p>';
|
||||
h += '<p style="margin:6px 0 0"><a href="/leaderboard" target="_blank" rel="noopener" style="color:var(--mint)">Full leaderboard: week, month, all time →</a></p>';
|
||||
$('lbList').innerHTML = h; $('lbCard').hidden = false;
|
||||
} catch (e) {}
|
||||
}
|
||||
async function loadDashboard() {
|
||||
try {
|
||||
loadNews();
|
||||
loadNews(); loadLeaderboard();
|
||||
const d = await (await fetch('/api/my/dashboard')).json();
|
||||
if (d.error) return;
|
||||
chatSync(d);
|
||||
|
||||
+3
-1
@@ -258,6 +258,8 @@
|
||||
</div>
|
||||
<div class="card"><h3>Earning levels</h3>
|
||||
<p id="qualLine" class="muted small">…</p></div>
|
||||
<div class="card" id="lbCard" hidden><div class="card-head"><h3>Leaderboard</h3><span class="sub"><a href="/leaderboard" target="_blank" rel="noopener" style="color:var(--mint);text-decoration:none">this week's contest</a></span></div>
|
||||
<div id="lbList" class="small"></div></div>
|
||||
<div class="card" id="newsCard" hidden><div class="card-head"><h3>What's new</h3><span class="sub"><a href="/whats-new" target="_blank" rel="noopener" style="color:var(--mint);text-decoration:none">release notes and roadmap</a></span></div>
|
||||
<div id="newsList" class="small"></div></div>
|
||||
<div class="card" id="promoCard"><h3>Have a promo code?</h3>
|
||||
@@ -917,7 +919,7 @@
|
||||
<script src="/assets/common.js?v=20260913a"></script>
|
||||
<script src="/assets/wallet.js?v=20260911a"></script>
|
||||
<script src="/assets/promo.js?v=20260911a"></script>
|
||||
<script src="/assets/my.js?v=20260914a"></script>
|
||||
<script src="/assets/my.js?v=20260914b"></script>
|
||||
<script src="/assets/chat.js?v=20260907l"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -31,8 +31,9 @@ const promos = require('./promos'); // partner promo codes -> free ad credits
|
||||
const blog = require('./blog');
|
||||
const adminMember = require('./adminmember');
|
||||
const syndicate = require('./syndicate');
|
||||
const releases = require('./releases'); // release notes + roadmap: /whats-new, Overview card, Admin > Releases (Marty, 2026-09-14) // blog -> Blotato -> X + Instagram on publish (Marty, 2026-09-13) // admin member card: search, drilldown, edits (Marty, 2026-09-13) // admin-written coaching articles, server-rendered public /blog with SEO metadata (Marty, 2026-09-12)
|
||||
const TRAFFIC_PAGES = new Set(['/', '/ledger', '/contract', '/shorts', '/plays', '/wallets', '/launch', '/partners', '/earning', '/blog', '/whats-new']);
|
||||
const releases = require('./releases');
|
||||
const leaderboard = require('./leaderboard'); // referral contest: /leaderboard, Overview card, weekly + monthly winners (Marty, 2026-09-14) // release notes + roadmap: /whats-new, Overview card, Admin > Releases (Marty, 2026-09-14) // blog -> Blotato -> X + Instagram on publish (Marty, 2026-09-13) // admin member card: search, drilldown, edits (Marty, 2026-09-13) // admin-written coaching articles, server-rendered public /blog with SEO metadata (Marty, 2026-09-12)
|
||||
const TRAFFIC_PAGES = new Set(['/', '/ledger', '/contract', '/shorts', '/plays', '/wallets', '/launch', '/partners', '/earning', '/blog', '/whats-new', '/leaderboard']);
|
||||
let tankWaitCache = null; // dashboard: who is waiting for a sponsor (refreshed every minute)
|
||||
const geo = require('./geo'); // viewer country -> tier (DB-IP lite), for campaign targeting
|
||||
const burner = require('./burner'); // automatic on-chain credit burns (inert without ENGINE_KEY)
|
||||
@@ -367,6 +368,10 @@ async function boot() {
|
||||
loadOpenTokens();
|
||||
syndicate.init({ dataDir: DATA_DIR, publicDir: PUBLIC_DIR, uploadsDir: UPLOADS_DIR });
|
||||
releases.init({ dataDir: DATA_DIR });
|
||||
leaderboard.init({ chain, accounts, ads, dataDir: DATA_DIR, siteConfig, pushFeed,
|
||||
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);
|
||||
setInterval(() => leaderboard.rolloverTick().catch(e => console.error('leaderboard rollover', e.message)), 60 * 60 * 1000);
|
||||
console.log('blog syndication:', syndicate.enabled() ? 'on (X + Instagram via Blotato)' : 'off (no blotato.key)');
|
||||
setInterval(() => tankNotifyTick().catch(e => console.error('tank notify', e.message)), 15 * 60 * 1000); // new tank arrivals -> Telegram
|
||||
geo.init({ dataDir: DATA_DIR }).catch(e => console.error('geo init', e.message));
|
||||
@@ -400,6 +405,7 @@ function siteConfig() {
|
||||
telegramEchoChatId: '', telegramEchoTopicId: '', telegramEchoEvents: 'payouts', // shared cross-program payments topic
|
||||
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
|
||||
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>
|
||||
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)
|
||||
geoTier2: '', // empty = built-in default (rest of Western/Central Europe, JP, KR, SG, HK, TW, IL, Gulf, ZA, BR, MX, AR, CL, CO ...); tier 3 = everything else
|
||||
@@ -1215,6 +1221,11 @@ const server = http.createServer(async (req, res) => {
|
||||
}
|
||||
// -- release notes + roadmap (public read; admin write) (Marty, 2026-09-14)
|
||||
if (p === '/api/releases' && req.method === 'GET') return json(res, 200, releases.publicView());
|
||||
if (p === '/api/leaderboard' && req.method === 'GET') { // public standings; signed-in members also get their own row
|
||||
const s = await auth.fromRequest(req);
|
||||
const period = ['week', 'month', 'all', 'lastweek', 'lastmonth'].includes(u.searchParams.get('period')) ? u.searchParams.get('period') : 'week';
|
||||
return json(res, 200, await leaderboard.view(period, s && s.email ? s.email : null));
|
||||
}
|
||||
if (p === '/api/admin/releases' && req.method === 'GET') {
|
||||
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
||||
return json(res, 200, { notes: releases.notes(), roadmap: releases.roadmap(), tags: releases.TAGS, statuses: releases.STATUSES });
|
||||
@@ -2414,6 +2425,7 @@ const server = http.createServer(async (req, res) => {
|
||||
res.writeHead(200, baseHeaders({ 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'public, max-age=300' }));
|
||||
return res.end(blog.renderIndex(posts, pg, tag));
|
||||
}
|
||||
if (p === '/leaderboard') { res.writeHead(200, baseHeaders({ 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'public, max-age=60' })); return res.end(await leaderboard.renderPage()); }
|
||||
if (p === '/whats-new') { res.writeHead(200, baseHeaders({ 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'public, max-age=120' })); return res.end(releases.renderPage()); }
|
||||
if (p === '/blog/feed.xml') { res.writeHead(200, baseHeaders({ 'Content-Type': 'application/rss+xml; charset=utf-8', 'Cache-Control': 'public, max-age=900' })); return res.end(blog.rss(await blog.listPublished())); }
|
||||
if (p === '/sitemap.xml') { res.writeHead(200, baseHeaders({ 'Content-Type': 'application/xml; charset=utf-8', 'Cache-Control': 'public, max-age=3600' })); return res.end(blog.sitemap(await blog.listPublished())); }
|
||||
|
||||
Reference in New Issue
Block a user