Anti-fraud: one account per person enforced at sign-up (device cookie + IP), flags, admin duplicate signals, suspend switch

Marty, 2026-09-16, after @megamol created megamol2/megamol3 under his own link and bought $20 on each
to fake his two qualifying buyers. fraud.js records sign-up IP/UA/browser id (iap.dev cookie set with
the code request) and last-seen on sign-in. New accounts: dup-device (browser already has an account)
and sponsor-device are refused, ip-burst (> fraudMaxSignupsPerIpDay, default 2, per 24h) is refused;
sponsor-ip and shared-ip are flagged only. Flagged/suspended accounts never count on the leaderboard
and cannot adopt from the tank; suspended accounts are signed out everywhere (auth.fromRequest
wrapper) and refused at sign-in. Admin > Members: Duplicate signals card (shared browser / IP,
flagged, suspended), flags badge, Suspend/Unsuspend; GET /api/admin/fraud; PATCH members {suspend,
reason, flags}. Telegram admin alert on every block/flag. Privacy page + chatbot prompt updated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-16 14:39:13 -05:00
parent 125be174e3
commit 34c62b9d3a
7 changed files with 224 additions and 9 deletions
+44 -3
View File
@@ -23,6 +23,7 @@ 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 fraud = require('./fraud');
const coach = require('./coach'); // coaching view, nudges, digest, prospects, link stats
const tank = require('./tank'); // holding tank: unsponsored free members, adoptions, pay-it-forward
const legacy = require('./legacy'); // Faucet Wave / Tier One Ads bridge: welcome-back credits for listed emails
@@ -401,6 +402,11 @@ async function boot() {
snapshot.init({ dataDir: DATA_DIR, db, chain, siteConfig, send: async (c, t, th) => { await telegramSend(c, t, th); return true; } });
setInterval(() => snapshot.tick().catch(e => console.error('snapshot', e.message)), 10 * 60 * 1000);
tank.init({ dataDir: DATA_DIR, chain, accounts, messages, mailer, site: 'https://instantadpay.com' });
await fraud.init({ dataDir: DATA_DIR });
{ // a suspended account is signed out everywhere: every member route sees no session
const realFrom = auth.fromRequest.bind(auth);
auth.fromRequest = async (req) => { const sess = await realFrom(req); if (sess && sess.email && fraud.isSuspended(sess.email)) return null; return sess; };
}
legacy.init({ dataDir: DATA_DIR });
traffic.init({ dataDir: DATA_DIR });
promos.init({ dataDir: DATA_DIR });
@@ -414,7 +420,7 @@ async function boot() {
audit.init({ dataDir: DATA_DIR, notify: text => { const sc = siteConfig(); if (sc.telegramBotToken && sc.telegramAdminChatId) telegramSend(sc.telegramAdminChatId, text).catch(() => {}); else if (ADMIN_EMAIL && mailer.hasKey()) mailer.send(ADMIN_EMAIL, 'InstantAdPay: counter audit', text).catch(() => {}); } });
setTimeout(() => audit.dailyTick(), 5 * 60 * 1000); setInterval(() => audit.dailyTick(), 24 * 60 * 60 * 1000);
videomaker.init({ dataDir: DATA_DIR, spaces, accounts });
leaderboard.init({ chain, accounts, ads, dataDir: DATA_DIR, siteConfig, pushFeed, adminEmail: ADMIN_EMAIL,
leaderboard.init({ chain, accounts, ads, fraud, dataDir: DATA_DIR, siteConfig, pushFeed, adminEmail: ADMIN_EMAIL,
notify: async text => { const sc = siteConfig(); if (!sc.telegramBotToken || !sc.telegramEchoChatId) return; await telegramSend(sc.telegramEchoChatId, text, sc.telegramEchoTopicId); if (String(sc.leaderboardAnnounceGeneral || '1') !== '0') await telegramSend(sc.telegramEchoChatId, text, null); } });
setTimeout(() => leaderboard.rolloverTick().catch(e => console.error('leaderboard rollover', e.message)), 90 * 1000);
setInterval(() => leaderboard.rolloverTick().catch(e => console.error('leaderboard rollover', e.message)), 60 * 60 * 1000);
@@ -448,6 +454,7 @@ function siteConfig() {
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/',
fraudMaxSignupsPerIpDay: 2, fraudBlockSharedDevice: 'on', // anti-fraud: accounts per IP per day; block a second account from the same browser
telegramEchoChatId: '', telegramEchoTopicId: '', telegramEchoEvents: 'payouts', // shared cross-program payments topic
telegramAdminChatId: '', // private chat for admin alerts (sign-up guard bursts); falls back to ADMIN_EMAIL
launchAt: '', // public launch moment, ISO 8601 with offset (e.g. 2026-09-18T19:00:00-05:00): countdown on /launch + dashboard mark
@@ -690,6 +697,16 @@ async function sponsorGainNudge(sp, buyerName, lostNames) {
await messages.deliver(1, ADMIN_EMAIL || 'house@instantadpay.com', [sp.email], subject, html);
} catch (e) {}
}
// anti-fraud admin alert (Telegram admin chat, else email): who, which flags, and whether the sign-up was blocked
function fraudAlert(email, fc, spAcct, blocked) {
try {
const sc = siteConfig();
const text = (blocked ? '\u26D4 InstantAdPay sign-up BLOCKED: ' : '\u{1F6A9} InstantAdPay sign-up flagged: ') + String(email).replace(/^(.{2}).*(@.*)$/, '$1***$2')
+ ' [' + (fc.flags || []).join(', ') + ']' + (fc.ip ? ' ip ' + fc.ip : '') + (spAcct ? ' sponsor ' + (spAcct.username ? '@' + spAcct.username : spAcct.email) : '') + '. Admin > Members > Duplicate signals.';
if (sc.telegramBotToken && sc.telegramAdminChatId) telegramSend(sc.telegramAdminChatId, text).catch(() => {});
else if (ADMIN_EMAIL && mailer.hasKey()) mailer.send(ADMIN_EMAIL, 'InstantAdPay: sign-up ' + (blocked ? 'blocked' : 'flagged'), text).catch(() => {});
} catch (e) {}
}
const sponsorRoutedLast = new Map(); // buyer email -> ts (one alert per buyer per hour)
function sponsorRoutedAlert(who, spd, routed, skipped) {
const k = String(who || '?'); if (Date.now() - (sponsorRoutedLast.get(k) || 0) < 3600000) return; sponsorRoutedLast.set(k, Date.now());
@@ -1210,6 +1227,7 @@ const server = http.createServer(async (req, res) => {
if (p === '/api/auth/email/start' && req.method === 'POST') {
const b = await readBody(req);
const e = String(b.email || '').trim().toLowerCase();
const devHdr = fraud.deviceOf(req) ? undefined : { 'Set-Cookie': fraud.deviceCookie(fraud.newDeviceId(), IS_PROD) }; // browser id for one-account-per-person checks
if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(e)) return json(res, 400, { error: 'That email address does not look right.' });
const prev = emailCodes.get(e);
if (prev && Date.now() < prev.nextAt) { console.log('signup-guard cooldown', clientIp(req), e.replace(/^(.).*(@.*)$/, '$1***$2')); return json(res, 429, { error: 'Code already sent. Give it a minute, then try again.' }); }
@@ -1222,9 +1240,9 @@ const server = http.createServer(async (req, res) => {
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 });
return json(res, 200, { ok: true, sent: true }, devHdr);
}
if (!IS_PROD) return json(res, 200, { ok: true, sent: false, devCode: code });
if (!IS_PROD) return json(res, 200, { ok: true, sent: false, devCode: code }, devHdr);
return json(res, 503, { error: 'Email sign-in is not configured yet.' });
}
if (p === '/api/auth/email/verify' && req.method === 'POST') {
@@ -1239,8 +1257,19 @@ const server = http.createServer(async (req, res) => {
const ref = parseCookies(req)['iap.sponsor'] || '';
const via = parseCookies(req)['iap.angle'] || '';
const joinedRef = decodeURIComponent(parseCookies(req)['iap.ref'] || '') || null;
if (fraud.isSuspended(e)) return json(res, 403, { error: 'This account is suspended. Contact support.' });
let fraudFlags = [];
const existing = await accounts.byEmail(e);
if (!existing) { // anti-fraud checks apply to NEW accounts only (Marty, 2026-09-16: one account per person)
let spAcct = null; if (ref) { try { spAcct = await accounts.byCode(String(ref).toLowerCase()); if (!spAcct) spAcct = await accounts.byUsername(String(ref).toLowerCase()); } catch (err) {} }
const fc = await fraud.checkSignup(req, spAcct, siteConfig());
fraudFlags = fc.flags;
if (fc.block) { console.log('signup blocked', fc.flags.join(','), clientIp(req), e.replace(/^(.).*(@.*)$/, '$1***$2')); fraudAlert(e, fc, spAcct, true); return json(res, 403, { error: fc.block }); }
}
const r = await accounts.ensure(e, ref, via, joinedRef); // first touch wins; existing accounts unchanged
if (r.error) return json(res, 400, r);
if (r.created) { fraud.recordSignup(e, req, fraudFlags).catch(() => {}); if (fraudFlags.length) fraudAlert(e, { flags: fraudFlags, ip: fraud.ipOf(req) }, null, false); }
else fraud.recordSeen(e, req).catch(() => {});
// the lead is in the door: queue the getting-started sequence (opt-in box is pre-checked on both forms)
if (r.created && (b.followups || b.newsletter)) drip.enqueue(e, ref, via).catch(() => {});
// a wallet-only session (signed with a wallet, no account) finishing setup:
@@ -1818,6 +1847,7 @@ const server = http.createServer(async (req, res) => {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const b = await readBody(req);
if (fraud.excluded(s.email)) return json(res, 403, { error: 'Adoptions are not available on this account. Contact support.' });
const r = await tank.adopt(s.email, b.who, b.note);
if (r.ok) { // tell the payments topic who picked whom up (Marty, 2026-09-12)
pushFeed({ type: 'Adopted', sponsor: r.adopterName, member: r.adopteeName, ts: Date.now() });
@@ -2752,15 +2782,26 @@ const server = http.createServer(async (req, res) => {
m.sponsorVia = sp ? (t === String(sp.username || '').toLowerCase() ? 'username' : t === String(sp.code || '').toLowerCase() ? 'code' : 'member #') : (t ? 'unresolved' : '');
}
for (const m of members) { try { const ps = await accounts.positions(m.email); m.positions = ps.length; m.positionIds = ps.map(p => p.memberId).filter(Boolean); } catch (e) { m.positions = 0; } }
for (const m of members) { try { const sg = await fraud.get(m.email); m.flags = sg ? sg.flags : []; m.suspended = !!(sg && sg.suspended); m.lastIp = sg ? sg.lastIp : ''; } catch (e) { m.flags = []; m.suspended = false; } }
return json(res, 200, { members });
}
if (p === '/api/admin/members' && req.method === 'PATCH') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
const b = await readBody(req);
if (!b.email) return json(res, 400, { error: 'Which member?' });
if (b.suspend !== undefined) { // anti-fraud switch: suspended accounts cannot sign in; flags keep them off the leaderboard and out of adoptions
if (b.suspend) await fraud.suspend(b.email, b.reason || 'duplicate account'); else await fraud.unsuspend(b.email);
if (b.flags) await fraud.addFlags(b.email, [].concat(b.flags));
return json(res, 200, { ok: true, signals: await fraud.get(b.email) });
}
if (b.flags !== undefined) { const f = b.flags === null ? (await fraud.clearFlags(b.email), []) : await fraud.addFlags(b.email, [].concat(b.flags)); return json(res, 200, { ok: true, flags: f }); }
const r = await accounts.setSponsorRef(b.email, b.sponsorRef);
return json(res, r.error ? 400 : 200, r);
}
if (p === '/api/admin/fraud' && req.method === 'GET') { // duplicate signals: shared browsers / IPs, flagged and suspended accounts
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
return json(res, 200, await fraud.report());
}
if (p === '/api/admin/campaigns' && req.method === 'GET') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
return json(res, 200, { campaigns: await ads.adminList(), rates: ads.rates(), bannerSizes: ads.bannerSizes(), houseOwner: ads.HOUSE_OWNER });