// 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 { 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'); let QR = null; try { QR = require('qrcode'); } catch (e) { /* optional */ } const chatbot = require('./chatbot'); 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 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 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; } 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(); // 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') + ')' })); }); }); } 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)) }); 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 }); 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); // 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 }, 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' }; const CSP = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; media-src 'self' https: blob:; connect-src 'self'; font-src 'self' data: https://fonts.gstatic.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', 'Cache-Control': ext === '.html' ? 'no-cache' : '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 || ''; return h === 'Bearer ' + ADMIN_PASSWORD; } // 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. async function nudgeReferrer(ref) { try { const t = String(ref || '').trim().toLowerCase(); if (!t || /^\d+$/.test(t) || !mailer.hasKey()) return; const owner = await accounts.byCode(t); if (!owner || !owner.email || owner.address) return; // already activated-ready mailer.send(owner.email, 'Someone just joined through your InstantAdPay link', 'Good news: a new member just signed up through your share link.\n\n' + 'One thing to do so you never miss a payment: sign in and switch on payouts ' + '(one free wallet step). The contract locks each buyer to their sponsor at their ' + 'first purchase, so have payouts on before your people start buying.\n\n' + 'https://instantadpay.com/my\n\nInstantAdPay').catch(e => console.error('nudge failed', e.message)); } catch (e) {} } // ---- 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/ โ€” first-touch cookie. // 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') { const tok = m[1].toLowerCase(); const cookies = parseCookies(req); const headers = { Location: '/' }; if (!cookies['iap.sponsor']) { headers['Set-Cookie'] = `iap.sponsor=${tok}; Path=/; SameSite=Lax; Max-Age=${180 * 24 * 3600}${IS_PROD ? '; Secure' : ''}`; } res.writeHead(302, baseHeaders(headers)); return res.end(); } // -- public API if (p === '/api/config' && req.method === 'GET') { const c = chain.getConfig(); 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 }, siteConfig())); } if (p === '/api/catalog' && req.method === 'GET') { return json(res, 200, { products: await chain.catalog() }); } 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/ 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') { const tok = parseCookies(req)['iap.sponsor'] || ''; const sponsorId = await resolveSponsorToken(tok); return json(res, 200, { ref: tok, sponsorId, invited: !!tok }); } 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'] || ''; // first-touch attribution const r = await accounts.signup(b.email, b.password, ref); if (r.error) return json(res, 400, r); nudgeReferrer(ref).catch(() => {}); 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) 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(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 r = await accounts.ensure(e, ref); // first touch wins; existing accounts unchanged if (r.error) return json(res, 400, r); if (r.created) nudgeReferrer(ref).catch(() => {}); 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 }, { 'Set-Cookie': auth.sessionCookie(token) }); } // -- wallet auth: link-to-account when an email session exists, or // wallet-first sign-in for crypto-native users 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) { 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); 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/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; const sponsorId = await resolveSponsorToken((acct && acct.sponsorRef) || parseCookies(req)['iap.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 }; if (memberId) { try { const mm = await chain.member(memberId); out.buyerCount = mm.buyerCount; out.onchainSponsorId = mm.sponsorId; out.credits = await chain.creditBalance(memberId, 0); } 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(() => {}); const out = { memberId, 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) out.welcomeCredits = await ads.grantWelcome(out.email); else { out.welcomeCredits = 0; out.gauntletPending = true; } } if (out.email) out.inboxUnread = await ads.unreadCount(out.email); // delivers pending solos too // achievement milestones (same ladder as the Overview stepper) + one-time credit bonuses { 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); } 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; out.credits = await ads.availableCredits(memberId); } 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; } // who joined through this member (code and, once on-chain, numeric id) const refs = []; if (acct && acct.code) refs.push(acct.code); 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' })); return json(res, 200, out); } 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 r = await accounts.setUsername(s.email, b.username); 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', { excludeEmail: s.email }); 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 || 5 }); } // 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 || 5) * 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 || 5) * 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); 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 })) })); 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])); } // -- featured rotation: the live featured links + dilution stats if (p === '/api/featured' && req.method === 'GET') { const items = await ads.serveFeatured(); 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: '#0b1512', light: '#eef7f3' } }); 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); const r = await accounts.setProfile(s.email, avatar, bio); 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 ladder = [a, ...await uplineSlides(a.email, 2)].slice(0, 3) .map(x => ({ name: x.username ? '@' + x.username : x.memberId ? 'member #' + x.memberId : 'a member', bannerUrl: x.lineBannerUrl || null, targetUrl: x.lineTargetUrl || null })); const joinPath = '/join/' + (a.username || a.code); return json(res, 200, { name: a.username ? '@' + a.username : 'member #' + (a.memberId || 0), avatarUrl: a.avatarUrl || null, bio: a.bio || null, joinUrl: joinPath, qrUrl: '/api/qr?d=' + encodeURIComponent('https://instantadpay.com' + joinPath), ladder }); } // -- 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 ad = await ads.serveVideo({ excludeEmail: s.email }); // 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 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); 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); 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); 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.' }); 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 = s.email + ':' + 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]; fs.writeFileSync(path.join(UPLOADS_DIR, name), buf); uploadCounts.set(key, (uploadCounts.get(key) || 0) + 1); return json(res, 200, { url: '/uploads/' + name, type: isVideo ? 'video' : 'image' }); } 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); // 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(); } 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.purchasedCredits = memberId ? await ads.availableCredits(memberId) : 0; out.earnedCredits = await ads.earnedBalance(s.email); out.availableCredits = out.purchasedCredits + out.earnedCredits; 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 }); } const r = await ads.createCampaign(s.email, memberId, b); 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 (Bearer ADMIN_PASSWORD) 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 if (req.method === 'GET') { 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 === '/my') return sendFile(res, path.join(PUBLIC_DIR, 'my.html')); if (p === '/shorts') return sendFile(res, path.join(PUBLIC_DIR, 'shorts.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')); if (/^\/wall\/[A-Za-z0-9_]{1,20}$/.test(p)) 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); });