// 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 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 = {
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 = 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, '&').replace(/"/g, '"').replace(/'
+ ''
+ '' // keeps ?v= so shares stay on the angle
+ ''
+ '';
html = html.replace(/
[^<]*<\/title>/, '' + escA(title) + ' | InstantAdPay' + og);
if (ang) html = html.replace('', ''); // 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' });
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/',
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
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/
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
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 ba = await accounts.byMemberId(ev.buyerId);
const bn = ba && ba.username ? '@' + ba.username : 'One of your referrals';
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, 'Your referral just bought an ad package',
bn + ' 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)
async function telegramOnEvent(ev) {
const sc = siteConfig();
if (!sc.telegramBotToken || !sc.telegramChatId || !ev) return;
const mode = String(sc.telegramEvents || 'payouts');
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;
let line = null;
if (ev.type === 'TierPaid') line = '\u{1F4B8} Level ' + ev.tier + ' payout: ' + weiToPol(ev.amountWei) + ' POL \u2192 ' + who(ev.recipientId);
else if (ev.type === 'BuyerCounted' && mode !== 'payouts') line = '\u2B50 ' + who(ev.sponsorId) + ' now has ' + ev.newCount + ' 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 === 'MemberActivated' && mode === 'all') line = '\u{1F91D} ' + who(ev.id) + ' switched on payouts';
if (!line) return;
const text = line + ' \u00b7 verify' + (sc.telegramCtaUrl ? '\nJoin free' : '');
await telegramSend(sc.telegramChatId, text, sc.telegramTopicId);
}
// 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;
// -- join links: /join/ β 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= 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);
}
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('InstantAdPay