// 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 fs = require('fs'); const path = require('path'); 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 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 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(); async function boot() { await db.init({ dataDir: DATA_DIR }); // no-op without DATABASE_URL (JSON mode) chain.init({ onEvent: ev => 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 }); 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); } 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' }; const CSP = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; connect-src 'self'; font-src 'self' data: https://fonts.gstatic.com; form-action 'self'"; 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 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; } // 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); const acct = await accounts.byCode(t); 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,16})$/.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: 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') { // receipt relay so the browser never talks to the RPC directly (CSP stays 'self') try { const r = await chain.rpc('eth_getTransactionReceipt', [m[1]]); return json(res, 200, r ? { found: true, status: r.status, blockNumber: r.blockNumber } : { found: false }); } 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']); const out = { signedIn: true, email: s.email || (acct && acct.email) || null, address: s.address || (acct && acct.address) || null, memberId, 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; const out = { memberId, email: s.email || (acct && acct.email) || null, address: s.address || (acct && acct.address) || null, refCode: (acct && acct.code) || null, credits: 0, buyerCount: 0, earnedWei: '0', earnCount: 0, referrals: [] }; 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 => ({ email: r.email.replace(/^(.).*(@.*)$/, '$1***$2'), // privacy mask joined: r.created, status: r.address ? 'wallet linked' : 'joined free' })); return json(res, 200, out); } 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: evs.filter(e => (e.type === 'TierPaid' && e.recipientId === id) || (e.type === 'AwardPaid' && e.toId === id)), purchases: evs.filter(e => e.type === 'Purchase' && e.buyerId === id), referrals: 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 ad = await ads.serve(String(u.searchParams.get('type') || 'banner')); 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() }; out.availableCredits = memberId ? await ads.availableCredits(memberId) : 0; 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); if (!memberId) return json(res, 400, { error: 'Buy an ad package first. Campaigns spend the on-chain credits it mints.' }); const b = await readBody(req); const r = await ads.createCampaign(s.email, memberId, b); 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')); 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); });