Files
instantadpay/server.js
T
martbost 9b60e6414e Referral purchase email names the buyer: @username in subject and body
memberLabel() resolves @username, or the owner's username for a linked
position, else member #id. Subject: '@name just bought a $20 ad package'.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 10:37:45 -05:00

2140 lines
140 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// InstantAdPay — membership advertising with immutable on-chain settlement.
// Zero-dependency Node server (RM Circle pattern): static pages + JSON API,
// wallet sign-in (SIWE), live contract ledger, sponsor join links.
//
// Chain selection (Amoy rehearsal vs mainnet) lives in data/config.json —
// see chain.js. Wipe the volume's accounts/sessions + flip config = launch.
const http = require('http');
const https = require('https');
const dns = require('dns');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const sendy = require('./sendy');
const { URL } = require('url');
const chain = require('./chain');
const auth = require('./auth');
const accounts = require('./accounts');
const ads = require('./ads');
const mailer = require('./mailer');
const messages = require('./messages');
const reports = require('./reports');
const drip = require('./drip');
const spaces = require('./spaces'); // DO Spaces video storage (inert unless DO_SPACES_* set)
let QR = null; try { QR = require('qrcode'); } catch (e) { /* optional */ }
const chatbot = require('./chatbot');
const coach = require('./coach'); // coaching view, nudges, digest, prospects, link stats
const tank = require('./tank'); // holding tank: unsponsored free members, adoptions, pay-it-forward
const legacy = require('./legacy'); // Faucet Wave / Tier One Ads bridge: welcome-back credits for listed emails
const traffic = require('./traffic'); // public page views by referring domain (admin Traffic tab)
const TRAFFIC_PAGES = new Set(['/', '/ledger', '/contract', '/shorts', '/plays', '/wallets', '/launch']);
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)
const PORT = Number(process.env.PORT || 3000);
const ROOT = __dirname;
const PUBLIC_DIR = path.join(ROOT, 'public');
const DATA_DIR = process.env.DATA_DIR || path.join(ROOT, 'data');
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'changeme';
const ADMIN_EMAIL = String(process.env.ADMIN_EMAIL || '').trim().toLowerCase();
// Admin portal sessions: email-code sign-in allowlisted to ADMIN_EMAIL, kept
// in the volume so a restart doesn't log the admin out. Separate cookie and
// store from member sessions; the Bearer ADMIN_PASSWORD API path still works.
const ADMIN_SESS_FILE = path.join(DATA_DIR, 'admin-sessions.json');
const ADMIN_TTL = 12 * 60 * 60 * 1000;
let adminSessions = {};
try { adminSessions = JSON.parse(fs.readFileSync(ADMIN_SESS_FILE, 'utf8')) || {}; } catch (e) { adminSessions = {}; }
function saveAdminSessions() {
const now = Date.now();
for (const k of Object.keys(adminSessions)) if (!adminSessions[k] || adminSessions[k].expires < now) delete adminSessions[k];
try { fs.writeFileSync(ADMIN_SESS_FILE, JSON.stringify(adminSessions), { mode: 0o600 }); } catch (e) {}
}
function mintAdminSession(email) {
const t = crypto.randomBytes(32).toString('hex');
adminSessions[t] = { email, expires: Date.now() + ADMIN_TTL };
saveAdminSessions();
return t;
}
function adminTokenOf(req) { const m = /(?:^|;\s*)iap\.adm=([^;]+)/.exec(req.headers.cookie || ''); return m ? decodeURIComponent(m[1]) : null; }
function adminFromRequest(req) { const t = adminTokenOf(req); const s = t && adminSessions[t]; return (s && s.expires > Date.now()) ? s : null; }
function dropAdminSession(req) { const t = adminTokenOf(req); if (t && adminSessions[t]) { delete adminSessions[t]; saveAdminSessions(); } }
function adminCookie(t) { return 'iap.adm=' + encodeURIComponent(t) + '; Path=/; HttpOnly; SameSite=Lax; Max-Age=' + (ADMIN_TTL / 1000) + (IS_PROD ? '; Secure' : ''); }
function clearAdminCookie() { return 'iap.adm=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0'; }
const IS_PROD = process.env.NODE_ENV === 'production';
const SITE_FILE = path.join(DATA_DIR, 'site.json');
const db = require('./db');
fs.mkdirSync(DATA_DIR, { recursive: true });
const UPLOADS_DIR = path.join(DATA_DIR, 'uploads'); // solo-ad media lives on the volume
fs.mkdirSync(UPLOADS_DIR, { recursive: true });
const uploadCounts = new Map(); // email:day -> uploads today
const gauntletTokens = new Map(); // email -> welcome-tour token (server-clock dwell floor)
const videoTokens = new Map(); // email -> watch-to-earn video token (server-clock watch floor)
const faucetHits = new Map(); // address -> last faucet ts (rehearsal test-POL faucet rate limit)
const visitTokens = new Map(); // email -> verified-visit token (dwell + captcha floor)
// walk the referral chain upward via sponsorRef (code/username/member id)
async function uplineSlides(email, depth = 3) {
const out = [];
let cur = await accounts.byEmail(email);
for (let i = 0; i < depth && cur; i++) {
const ref = String(cur.sponsorRef || '').trim().toLowerCase();
if (!ref) break;
let s = null;
if (/^\d+$/.test(ref)) s = await accounts.byMemberId(Number(ref));
if (!s) s = await accounts.byCode(ref);
if (!s) s = await accounts.byUsername(ref);
if (!s || s.email === cur.email) break;
out.push(s);
cur = s;
}
return out;
}
// Wall ownership ladder: position 1 is always the member's own line banner;
// positions 2 and 3 become theirs at 2 and 5 qualifying buyers (the same
// thresholds that open payout levels 2 and 3). Until then, or while an unlocked
// slot is empty, the slot shows an upline's banner, then a house ad.
const catalogCache = { at: 0, products: null };
const wallUnlockedFor = bc => (bc >= 5 ? 3 : bc >= 2 ? 2 : 1);
// every on-chain member id this session controls: the main wallet plus linked
// positions (Qualified Start). Credits pool across them on the dashboard.
async function myMemberIds(s) {
const main = await auth.refreshMemberId(s);
const ids = main ? [main] : [];
if (s && s.email) for (const p of await accounts.positions(s.email)) if (p.memberId && !ids.includes(p.memberId)) ids.push(p.memberId);
return { main, ids };
}
function parseWallOffers(a) {
try { const v = JSON.parse((a && a.wallOffers) || '[]'); return Array.isArray(v) ? v.slice(0, 2) : []; } catch (e) { return []; }
}
// admin fallback ads for wall positions 2 & 3 when a member has no upline.
// Configurable by dropping data/admin-wall-ads.json ([{name,targetUrl,bannerUrl}]).
function getAdminWallAds() {
try { const j = JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'admin-wall-ads.json'), 'utf8')); if (Array.isArray(j) && j.length) return j; } catch (e) {}
return [{ name: 'InstantAdPay', targetUrl: 'https://instantadpay.com/', bannerUrl: null }];
}
const chatHits = new Map();
function chatLimited(ip) {
const now = Date.now(), rec = chatHits.get(ip);
if (!rec || now > rec.reset) { chatHits.set(ip, { count: 1, reset: now + 60000 }); return false; }
rec.count += 1;
return rec.count > 10;
}
// magic-code sign-in: emailLower -> {code, exp, tries}
const emailCodes = new Map();
// ── sign-up code guard (Marty, 2026-09-11): the email box is one field and one
// tap, so nothing visible stands in a human's way. Bots hit four invisible walls:
// a honeypot field, a minimum form age, per-IP + global send limits, and, only
// once an IP trips a limit, the same icon check the ad viewer uses.
const CODE_LIMITS = { per10m: 5, perDay: 20, globalPerMin: 60, passMs: 5 * 60 * 1000, minFormMs: 2000 };
const codeHits = new Map(); // ip -> { t: [send timestamps, 24h], passUntil, chal: { answer, exp } }
const codeGlobal = { minute: 0, n: 0 };
const codeAlert = { last: 0, trips: 0, ips: new Set() };
function clientIp(req) { return String(req.headers['x-forwarded-for'] || req.socket.remoteAddress || '').split(',')[0].trim() || 'unknown'; }
// the viewer's country and tier for ad targeting (null tier = unknown: never matches a restricted campaign)
function viewerGeo(req) { const cc = geo.countryOf(clientIp(req)); return { cc, tier: geo.tierOf(cc, siteConfig()) }; }
function codeChallenge(rec) {
const pick = CAPTCHA.slice().sort(() => Math.random() - 0.5).slice(0, 5);
const answer = Math.floor(Math.random() * pick.length);
rec.chal = { answer: pick[answer][0], exp: Date.now() + 5 * 60 * 1000 };
return { prompt: pick[answer][1], options: pick.map(x => x[0]) };
}
// returns null to allow the send, or { status, body } to answer with instead
const guardLog = (req, why, b) => console.log('signup-guard', why, clientIp(req), String(b.email || '').replace(/^(.).*(@.*)$/, '$1***$2'));
function codeGuard(req, b) {
const now = Date.now();
// honeypot: bots fill it, humans never see it. Some form-filler extensions fill every field,
// hidden or not (seen 2026-09-11), so a filled honeypot is a CHALLENGE, never a silent drop:
// a person passes the icon check and gets the code, a bot cannot.
const hp = !!b.hp_field_x9;
const fts = Number(b.fts) || 0;
if (!fts || now - fts < CODE_LIMITS.minFormMs) { guardLog(req, 'form-age', b); return { status: 400, body: { error: 'Give the page a second, then tap again.' } }; }
if (now - fts > 12 * 3600 * 1000) { guardLog(req, 'form-stale', b); return { status: 400, body: { error: 'This page has been open a long time. Refresh it, then tap again.' } }; }
const minute = Math.floor(now / 60000);
if (codeGlobal.minute !== minute) { codeGlobal.minute = minute; codeGlobal.n = 0; }
if (codeGlobal.n >= CODE_LIMITS.globalPerMin) { guardLog(req, 'global-limit', b); codeTrip(req, 'global'); return { status: 429, body: { error: 'Busy right now. Try again in a minute.' } }; }
const ip = clientIp(req);
const rec = codeHits.get(ip) || { t: [], passUntil: 0, chal: null };
rec.t = rec.t.filter(ts => now - ts < 24 * 3600 * 1000);
const n10 = rec.t.filter(ts => now - ts < 10 * 60 * 1000).length;
const limited = hp || n10 >= CODE_LIMITS.per10m || rec.t.length >= CODE_LIMITS.perDay;
if (limited && now >= rec.passUntil) {
const pick = String(b.pick || '');
if (pick && rec.chal && rec.chal.exp > now && pick === rec.chal.answer) { rec.passUntil = now + CODE_LIMITS.passMs; rec.chal = null; }
else {
guardLog(req, pick ? 'wrong-pick' : hp ? 'honeypot-challenge' : 'ip-limit', b); if (!hp) codeTrip(req, ip);
const challenge = codeChallenge(rec); codeHits.set(ip, rec);
return { status: 429, body: { error: pick ? 'That was not it. Try once more.' : 'Quick check before we send another code.', challenge } };
}
}
rec.t.push(now); codeHits.set(ip, rec); codeGlobal.n += 1;
if (codeHits.size > 5000) for (const [k, v] of codeHits) { if (!v.t.length || now - v.t[v.t.length - 1] > 24 * 3600 * 1000) codeHits.delete(k); }
return null;
}
// burst alert: at most one message per 10 minutes, to the admin Telegram chat if set, else the admin email
function codeTrip(req, ip) {
codeAlert.trips += 1; codeAlert.ips.add(ip);
if (Date.now() - codeAlert.last < 10 * 60 * 1000) return;
codeAlert.last = Date.now();
const text = '\u26A0\uFE0F InstantAdPay sign-up guard: ' + codeAlert.trips + ' blocked code request' + (codeAlert.trips === 1 ? '' : 's') + ' from ' + codeAlert.ips.size + ' source' + (codeAlert.ips.size === 1 ? '' : 's') + ' (' + [...codeAlert.ips].slice(0, 5).join(', ') + ') in the last window.';
codeAlert.trips = 0; codeAlert.ips = new Set();
const sc = siteConfig();
if (sc.telegramBotToken && sc.telegramAdminChatId) telegramSend(sc.telegramAdminChatId, text).catch(() => {});
else if (ADMIN_EMAIL && mailer.hasKey()) mailer.send(ADMIN_EMAIL, 'InstantAdPay: sign-up guard tripped', text).catch(() => {});
}
// earn-view tokens: emailLower -> {token, ts} (one live token per member)
const earnTokens = new Map();
// human-check pairs for the view verifier: [emoji shown, word named in the prompt]
const CAPTCHA = [['🚀', 'rocket'], ['⚡', 'lightning bolt'], ['🔑', 'key'], ['🎯', 'target'],
['🌊', 'wave'], ['🔥', 'flame'], ['💎', 'diamond'], ['🧲', 'magnet'], ['🔔', 'bell'], ['🌙', 'moon']];
// ── frame-breaking check ─────────────────────────────────
// Surf views frame the advertiser's URL full screen, so a target that refuses
// framing (X-Frame-Options / CSP frame-ancestors) would burn members' views on
// a blank frame. Catch it the moment the campaign is submitted. The lookup
// also refuses private/internal addresses so member URLs can't probe our LAN.
const PRIVATE_IP = /^(127\.|10\.|192\.168\.|169\.254\.|0\.|172\.(1[6-9]|2\d|3[01])\.|::1$|::$|f[cd])/i;
function frameFetch(url, depth) {
return new Promise(resolve => {
let u;
try { u = new URL(String(url || '')); } catch (e) { return resolve({ error: 'that is not a valid URL' }); }
if (!/^https?:$/.test(u.protocol)) return resolve({ error: 'only http(s) URLs work' });
if (u.port && u.port !== '80' && u.port !== '443') return resolve({ error: 'custom ports are not allowed' });
if (u.hostname === 'localhost' || u.hostname.endsWith('.local')) return resolve({ error: 'that address is not reachable from here' });
dns.lookup(u.hostname, (de, addr) => {
if (de) return resolve({ error: 'that domain does not resolve' });
if (PRIVATE_IP.test(addr)) return resolve({ error: 'that address is not reachable from here' });
const mod = u.protocol === 'https:' ? https : http;
const rq = mod.get(u.href, { timeout: 8000,
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; InstantAdPay-FrameCheck/1.0)', Accept: 'text/html' } }, r => {
const loc = r.headers.location;
r.resume(); // headers are all we need
if ([301, 302, 303, 307, 308].includes(r.statusCode) && loc && depth < 4) {
rq.destroy();
let next; try { next = new URL(loc, u.href).href; } catch (e2) { return resolve({ error: 'it redirects somewhere invalid' }); }
return resolve(frameFetch(next, depth + 1)); // every hop re-runs the private-IP guard
}
resolve({ status: r.statusCode, xfo: r.headers['x-frame-options'] || '', csp: r.headers['content-security-policy'] || '' });
rq.destroy();
});
rq.on('timeout', () => { rq.destroy(); resolve({ error: 'it did not answer within 8 seconds' }); });
rq.on('error', e2 => resolve({ error: 'it did not answer (' + (e2.code || 'connection failed') + ')' }));
});
});
}
// shared upload path for member creatives (/api/my/upload) and admin house-ad
// creatives (/api/admin/upload): `who` keys the per-day upload counter
async function handleUpload(req, res, who) {
const ct = String(req.headers['content-type'] || '').split(';')[0].trim().toLowerCase();
const EXT = { 'image/png': 'png', 'image/jpeg': 'jpg', 'image/webp': 'webp', 'image/gif': 'gif',
'video/mp4': 'mp4', 'video/webm': 'webm' };
if (!EXT[ct]) return json(res, 400, { error: 'Use a PNG, JPG, WebP, GIF, MP4 or WebM file.' });
const isVideo = ct.startsWith('video/');
const key = who + ':' + new Date().toISOString().slice(0, 10);
if ((uploadCounts.get(key) || 0) >= 10) return json(res, 400, { error: 'Upload limit for today reached (10 files).' });
let buf;
try { buf = await readRaw(req, isVideo ? 25 * 1024 * 1024 : 3 * 1024 * 1024); }
catch (e) { return json(res, 400, { error: 'File too large. Images up to 3MB, video up to 25MB.' }); }
const magicOk = buf.length > 16 && (
(ct === 'image/png' && buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47) ||
(ct === 'image/jpeg' && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) ||
(ct === 'image/webp' && buf.slice(0, 4).toString() === 'RIFF' && buf.slice(8, 12).toString() === 'WEBP') ||
(ct === 'image/gif' && buf.slice(0, 4).toString() === 'GIF8') ||
(ct === 'video/mp4' && buf.slice(4, 8).toString() === 'ftyp') ||
(ct === 'video/webm' && buf[0] === 0x1a && buf[1] === 0x45 && buf[2] === 0xdf && buf[3] === 0xa3));
if (!magicOk) return json(res, 400, { error: 'That file does not look like a real ' + EXT[ct].toUpperCase() + '.' });
const name = crypto.randomBytes(12).toString('hex') + '.' + EXT[ct];
uploadCounts.set(key, (uploadCounts.get(key) || 0) + 1);
// video goes to DO Spaces when configured (keeps big files off the volume);
// images stay local. Falls back to the volume if Spaces isn't set or errors.
if (isVideo && spaces.enabled()) {
try {
const url = await spaces.put('iap-uploads/' + name, buf, ct);
return json(res, 200, { url, type: 'video' });
} catch (e) { console.error('spaces put', e.message); /* fall through to volume */ }
}
fs.writeFileSync(path.join(UPLOADS_DIR, name), buf);
return json(res, 200, { url: '/uploads/' + name, type: isVideo ? 'video' : 'image' });
}
// lead-capture page hooks (og tags + copy live in public/assets/join.js too)
const JOIN_ANGLES = {
// legacy bridge pages (/from/<brand>): former members of Marty's closed EvolutionScript sites
'fw-adv': { t: 'Your next ad budget pays you back.', d: 'Faucet Wave closed. InstantAdPay was built for the people who bought ads there: packages from $5, seven formats, every package in your line paid out on Polygon in the same transaction. Welcome-back credits waiting.', url: 'https://instantadpay.com/from/faucetwave?seg=advertiser' },
'fw-earn': { t: 'Same daily habit. Real payouts on-chain.', d: 'You viewed ads on Faucet Wave. Here you view ads to earn credits, run your own campaign free, and get paid in POL to your own wallet when your line buys ads. Welcome-back credits waiting.', url: 'https://instantadpay.com/from/faucetwave' },
't1-adv': { t: 'Your next ad budget pays you back.', d: 'Tier One Ads closed. InstantAdPay was built for the people who bought ads there: packages from $5, seven formats, every package in your line paid out on Polygon in the same transaction. Welcome-back credits waiting.', url: 'https://instantadpay.com/from/tieroneads?seg=advertiser' },
't1-earn': { t: 'Same daily habit. Real payouts on-chain.', d: 'You viewed ads on Tier One Ads. Here you view ads to earn credits, run your own campaign free, and get paid in POL to your own wallet when your line buys ads. Welcome-back credits waiting.', url: 'https://instantadpay.com/from/tieroneads' },
instant: { t: 'Paid before the page reloads.', d: 'A smart contract on Polygon splits every ad package the moment it sells. Same transaction, real wallets, public ledger. Join free by email.' },
adspend: { t: 'You were buying ads anyway.', d: 'Here the ad spend in your line pays you, in the same transaction, on a public ledger. Seven formats, packages from $5. Join free.' },
free: { t: 'Watch first. Spend never.', d: 'Join free, view a few ads, earn credits, run your first campaign for zero dollars. Every payout public on Polygon.' },
ledger: { t: 'No back office. No payday.', d: 'Every payout is a public transaction on Polygon you can read yourself. Nothing is ever held. Join free by email.' },
two: { t: 'Two buyers open level two.', d: 'Every direct buyer pays you 50 percent from their first package. Two qualifying buyers open level two, five open level three. Written in a verified contract.' }
};
function serveJoinPage(res, tok, angle, ang, setCookies) {
let html;
try { html = fs.readFileSync(path.join(PUBLIC_DIR, 'join.html'), 'utf8'); } catch (e) { res.writeHead(404, baseHeaders({ 'Content-Type': 'text/plain' })); return res.end('Not found'); }
const base = 'https://instantadpay.com';
const url = (ang && ang.url) || (base + '/join/' + tok + (angle ? '?v=' + angle : ''));
const title = ang ? ang.t : 'Advertise and earn. Paid on-chain, instantly.';
const desc = ang ? ang.d : 'You are invited to InstantAdPay: real ad packages with same-transaction payouts on Polygon, every payment public. Join free by email.';
const escA = t => String(t).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;');
const og = '<meta property="og:type" content="website"><meta property="og:site_name" content="InstantAdPay">'
+ '<meta property="og:title" content="' + escA(title) + '"><meta property="og:description" content="' + escA(desc) + '">'
+ '<meta property="og:url" content="' + escA(url) + '">' // keeps ?v= so shares stay on the angle
+ '<meta property="og:image" content="' + base + '/banners/iap-hero-1200x630.png"><meta property="og:image:width" content="1200"><meta property="og:image:height" content="630">'
+ '<meta name="twitter:card" content="summary_large_image"><meta name="twitter:title" content="' + escA(title) + '"><meta name="twitter:description" content="' + escA(desc) + '"><meta name="twitter:image" content="' + base + '/banners/iap-hero-1200x630.png">';
html = html.replace(/<title>[^<]*<\/title>/, '<title>' + escA(title) + ' | InstantAdPay</title>' + og);
if (ang) html = html.replace('<body>', '<body class="squeeze">'); // angle pages: squeeze layout from the first paint
const headers = { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store, must-revalidate' };
if (setCookies && setCookies.length) headers['Set-Cookie'] = setCookies;
res.writeHead(200, baseHeaders(headers));
res.end(html);
}
async function frameCheck(url) {
const h = await frameFetch(url, 0);
if (h.error) return { ok: false, reason: 'We checked your URL and ' + h.error + '. Fix the URL and try again.' };
if (h.status >= 400) return { ok: false, reason: 'Your URL answers with HTTP ' + h.status + '. Point the campaign at a working page.' };
if (/deny|sameorigin/i.test(String(h.xfo)))
return { ok: false, reason: 'That site blocks framing (X-Frame-Options), so it would show members a blank page in the ad viewer. Use a landing page that allows framing.' };
const fa = /frame-ancestors\s+([^;]+)/i.exec(String(h.csp));
if (fa && !fa[1].split(/\s+/).some(x => {
const v = x.replace(/['"]/g, '').toLowerCase();
return v === '*' || v === 'https:' || v.includes('instantadpay.com');
}))
return { ok: false, reason: 'That site blocks framing (CSP frame-ancestors), so it would show members a blank page in the ad viewer. Use a landing page that allows framing.' };
return { ok: true };
}
async function boot() {
await db.init({ dataDir: DATA_DIR }); // no-op without DATABASE_URL (JSON mode)
chain.init({ onEvent: ev => { attachNames([ev]).then(a => pushFeed(a[0])).catch(() => pushFeed(ev)); emailOnEvent(ev).catch(() => {}); telegramOnEvent(ev).catch(() => {}); } });
auth.init({ dataDir: DATA_DIR, chain, isProd: IS_PROD, site: 'instantadpay.com' });
accounts.init({ dataDir: DATA_DIR });
ads.init({ dataDir: DATA_DIR, chain });
mailer.init({ dataDir: DATA_DIR });
messages.init({ dataDir: DATA_DIR });
reports.init({ dataDir: DATA_DIR });
drip.init({ dataDir: DATA_DIR, mailer, accounts, chain, site: 'https://instantadpay.com' });
chatbot.init({ dataDir: DATA_DIR, chain });
setTimeout(() => ads.dailySweep().catch(e => console.error('sweep', e.message)), 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
// follow-up email sequence: send whatever came due (every 10 min, first pass shortly after boot)
coach.init({ dataDir: DATA_DIR, chain, accounts, mailer });
tank.init({ dataDir: DATA_DIR, chain, accounts, messages, mailer, site: 'https://instantadpay.com' });
legacy.init({ dataDir: DATA_DIR });
traffic.init({ dataDir: DATA_DIR });
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));
setInterval(() => geo.refresh().catch(e => console.error('geo refresh', e.message)), 24 * 3600 * 1000); // monthly file, checked daily
setInterval(() => tank.sweep().catch(e => console.error('tank sweep', e.message)), 60 * 60 * 1000); // adoptions past their 7-day window
burner.init({ chain, ads });
setTimeout(() => coach.nudgeTick().catch(e => console.error('coach', e.message)), 90 * 1000);
setInterval(() => coach.nudgeTick().catch(e => console.error('coach', e.message)), 60 * 60 * 1000);
setTimeout(() => burner.tick().catch(e => console.error('burner', e.message)), 45 * 1000);
setInterval(() => burner.tick().catch(e => console.error('burner', e.message)), 5 * 60 * 1000);
setTimeout(() => drip.tick().catch(e => console.error('drip', e.message)), 30 * 1000);
setInterval(() => drip.tick().catch(e => console.error('drip', e.message)), 10 * 60 * 1000);
// NAS reconcile: pull syndicated delivery into the unified credit pool
// (inert unless NAS_DB_* is set). Every 5 min after a short warm-up.
if (ads.nasEnabled()) {
console.log('NAS syndication enabled');
setTimeout(() => ads.reconcileNas().catch(e => console.error('nas reconcile', e.message)), 90 * 1000);
setInterval(() => ads.reconcileNas().catch(e => console.error('nas reconcile', e.message)), 5 * 60 * 1000);
}
}
function siteConfig() {
let saved = {};
try { saved = JSON.parse(fs.readFileSync(SITE_FILE, 'utf8')); } catch (e) {}
return Object.assign({
siteName: 'InstantAdPay',
tagline: 'Advertise and earn. Locked in code, not promises.',
rehearsal: true, // shows the testnet banner; flipped off at mainnet launch
// payment-proof Telegram feed (blank = off) and the P&L pane's fixed monthly cost
telegramBotToken: '', telegramChatId: '', telegramTopicId: '', telegramEvents: 'payouts', telegramCtaUrl: 'https://instantadpay.com/',
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
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
pnlFixedMonthlyUsd: 0
}, saved);
}
// ---- helpers ----
const MIME = { '.html': 'text/html; charset=utf-8', '.css': 'text/css', '.js': 'text/javascript',
'.png': 'image/png', '.jpg': 'image/jpeg', '.svg': 'image/svg+xml', '.webp': 'image/webp',
'.ico': 'image/x-icon', '.json': 'application/json', '.mp4': 'video/mp4', '.woff2': 'font/woff2',
'.gif': 'image/gif', '.webm': 'video/webm', '.txt': 'text/plain; charset=utf-8', '.xml': 'application/xml; charset=utf-8' };
const CSP = "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net 'sha256-NzvNrqk5jB9YZATwo5BF4JoRlJ02HsnFikbKXgEPdaQ='; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: blob: https:; media-src 'self' https: blob:; connect-src 'self' https://*.walletconnect.com wss://*.walletconnect.com https://*.walletconnect.org wss://*.walletconnect.org https://*.reown.com wss://*.reown.com https://*.reown.org wss://*.reown.org https://*.web3modal.org https://*.drpc.org https://*.publicnode.com https://*.coinbase.com; font-src 'self' data: https://fonts.gstatic.com https://fonts.reown.com; form-action 'self'; frame-src https: http:";
function baseHeaders(extra) {
return Object.assign({ 'Content-Security-Policy': CSP, 'X-Content-Type-Options': 'nosniff',
'Referrer-Policy': 'strict-origin-when-cross-origin' }, extra || {});
}
function json(res, code, obj, extra) {
const body = JSON.stringify(obj);
res.writeHead(code, baseHeaders(Object.assign({ 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }, extra)));
res.end(body);
}
function sendFile(res, file) {
fs.readFile(file, (err, data) => {
if (err) { res.writeHead(404, baseHeaders({ 'Content-Type': 'text/plain' })); return res.end('Not found'); }
const ext = path.extname(file).toLowerCase();
res.writeHead(200, baseHeaders({ 'Content-Type': MIME[ext] || 'application/octet-stream',
// HTML is never stored (so a fresh load always gets the current asset
// versions — aggressive in-app wallet browsers were serving stale pages
// that pointed at old, since-fixed JS); versioned assets cache for an hour.
'Cache-Control': ext === '.html' ? 'no-store, must-revalidate' : 'public, max-age=3600' }));
res.end(data);
});
}
function readRaw(req, maxBytes) {
return new Promise((resolve, reject) => {
const chunks = [];
let n = 0;
req.on('data', c => {
n += c.length;
if (n > maxBytes) { req.destroy(); reject(new Error('too big')); return; }
chunks.push(c);
});
req.on('end', () => resolve(Buffer.concat(chunks)));
req.on('error', reject);
});
}
function readBody(req) {
return new Promise((resolve, reject) => {
let d = ''; let n = 0;
req.on('data', c => { n += c.length; if (n > 64 * 1024) { req.destroy(); reject(new Error('too big')); } d += c; });
req.on('end', () => { try { resolve(d ? JSON.parse(d) : {}); } catch (e) { reject(e); } });
req.on('error', reject);
});
}
function parseCookies(req) {
const out = {};
for (const p of (req.headers.cookie || '').split(';')) {
const i = p.indexOf('='); if (i > 0) out[p.slice(0, i).trim()] = decodeURIComponent(p.slice(i + 1).trim());
}
return out;
}
function isAdmin(req) {
const h = req.headers.authorization || '';
if (h === 'Bearer ' + ADMIN_PASSWORD) return true;
return !!adminFromRequest(req); // /admin portal session
}
// attach a memberId->username map to events so activity shows real people
async function attachNames(evts) {
try {
const ids = [];
for (const ev of evts)
for (const k of ['id', 'buyerId', 'recipientId', 'skippedId', 'sponsorId', 'newBuyerId', 'toId', 'memberId'])
if (ev[k]) ids.push(ev[k]);
const names = await accounts.namesForMembers(ids);
if (!Object.keys(names).length) return evts;
return evts.map(ev => Object.assign({}, ev, { names }));
} catch (e) { return evts; }
}
// A sponsor token is a numeric chain id or a site share code. Codes resolve
// to the referrer's CURRENT chain id, so activation any time before the
// referral's first purchase still locks the line to them.
async function resolveSponsorToken(tok) {
const t = String(tok || '').trim().toLowerCase();
if (!t) return 0;
if (/^\d+$/.test(t)) return Number(t);
let acct = await accounts.byCode(t);
if (!acct) acct = await accounts.byUsername(t); // vanity links: /join/<username>
if (!acct || !acct.address) return 0;
try { return await chain.memberIdByAccount(acct.address); } catch (e) { return 0; }
}
// 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 {
const a = await accounts.byMemberId(id);
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 (!mailer.hasKey() || !ev) return;
const notify = async (memberId, subject, body) => {
if (!memberId) 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(() => {});
};
if (ev.type === 'Purchase') {
const cc = chain.getConfig();
const txUrl = (cc.explorer ? cc.explorer.replace(/\/+$/, '') : 'https://polygonscan.com') + '/tx/' + ev.tx;
let bal = ev.creditAmount;
try { bal = await chain.creditBalance(ev.buyerId, ev.creditType); } catch (e) {}
await notify(ev.buyerId, 'Your InstantAdPay purchase is confirmed',
'Your purchase is complete and settled on-chain.\n\n' +
'Ad credits added: ' + ev.creditAmount + '\n' +
'Your ad-credit balance is now: ' + bal + '\n' +
'Amount paid: ' + weiToPol(ev.paidWei) + ' POL\n\n' +
'View your transaction on the blockchain:\n' + txUrl);
// tell the buyer's DIRECT sponsor their referral just bought (upline earners
// are separately notified by the TierPaid payout email when they earn)
try {
const buyer = await chain.member(ev.buyerId);
if (buyer && buyer.sponsorId) {
const sp = await accounts.byMemberId(buyer.sponsorId);
if (sp && sp.email) {
const bn = await memberLabel(ev.buyerId); // @username at a glance (Marty, 2026-09-12)
const usd = ('$' + (ev.priceCents / 100).toFixed(2)).replace(/\.00$/, '');
const qual = ev.priceCents >= 2000
? ' This is a $20+ purchase, so it counts toward your qualification.'
: ' (Purchases under $20 do not count toward qualification.)';
mailer.send(sp.email, bn + ' just bought a ' + usd + ' ad package',
'Your referral ' + bn + ' (member #' + ev.buyerId + ') just purchased a package (' + usd + ' — ' + ev.creditAmount + ' credits).' + qual + '\n\n' +
'See your team and the live ledger: https://instantadpay.com/my\n\nInstantAdPay').catch(() => {});
}
}
} catch (e) {}
}
else if (ev.type === 'TierPaid') await notify(ev.recipientId, 'You just got paid on InstantAdPay', 'A level-' + ev.tier + ' payout of ' + weiToPol(ev.amountWei) + ' POL just landed in your wallet.');
else if (ev.type === 'AwardPaid') await notify(ev.toId, 'You just got paid on InstantAdPay', weiToPol(ev.amountWei) + ' POL just landed in your wallet.');
else if (ev.type === 'PassedUp' && ev.reason === 'send-failed') {
// the member WAS qualified but their wallet rejected the POL (usually a smart-contract
// wallet that needs more than the capped gas): tell them and the admin, loudly
await notify(ev.skippedId, 'Your wallet rejected a payout on InstantAdPay', 'A level-' + ev.tier + ' payout tried to reach your linked wallet and the wallet refused the transfer, so it passed to the next qualified member. This happens with some smart-contract wallets. Link a regular wallet address (MetaMask, SafePal, Phantom) on the Wallet tab so the next payout lands.');
if (ADMIN_EMAIL) mailer.send(ADMIN_EMAIL, 'InstantAdPay: payout send-failed for member #' + ev.skippedId, 'A level-' + ev.tier + ' payout to member #' + ev.skippedId + ' failed at the wallet (send-failed) and passed up. Tx: ' + ev.tx + '\n\nThe member has been emailed to link a regular wallet.').catch(() => {});
}
else if (ev.type === 'PassedUp') await notify(ev.skippedId, 'A payout passed you by on InstantAdPay', 'A level-' + ev.tier + ' payout passed you by because you were not qualified yet. Get qualified so you catch the next one.');
}
// payment-proof Telegram feed (same pattern as the RM Circle proof channel): one compact
// line per event, admin-configured under Settings > Site (telegramBotToken, telegramChatId,
// optional telegramTopicId, telegramEvents = payouts | payouts+purchases | all, telegramCtaUrl)
// A second target, telegramEchoChatId + telegramEchoTopicId (+ telegramEchoEvents), echoes
// the same lines into a shared cross-program payments topic that RM Circle also posts to,
// so every line there carries the program name.
async function telegramOnEvent(ev) {
const sc = siteConfig();
if (!sc.telegramBotToken || !ev) return;
if (!sc.telegramChatId && !sc.telegramEchoChatId) return;
const names = await accounts.namesForMembers([ev.recipientId, ev.buyerId, ev.sponsorId, ev.newBuyerId, ev.id].filter(Boolean)).catch(() => ({}));
const who = id => '#' + id + (names[id] ? ' @' + names[id] : '');
const cc = chain.getConfig();
const tx = (cc.explorer ? cc.explorer.replace(/\/+$/, '') : 'https://polygonscan.com') + '/tx/' + ev.tx;
const build = (mode) => {
let line = null;
if (ev.type === 'TierPaid') line = '\u{1F4B8} Level ' + ev.tier + ' payout: <b>' + weiToPol(ev.amountWei) + ' POL</b> \u2192 ' + who(ev.recipientId);
else if (ev.type === 'BuyerCounted' && mode !== 'payouts') line = '\u2B50 ' + who(ev.sponsorId) + ' now has <b>' + ev.newCount + '</b> qualifying buyer' + (ev.newCount === 1 ? '' : 's') + (ev.newCount === 2 ? ' \u00b7 level 2 open' : ev.newCount === 5 ? ' \u00b7 level 3 open' : '');
else if (ev.type === 'Purchase' && mode !== 'payouts') line = '\u{1F9FE} ' + who(ev.buyerId) + ' bought a $' + Math.round(ev.priceCents / 100) + ' package';
else if (ev.type === 'AwardPaid') line = '\u{1F4B8} Award payout: <b>' + weiToPol(ev.amountWei) + ' POL</b> \u2192 ' + who(ev.toId);
else if (ev.type === 'MemberActivated' && mode === 'all') line = '\u{1F91D} ' + who(ev.id) + ' switched on payouts';
if (!line) return null;
return line + ' \u00b7 <a href="' + tx + '">verify</a>' + (sc.telegramCtaUrl ? '\n<a href="' + sc.telegramCtaUrl + '">Join free</a>' : '');
};
if (sc.telegramChatId) { const t = build(String(sc.telegramEvents || 'payouts')); if (t) await telegramSend(sc.telegramChatId, t, sc.telegramTopicId); }
if (sc.telegramEchoChatId) { const t = build(String(sc.telegramEchoEvents || 'payouts')); if (t) await telegramSend(sc.telegramEchoChatId, '\u{1F7E0} <b>InstantAdPay</b> \u00b7 ' + t, sc.telegramEchoTopicId); }
}
// holding-tank arrivals -> one digest line in the shared payments topic (Marty, 2026-09-12): who is
// waiting for a sponsor, by username, so builders go adopt them. Runs every 15 min, posts only
// when someone new landed since the last check.
async function tankNotifyTick() {
const sc = siteConfig();
if (!sc.telegramBotToken || !sc.telegramEchoChatId) return;
const f = path.join(DATA_DIR, 'tank-notify.json');
let st = { last: 0 }; try { st = JSON.parse(fs.readFileSync(f, 'utf8')); } catch (e) {}
const since = st.last || (Date.now() - 24 * 3600 * 1000);
const fresh = (await tank.waiting()).filter(w => (w.joined || 0) > since);
st.last = Date.now(); fs.writeFileSync(f, JSON.stringify(st));
if (!fresh.length) return;
const named = fresh.filter(w => w.username).map(w => '@' + w.username);
const who = named.length ? ': ' + named.slice(0, 8).join(', ') + (named.length > 8 ? ' and ' + (named.length - 8) + ' more' : '') : '';
const text = '\u{1FAA3} <b>InstantAdPay</b> \u00b7 ' + fresh.length + ' new member' + (fresh.length === 1 ? '' : 's') + ' waiting for a sponsor in the holding tank' + who
+ '\nAdopt from My line \u203a Holding tank: <a href="https://instantadpay.com/my#line">instantadpay.com/my</a>';
await telegramSend(sc.telegramEchoChatId, text, sc.telegramEchoTopicId);
}
// one sendMessage call; never throws, never logs the token
async function telegramSend(chatId, text, threadId) {
const sc = siteConfig();
if (!sc.telegramBotToken || !chatId) return;
const body = JSON.stringify(Object.assign({ chat_id: chatId, text, parse_mode: 'HTML', disable_web_page_preview: true }, threadId ? { message_thread_id: Number(threadId) } : {}));
await new Promise((resolve) => {
const rq = https.request({ hostname: 'api.telegram.org', path: '/bot' + sc.telegramBotToken + '/sendMessage', method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }, timeout: 10000 }, r => { r.resume(); r.on('end', resolve); });
rq.on('error', () => resolve()); rq.on('timeout', () => { rq.destroy(); resolve(); }); rq.end(body);
});
}
// ---- live feed (SSE) ----
const feedClients = new Set();
function pushFeed(ev) {
const line = 'data: ' + JSON.stringify(ev) + '\n\n';
for (const res of feedClients) { try { res.write(line); } catch (e) { feedClients.delete(res); } }
}
// ---- server ----
const server = http.createServer(async (req, res) => {
try {
const u = new URL(req.url, 'http://x');
const p = u.pathname;
// -- traffic log: public page views by referring domain (admin > Traffic)
if (req.method === 'GET' && (TRAFFIC_PAGES.has(p) || /^\/(join|from|wall)\/[^/]+$/.test(p))) traffic.hit(p, req.headers.referer, req.headers['user-agent']);
// -- join links: /join/<memberId or share code> — LAST-touch cookie (Marty,
// 2026-09-10): the link a visitor opened most recently is the sponsor shown
// and used, and it locks the moment the account is created (accounts.ensure
// never changes an existing account's sponsor; the contract binds the buyer at
// their first purchase). Codes resolve LATE (at buy time) to whatever chain id
// the referrer has by then, so free members refer from day one.
let m = /^\/join\/([A-Za-z0-9_]{1,20})$/.exec(p);
if (m && (req.method === 'GET' || req.method === 'HEAD')) {
// lead-capture page: email first, wallet later. ?v=<angle> picks the hook
// copy and is remembered so the account records which angle converted.
const tok = m[1].toLowerCase();
const cookies = parseCookies(req);
const angle = String(u.searchParams.get('v') || '').toLowerCase();
const ang = JOIN_ANGLES[angle] || null;
if (req.method === 'GET') coach.recordView(tok, ang ? angle : '', req.headers.referer); // link stats per angle + source
const cookieTail = `; Path=/; SameSite=Lax; Max-Age=${30 * 24 * 3600}${IS_PROD ? '; Secure' : ''}`; // 30 days: whoever brings them back gets the credit
const set = [];
set.push('iap.sponsor=' + tok + cookieTail); // last touch wins
if (ang) set.push('iap.angle=' + angle + cookieTail);
if (!cookies['iap.ref']) set.push('iap.ref=' + encodeURIComponent(coach.refHost(req.headers.referer)) + cookieTail); // first-touch source
return serveJoinPage(res, tok, ang ? angle : '', ang, set);
}
// -- legacy bridge: /from/faucetwave | /from/tieroneads [?seg=advertiser]. Same squeeze page
// with brand copy, NO sponsor (the sponsor cookie is cleared so they land in the holding
// tank for adoption), the angle remembered so the welcome-back grant fires at signup, and
// a forced first-touch source so link stats / admin can see the legacy arrivals.
m = /^\/from\/(faucetwave|tieroneads)$/.exec(p);
if (m && (req.method === 'GET' || req.method === 'HEAD')) {
const brand = m[1];
const key = (brand === 'faucetwave' ? 'fw' : 't1') + (String(u.searchParams.get('seg') || '').toLowerCase().startsWith('adv') ? '-adv' : '-earn');
const cookieTail = `; Path=/; SameSite=Lax; Max-Age=${30 * 24 * 3600}${IS_PROD ? '; Secure' : ''}`;
const src = String(u.searchParams.get('src') || '').toLowerCase().replace(/[^a-z0-9.\-]/g, '').slice(0, 40); // e.g. the old domain's splash page
const set = ['iap.sponsor=; Path=/; SameSite=Lax; Max-Age=0' + (IS_PROD ? '; Secure' : ''), 'iap.angle=' + key + cookieTail, 'iap.ref=' + encodeURIComponent('legacy:' + brand + (src ? ':' + src : '')) + cookieTail];
return serveJoinPage(res, '', key, JOIN_ANGLES[key], set);
}
if (p === '/unsubscribe' && req.method === 'GET') {
const r = await drip.unsubscribe(u.searchParams.get('e'), u.searchParams.get('t'));
const msg = r.error ? r.error : 'Done. You will not get any more follow-up emails from InstantAdPay. Your account is unchanged.';
res.writeHead(r.error ? 400 : 200, baseHeaders({ 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' }));
return res.end('<!doctype html><html><head><meta charset="utf-8"><title>InstantAdPay</title><link rel="stylesheet" href="/assets/site.css?v=20260909a"></head><body><div class="wrap" style="max-width:560px;padding:80px 22px"><a href="/"><img src="/logo.png" alt="InstantAdPay" style="height:34px"></a><h1 style="font-size:30px;margin:26px 0 12px">' + (r.error ? 'Hmm.' : 'Unsubscribed.') + '</h1><p>' + msg + '</p><p><a class="btn small sec" href="/my">Member area</a></p></div></body></html>');
}
// -- public API
if (p === '/api/config' && req.method === 'GET') {
const c = chain.getConfig();
// public copy of the site settings: never anything that looks like a credential
const pubSite = {};
for (const [k, v] of Object.entries(siteConfig())) if (!/secret|token|password|private|apikey|api_key/i.test(k)) pubSite[k] = v;
return json(res, 200, Object.assign({ contract: c.contract, chainId: c.chainId,
chainName: c.chainName, explorer: c.explorer, rpc: c.rpcs[0],
emailAuth: mailer.hasKey() || !IS_PROD }, pubSite));
}
if (p === '/api/moonpay-url' && req.method === 'GET') {
// Card on-ramp deep link. With MoonPay keys set — PUBLIC key via
// MOONPAY_PUBLIC_KEY env or site config, SECRET key via MOONPAY_SECRET_KEY
// env ONLY (never site config, since /api/config exposes siteConfig) —
// returns a SIGNED checkout URL prefilled with the buyer's own wallet and
// a POL amount; otherwise a generic MoonPay buy page. Zero custody either
// way: MoonPay is merchant of record and the crypto goes straight to the
// buyer's wallet — this site never touches or holds anyone's money.
const addr = (u.searchParams.get('address') || '').trim();
let pol = Math.round(Number(u.searchParams.get('pol')) || 0);
if (!pol || pol < 30) pol = 30;
if (pol > 100000) pol = 100000;
const pk = (process.env.MOONPAY_PUBLIC_KEY || siteConfig().moonpayPublicKey || '').trim();
const sk = (process.env.MOONPAY_SECRET_KEY || '').trim();
if (pk && sk && /^0x[0-9a-fA-F]{40}$/.test(addr)) {
const qs = '?apiKey=' + encodeURIComponent(pk) + '&currencyCode=pol_polygon&walletAddress=' + encodeURIComponent(addr) + '&quoteCurrencyAmount=' + pol;
const sig = crypto.createHmac('sha256', sk).update(qs).digest('base64');
return json(res, 200, { url: 'https://buy.moonpay.com/' + qs + '&signature=' + encodeURIComponent(sig), signed: true, pol });
}
return json(res, 200, { url: 'https://www.moonpay.com/buy/matic', signed: false, pol }); // MoonPay's Polygon page still lives at the old slug; /buy/pol renders their 404 (same fix as RM Circle)
}
if (p === '/api/catalog' && req.method === 'GET') {
// five live quotes per call: serve a 20-second cache so pages that load the
// ladder (join, home, buy) are not waiting on the RPC every time
if (!catalogCache.at || Date.now() - catalogCache.at > 20000) {
try { catalogCache.products = await chain.catalog(); catalogCache.at = Date.now(); }
catch (e) { if (!catalogCache.products) throw e; }
}
return json(res, 200, { products: catalogCache.products });
}
if (p === '/api/feed' && req.method === 'GET') {
return json(res, 200, { events: await attachNames(chain.recentEvents(Number(u.searchParams.get('n')) || 100)) });
}
if (p === '/api/feed/live' && req.method === 'GET') {
res.writeHead(200, baseHeaders({ 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-store', Connection: 'keep-alive' }));
res.write(': connected\n\n');
feedClients.add(res);
req.on('close', () => feedClients.delete(res));
return;
}
m = /^\/api\/tx\/(0x[0-9a-fA-F]{64})$/.exec(p);
if (m && req.method === 'GET') {
// tx relay so the browser never talks to the RPC directly (CSP stays 'self').
// Serves both the wallet's waitTx poll (found/status) and the built-in
// /tx/<hash> viewer (full details + decoded events).
try {
const r = await chain.rpc('eth_getTransactionReceipt', [m[1]]);
if (!r) return json(res, 200, { found: false });
const out = { found: true, status: r.status, blockNumber: r.blockNumber, gasUsed: r.gasUsed };
try {
const t = await chain.rpc('eth_getTransactionByHash', [m[1]]);
if (t) { out.from = t.from; out.to = t.to; out.valueWei = BigInt(t.value || '0x0').toString(); }
} catch (e) {}
try {
const blk = await chain.rpc('eth_getBlockByNumber', [r.blockNumber, false]);
if (blk) out.ts = blk.timestamp;
} catch (e) {}
try {
out.events = await attachNames((r.logs || []).map(chain.decodeLog).filter(Boolean)
.map(ev => Object.assign(ev, { tx: m[1] })));
} catch (e) { out.events = []; }
return json(res, 200, out);
} catch (e) { return json(res, 200, { found: false, rpcError: true }); }
}
if (p === '/api/sponsor' && req.method === 'GET') {
// The account's stored sponsor is authoritative — it persists across
// devices, cleared cookies, and return visits. Fall back to the first-touch
// cookie only for anonymous visitors with no account sponsor yet. (Reading
// the cookie alone was orphaning buyers to root when the cookie was absent.)
const s = await auth.fromRequest(req);
const acct = s && s.email ? await accounts.byEmail(s.email) : null;
const tok = (acct && acct.sponsorRef) || parseCookies(req)['iap.sponsor'] || '';
let sponsorId = await resolveSponsorToken(tok);
// orphan fallback: an unresolvable/absent sponsor (dead link, no link) lands
// the new member under the configured catch position (#1) instead of root
if (!sponsorId && (!acct || acct.memberId !== (Number(siteConfig().defaultSponsorId) || 1))) sponsorId = Number(siteConfig().defaultSponsorId) || 1;
let name = null, avatarUrl = null;
if (tok) {
const t = tok.toLowerCase();
let a = await accounts.byCode(t); if (!a) a = await accounts.byUsername(t);
if (!a && /^\d+$/.test(tok)) a = await accounts.byMemberId(Number(tok));
if (a) { name = a.username ? '@' + a.username : (a.memberId ? 'member #' + a.memberId : null); avatarUrl = a.avatarUrl || null; }
}
return json(res, 200, { ref: tok, sponsorId, invited: !!tok, name, avatarUrl });
}
if (p === '/api/stats' && req.method === 'GET') {
let members = 0; try { members = await chain.memberCount(); } catch (e) {}
return json(res, 200, Object.assign({ onchainMembers: members, siteAccounts: await accounts.count() }, chain.totals()));
}
// -- 24/7 assistant
if (p === '/api/chat' && req.method === 'POST') {
const ip = req.socket.remoteAddress || 'x';
if (chatLimited(ip)) return json(res, 429, { error: 'Give it a minute, then ask again.' });
const b = await readBody(req);
const r = await chatbot.answer(b.message, b.history);
return json(res, r.error ? 400 : 200, r);
}
// -- accounts: email + password is the normal join path (wallet comes
// out only at purchase / payout-activation time and gets linked then)
if (p === '/api/signup' && req.method === 'POST') {
const b = await readBody(req);
const ref = parseCookies(req)['iap.sponsor'] || ''; // last-touch attribution, locked at account creation
const r = await accounts.signup(b.email, b.password, ref);
if (r.error) return json(res, 400, r);
// sponsor is notified once the new member picks a username (onboarding),
// so the email can name them — see /api/my/profile
if (b.newsletter) sendy.subscribe(r.account.email, r.account.username || '').catch(() => {}); // pre-checked opt-in, silent
const token = await auth.mintSession({ email: r.account.email });
return json(res, 200, { ok: true, account: r.account }, { 'Set-Cookie': auth.sessionCookie(token) });
}
if (p === '/api/login' && req.method === 'POST') {
const b = await readBody(req);
const r = await accounts.login(b.email, b.password);
if (r.error) return json(res, 400, r);
let memberId = 0;
if (r.account.address) { try { memberId = await chain.memberIdByAccount(r.account.address); } catch (e) {} }
const token = await auth.mintSession({ email: r.account.email, address: r.account.address, memberId });
return json(res, 200, { ok: true, account: r.account }, { 'Set-Cookie': auth.sessionCookie(token) });
}
// -- passwordless: email code sign-in (signup and login are the same act)
if (p === '/api/auth/email/start' && req.method === 'POST') {
const b = await readBody(req);
const e = String(b.email || '').trim().toLowerCase();
if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(e)) return json(res, 400, { error: 'That email address does not look right.' });
const prev = emailCodes.get(e);
if (prev && Date.now() < prev.nextAt) { console.log('signup-guard cooldown', clientIp(req), e.replace(/^(.).*(@.*)$/, '$1***$2')); return json(res, 429, { error: 'Code already sent. Give it a minute, then try again.' }); }
const guard = codeGuard(req, b); // honeypot, form age, per-IP + global limits, icon check once limited
if (guard) return json(res, guard.status, guard.body);
const code = String(Math.floor(100000 + Math.random() * 900000));
emailCodes.set(e, { code, exp: Date.now() + 15 * 60 * 1000, tries: 0, nextAt: Date.now() + 60 * 1000 });
if (mailer.hasKey()) {
try { await mailer.sendCode(e, code); } catch (err) {
console.error('sendCode failed', err.message);
return json(res, 502, { error: 'Could not send the email. Try again in a minute.' });
}
return json(res, 200, { ok: true, sent: true });
}
if (!IS_PROD) return json(res, 200, { ok: true, sent: false, devCode: code });
return json(res, 503, { error: 'Email sign-in is not configured yet.' });
}
if (p === '/api/auth/email/verify' && req.method === 'POST') {
const b = await readBody(req);
const e = String(b.email || '').trim().toLowerCase();
const rec = emailCodes.get(e);
if (!rec || rec.exp < Date.now()) return json(res, 400, { error: 'Code expired. Request a fresh one.' });
rec.tries += 1;
if (rec.tries > 6) { emailCodes.delete(e); return json(res, 400, { error: 'Too many tries. Request a fresh code.' }); }
if (String(b.code || '').trim() !== rec.code) return json(res, 400, { error: 'That code does not match.' });
emailCodes.delete(e);
const ref = parseCookies(req)['iap.sponsor'] || '';
const via = parseCookies(req)['iap.angle'] || '';
const joinedRef = decodeURIComponent(parseCookies(req)['iap.ref'] || '') || null;
const r = await accounts.ensure(e, ref, via, joinedRef); // first touch wins; existing accounts unchanged
if (r.error) return json(res, 400, r);
// the lead is in the door: queue the getting-started sequence (opt-in box is pre-checked on both forms)
if (r.created && (b.followups || b.newsletter)) drip.enqueue(e, ref, via).catch(() => {});
// a wallet-only session (signed with a wallet, no account) finishing setup:
// adopt that wallet into the email account so member #, purchases and
// payouts stay attached, then retire the wallet-only session
const prior = await auth.fromRequest(req);
if (prior && prior.address && !prior.email) {
const lr = await accounts.linkWallet(e, prior.address);
if (lr.error) return json(res, 400, lr);
r.account = lr.account || await accounts.byEmail(e);
await auth.logout(req);
}
if (r.created) { sendWelcome(e, ref).catch(() => {}); } // sponsor notified at username set (/api/my/profile)
// legacy bridge: a listed former Faucet Wave / Tier One Ads member gets welcome-back credits once
if (r.created && /^(fw|t1)-(adv|earn)$/.test(via)) {
try { const g = legacy.grant(e, siteConfig()); if (g) { await ads.addEarned(e, g.credits); console.log('legacy grant', g.brand, g.seg, g.credits, e); } }
catch (err) { console.error('legacy grant', err.message); }
}
if (r.created && b.newsletter) sendy.subscribe(r.account.email, r.account.username || '').catch(() => {}); // pre-checked opt-in, silent, new joins only
let memberId = 0;
if (r.account.address) { try { memberId = await chain.memberIdByAccount(r.account.address); } catch (err) {} }
const token = await auth.mintSession({ email: r.account.email, address: r.account.address, memberId });
return json(res, 200, { ok: true, account: r.account, created: !!r.created }, { 'Set-Cookie': auth.sessionCookie(token) });
}
// -- wallet auth: link-to-account when an email session exists, or
// wallet-first sign-in (no UI door since 2026-09-09; a wallet-only session
// is walked to the email card, which adopts the wallet on verify)
if (p === '/api/auth/challenge' && req.method === 'POST') {
const b = await readBody(req);
const r = auth.makeChallenge(b.address);
return json(res, r.error ? 400 : 200, r);
}
if (p === '/api/auth/verify' && req.method === 'POST') {
const b = await readBody(req);
const r = await auth.verifyChallenge(b.address, b.signature);
if (r.error) return json(res, 400, r);
let memberId = 0;
try { memberId = await chain.memberIdByAccount(r.address); } catch (e) {}
const s = await auth.fromRequest(req);
if (s && s.email && b.asPosition) {
// Qualified Start: a second (third…) wallet on the same account. It becomes
// its own on-chain member under this member's id when it buys; the session
// stays on the main wallet.
// policy (Marty, 2026-09-10): positions exist to qualify, so at most five per account, and a
// wallet that is already registered on-chain under anything other than this account's main
// member is refused (a position-under-position chain would recapture 70% of a self-buy)
const MAX_POS = 5;
const have = await accounts.positions(s.email);
if (!have.find(p => p.address === r.address.toLowerCase()) && have.length >= MAX_POS)
return json(res, 400, { error: 'You can link up to ' + MAX_POS + ' positions, which is everything Qualified Start needs. Once qualified, buy from your main wallet so your sponsor is paid in full.' });
if (memberId) {
const mainId = await auth.refreshMemberId(s);
let mm = null; try { mm = await chain.member(memberId); } catch (e) {}
if (mm && mainId && mm.sponsorId !== mainId)
return json(res, 400, { error: 'That wallet is already registered on-chain under ' + (mm.sponsorId ? 'member #' + mm.sponsorId : 'no sponsor') + ', not under your main member #' + mainId + '. Only positions registered directly under you can be linked.' });
}
const pr = await accounts.addPosition(s.email, r.address);
if (pr.error) return json(res, 400, pr);
if (memberId) await accounts.setPositionMember(r.address, memberId);
return json(res, 200, { ok: true, position: true, address: r.address, memberId });
}
if (s && s.email) {
const lr = await accounts.linkWallet(s.email, r.address);
if (lr.error) return json(res, 400, lr);
await auth.updateSession(s.token, { address: r.address, memberId });
return json(res, 200, { ok: true, linked: true, address: r.address, memberId });
}
const acct = await accounts.byAddress(r.address);
if (!acct && await accounts.positionOwner(r.address))
return json(res, 400, { error: 'That wallet is a linked position on an account. Sign in with that account\'s email instead.' });
const token = await auth.mintSession({ email: acct ? acct.email : null, address: r.address, memberId });
return json(res, 200, { ok: true, address: r.address, memberId },
{ 'Set-Cookie': auth.sessionCookie(token) });
}
if (p === '/api/auth/logout' && req.method === 'POST') {
await auth.logout(req);
return json(res, 200, { ok: true }, { 'Set-Cookie': auth.clearCookie() });
}
if (p === '/api/gas' && req.method === 'GET') {
try { return json(res, 200, await chain.suggestedFees()); }
catch (e) { return json(res, 200, {}); }
}
if (p === '/api/me' && req.method === 'GET') {
const s = await auth.fromRequest(req);
if (!s) return json(res, 200, { signedIn: false });
const memberId = await auth.refreshMemberId(s);
const acct = (s.email && await accounts.byEmail(s.email)) || (s.address && await accounts.byAddress(s.address)) || null;
let sponsorId = await resolveSponsorToken((acct && acct.sponsorRef) || parseCookies(req)['iap.sponsor']);
const _defSpon = Number(siteConfig().defaultSponsorId) || 1;
if (!sponsorId && memberId !== _defSpon) sponsorId = _defSpon; // orphan fallback → #1 (never self-sponsor)
if (memberId && acct && acct.memberId !== memberId) accounts.setMemberId(acct.email, memberId).catch(() => {});
const out = { signedIn: true, email: s.email || (acct && acct.email) || null,
address: s.address || (acct && acct.address) || null, memberId,
username: (acct && acct.username) || null,
refCode: (acct && acct.code) || null, sponsorId,
// profile + line-banner fields so the Profile pane repopulates on reload (were being saved but not returned)
avatarUrl: (acct && acct.avatarUrl) || null, bio: (acct && acct.bio) || null,
socials: (acct && acct.socials) || null,
wallOffers: parseWallOffers(acct),
lineBannerUrl: (acct && acct.lineBannerUrl) || null, lineTargetUrl: (acct && acct.lineTargetUrl) || null };
if (memberId) {
try {
const mm = await chain.member(memberId);
out.buyerCount = mm.buyerCount;
out.onchainSponsorId = mm.sponsorId;
const bal = await ads.balances((await myMemberIds(s)).ids, s.email);
out.credits = bal.total; out.creditedCredits = bal.credited; out.earnedCredits = bal.earned; out.inCampaigns = bal.inCampaigns; out.availableCredits = bal.available;
} catch (e) { out.chainReadError = true; }
}
return json(res, 200, out);
}
if (p === '/api/my/dashboard' && req.method === 'GET') {
const s = await auth.fromRequest(req);
if (!s) return json(res, 401, { error: 'Sign in first.' });
const memberId = await auth.refreshMemberId(s);
const acct = (s.email && await accounts.byEmail(s.email)) || (s.address && await accounts.byAddress(s.address)) || null;
if (memberId && acct && acct.memberId !== memberId) accounts.setMemberId(acct.email, memberId).catch(() => {});
let tankWaiting = null; // top of every Overview: people waiting for a sponsor (Marty, 2026-09-12)
try {
if (!tankWaitCache || Date.now() - tankWaitCache.ts > 60000) tankWaitCache = { ts: Date.now(), list: await tank.waiting() };
const tw = tankWaitCache.list;
tankWaiting = { count: tw.length, names: tw.slice(0, 6).map(w => w.name), eligible: !!(await tank.eligibility(s.email)).ok };
} catch (e) {}
const out = { memberId, tankWaiting, email: s.email || (acct && acct.email) || null,
address: s.address || (acct && acct.address) || null,
username: (acct && acct.username) || null,
refCode: (acct && acct.code) || null, credits: 0, buyerCount: 0,
earnedWei: '0', earnCount: 0, referrals: [], welcomeCredits: 0 };
if (out.email) {
// welcome credits unlock via the welcome tour when an upline with a
// line banner exists; members with no tour to walk get them instantly
const welcomed = await ads.welcomeGranted(out.email);
const tour = welcomed ? [] : (await uplineSlides(out.email)).filter(a => a.lineTargetUrl);
if (welcomed || !tour.length) { await ads.grantWelcome(out.email); out.welcomeCredits = ads.rates().welcomeCredits || 0; } // the welcome amount itself; the rest of the earned pool is viewing rewards, milestones and credits we add
else { out.welcomeCredits = 0; out.gauntletPending = true; }
}
if (out.email) out.inboxUnread = await ads.unreadCount(out.email); // delivers pending solos too
if (out.email) { // sponsor chat: presence heartbeat + unread + my availability + direct sponsor
accounts.touchSeen(out.email).catch(() => {});
out.chatUnread = await messages.chatUnread(out.email);
out.chatAvailable = (acct && acct.chatAvailable !== false);
const spon = await accounts.sponsorOf(out.email);
if (spon && spon.email) out.sponsor = {
email: spon.email,
name: spon.username ? '@' + spon.username : (spon.memberId ? 'member #' + spon.memberId : 'your sponsor'),
online: (Date.now() - (spon.lastSeen || 0)) < 60000,
available: spon.chatAvailable !== false };
}
if (out.email) { // unmissable login modal when the upline sent a message
const un = await messages.newestUnread(out.email);
if (un) {
const nm = un.fromMember ? await accounts.namesForMembers([un.fromMember]) : {};
out.sponsorMsg = { id: un.id, subject: un.subject, body: un.body,
fromName: (un.fromMember && nm[un.fromMember]) ? '@' + nm[un.fromMember] : (un.fromMember ? 'member #' + un.fromMember : 'your sponsor') };
}
}
if (memberId) {
try {
const mm = await chain.member(memberId);
out.buyerCount = mm.buyerCount;
const bal = await ads.balances((await myMemberIds(s)).ids, s.email);
out.credits = bal.total; out.creditedCredits = bal.credited; out.earnedCredits = bal.earned; out.inCampaigns = bal.inCampaigns; out.availableCredits = bal.available;
} catch (e) { out.chainReadError = true; }
let earned = 0n, n = 0;
for (const ev of chain.recentEvents(600)) {
if ((ev.type === 'TierPaid' && ev.recipientId === memberId) || (ev.type === 'AwardPaid' && ev.toId === memberId)) {
earned += BigInt(ev.amountWei); n += 1;
}
}
out.earnedWei = earned.toString();
out.earnCount = n;
}
// achievement milestones (same ladder as the Overview stepper) + one-time credit
// bonuses — computed AFTER buyerCount is read from chain above (else always 0)
{
const bc = out.buyerCount || 0;
const reached = [];
if (out.memberId) reached.push('payouts');
if (bc >= 1) reached.push('firstBuyer');
if (bc >= 2) reached.push('level2');
if (bc >= 5) reached.push('level3');
out.milestonesReached = reached;
if (out.email && reached.length) out.milestonesGranted = await ads.grantMilestones(out.email, reached);
}
// who joined through this member: their invite link uses username when set,
// else code, and the numeric id once on-chain — match all three
const refs = [];
if (acct && acct.code) refs.push(acct.code);
if (acct && acct.username) refs.push(acct.username);
if (memberId) refs.push(String(memberId));
const joined = await accounts.listByReferrer(refs);
out.referrals = joined.map(r => ({
name: r.username || r.email.replace(/^(.).*(@.*)$/, '$1***$2'), // username, else privacy mask
joined: r.created,
status: r.address ? 'wallet linked' : 'joined free'
}));
out.isAdmin = !!(ADMIN_EMAIL && out.email && String(out.email).toLowerCase() === ADMIN_EMAIL); // shows the Admin link
out.wallUnlocked = wallUnlockedFor(out.buyerCount || 0); // how many wall positions are the member's own
return json(res, 200, out);
}
// -- linked positions (Qualified Start): list, refresh from chain, unlink
if (p === '/api/my/positions' && req.method === 'GET') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const acct = await accounts.byEmail(s.email);
const mainId = await auth.refreshMemberId(s);
const list = await accounts.positions(s.email);
const out = [];
const posIds = [mainId, ...list.map(p => p.memberId)].filter(Boolean);
let balPer = {}, credited = 0; try { const bb = await ads.balances(posIds, s.email); credited = bb.credited; for (const p of bb.per) balPer[p.memberId] = p.avail; } catch (e) {}
for (const pos of list) {
let id = pos.memberId;
if (!id) { try { id = await chain.memberIdByAccount(pos.address); if (id) await accounts.setPositionMember(pos.address, id); } catch (e) {} }
const row = { address: pos.address, memberId: id || 0, buyerCount: 0, counted: false, credits: 0, created: pos.created };
if (id) {
try { const mm = await chain.member(id); row.buyerCount = mm.buyerCount; row.counted = mm.countedAsBuyer; row.sponsorId = mm.sponsorId; } catch (e) {}
row.credits = balPer[id] != null ? balPer[id] : 0;
}
out.push(row);
}
const main = { address: (acct && acct.address) || null, memberId: mainId, credits: 0, buyerCount: 0 };
if (mainId) {
main.credits = balPer[mainId] != null ? balPer[mainId] : 0;
try { main.buyerCount = (await chain.member(mainId)).buyerCount; } catch (e) {}
}
// live POL balances (the link signature proved the wallet is theirs) + a POL/USD rate from the catalog
const bal = async a => { try { return BigInt(await chain.rpc('eth_getBalance', [a, 'latest'])).toString(); } catch (e) { return null; } };
if (main.address) main.balanceWei = await bal(main.address);
for (const row of out) row.balanceWei = await bal(row.address);
let polUsd = 0; try { const cat = await chain.catalog(); const pk = cat.find(x => x.costWei); if (pk) polUsd = (pk.priceCents / 100) / (Number(BigInt(pk.costWei)) / 1e18); } catch (e) {}
return json(res, 200, { main, positions: out, polUsd, totalCredits: main.credits + out.reduce((n, r) => n + r.credits, 0), credited });
}
if (p === '/api/my/positions/remove' && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const b = await readBody(req);
const r = await accounts.removePosition(s.email, b.address);
return json(res, r.error ? 400 : 200, r);
}
// -- coaching: every direct's ladder rung, stalled flag, and what to say
// -- holding tank: waiting members, my adoptions, adopt, release (pay it forward)
if (p === '/api/my/tank' && req.method === 'GET') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
return json(res, 200, await tank.view(s.email));
}
if (p === '/api/my/tank/adopt' && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const b = await readBody(req);
const r = await tank.adopt(s.email, b.who, b.note);
return json(res, r.error ? 400 : 200, r);
}
if (p === '/api/my/tank/release' && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const b = await readBody(req);
const r = await tank.release(s.email, b.email);
return json(res, r.error ? 400 : 200, r);
}
if (p === '/api/my/tank/contacted' && req.method === 'POST') { // sponsor reached the lead off-site: reset the rescue clock
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const b = await readBody(req);
const r = await tank.markContacted(s.email, b.email);
return json(res, r.error ? 400 : 200, r);
}
if (p === '/api/my/gift' && req.method === 'POST') { // PIF: log a wallet-to-wallet POL gift and tell the recipient
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const b = await readBody(req);
const r = await tank.recordGift(s.email, b.email, b.tx, b.pol);
return json(res, r.error ? 400 : 200, r);
}
if (p === '/api/admin/tank' && req.method === 'GET') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
return json(res, 200, await tank.adminView());
}
// -- admin Traffic tab: page views + join-page views + signups + $20 buyers, by referring
// domain / first-touch source, by landing page, by angle and by day (Marty, 2026-09-12)
if (p === '/api/admin/traffic' && req.method === 'GET') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
const days = Math.min(365, Math.max(1, Number(u.searchParams.get('days')) || 30));
const since = Date.now() - days * 86400000, sinceDay = new Date(since).toISOString().slice(0, 10);
const hosts = {}, paths = {}, daily = {}, angles = {};
const H = k => (hosts[k] = hosts[k] || { source: k, hits: 0, joinViews: 0, signups: 0, registered: 0, buyers: 0 });
const Dy = k => (daily[k] = daily[k] || { day: k, hits: 0, signups: 0 });
for (const r of await traffic.rows(sinceDay)) { H(r.host).hits += r.n; paths[r.path] = (paths[r.path] || 0) + r.n; Dy(r.day).hits += r.n; }
for (const v of await coach.viewsSince(since)) { H(v.ref || 'direct').joinViews += 1; const a = angles[v.angle || 'plain'] = angles[v.angle || 'plain'] || { angle: v.angle || 'plain', views: 0, signups: 0 }; a.views += 1; }
const buyerIds = new Set(); for (const ev of chain.recentEvents(1e9)) if (ev.type === 'BuyerCounted' && ev.newBuyerId) buyerIds.add(ev.newBuyerId);
for (const a of await accounts.listAll(5000)) {
if (!a.created || a.created < since) continue;
const h = H(a.joinedRef || 'direct'); h.signups += 1; if (a.memberId) h.registered += 1; if (a.memberId && buyerIds.has(a.memberId)) h.buyers += 1;
Dy(new Date(a.created).toISOString().slice(0, 10)).signups += 1;
const k = a.joinedVia || 'plain'; angles[k] = angles[k] || { angle: k, views: 0, signups: 0 }; angles[k].signups += 1;
}
const sources = Object.values(hosts).sort((x, y) => (y.hits + y.joinViews + y.signups * 10) - (x.hits + x.joinViews + x.signups * 10));
return json(res, 200, { days, sources, paths: Object.entries(paths).map(([path, hits]) => ({ path, hits })).sort((x, y) => y.hits - x.hits),
angles: Object.values(angles).sort((x, y) => y.views - x.views), daily: Object.values(daily).sort((x, y) => x.day < y.day ? -1 : 1),
totals: { hits: sources.reduce((n, s) => n + s.hits, 0), joinViews: sources.reduce((n, s) => n + s.joinViews, 0), signups: sources.reduce((n, s) => n + s.signups, 0), buyers: sources.reduce((n, s) => n + s.buyers, 0) } });
}
if (p === '/api/my/coach' && req.method === 'GET') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const cv = await coach.coachView(s.email);
for (const d of cv.directs) if (d.free) { try { d.rescue = await tank.rescueInfo(s.email, await accounts.byEmail(d.email)); } catch (e) {} } // dormant-lead clock
return json(res, 200, cv);
}
// -- link stats: views, joins and buyers per angle link
if (p === '/api/my/linkstats' && req.method === 'GET') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
return json(res, 200, await coach.linkStats(s.email));
}
// -- prospects: the member's own follow-up list
if (p === '/api/my/prospects' && req.method === 'GET') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
return json(res, 200, { prospects: await coach.prospects(s.email), statuses: coach.STATUSES });
}
if (p === '/api/my/prospects' && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const r = await coach.saveProspect(s.email, await readBody(req));
return json(res, r.error ? 400 : 200, r);
}
if (p === '/api/my/prospects/remove' && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const b = await readBody(req);
const r = await coach.removeProspect(s.email, b.id);
return json(res, r.error ? 400 : 200, r);
}
if (p === '/api/my/profile' && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const b = await readBody(req);
const before = await accounts.byEmail(s.email);
// a username is permanent once set: the invite link, the public wall and every
// banner already printed carry it (Marty, 2026-09-10)
if (before && before.username && String(b.username || '').trim().toLowerCase() !== String(before.username).toLowerCase())
return json(res, 400, { error: 'Your username is locked. Your invite link, your public page and any banners you shared all carry @' + before.username + '. Contact support if it truly has to change.' });
const r = await accounts.setUsername(s.email, b.username);
// First time a username is set (onboarding): now there's a real name to
// show, so notify the sponsor here rather than at signup (where it'd just
// say "a new member"). Fires once — only on the empty→set transition.
if (!r.error && before && !before.username && b.username) {
const acct = await accounts.byEmail(s.email);
notifyNewReferral(acct && acct.sponsorRef, acct).catch(() => {});
}
return json(res, r.error ? 400 : 200, r);
}
// -- earn credits by viewing ads (attention-gated daily claim)
if (p === '/api/my/earn' && req.method === 'GET') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
return json(res, 200, await ads.viewStatus(s.email));
}
// fraud-guarded view flow: the server issues a single-use token when it
// serves the ad, and only counts the view if the dwell elapsed on the
// SERVER clock. Client-side focus tracking pauses the countdown; this is
// the floor a script cannot cheat past.
if (p === '/api/my/earnview' && req.method === 'GET') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const status = await ads.viewStatus(s.email);
if (status.views >= status.target || status.claimed) return json(res, 200, { ad: null, status });
const type = String(u.searchParams.get('type') || 'banner');
// members never see (or earn from) their own campaigns in the viewer
const ad = await ads.serve(type === 'text' ? 'text' : 'banner', Object.assign({ excludeEmail: s.email }, viewerGeo(req)));
if (!ad) return json(res, 200, { ad: null, status });
const token = crypto.randomBytes(16).toString('hex');
// the viewer tab frames the advertiser's REAL url (no click counted for a paid view)
earnTokens.set(s.email, { token, ts: Date.now(), adId: ad.id,
targetUrl: await ads.targetOf(ad.id), adName: ad.title || ad.name || null });
return json(res, 200, { ad, token, viewUrl: '/view/' + token, status });
}
// the viewer tab asks where to point the frame (does not consume the token)
if (p === '/api/my/viewinfo' && req.method === 'GET') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const t = earnTokens.get(s.email);
if (!t || t.token !== String(u.searchParams.get('token') || ''))
return json(res, 400, { error: 'That view is no longer open. Head back to the dashboard and load the next ad.' });
return json(res, 200, { targetUrl: t.targetUrl, adName: t.adName || null,
dwell: ads.rates().viewDwellSeconds || 10 });
}
// human check: handed out only once the dwell has elapsed on the SERVER clock
if (p === '/api/my/viewchallenge' && req.method === 'GET') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const t = earnTokens.get(s.email);
if (!t || t.token !== String(u.searchParams.get('token') || ''))
return json(res, 400, { error: 'That view is no longer open.' });
const dwellMs = (ads.rates().viewDwellSeconds || 10) * 1000;
const age = Date.now() - t.ts;
if (age < dwellMs - 400) return json(res, 200, { early: true, wait: Math.ceil((dwellMs - age) / 1000) });
if (age > 5 * 60 * 1000) { earnTokens.delete(s.email); return json(res, 400, { error: 'That ad went stale. Load a fresh one.' }); }
const pick = CAPTCHA.slice().sort(() => Math.random() - 0.5).slice(0, 5);
const answer = Math.floor(Math.random() * pick.length);
t.challenge = { answer };
return json(res, 200, { prompt: pick[answer][1], options: pick.map(x => x[0]) });
}
if (p === '/api/my/adview' && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const b = await readBody(req);
const t = earnTokens.get(s.email);
const dwellMs = (ads.rates().viewDwellSeconds || 10) * 1000;
if (!t || t.token !== String(b.token || '')) return json(res, 400, { error: 'That view did not check out. Load the next ad and let it finish.' });
const age = Date.now() - t.ts;
if (age < dwellMs - 400) return json(res, 400, { error: 'Watch the full ad first.' });
if (age > 5 * 60 * 1000) { earnTokens.delete(s.email); return json(res, 400, { error: 'That ad went stale. Load a fresh one.' }); }
// the human check must be solved on the same token
if (!t.challenge) return json(res, 400, { error: 'Finish the quick check first.', retry: true });
if (Number(b.answer) !== t.challenge.answer) {
t.attempts = (t.attempts || 0) + 1;
t.challenge = null; // force a fresh challenge for the next try
if (t.attempts >= 3) { earnTokens.delete(s.email); return json(res, 400, { error: 'Three misses — that view is void. Head back and load the next ad.' }); }
return json(res, 400, { error: 'Wrong pick.', retry: true });
}
earnTokens.delete(s.email); // single use
return json(res, 200, await ads.recordView(s.email));
}
if (p === '/api/my/claim' && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const r = await ads.claimDaily(s.email);
return json(res, r.error ? 400 : 200, r);
}
// -- downline lineage: 3 levels, usernames+IDs; email only for directs
if (p === '/api/my/line' && req.method === 'GET') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const levels = await accounts.downline(s.email, 3);
// what each person has paid THIS member so far: sum of TierPaid events where
// this member is the recipient and that person is the buyer (all indexed events)
// ...counting payouts to every position this account owns (main id + linked positions)
const myId = await auth.refreshMemberId(s);
const own = (await accounts.positions(s.email)).filter(p => p.memberId);
const myIds = new Set([myId, ...own.map(p => p.memberId)].filter(Boolean));
const earnedBy = {};
const qualified = new Set(); // members the contract counted as this account's qualifying buyers ($20+)
if (myIds.size) {
for (const ev of chain.recentEvents(1e9)) {
if (ev.type === 'TierPaid' && myIds.has(ev.recipientId) && ev.buyerId)
earnedBy[ev.buyerId] = (BigInt(earnedBy[ev.buyerId] || '0') + BigInt(ev.amountWei)).toString();
if (ev.type === 'BuyerCounted' && myIds.has(ev.sponsorId) && ev.newBuyerId) qualified.add(ev.newBuyerId);
}
}
const out = levels.map(L => ({ level: L.level, members: L.members.map(m => ({
memberId: m.memberId || 0,
name: m.username ? '@' + m.username : m.memberId ? 'member #' + m.memberId : 'member',
email: L.level === 1 ? m.email : null, // directs only
joined: m.created,
qualified: !!(m.memberId && qualified.has(m.memberId)),
earnedWei: (m.memberId && earnedBy[m.memberId]) || '0' })) }));
// the member's own linked positions sit on level 1 too, labelled as theirs
if (own.length) {
if (!out.find(L => L.level === 1)) out.unshift({ level: 1, members: [] });
const L1 = out.find(L => L.level === 1);
own.forEach((p, i) => L1.members.push({ memberId: p.memberId, name: 'You · position ' + (i + 2), own: true,
email: null, joined: p.created, earnedWei: earnedBy[p.memberId] || '0' }));
}
return json(res, 200, { levels: out, counts: out.map(L => L.members.length) });
}
// -- broadcast a message to your downline (1/day), on-site inbox + email
if (p === '/api/my/broadcast' && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const b = await readBody(req);
const subject = String(b.subject || '').trim().slice(0, 160);
const body = ads.sanitizeRich(b.body);
const plain = body.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
if (!subject) return json(res, 400, { error: 'Give your message a subject.' });
if (plain.length < 10) return json(res, 400, { error: 'Write a message first.' });
const last = await messages.lastBroadcastAt(s.email);
if (Date.now() - last < 24 * 3600 * 1000)
return json(res, 429, { error: 'You can send one broadcast a day. Try again in ' + Math.ceil((24 * 3600 * 1000 - (Date.now() - last)) / 3600000) + 'h.' });
const depth = b.scope === 'direct' ? 1 : 3;
const levels = await accounts.downline(s.email, depth);
const recipients = [...new Set(levels.flatMap(L => L.members.map(m => m.email)).filter(Boolean))];
if (!recipients.length) return json(res, 400, { error: 'No one in your line to message yet.' });
const memberId = s.memberId || await auth.refreshMemberId(s);
await messages.deliver(memberId, s.email, recipients, subject, body);
// email each recipient too (best-effort; never blocks the on-site delivery)
if (mailer.hasKey()) {
const who = (await accounts.byEmail(s.email));
const from = who && who.username ? '@' + who.username : 'your sponsor';
for (const to of recipients) {
mailer.send(to, 'Message from ' + from + ': ' + subject,
plain + '\n\n— sent via your InstantAdPay upline. Read it in your dashboard: https://instantadpay.com/my#line')
.catch(() => {});
}
}
return json(res, 200, { ok: true, sent: recipients.length });
}
// -- sponsor messages: this member's inbox from their upline
if (p === '/api/my/messages' && req.method === 'GET') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const items = await messages.inbox(s.email);
const names = await accounts.namesForMembers([...new Set(items.map(i => i.fromMember).filter(Boolean))]);
for (const i of items) i.fromName = (i.fromMember && names[i.fromMember]) ? '@' + names[i.fromMember]
: i.fromMember ? 'member #' + i.fromMember : 'your upline';
return json(res, 200, { items, unread: items.filter(i => !i.read).length });
}
m = /^\/api\/my\/messages\/(\d+)\/read$/.exec(p);
if (m && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
return json(res, 200, await messages.markRead(s.email, m[1]));
}
// ── SPONSOR CHAT (two-way): presence-aware 1:1 threads up/down the line ──
const chatOnline = ts => (Date.now() - (ts || 0)) < 60000;
const chatName = a => !a ? 'member' : (a.username ? '@' + a.username : (a.memberId ? 'member #' + a.memberId : 'member'));
// lightweight presence heartbeat (called on a timer while the dashboard is open)
if (p === '/api/my/ping' && req.method === 'GET') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 200, { ok: true, chatUnread: 0 });
accounts.touchSeen(s.email).catch(() => {});
return json(res, 200, { ok: true, chatUnread: await messages.chatUnread(s.email) });
}
// training center content (admin-curated via data/training.json)
if (p === '/api/training' && req.method === 'GET') {
const s = await auth.fromRequest(req);
if (!s) return json(res, 401, { error: 'Sign in first.' });
let items = [];
try { const j = JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'training.json'), 'utf8')); if (Array.isArray(j)) items = j; } catch (e) {}
if (!items.length) items = [{ title: 'Getting started with InstantAdPay',
desc: 'How the platform works, how every payout splits on-chain to real wallets, and how to build your line.',
docUrl: 'https://instantadpay.com/' }];
return json(res, 200, { items });
}
// daily login bonus (once/day, gentle streak) — granted after the login flow
if (p === '/api/my/login-bonus' && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
return json(res, 200, await ads.grantLoginBonus(s.email));
}
// -- rehearsal test-POL faucet: top a connected wallet up to 10 test-POL
// (anvil_setBalance, no key needed). Rehearsal-only, rate-limited.
if (p === '/api/my/faucet' && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s) return json(res, 401, { error: 'Sign in first.' });
if (!siteConfig().rehearsal) return json(res, 400, { error: 'The faucet is only open during the rehearsal.' });
const b = await readBody(req);
const addr = String(b.address || '').trim().toLowerCase();
if (!/^0x[0-9a-f]{40}$/.test(addr)) return json(res, 400, { error: 'Connect your wallet first.' });
// Amoy is a public testnet: testers fund their own connected wallet from
// the public Amoy faucet (no server-minted balance). We just echo the
// address + faucet link; the client copies the address and opens it.
let balHex = '0x0'; try { balHex = await chain.rpc('eth_getBalance', [addr, 'latest']); } catch (e) {}
return json(res, 200, { ok: true, faucetUrl: 'https://faucet.polygon.technology/', address: addr, balanceWei: BigInt(balHex || '0x0').toString() });
}
// -- report an ad (auto-approved ads need a safety valve): store + notify admin
if (p === '/api/report-ad' && req.method === 'POST') {
const b = await readBody(req);
if (!Number(b.campaignId)) return json(res, 400, { error: 'Which ad?' });
const s = await auth.fromRequest(req);
const who = (s && s.email) || '';
const rec = await reports.add(b.campaignId, who, b.reason, b.note);
try {
const adminEmail = process.env.ADMIN_EMAIL || ''; // private env only — siteConfig is exposed via /api/config
if (adminEmail && mailer.hasKey()) {
mailer.send(adminEmail, 'Ad reported on InstantAdPay (campaign #' + rec.campaignId + ')',
'A member flagged an ad.\n\nCampaign: #' + rec.campaignId + '\nReason: ' + rec.reason
+ '\nReported by: ' + (who || 'anonymous') + '\nNote: ' + (String(b.note || '').slice(0, 500) || '(none)')
+ '\n\nPause or review it from the admin.').catch(() => {});
}
} catch (e) {}
return json(res, 200, { ok: true });
}
if (p === '/api/my/chat/send' && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const me = s.email.toLowerCase();
const b = await readBody(req);
const to = String(b.to || '').trim().toLowerCase();
const text = String(b.body || '').replace(/<[^>]*>/g, '').replace(/\s+$/, '').slice(0, 2000).trim();
if (!to || to === me) return json(res, 400, { error: 'Pick who to message.' });
if (!text) return json(res, 400, { error: 'Write a message first.' });
const target = await accounts.byEmail(to);
if (!target) return json(res, 404, { error: 'No such member.' });
// authorize: existing thread, my direct sponsor, or someone in my downline
let ok = (await messages.thread(me, to, 0, 1)).length > 0;
if (!ok) { const spon = await accounts.sponsorOf(me); ok = !!(spon && spon.email && spon.email.toLowerCase() === to); }
if (!ok) ok = await accounts.isDownlineOf(me, to);
if (!ok) return json(res, 403, { error: 'You can only message your direct sponsor or someone in your line.' });
if ((await accounts.getMutes(to)).map(x => String(x).toLowerCase()).includes(me))
return json(res, 403, { error: 'They are not accepting messages from you right now.' });
const memberId = s.memberId || await auth.refreshMemberId(s);
const msg = await messages.sendChat(memberId, me, to, text);
// email only when they are offline AND I have not messaged them in ~10 min (no mid-chat spam)
try {
if (mailer.hasKey() && !chatOnline(target.lastSeen)) {
const mine = (await messages.thread(me, to, 0, 400)).filter(x => x.id !== msg.id && String(x.fromEmail).toLowerCase() === me);
const lastMineTs = mine.length ? mine[mine.length - 1].sent : 0;
if (Date.now() - lastMineTs > 10 * 60 * 1000) {
const who = await accounts.byEmail(me);
const from = who && who.username ? '@' + who.username : 'someone in your InstantAdPay line';
mailer.send(to, 'New message from ' + from,
text.slice(0, 400) + '\n\n— reply in your dashboard: https://instantadpay.com/my').catch(() => {});
}
}
} catch (e) {}
return json(res, 200, { ok: true, message: { id: msg.id, sent: msg.sent, fromMe: true, body: text } });
}
if (p === '/api/my/chat/thread' && req.method === 'GET') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const me = s.email.toLowerCase();
const other = String(u.searchParams.get('with') || '').trim().toLowerCase();
const after = Number(u.searchParams.get('after')) || 0;
if (!other) return json(res, 400, { error: 'Who with?' });
// may view a thread I'm party to (existing), or one I'm allowed to start
let ok = (await messages.thread(me, other, 0, 1)).length > 0;
if (!ok) { const spon = await accounts.sponsorOf(me); ok = !!(spon && spon.email && spon.email.toLowerCase() === other); }
if (!ok) ok = await accounts.isDownlineOf(me, other);
if (!ok) return json(res, 403, { error: 'Not your conversation.' });
const msgs = await messages.thread(me, other, after, 300);
await messages.markChatRead(me, other);
accounts.touchSeen(me).catch(() => {});
const oa = await accounts.byEmail(other);
const iMute = (await accounts.getMutes(me)).map(x => String(x).toLowerCase()).includes(other);
const theyMuteMe = (await accounts.getMutes(other)).map(x => String(x).toLowerCase()).includes(me);
return json(res, 200, {
messages: msgs.map(x => ({ id: x.id, fromMe: String(x.fromEmail).toLowerCase() === me, body: x.body, sent: x.sent })),
otherName: chatName(oa), online: oa ? chatOnline(oa.lastSeen) : false,
available: oa ? oa.chatAvailable !== false : true, iMute, blocked: theyMuteMe,
canMute: await accounts.isDownlineOf(me, other) });
}
if (p === '/api/my/chat/threads' && req.method === 'GET') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const list = await messages.threadList(s.email.toLowerCase());
for (const t of list) {
const a = await accounts.byEmail(t.email);
t.name = chatName(a); t.online = a ? chatOnline(a.lastSeen) : false;
}
list.sort((x, y) => (y.last.sent || 0) - (x.last.sent || 0));
const meAcct = await accounts.byEmail(s.email);
return json(res, 200, { threads: list, available: meAcct ? meAcct.chatAvailable !== false : true });
}
if (p === '/api/my/chat/available' && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const b = await readBody(req);
return json(res, 200, await accounts.setChatAvailable(s.email, !!b.available));
}
if (p === '/api/my/chat/mute' && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const b = await readBody(req);
const target = String(b.email || '').trim().toLowerCase();
if (!target) return json(res, 400, { error: 'Who?' });
const allowed = (await accounts.isDownlineOf(s.email, target)) || (await messages.thread(s.email.toLowerCase(), target, 0, 1)).length > 0;
if (!allowed) return json(res, 403, { error: 'You can only mute someone in your line.' });
return json(res, 200, await accounts.setMute(s.email, target, !!b.muted));
}
// -- featured rotation: the live featured links + dilution stats
if (p === '/api/featured' && req.method === 'GET') {
const items = await ads.serveFeatured(viewerGeo(req));
const names = await accounts.namesForMembers([...new Set(items.map(i => i.memberId).filter(Boolean))]);
for (const i of items) i.by = (i.memberId && names[i.memberId]) ? '@' + names[i.memberId] : (i.memberId ? 'member #' + i.memberId : null);
return json(res, 200, { items });
}
if (p === '/api/featured/stats' && req.method === 'GET') {
return json(res, 200, await ads.featuredStats());
}
// -- QR code (SVG) for any InstantAdPay URL — used on bio/wall pages
if (p === '/api/qr' && req.method === 'GET') {
const data = String(u.searchParams.get('d') || '').slice(0, 300);
if (!QR || !data) { res.writeHead(404, baseHeaders()); return res.end(); }
try {
const svg = await QR.toString(data, { type: 'svg', margin: 1, width: 240,
color: { dark: '#0e7d5f', light: '#f2fbf8' } }); // mint-green modules, still high-contrast for reliable scanning
res.writeHead(200, baseHeaders({ 'Content-Type': 'image/svg+xml', 'Cache-Control': 'public, max-age=3600' }));
return res.end(svg);
} catch (e) { res.writeHead(500, baseHeaders()); return res.end(); }
}
// -- profile: avatar + bio
if (p === '/api/my/profile-details' && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const b = await readBody(req);
const avatar = b.avatarUrl === undefined ? undefined : String(b.avatarUrl || '').trim();
if (avatar && !/^(\/uploads\/[a-z0-9]{24}\.(png|jpg|webp|gif)|https:\/\/[^\s]+)$/i.test(avatar))
return json(res, 400, { error: 'Avatar must be an uploaded image or an https image URL.' });
const bio = b.bio === undefined ? undefined : String(b.bio || '').trim().slice(0, 600);
// socials: {platform: url}; keep only known platforms with valid https urls
let socials;
if (b.socials !== undefined) {
const PLAT = ['facebook', 'twitter', 'youtube', 'instagram', 'tiktok', 'telegram', 'linkedin', 'website', 'video'];
const clean = {};
for (const p of PLAT) {
const v = String((b.socials && b.socials[p]) || '').trim();
if (v && /^https:\/\/[^\s]+$/i.test(v) && v.length <= 300) clean[p] = v;
}
// intro video on the public wall: a YouTube, Vimeo or direct .mp4/.webm link only
if (clean.video && !/^https:\/\/((www\.|m\.)?youtube\.com\/(watch\?|shorts\/|embed\/)|youtu\.be\/|(www\.)?vimeo\.com\/\d+|player\.vimeo\.com\/video\/\d+|[^\s]+\.(mp4|webm)(\?|$))/i.test(clean.video))
return json(res, 400, { error: 'The intro video needs to be a YouTube link, a Vimeo link, or a direct .mp4 link.' });
socials = Object.keys(clean).length ? JSON.stringify(clean) : null;
}
const r = await accounts.setProfile(s.email, avatar, bio, socials);
return json(res, r.error ? 400 : 200, r);
}
// -- wall positions 2 & 3: the member's own offers, unlocked at 2 / 5 qualifying buyers
if (p === '/api/my/wall-offers' && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const b = await readBody(req);
const src = Array.isArray(b.offers) ? b.offers.slice(0, 2) : [];
const out = [];
for (let i = 0; i < 2; i++) {
const o = src[i] || {};
const targetUrl = String(o.targetUrl || '').trim();
const bannerUrl = String(o.bannerUrl || '').trim();
const title = String(o.title || '').trim().slice(0, 60);
if (targetUrl && !/^https:\/\/[^\s]+$/i.test(targetUrl)) return json(res, 400, { error: 'Position ' + (i + 2) + ': the link must start with https://' });
if (bannerUrl && !/^(\/uploads\/[a-z0-9]{24}\.(png|jpg|webp|gif)|https:\/\/[^\s]+)$/i.test(bannerUrl)) return json(res, 400, { error: 'Position ' + (i + 2) + ': banner must be an uploaded image or an https image URL.' });
out.push(targetUrl ? { title: title || null, bannerUrl: bannerUrl || null, targetUrl } : null);
}
const r = await accounts.setWallOffers(s.email, out.some(Boolean) ? JSON.stringify(out) : null);
return json(res, r.error ? 400 : 200, r);
}
// -- line banner: the member's viral slot on welcome tours + their wall
if (p === '/api/my/linebanner' && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const b = await readBody(req);
const target = String(b.targetUrl || '').trim();
if (!/^https?:\/\/[^\s]+$/i.test(target)) return json(res, 400, { error: 'Destination URL must start with http(s)://' });
const fc = await frameCheck(target); // welcome tours frame it full screen
if (!fc.ok) return json(res, 400, { error: fc.reason });
const banner = String(b.bannerUrl || '').trim();
if (banner && !/^(\/uploads\/[a-z0-9]{24}\.(png|jpg|webp|gif)|https:\/\/[^\s]+)$/i.test(banner))
return json(res, 400, { error: 'Banner must be an uploaded image or an https image URL.' });
const r = await accounts.setLineBanner(s.email, banner || null, target);
return json(res, r.error ? 400 : 200, r);
}
// -- welcome tour (gauntlet): meet the 3-level upline, then unlock welcome credits
if (p === '/api/my/gauntlet' && req.method === 'GET') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
if (await ads.welcomeGranted(s.email)) return json(res, 200, { pending: false });
const slides = (await uplineSlides(s.email)).filter(a => a.lineTargetUrl)
.map((a, i) => ({ name: a.username ? '@' + a.username : a.memberId ? 'member #' + a.memberId : 'a member',
bannerUrl: a.lineBannerUrl || null, targetUrl: a.lineTargetUrl }));
if (!slides.length) return json(res, 200, { pending: false });
const token = crypto.randomBytes(16).toString('hex');
gauntletTokens.set(s.email, { token, ts: Date.now(), n: slides.length });
return json(res, 200, { pending: true, slides, dwell: 10, token });
}
if (p === '/api/my/gauntlet/complete' && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const b = await readBody(req);
const t = gauntletTokens.get(s.email);
if (!t || t.token !== String(b.token || '')) return json(res, 400, { error: 'That tour is no longer open. Reload and try again.' });
if (Date.now() - t.ts < t.n * 10 * 1000 - 1500) return json(res, 400, { error: 'Give each site its ten seconds first.' });
gauntletTokens.delete(s.email);
await ads.grantWelcome(s.email);
return json(res, 200, { ok: true, credited: ads.rates().welcomeCredits || 0 });
}
// -- public banner wall
m = /^\/api\/wall\/([A-Za-z0-9_]{1,20})$/.exec(p);
if (m && req.method === 'GET') {
const tok = m[1].toLowerCase();
let a = await accounts.byUsername(tok);
if (!a) a = await accounts.byCode(tok);
if (!a) return json(res, 404, { error: 'No wall under that name.' });
const ownName = a.username ? '@' + a.username : a.memberId ? 'member #' + a.memberId : 'a member';
let bc = 0;
if (a.memberId) { try { bc = (await chain.member(a.memberId)).buyerCount || 0; } catch (e) {} }
const unlocked = wallUnlockedFor(bc);
const offers = parseWallOffers(a);
// uplines with a live banner, in order (an upline with nothing set is skipped, not shown empty)
const ups = (await uplineSlides(a.email, 2)).filter(x => x.lineTargetUrl)
.map(x => ({ name: x.username ? '@' + x.username : x.memberId ? 'member #' + x.memberId : 'a member',
bannerUrl: x.lineBannerUrl || null, targetUrl: x.lineTargetUrl, upline: true }));
const adAds = getAdminWallAds(); let ai = 0;
const houseAd = () => { if (!adAds.length) return null; const ad = adAds[ai++ % adAds.length]; return { name: ad.name || 'InstantAdPay', bannerUrl: ad.bannerUrl || null, targetUrl: ad.targetUrl || 'https://instantadpay.com/', admin: true }; };
const ladder = [{ name: ownName, bannerUrl: a.lineBannerUrl || null, targetUrl: a.lineTargetUrl || null, own: true }];
for (let i = 1; i < 3; i++) {
const o = offers[i - 1];
if (i < unlocked && o && o.targetUrl) { ladder.push({ name: (o.title || ownName), bannerUrl: o.bannerUrl || null, targetUrl: o.targetUrl, own: true }); continue; }
const u = ups.shift(); if (u) { ladder.push(u); continue; }
const h = houseAd(); if (h) ladder.push(h);
}
const finalLadder = ladder.slice(0, 3);
// the wall owner's achievement badge (their highest reached tier)
let badge = null;
if (a.memberId) {
const t = bc >= 5 ? ['nexus', 'Nexus'] : bc >= 2 ? ['circuit', 'Circuit'] : bc >= 1 ? ['surge', 'Surge'] : ['spark', 'Spark'];
badge = { img: '/badges/badge-' + t[0] + '.jpg?v=2', label: t[1] };
}
const joinPath = '/join/' + (a.username || a.code);
let socials = null; try { socials = a.socials ? JSON.parse(a.socials) : null; } catch (e) {}
return json(res, 200, { name: a.username ? '@' + a.username : 'member #' + (a.memberId || 0),
avatarUrl: a.avatarUrl || null, bio: a.bio || null, socials, badge,
joinUrl: joinPath, qrUrl: '/api/qr?d=' + encodeURIComponent('https://instantadpay.com' + joinPath), ladder: finalLadder, unlocked, buyerCount: bc });
}
// -- watch-to-earn video ads: serve one, then reward a server-clock-verified watch
if (p === '/api/my/videos' && req.method === 'GET') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const status = await ads.videoStatus(s.email);
if (status.left <= 0) return json(res, 200, { ad: null, status });
const orientation = String(u.searchParams.get('orientation') || ''); // 'portrait' = Shorts reel, 'landscape' = Watch videos tab
const ad = await ads.serveVideo(Object.assign({ excludeEmail: s.email, orientation }, viewerGeo(req))); // never your own video
if (!ad) return json(res, 200, { ad: null, status });
const token = crypto.randomBytes(16).toString('hex');
videoTokens.set(s.email, { token, ts: Date.now(), id: ad.id, secs: ad.watchSecs });
return json(res, 200, { ad, token, status });
}
if (p === '/api/my/videowatch' && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const b = await readBody(req);
const t = videoTokens.get(s.email);
if (!t || t.token !== String(b.token || '')) return json(res, 400, { error: 'That video is no longer open. Load the next one.' });
const age = Date.now() - t.ts;
if (age < t.secs * 1000 - 600) return json(res, 400, { error: 'Watch the full video first.' });
if (age > t.secs * 1000 + 10 * 60 * 1000) { videoTokens.delete(s.email); return json(res, 400, { error: 'That watch went stale. Load a fresh video.' }); }
videoTokens.delete(s.email); // single use
if (await ads.hasWatchedVideoToday(s.email, t.id)) // once-per-day-per-video: no double earning
return json(res, 200, { ok: true, credited: 0, status: await ads.videoStatus(s.email), already: true });
const tier = await ads.chargeVideoView(t.id); // charge advertiser; null if it ran dry
if (!tier) return json(res, 200, { ok: true, credited: 0, status: await ads.videoStatus(s.email), gone: true });
await ads.addEarned(s.email, tier.reward);
await ads.markVideoSeen(s.email, t.id);
const status = await ads.recordVideoWatch(s.email);
return json(res, 200, { ok: true, credited: tier.reward, status });
}
// -- verified visits: view a member's site (new tab) for the dwell, pass a
// human check, and it counts as one guaranteed unique visit for the pack
if (p === '/api/my/visits' && req.method === 'GET') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const status = await ads.visitStatus(s.email);
if (status.count >= status.cap) return json(res, 200, { ad: null, status });
const ad = await ads.serveVisit(s.email, viewerGeo(req));
if (!ad) return json(res, 200, { ad: null, status });
const token = crypto.randomBytes(16).toString('hex');
visitTokens.set(s.email, { token, ts: Date.now(), id: ad.id });
return json(res, 200, { ad, token, status });
}
if (p === '/api/my/visitchallenge' && req.method === 'GET') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const t = visitTokens.get(s.email);
if (!t || t.token !== String(u.searchParams.get('token') || '')) return json(res, 400, { error: 'That visit is no longer open.' });
const dwellMs = (ads.rates().visitDwellSeconds || 8) * 1000;
const age = Date.now() - t.ts;
if (age < dwellMs - 400) return json(res, 200, { early: true, wait: Math.ceil((dwellMs - age) / 1000) });
if (age > 5 * 60 * 1000) { visitTokens.delete(s.email); return json(res, 400, { error: 'That visit went stale. Load a fresh one.' }); }
const pick = CAPTCHA.slice().sort(() => Math.random() - 0.5).slice(0, 5);
const answer = Math.floor(Math.random() * pick.length);
t.challenge = { answer };
return json(res, 200, { prompt: pick[answer][1], options: pick.map(x => x[0]) });
}
if (p === '/api/my/visitdone' && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const b = await readBody(req);
const t = visitTokens.get(s.email);
if (!t || t.token !== String(b.token || '')) return json(res, 400, { error: 'That visit did not check out. Load the next one.' });
const dwellMs = (ads.rates().visitDwellSeconds || 8) * 1000;
if (Date.now() - t.ts < dwellMs - 400) return json(res, 400, { error: 'Give the site the full visit first.' });
if (!t.challenge) return json(res, 400, { error: 'Finish the quick check first.', retry: true });
if (Number(b.answer) !== t.challenge.answer) { t.challenge = null; return json(res, 400, { error: 'Wrong pick.', retry: true }); }
visitTokens.delete(s.email);
const r = await ads.completeVisit(s.email, t.id);
if (r.error) return json(res, 400, r);
return json(res, 200, Object.assign(r, { status: await ads.visitStatus(s.email) }));
}
// -- onsite solo ads: member inbox with read rewards
if (p === '/api/my/inbox' && req.method === 'GET') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const r = await ads.inboxList(s.email, viewerGeo(req));
const names = await accounts.namesForMembers([...new Set(r.items.map(i => i.fromMemberId).filter(Boolean))]);
for (const i of r.items) i.fromName = (i.fromMemberId && names[i.fromMemberId]) ? '@' + names[i.fromMemberId]
: i.fromMemberId ? 'member #' + i.fromMemberId : 'a member';
return json(res, 200, r);
}
m = /^\/api\/my\/inbox\/(\d+)$/.exec(p);
if (m && req.method === 'GET') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const r = await ads.inboxOpen(s.email, m[1]);
if (!r.error) {
const names = r.fromMemberId ? await accounts.namesForMembers([r.fromMemberId]) : {};
r.fromName = (r.fromMemberId && names[r.fromMemberId]) ? '@' + names[r.fromMemberId]
: r.fromMemberId ? 'member #' + r.fromMemberId : 'a member';
}
return json(res, r.error ? 404 : 200, r);
}
// media upload for solo ads: raw body, size-capped, magic-byte verified
if (p === '/api/my/upload' && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
return handleUpload(req, res, s.email);
}
m = /^\/api\/my\/inbox\/(\d+)\/visit$/.exec(p);
if (m && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const r = await ads.markSoloVisit(s.email, m[1]);
return json(res, r.error ? 400 : 200, r);
}
m = /^\/api\/my\/inbox\/(\d+)\/claim$/.exec(p);
if (m && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const r = await ads.claimSoloRead(s.email, m[1]);
return json(res, r.error ? 400 : 200, r);
}
if (p === '/api/my/activity' && req.method === 'GET') {
const s = await auth.fromRequest(req);
if (!s) return json(res, 401, { error: 'Sign in first.' });
const id = s.memberId || await auth.refreshMemberId(s);
if (!id) return json(res, 200, { memberId: 0, earnings: [], purchases: [], referrals: [] });
const evs = chain.recentEvents(600);
return json(res, 200, {
memberId: id,
earnings: await attachNames(evs.filter(e => (e.type === 'TierPaid' && e.recipientId === id) || (e.type === 'AwardPaid' && e.toId === id))),
purchases: await attachNames(evs.filter(e => e.type === 'Purchase' && e.buyerId === id)),
referrals: await attachNames(evs.filter(e => (e.type === 'MemberActivated' && e.sponsorId === id) || (e.type === 'BuyerCounted' && e.sponsorId === id)))
});
}
// -- ad engine (spec §8b v1: banners, text, login ads)
if (p === '/api/ads/slot' && req.method === 'GET') {
const t = String(u.searchParams.get('type') || 'banner');
const ad = await ads.serve(t, Object.assign({ width: Number(u.searchParams.get('w')) || 0, height: Number(u.searchParams.get('h')) || 0 }, viewerGeo(req)));
// login ads: the member clicks "Open Ad" (a real, counted click into a
// new tab) while the countdown runs on our interstitial page
if (ad && t === 'login') ad.dwell = ads.rates().loginDwellSeconds || 10;
return json(res, 200, { ad });
}
m = /^\/api\/ads\/click\/(\d+)$/.exec(p);
if (m && req.method === 'GET') {
const target = await ads.click(m[1]);
if (!target) { res.writeHead(404, baseHeaders()); return res.end(); }
coach.recordClick(m[1], req.headers.referer); // where the click happened
res.writeHead(302, baseHeaders({ Location: target }));
return res.end();
}
if (p === '/api/my/campaigns' && req.method === 'GET') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const memberId = await auth.refreshMemberId(s);
const out = { campaigns: await ads.listCampaigns(s.email), rates: ads.rates(), bannerSizes: ads.bannerSizes() };
out.clickSources = await coach.clickSources(out.campaigns.map(c => c.id));
out.hours = await ads.hoursFor(out.campaigns.map(c => c.id)); // on-site views per UTC hour, last 7 days
out.geo = await ads.geoFor(out.campaigns.map(c => c.id)); // on-site serves per viewer country
{ const t = geo.tierLists(siteConfig()); out.tiers = { t1: [...t.t1], t2: [...t.t2] }; out.geoReady = geo.status().loaded; }
const pool = await ads.balances((await myMemberIds(s)).ids, s.email);
out.purchasedCredits = pool.total;
out.largestPosition = pool.best.avail; // a single campaign budget has to fit one position
out.positionCount = pool.per.length;
out.creditedCredits = pool.credited; // refunded/credited purchased money, free (counts inside purchasedCredits)
out.earnedCredits = pool.earned; // earned pool minus what live campaigns already hold
out.earnedReserved = pool.earnedReserved;
out.inCampaigns = pool.inCampaigns; // budget still to deliver across live campaigns
out.availableCredits = pool.available;
return json(res, 200, out);
}
if (p === '/api/my/campaigns' && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const memberId = await auth.refreshMemberId(s); // 0 is fine: earned credits fund banner/text
const b = await readBody(req);
if (!['login', 'solo', 'video', 'featured'].includes(String(b.type || ''))) { // banner/text surf views frame the target; login/video/solo/featured open in a new tab or play in our own player
const fc = await frameCheck(b.targetUrl);
if (!fc.ok) return json(res, 400, { error: fc.reason });
}
// charge the best-funded of the member's positions (main + Qualified Start
// wallets). A campaign burns from one member id, so the budget must fit inside it.
const ids = (await myMemberIds(s)).ids;
const pool = await ads.balances(ids, s.email);
const fundId = pool.best.memberId || memberId;
const earnedNow = String(b.type) !== 'login' ? pool.earned : 0;
const budget = Math.floor(Number(b.budget) || 0);
if (pool.per.length > 1 && budget > pool.best.avail + pool.credited + earnedNow && budget <= pool.total + earnedNow)
return json(res, 400, { error: 'Your credits are spread across ' + pool.per.length + ' positions and one campaign spends from one of them. The largest single position holds ' + pool.best.avail + ' credits: set the budget to that or less, or run two campaigns.' });
const r = await ads.createCampaign(s.email, fundId, b, ids);
return json(res, r.error ? 400 : 200, r);
}
m = /^\/api\/my\/campaigns\/(\d+)\/topup$/.exec(p);
if (m && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const memberId = await auth.refreshMemberId(s);
const b = await readBody(req);
const r = await ads.topUpCampaign(s.email, memberId, m[1], b.credits);
return json(res, r.error ? 400 : 200, r);
}
m = /^\/api\/my\/campaigns\/(\d+)\/(pause|resume)$/.exec(p);
if (m && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const r = await ads.setStatus(s.email, m[1], m[2] === 'pause' ? 'paused' : 'active');
return json(res, r.error ? 400 : 200, r);
}
// -- admin portal: email magic-code sign-in, allowlisted to ADMIN_EMAIL
if (p === '/api/admin/auth/start' && req.method === 'POST') {
const b = await readBody(req);
const e = String(b.email || '').trim().toLowerCase();
if (!ADMIN_EMAIL) return json(res, 503, { error: 'ADMIN_EMAIL is not set on the server.' });
if (!e || e !== ADMIN_EMAIL) return json(res, 403, { error: 'That address is not the admin.' });
const k = 'admin:' + e;
const prev = emailCodes.get(k);
if (prev && Date.now() < prev.nextAt) return json(res, 429, { error: 'Code already sent. Give it a minute, then try again.' });
const code = String(Math.floor(100000 + Math.random() * 900000));
emailCodes.set(k, { code, exp: Date.now() + 15 * 60 * 1000, tries: 0, nextAt: Date.now() + 60 * 1000 });
if (mailer.hasKey()) {
try { await mailer.sendCode(e, code); } catch (err) {
console.error('admin sendCode failed', err.message);
return json(res, 502, { error: 'Could not send the email. Try again in a minute.' });
}
return json(res, 200, { ok: true, sent: true });
}
if (!IS_PROD) return json(res, 200, { ok: true, sent: false, devCode: code });
return json(res, 503, { error: 'Email sign-in is not configured yet.' });
}
if (p === '/api/admin/auth/verify' && req.method === 'POST') {
const b = await readBody(req);
const e = String(b.email || '').trim().toLowerCase();
const k = 'admin:' + e;
const rec = emailCodes.get(k);
if (!rec || rec.exp < Date.now()) return json(res, 400, { error: 'Code expired. Request a fresh one.' });
rec.tries += 1;
if (rec.tries > 6) { emailCodes.delete(k); return json(res, 400, { error: 'Too many tries. Request a fresh code.' }); }
if (String(b.code || '').trim() !== rec.code) return json(res, 400, { error: 'That code does not match.' });
emailCodes.delete(k);
if (e !== ADMIN_EMAIL) return json(res, 403, { error: 'That address is not the admin.' });
const token = mintAdminSession(e);
return json(res, 200, { ok: true, email: e }, { 'Set-Cookie': adminCookie(token) });
}
if (p === '/api/admin/auth/logout' && req.method === 'POST') {
dropAdminSession(req);
return json(res, 200, { ok: true }, { 'Set-Cookie': clearAdminCookie() });
}
if (p === '/api/admin/me' && req.method === 'GET') {
if (!isAdmin(req)) return json(res, 200, { admin: false });
return json(res, 200, { admin: true, email: ADMIN_EMAIL });
}
if (p === '/api/admin/overview' && req.method === 'GET') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
const camps = await ads.adminList();
const byStatus = {}, byType = {};
for (const c of camps) { byStatus[c.status] = (byStatus[c.status] || 0) + 1; byType[c.type] = (byType[c.type] || 0) + 1; }
let memberCount = null; try { memberCount = await chain.memberCount(); } catch (e) {}
const cc = chain.getConfig();
return json(res, 200, { accounts: await accounts.count(), memberCount, campaigns: camps.length,
house: camps.filter(c => c.house).length, byStatus, byType,
openReports: await reports.openCount(), pendingBurns: (await ads.pendingBurns()).length,
followups: await drip.stats(),
chain: { contract: cc.contract, chainId: cc.chainId, chainName: cc.chainName, explorer: cc.explorer },
site: siteConfig(), rates: ads.rates() });
}
if (p === '/api/admin/members' && req.method === 'GET') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
const members = await accounts.listAll(500);
// resolve each sponsor token (username, share code or member #) to the sponsor's name
const byTok = {};
for (const m of members) for (const t of [m.username, m.code, m.memberId ? String(m.memberId) : null]) if (t) byTok[String(t).toLowerCase()] = m;
for (const m of members) {
const t = String(m.sponsorRef || '').toLowerCase();
const sp = t ? byTok[t] : null;
m.sponsorName = sp ? (sp.username ? '@' + sp.username : (sp.memberId ? 'member #' + sp.memberId : sp.email)) : null;
m.sponsorVia = sp ? (t === String(sp.username || '').toLowerCase() ? 'username' : t === String(sp.code || '').toLowerCase() ? 'code' : 'member #') : (t ? 'unresolved' : '');
}
for (const m of members) { try { const ps = await accounts.positions(m.email); m.positions = ps.length; m.positionIds = ps.map(p => p.memberId).filter(Boolean); } catch (e) { m.positions = 0; } }
return json(res, 200, { members });
}
if (p === '/api/admin/members' && req.method === 'PATCH') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
const b = await readBody(req);
if (!b.email) return json(res, 400, { error: 'Which member?' });
const r = await accounts.setSponsorRef(b.email, b.sponsorRef);
return json(res, r.error ? 400 : 200, r);
}
if (p === '/api/admin/campaigns' && req.method === 'GET') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
return json(res, 200, { campaigns: await ads.adminList(), rates: ads.rates(), bannerSizes: ads.bannerSizes(), houseOwner: ads.HOUSE_OWNER });
}
if (p === '/api/admin/campaigns' && req.method === 'POST') { // free house ad
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
const b = await readBody(req);
if (!['login', 'solo', 'video', 'featured'].includes(String(b.type || ''))) {
const fc = await frameCheck(b.targetUrl);
if (!fc.ok) return json(res, 400, { error: fc.reason });
}
const r = await ads.createHouseCampaign(b);
return json(res, r.error ? 400 : 200, r);
}
m = /^\/api\/admin\/campaigns\/(\d+)\/(pause|resume)$/.exec(p);
if (m && req.method === 'POST') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
const r = await ads.adminSetStatus(m[1], m[2] === 'pause' ? 'paused' : 'active');
return json(res, r.error ? 400 : 200, r);
}
if (p === '/api/admin/reports' && req.method === 'GET') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
return json(res, 200, { reports: await reports.list(200) });
}
m = /^\/api\/admin\/reports\/(\d+)\/resolve$/.exec(p);
if (m && req.method === 'POST') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
return json(res, 200, await reports.resolve(m[1]));
}
if (p === '/api/admin/upload' && req.method === 'POST') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
return handleUpload(req, res, 'admin');
}
if (p === '/api/admin/rates' && req.method === 'GET') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
return json(res, 200, { rates: ads.rates() });
}
if (p === '/api/admin/drip' && req.method === 'GET') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
return json(res, 200, { sequence: drip.sequence(), defaults: drip.DEFAULT_SEQUENCE, stats: await drip.stats(), mailReady: mailer.hasKey() });
}
if (p === '/api/admin/drip' && req.method === 'PATCH') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
const b = await readBody(req);
const r = b.reset ? drip.resetSequence() : drip.setSequence(b.sequence);
return json(res, r.error ? 400 : 200, r);
}
if (p === '/api/admin/drip/test' && req.method === 'POST') { // send one step to the admin inbox
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
const b = await readBody(req);
if (!ADMIN_EMAIL) return json(res, 400, { error: 'ADMIN_EMAIL is not set.' });
if (!mailer.hasKey()) return json(res, 400, { error: 'No mail key on the server.' });
try { const r = await drip.sendStep(ADMIN_EMAIL, Number(b.step) || 0, ADMIN_EMAIL); return json(res, r.error ? 400 : 200, r); }
catch (e) { return json(res, 502, { error: 'Send failed: ' + e.message }); }
}
// wall fallback ads: shown in wall positions a member has not earned or filled, when no upline banner exists
if (p === '/api/admin/wall-ads' && req.method === 'GET') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
let saved = null; try { saved = JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'admin-wall-ads.json'), 'utf8')); } catch (e) {}
return json(res, 200, { ads: Array.isArray(saved) ? saved : [], defaults: getAdminWallAds(), usingDefaults: !Array.isArray(saved) || !saved.length });
}
if (p === '/api/admin/wall-ads' && req.method === 'PATCH') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
const b = await readBody(req);
const src = Array.isArray(b.ads) ? b.ads.slice(0, 20) : [];
const out = [];
for (const o of src) {
const name = String((o && o.name) || '').trim().slice(0, 60);
const targetUrl = String((o && o.targetUrl) || '').trim();
const bannerUrl = String((o && o.bannerUrl) || '').trim();
if (!targetUrl) continue;
if (!/^https:\/\/[^\s]+$/i.test(targetUrl)) return json(res, 400, { error: 'Every wall ad needs an https:// link (' + (name || targetUrl) + ').' });
if (bannerUrl && !/^(\/uploads\/[a-z0-9]{24}\.(png|jpg|webp|gif)|https:\/\/[^\s]+)$/i.test(bannerUrl)) return json(res, 400, { error: 'Banner must be an uploaded image or an https image URL (' + (name || targetUrl) + ').' });
out.push({ name: name || 'InstantAdPay', targetUrl, bannerUrl: bannerUrl || null });
}
const file = path.join(DATA_DIR, 'admin-wall-ads.json');
if (out.length) fs.writeFileSync(file, JSON.stringify(out, null, 2)); else { try { fs.unlinkSync(file); } catch (e) {} }
return json(res, 200, { ok: true, ads: out, usingDefaults: !out.length });
}
if (p === '/api/admin/site' && req.method === 'GET') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
return json(res, 200, { site: siteConfig() });
}
// -- profit and loss from the chain index: volume, platform fees, member payouts,
// pass-ups, per period (by block: ~43,200 Polygon blocks a day), plus the fee
// wallets' live balances and an admin-entered fixed monthly cost
if (p === '/api/admin/pnl' && req.method === 'GET') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
const days = Math.max(0, Number(u.searchParams.get('days') || 30));
let latest = 0; try { latest = parseInt(await chain.rpc('eth_blockNumber', []), 16); } catch (e) {}
const fromBlock = days ? latest - Math.round(days * 43200) : 0;
const evs = chain.recentEvents(1e9).filter(e => !days || e.block >= fromBlock);
const sum = (list, f) => list.reduce((n, e) => n + BigInt(f(e) || '0'), 0n);
const purchases = evs.filter(e => e.type === 'Purchase');
const tier = evs.filter(e => e.type === 'TierPaid');
const admin = evs.filter(e => e.type === 'AdminPaid');
const passed = evs.filter(e => e.type === 'PassedUp');
const byTier = {};
for (const t of [1, 2, 3]) byTier[t] = sum(tier.filter(e => e.tier === t), e => e.amountWei).toString();
const byPkg = {};
for (const e of purchases) { const k = '$' + Math.round(e.priceCents / 100); byPkg[k] = (byPkg[k] || 0) + 1; }
let polUsd = 0; try { const cat = await chain.catalog(); const pk = (cat.products || cat).find(x => x.costWei); if (pk) polUsd = (pk.priceCents / 100) / (Number(BigInt(pk.costWei)) / 1e18); } catch (e) {}
const wallets = { feeA: '0x7627fc78876948ac9d95c1c9eb061e7d6d647b70', feeB: '0x8b7d33849a2c4d92c985be31e46dc564ba901ad2', engine: burner.status().address || null };
const balances = {};
for (const [k, a] of Object.entries(wallets)) { if (!a) continue; try { balances[k] = BigInt(await chain.rpc('eth_getBalance', [a, 'latest'])).toString(); } catch (e) { balances[k] = null; } }
return json(res, 200, { days, fromBlock, latest, polUsd,
purchases: { count: purchases.length, volumeWei: sum(purchases, e => e.paidWei).toString(), usdCents: purchases.reduce((n, e) => n + (e.priceCents || 0), 0), byPackage: byPkg },
platformWei: sum(admin, e => e.amountWei).toString(), memberPayoutsWei: sum(tier, e => e.amountWei).toString(), byTier,
passedUp: { count: passed.length, unqualified: passed.filter(e => e.reason === 'unqualified').length, sendFailed: passed.filter(e => e.reason === 'send-failed').length },
wallets, balances, fixedMonthlyUsd: Number(siteConfig().pnlFixedMonthlyUsd) || 0, burner: burner.status() });
}
if (p === '/api/admin/burner' && req.method === 'GET') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
return json(res, 200, burner.status());
}
if (p === '/api/admin/burner/run' && req.method === 'POST') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
return json(res, 200, await burner.tick());
}
// -- admin (Bearer ADMIN_PASSWORD, or the /admin portal session)
if (p === '/api/admin/burns' && req.method === 'GET') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
return json(res, 200, { pending: await ads.pendingBurns() });
}
if (p === '/api/admin/burns/mark' && req.method === 'POST') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
const b = await readBody(req);
const r = await ads.markBurned(b.id, b.tx);
return json(res, r.error ? 400 : 200, r);
}
if (p === '/api/admin/rates' && req.method === 'PATCH') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
const b = await readBody(req);
return json(res, 200, { ok: true, rates: ads.setRates(b) });
}
if (p === '/api/admin/site' && req.method === 'PATCH') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
const b = await readBody(req);
const cur = siteConfig();
fs.writeFileSync(SITE_FILE, JSON.stringify(Object.assign(cur, b), null, 2));
return json(res, 200, { ok: true, site: siteConfig() });
}
if (p === '/api/admin/chain' && req.method === 'PATCH') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
const b = await readBody(req);
const file = path.join(DATA_DIR, 'config.json');
let cur = {}; try { cur = JSON.parse(fs.readFileSync(file, 'utf8')); } catch (e) {}
fs.writeFileSync(file, JSON.stringify(Object.assign(cur, b), null, 2));
chain.reloadConfig();
return json(res, 200, { ok: true, config: chain.getConfig() });
}
// -- pages (HEAD answered like GET so link previewers and crawlers see 200; Node drops the body)
if (req.method === 'GET' || req.method === 'HEAD') {
if (p === '/') return sendFile(res, path.join(PUBLIC_DIR, 'index.html'));
if (p === '/ledger') return sendFile(res, path.join(PUBLIC_DIR, 'ledger.html'));
if (p === '/contract') return sendFile(res, path.join(PUBLIC_DIR, 'contract.html'));
if (p === '/terms') return sendFile(res, path.join(PUBLIC_DIR, 'terms.html'));
if (p === '/privacy') return sendFile(res, path.join(PUBLIC_DIR, 'privacy.html'));
if (p === '/disclaimer') return sendFile(res, path.join(PUBLIC_DIR, 'disclaimer.html'));
if (p === '/my') return sendFile(res, path.join(PUBLIC_DIR, 'my.html'));
if (p === '/admin') return sendFile(res, path.join(PUBLIC_DIR, 'admin.html'));
if (p === '/shorts') return sendFile(res, path.join(PUBLIC_DIR, 'shorts.html'));
if (p === '/plays') return sendFile(res, path.join(PUBLIC_DIR, 'plays.html'));
if (p === '/wallets') return sendFile(res, path.join(PUBLIC_DIR, 'wallets.html'));
if (p === '/launch') return sendFile(res, path.join(PUBLIC_DIR, 'launch.html'));
if (/^\/view\/[a-f0-9]{32}$/.test(p)) return sendFile(res, path.join(PUBLIC_DIR, 'view.html'));
m = /^\/uploads\/([a-z0-9]{24}\.(?:png|jpg|webp|gif|mp4|webm))$/.exec(p);
if (m) return sendFile(res, path.join(UPLOADS_DIR, m[1]));
if (/^\/tx\/0x[0-9a-fA-F]{64}$/.test(p)) return sendFile(res, path.join(PUBLIC_DIR, 'tx.html'));
m = /^\/wall\/([A-Za-z0-9_]{1,20})$/.exec(p);
if (m) { // server-inject per-member OG tags so shared bio links preview correctly (crawlers don't run JS)
try {
const tok = m[1].toLowerCase();
let a = await accounts.byUsername(tok); if (!a) a = await accounts.byCode(tok);
let html = fs.readFileSync(path.join(PUBLIC_DIR, 'wall.html'), 'utf8');
if (a) {
const nm = a.username ? '@' + a.username : 'member #' + (a.memberId || 0);
const esc = t => String(t || '').replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
const title = nm + ' on InstantAdPay';
const desc = a.bio ? esc(a.bio).slice(0, 200) : 'Join ' + nm + '’s line on InstantAdPay — instant on-chain ad payouts, free to join.';
const img = a.avatarUrl && /^https:/.test(a.avatarUrl) ? a.avatarUrl : 'https://instantadpay.com/banners/iap-hero-1200x630.png';
const url = 'https://instantadpay.com/wall/' + (a.username || a.code);
const og = [
'<meta property="og:type" content="profile">',
'<meta property="og:site_name" content="InstantAdPay">',
'<meta property="og:url" content="' + url + '">',
'<meta property="og:title" content="' + esc(title) + '">',
'<meta property="og:description" content="' + desc + '">',
'<meta property="og:image" content="' + esc(img) + '">',
'<meta name="twitter:card" content="summary_large_image">',
'<meta name="twitter:title" content="' + esc(title) + '">',
'<meta name="twitter:description" content="' + desc + '">',
'<meta name="twitter:image" content="' + esc(img) + '">',
'<meta name="description" content="' + desc + '">',
'<link rel="canonical" href="' + url + '">'
].join('\n');
html = html.replace('<title>Banner wall | InstantAdPay</title>', '<title>' + esc(title) + '</title>').replace('<!--OG-->', og);
}
res.writeHead(200, baseHeaders({ 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-cache' }));
return res.end(html);
} catch (e) { return sendFile(res, path.join(PUBLIC_DIR, 'wall.html')); }
}
const safe = path.normalize(p).replace(/^([.\\/])+/, '');
const file = path.join(PUBLIC_DIR, safe);
if (file.startsWith(PUBLIC_DIR) && fs.existsSync(file) && fs.statSync(file).isFile()) return sendFile(res, file);
}
res.writeHead(404, baseHeaders({ 'Content-Type': 'text/plain' }));
res.end('Not found');
} catch (e) {
console.error('request error', req.url, e.message);
try { json(res, 500, { error: 'server error' }); } catch (_) {}
}
});
boot().then(() => {
server.listen(PORT, () => console.log(`InstantAdPay site on :${PORT} — chain: ${chain.getConfig().chainName} — store: ${db.enabled() ? 'MySQL' : 'volume JSON'}`));
}).catch(e => { console.error('boot failed:', e.message); process.exit(1); });