Files
linkspin/leaderboard.js
T

147 lines
13 KiB
JavaScript

// 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; }
// 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 }))
.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>LinkSpin</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 : '')) + '\nlinkspin-test.saasy.top/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 => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[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 LinkSpin 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 | LinkSpin</title><meta name="description" content="' + esc(desc) + '"><link rel="canonical" href="https://linkspin-test.saasy.top/leaderboard"><meta property="og:title" content="LinkSpin leaderboard"><meta property="og:description" content="' + esc(desc) + '"><meta property="og:image" content="https://linkspin-test.saasy.top/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, 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">' + 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=20260914a"></script><script src="/assets/blog-page.js?v=20260914a"></script></body></html>';
return h;
}
module.exports = { init, view, compute, renderPage, rolloverTick, winners, periodBounds };