LinkSpin test area: InstantAdPay engine fork rebranded, network registry, sponsor carry-over with engine activation and claim window, rotator with /r/ redirects, link-domain mini-sites

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-15 16:16:52 -05:00
commit 010e8d7ffc
130 changed files with 20096 additions and 0 deletions
+248
View File
@@ -0,0 +1,248 @@
// Shared page runtime: site config, nav, formatting. Zero dependencies.
window.IAP = (function () {
let config = null;
const $ = id => document.getElementById(id);
async function getConfig() {
if (!config) config = await (await fetch('/api/config')).json();
return config;
}
// POL amounts display with two decimals (rounded half-up), e.g. 523.39
function fmtPol(wei) {
const cents = (BigInt(wei) + 5000000000000000n) / 10000000000000000n; // wei -> hundredths of a POL
const s = cents.toString().padStart(3, '0');
return s.slice(0, -2) + '.' + s.slice(-2);
}
const fmtUsd = cents => '$' + (cents / 100).toFixed(2);
function status(msg, cls) {
let el = $('status');
if (!el) { el = document.createElement('div'); el.id = 'status'; document.body.appendChild(el); }
el.textContent = msg; el.className = cls || ''; el.hidden = false;
clearTimeout(status._t);
if (cls === 'ok') status._t = setTimeout(() => { el.hidden = true; }, 6000);
}
async function renderNav(active) {
const c = await getConfig();
const nav = document.createElement('nav');
nav.innerHTML = '<div class="wrap">'
+ '<span class="logo-wrap"><a class="logo" href="/"><img src="/logo.png" alt="LinkSpin" style="height:30px;display:block"></a><span class="byline">Brought to you by the <b>Crypto Team Build Network</b></span></span>'
+ '<span class="links">'
+ '<a href="/#how" data-p="home">How it works</a>'
+ '<a href="/#packages" data-p="pricing">Ad packages</a>'
+ '<a href="/ledger" data-p="ledger">Live ledger</a>'
+ '<a href="/leaderboard" data-p="leaderboard">Leaderboard</a>'
+ '<a href="/contract" data-p="contract">The contract</a>'
+ '<a href="/my" data-p="my">Members</a>'
+ '</span><span id="navWallet" class="muted">…</span></div>';
document.body.prepend(nav);
if (c.rehearsal) {
const b = document.createElement('div');
b.className = 'rehearsal';
b.innerHTML = '<b>Testnet rehearsal</b>: running on ' + c.chainName + '. Purchases use valueless test POL while we prove every payout in public.';
document.body.prepend(b);
}
const a = nav.querySelector('[data-p="' + active + '"]');
if (a) a.className = 'active';
refreshNavWallet();
renderFooter();
}
function renderFooter() {
if (document.getElementById('iapFooter')) return;
const f = document.createElement('footer'); f.id = 'iapFooter';
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() + ' LinkSpin</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="/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);
}
async function refreshNavWallet() {
try {
const me = await (await fetch('/api/me')).json();
const el = $('navWallet');
if (!el) return;
if (me.signedIn) {
// identity order: username, then email, then wallet
const who = me.username
? '<b>@' + String(me.username).replace(/[&<>]/g, '') + '</b>'
: (me.email ? String(me.email).replace(/[&<>]/g, '')
: (me.address ? '<span class="mono">' + me.address.slice(0, 6) + '…' + me.address.slice(-4) + '</span>' : 'signed in'));
el.innerHTML = (me.memberId ? '<span class="badge">member #' + me.memberId + '</span> ' : '') + who;
} else {
el.innerHTML = '<a href="/my">Sign in</a>';
}
return me;
} catch (e) { return null; }
}
function describeEvent(ev, c) {
const pol = w => fmtPol(w) + ' POL';
// real people, not numbers: use usernames when the site knows them
const nm = id => (ev.names && ev.names[id])
? String(ev.names[id]).replace(/[&<>]/g, '')
: 'member #' + id;
switch (ev.type) {
case 'Purchase': return '🧾 ' + nm(ev.buyerId) + ' bought package #' + ev.productId
+ ' (' + fmtUsd(ev.priceCents) + ') for ' + pol(ev.paidWei) + ' → +' + ev.creditAmount.toLocaleString() + ' credits';
case 'TierPaid': return '💸 level ' + ev.tier + ' payout → ' + nm(ev.recipientId) + ': ' + pol(ev.amountWei)
+ (ev.hops ? ' (passed up ' + ev.hops + ')' : '');
case 'PassedUp': return '↷ level ' + ev.tier + ' passed over ' + nm(ev.skippedId) + ' (' + ev.reason + ')';
case 'AdminPaid': return '🏛 platform fee settled: ' + pol(ev.amountWei);
case 'BuyerCounted': return '⭐ ' + nm(ev.sponsorId) + ' now has ' + ev.newCount + ' qualifying buyer(s)';
case 'MemberActivated': return '👤 ' + nm(ev.id) + ' activated a payout wallet';
case 'AwardPaid': return '🎁 award: ' + pol(ev.amountWei) + ' → ' + nm(ev.toId);
case 'CreditsConsumed': return '📣 ' + nm(ev.memberId) + ' ran ads: −' + ev.amount.toLocaleString() + ' credits';
case 'PriceCached': return '🔮 oracle price refreshed';
case 'FallbackPriceUsed': return '🔮 cached price bridged an oracle gap';
default: return '· ' + ev.type;
}
}
function feedRow(ev, c) {
const div = document.createElement('div');
div.className = 'row t-' + ev.type;
const when = ev.ts ? new Date(ev.ts).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }) : '';
div.innerHTML = (when ? '<span class="when" title="' + new Date(ev.ts).toLocaleString() + '">' + when + '</span>' : '') + '<span>' + describeEvent(ev, c) + '</span>'
+ (c.explorer
? '<span class="tx"><a target="_blank" rel="noopener" href="' + c.explorer + '/tx/' + ev.tx + '">verify ↗</a></span>'
: '<span class="tx"><a href="/tx/' + ev.tx + '">verify ↗</a></span>'); // built-in viewer when the chain has no public explorer
return div;
}
// Render one served ad into #<elId>. Silent if no inventory.
// report an ad (auto-approved ads need a member-facing flag → admin notified)
function reportAd(campaignId) {
if (!campaignId) return;
const reason = (prompt('Report this ad. Reason: broken, inappropriate, spam, scam, or other', 'broken') || '').trim().toLowerCase();
if (!reason) return;
const note = prompt('Anything to add? (optional)') || '';
fetch('/api/report-ad', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ campaignId, reason, note }) })
.then(() => status('Thanks — this ad was reported to the admin for review.', 'ok'))
.catch(() => status('Could not send the report. Try again.', 'bad'));
}
const reportTag = ad => ' <a class="ad-report small muted" href="#" data-cid="' + ad.id + '" style="margin-left:8px">⚠ report</a>';
function wireReport(el) {
const rl = el.querySelector('.ad-report');
if (rl) rl.addEventListener('click', e => { e.preventDefault(); reportAd(Number(rl.dataset.cid)); });
}
async function adSlot(type, elId, opts) {
try {
const q = '/api/ads/slot?type=' + type + (opts && opts.width ? '&w=' + opts.width + '&h=' + opts.height : '');
const { ad } = await (await fetch(q)).json();
const el = $(elId);
if (!ad || !el) return;
el.hidden = false;
if (ad.imageUrl) {
el.style.textAlign = 'center';
el.innerHTML = '<a href="' + ad.targetUrl + '" target="_blank" rel="noopener nofollow">'
+ '<img src="' + ad.imageUrl + '" alt="advertisement" style="max-width:min(100%,728px);height:auto;display:block;margin:0 auto;border-radius:8px"></a>'
+ '<div class="small muted">member ad' + reportTag(ad) + '</div>';
} else {
el.innerHTML = '<a href="' + ad.targetUrl + '" target="_blank" rel="noopener nofollow"><b>' + ad.title + '</b>'
+ (ad.body ? ' · ' + ad.body : '') + '</a> <span class="small muted">member ad' + reportTag(ad) + '</span>';
}
wireReport(el);
el.hidden = false;
} catch (e) {}
}
// ── sign-up code request with the invisible guard fields (form age + honeypot)
// and the icon check the server asks for only after an IP trips a limit ──
const FORM_TS = Date.now();
function iconCheck(host, ch, note) {
return new Promise(resolve => {
host.hidden = false;
host.innerHTML = '<div class="small" style="margin:0 0 8px">' + (note ? esc(note) + ' ' : '') + 'Tap the <b>' + esc(ch.prompt) + '</b>.</div>'
+ '<div class="icon-check">' + ch.options.map(o => '<button type="button" class="ic-btn">' + esc(o) + '</button>').join('') + '</div>';
host.querySelectorAll('.ic-btn').forEach(b => b.addEventListener('click', () => { host.innerHTML = ''; host.hidden = true; resolve(b.textContent); }, { once: true }));
});
}
// honeypot fields (join + sign-in): read-only until a trusted focus, so browser autofill and
// password managers leave them alone; a value that appeared without a trusted event is ignored
function armHoneypots() {
document.querySelectorAll('.hp-field').forEach(el => {
if (el.dataset.armed) return; el.dataset.armed = '1'; el.readOnly = true;
const touch = e => { if (e.isTrusted) { el.readOnly = false; el.dataset.touched = '1'; } };
el.addEventListener('focus', touch); el.addEventListener('input', touch); el.addEventListener('keydown', touch);
});
}
armHoneypots(); document.addEventListener('DOMContentLoaded', armHoneypots);
const hpValue = el => (el && el.dataset.touched === '1') ? (el.value || '') : '';
async function requestCode(email, opts) {
const o = opts || {};
let pick = null;
for (let i = 0; i < 4; i++) {
const r = await (await fetch('/api/auth/email/start', { method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, fts: FORM_TS, hp_field_x9: hpValue(o.honeypot), pick }) })).json();
if (r.challenge && o.host) { pick = await iconCheck(o.host, r.challenge, r.error); continue; }
if (r.error) throw new Error(r.error);
return r;
}
throw new Error('Could not verify. Refresh the page and try again.');
}
const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
// ── founding-week checklist, read from the live account. Shared by /launch and
// the dashboard mark. Two items are the member's own call and persist locally.
const LAUNCH_KEY = 'iap.launch.manual';
const manualSet = () => { try { return new Set(JSON.parse(localStorage.getItem(LAUNCH_KEY) || '[]')); } catch (e) { return new Set(); } };
function launchToggle(key) { const s = manualSet(); if (s.has(key)) s.delete(key); else s.add(key); try { localStorage.setItem(LAUNCH_KEY, JSON.stringify([...s])); } catch (e) {} }
function launchChecks(me) {
const m = me || {}, man = manualSet();
const bc = Number(m.buyerCount || 0), refs = (m.referrals || []).length;
return [
{ key: 'username', title: 'Pick your username', done: !!m.username, href: '/my#profile', cta: 'Profile',
how: 'Profile tab. It becomes your invite link and your public page, and it is permanent.',
why: 'Every link, banner and video you hand out this week carries it. Change it later and the links you already sent die.' },
{ key: 'wallet', title: 'Link your wallet', done: !!m.address, href: '/my#wallet', cta: 'Wallet',
how: 'Wallet tab, Connect, sign the free message. MetaMask recommended. Never held crypto? The wallet guide in Training walks through buying POL with a card.',
why: 'Payouts go to this address. No wallet, nowhere to pay you.' },
{ key: 'payouts', title: 'Switch on payouts', done: !!m.memberId, href: '/my#wallet', cta: 'Wallet',
how: 'Wallet tab, one small transaction. It registers your address with the contract.',
why: 'The contract binds each buyer to their sponsor at their first purchase. If payouts are off when your first person buys, that commission is not yours.' },
{ key: 'level2', title: 'Qualify: open level 2', done: bc >= 2, href: '/my#buy', cta: 'Buy packages',
how: 'Two of your people buy a $20 or more package. Or use Qualified Start: add two positions from extra wallets in your own MetaMask and buy a $20 package from each (about $23 of POL in each wallet).',
why: 'Until you have two qualifying buyers, every level 2 payment from your team climbs past you.',
note: 'Qualifying buyers so far: <b>' + bc + '</b> of 2.' },
{ key: 'level3', title: 'The leader play: open all three levels', done: bc >= 5, href: '/my#buy', cta: 'Qualified Start',
how: 'Five qualifying buyers, real or Qualified Start, up to five linked positions. Once qualified, buy from your main wallet so your sponsor is paid in full.',
why: 'A leader whose team goes three deep this week collects level 3 from day one instead of watching those 10% payments pass upward. Optional for members, the play for leaders.',
note: 'Qualifying buyers so far: <b>' + bc + '</b> of 5.' },
{ key: 'banner', title: 'Upload your line banner', done: !!m.lineBannerUrl, href: '/my#profile', cta: 'Profile',
how: 'Profile tab, line banner. It shows on the welcome tour to everyone in your next three levels.',
why: 'Your first advertising to your own team, free, and it is live the moment they join.' },
{ key: 'links', title: 'Copy your links and pick a play', done: man.has('links'), manual: true,
how: 'Promo tools, Your links: the invite link and the five angle links, plus the matching hook videos. Then read the plays page and choose one.',
why: 'On launch day you send links, not explanations. Having them ready is the whole difference between a launch and a scramble.' },
{ key: 'two', title: 'Place your first two', done: refs >= 2, href: '/my#line', cta: 'My line',
how: 'Two people you have actually talked to, joined through your link, walked through items 1 to 3 on their own accounts.',
why: 'Your first two are the shape of your whole line. Choose them, do not wait for them.',
note: 'Joined through you so far: <b>' + refs + '</b>.' }
];
}
// In-page dialogs instead of window.prompt / confirm. Mobile Safari shows a red "Suppress dialogs"
// option on the second native pop-up in a row and, once tapped, swallows every later prompt on the
// site until reload. ask() resolves the typed value (null on cancel); confirmBox() resolves true/false.
function dialog(o) {
return new Promise(resolve => {
const esc = t => String(t == null ? '' : t).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
const back = document.createElement('div'); back.className = 'modal-back'; back.style.zIndex = '200';
const field = o.type === 'none' ? '' : o.type === 'textarea'
? '<textarea id="dlgInput" rows="5" style="width:100%;margin-top:12px">' + esc(o.value) + '</textarea>'
: '<input id="dlgInput" type="' + (o.type === 'number' ? 'number' : 'text') + '" ' + (o.type === 'number' ? 'inputmode="decimal" min="0" step="any" ' : '') + 'value="' + esc(o.value) + '" placeholder="' + esc(o.placeholder) + '" style="width:100%;margin-top:12px" autocomplete="off">';
back.innerHTML = '<div class="modal-card" role="dialog" aria-modal="true">' + (o.title ? '<h3 style="margin:0 0 8px">' + esc(o.title) + '</h3>' : '')
+ (o.text ? '<p class="muted small" style="margin:0;white-space:pre-line">' + esc(o.text) + '</p>' : '') + field
+ '<div style="display:flex;gap:10px;justify-content:flex-end;margin-top:16px;flex-wrap:wrap"><button type="button" class="btn sec small" id="dlgCancel">' + esc(o.cancel || 'Cancel') + '</button><button type="button" class="btn small" id="dlgOk">' + esc(o.ok || 'OK') + '</button></div></div>';
document.body.appendChild(back);
const inp = back.querySelector('#dlgInput');
const done = v => { document.removeEventListener('keydown', onKey); back.remove(); resolve(v); };
const okv = () => done(o.type === 'none' ? true : (inp ? inp.value : ''));
const onKey = e => { if (e.key === 'Escape') { e.preventDefault(); done(o.type === 'none' ? false : null); } else if (e.key === 'Enter' && o.type !== 'textarea') { e.preventDefault(); okv(); } };
document.addEventListener('keydown', onKey);
back.querySelector('#dlgOk').addEventListener('click', okv);
back.querySelector('#dlgCancel').addEventListener('click', () => done(o.type === 'none' ? false : null));
back.addEventListener('click', e => { if (e.target === back) done(o.type === 'none' ? false : null); });
setTimeout(() => { if (inp) { inp.focus(); if (inp.select && o.type !== 'textarea') inp.select(); } else back.querySelector('#dlgOk').focus(); }, 30);
});
}
function ask(o) { return dialog(Object.assign({ type: 'text', value: '', placeholder: '' }, o || {})); }
function confirmBox(text, o) { return dialog(Object.assign({ type: 'none', text, ok: 'Yes', cancel: 'No' }, o || {})); }
return { getConfig, fmtPol, fmtUsd, status, renderNav, refreshNavWallet, describeEvent, feedRow, adSlot, reportAd, requestCode, launchChecks, launchToggle, ask, confirmBox, $ };
})();