Legacy bridge pages, welcome-back credits, holding-tank alerts
- /from/faucetwave and /from/tieroneads (?seg=advertiser): the squeeze page with brand copy and no video, sponsor cookie cleared so arrivals land in the holding tank, angle + a legacy:<brand> source recorded on the account. - legacy.js: private list DATA_DIR/legacy.json (email -> brand, segment); listed emails get a one-time credit grant at account creation (legacyCreditsAdvertiser 500 / legacyCreditsEarner 150, admin-editable), recorded in legacy-grants.json. - Holding-tank alerts: dashboard payload carries who is waiting (Overview notice with usernames + adopt link), and a 15-minute tick posts new arrivals to the shared Telegram payments topic. - Chatbot prompt lines + admin setting labels. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,8 @@ let QR = null; try { QR = require('qrcode'); } catch (e) { /* optional */ }
|
||||
const chatbot = require('./chatbot');
|
||||
const coach = require('./coach'); // coaching view, nudges, digest, prospects, link stats
|
||||
const tank = require('./tank'); // holding tank: unsponsored free members, adoptions, pay-it-forward
|
||||
const legacy = require('./legacy'); // Faucet Wave / Tier One Ads bridge: welcome-back credits for listed emails
|
||||
let tankWaitCache = null; // dashboard: who is waiting for a sponsor (refreshed every minute)
|
||||
const geo = require('./geo'); // viewer country -> tier (DB-IP lite), for campaign targeting
|
||||
const burner = require('./burner'); // automatic on-chain credit burns (inert without ENGINE_KEY)
|
||||
|
||||
@@ -254,6 +256,11 @@ async function handleUpload(req, res, who) {
|
||||
}
|
||||
// lead-capture page hooks (og tags + copy live in public/assets/join.js too)
|
||||
const JOIN_ANGLES = {
|
||||
// legacy bridge pages (/from/<brand>): former members of Marty's closed EvolutionScript sites
|
||||
'fw-adv': { t: 'Your next ad budget pays you back.', d: 'Faucet Wave closed. InstantAdPay was built for the people who bought ads there: packages from $5, seven formats, every package in your line paid out on Polygon in the same transaction. Welcome-back credits waiting.', url: 'https://instantadpay.com/from/faucetwave?seg=advertiser' },
|
||||
'fw-earn': { t: 'Same daily habit. Real payouts on-chain.', d: 'You viewed ads on Faucet Wave. Here you view ads to earn credits, run your own campaign free, and get paid in POL to your own wallet when your line buys ads. Welcome-back credits waiting.', url: 'https://instantadpay.com/from/faucetwave' },
|
||||
't1-adv': { t: 'Your next ad budget pays you back.', d: 'Tier One Ads closed. InstantAdPay was built for the people who bought ads there: packages from $5, seven formats, every package in your line paid out on Polygon in the same transaction. Welcome-back credits waiting.', url: 'https://instantadpay.com/from/tieroneads?seg=advertiser' },
|
||||
't1-earn': { t: 'Same daily habit. Real payouts on-chain.', d: 'You viewed ads on Tier One Ads. Here you view ads to earn credits, run your own campaign free, and get paid in POL to your own wallet when your line buys ads. Welcome-back credits waiting.', url: 'https://instantadpay.com/from/tieroneads' },
|
||||
instant: { t: 'Paid before the page reloads.', d: 'A smart contract on Polygon splits every ad package the moment it sells. Same transaction, real wallets, public ledger. Join free by email.' },
|
||||
adspend: { t: 'You were buying ads anyway.', d: 'Here the ad spend in your line pays you, in the same transaction, on a public ledger. Seven formats, packages from $5. Join free.' },
|
||||
free: { t: 'Watch first. Spend never.', d: 'Join free, view a few ads, earn credits, run your first campaign for zero dollars. Every payout public on Polygon.' },
|
||||
@@ -264,7 +271,7 @@ 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 url = (ang && ang.url) || (base + '/join/' + tok + (angle ? '?v=' + angle : ''));
|
||||
const title = ang ? ang.t : 'Advertise and earn. Paid on-chain, instantly.';
|
||||
const desc = ang ? ang.d : 'You are invited to InstantAdPay: real ad packages with same-transaction payouts on Polygon, every payment public. Join free by email.';
|
||||
const escA = t => String(t).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<');
|
||||
@@ -311,6 +318,8 @@ async function boot() {
|
||||
// follow-up email sequence: send whatever came due (every 10 min, first pass shortly after boot)
|
||||
coach.init({ dataDir: DATA_DIR, chain, accounts, mailer });
|
||||
tank.init({ dataDir: DATA_DIR, chain, accounts, messages, mailer, site: 'https://instantadpay.com' });
|
||||
legacy.init({ dataDir: DATA_DIR });
|
||||
setInterval(() => tankNotifyTick().catch(e => console.error('tank notify', e.message)), 15 * 60 * 1000); // new tank arrivals -> Telegram
|
||||
geo.init({ dataDir: DATA_DIR }).catch(e => console.error('geo init', e.message));
|
||||
setInterval(() => geo.refresh().catch(e => console.error('geo refresh', e.message)), 24 * 3600 * 1000); // monthly file, checked daily
|
||||
setInterval(() => tank.sweep().catch(e => console.error('tank sweep', e.message)), 60 * 60 * 1000); // adoptions past their 7-day window
|
||||
@@ -342,6 +351,7 @@ function siteConfig() {
|
||||
telegramEchoChatId: '', telegramEchoTopicId: '', telegramEchoEvents: 'payouts', // shared cross-program payments topic
|
||||
telegramAdminChatId: '', // private chat for admin alerts (sign-up guard bursts); falls back to ADMIN_EMAIL
|
||||
launchAt: '', // public launch moment, ISO 8601 with offset (e.g. 2026-09-18T19:00:00-05:00): countdown on /launch + dashboard mark
|
||||
legacyCreditsAdvertiser: 500, legacyCreditsEarner: 150, // welcome-back credits for listed Faucet Wave / Tier One Ads emails arriving via /from/<brand>
|
||||
geoTier1: '', // comma-separated ISO country codes; empty = built-in default (US, CA, GB, AU, NZ, IE, DE, FR, NL, SE, NO, DK, FI, CH, AT, BE)
|
||||
geoTier2: '', // empty = built-in default (rest of Western/Central Europe, JP, KR, SG, HK, TW, IL, Gulf, ZA, BR, MX, AR, CL, CO ...); tier 3 = everything else
|
||||
pnlFixedMonthlyUsd: 0
|
||||
@@ -551,6 +561,24 @@ async function telegramOnEvent(ev) {
|
||||
if (sc.telegramChatId) { const t = build(String(sc.telegramEvents || 'payouts')); if (t) await telegramSend(sc.telegramChatId, t, sc.telegramTopicId); }
|
||||
if (sc.telegramEchoChatId) { const t = build(String(sc.telegramEchoEvents || 'payouts')); if (t) await telegramSend(sc.telegramEchoChatId, '\u{1F7E0} <b>InstantAdPay</b> \u00b7 ' + t, sc.telegramEchoTopicId); }
|
||||
}
|
||||
// holding-tank arrivals -> one digest line in the shared payments topic (Marty, 2026-09-12): who is
|
||||
// waiting for a sponsor, by username, so builders go adopt them. Runs every 15 min, posts only
|
||||
// when someone new landed since the last check.
|
||||
async function tankNotifyTick() {
|
||||
const sc = siteConfig();
|
||||
if (!sc.telegramBotToken || !sc.telegramEchoChatId) return;
|
||||
const f = path.join(DATA_DIR, 'tank-notify.json');
|
||||
let st = { last: 0 }; try { st = JSON.parse(fs.readFileSync(f, 'utf8')); } catch (e) {}
|
||||
const since = st.last || (Date.now() - 24 * 3600 * 1000);
|
||||
const fresh = (await tank.waiting()).filter(w => (w.joined || 0) > since);
|
||||
st.last = Date.now(); fs.writeFileSync(f, JSON.stringify(st));
|
||||
if (!fresh.length) return;
|
||||
const named = fresh.filter(w => w.username).map(w => '@' + w.username);
|
||||
const who = named.length ? ': ' + named.slice(0, 8).join(', ') + (named.length > 8 ? ' and ' + (named.length - 8) + ' more' : '') : '';
|
||||
const text = '\u{1FAA3} <b>InstantAdPay</b> \u00b7 ' + fresh.length + ' new member' + (fresh.length === 1 ? '' : 's') + ' waiting for a sponsor in the holding tank' + who
|
||||
+ '\nAdopt from My line \u203a Holding tank: <a href="https://instantadpay.com/my#line">instantadpay.com/my</a>';
|
||||
await telegramSend(sc.telegramEchoChatId, text, sc.telegramEchoTopicId);
|
||||
}
|
||||
// one sendMessage call; never throws, never logs the token
|
||||
async function telegramSend(chatId, text, threadId) {
|
||||
const sc = siteConfig();
|
||||
@@ -597,6 +625,18 @@ const server = http.createServer(async (req, res) => {
|
||||
if (!cookies['iap.ref']) set.push('iap.ref=' + encodeURIComponent(coach.refHost(req.headers.referer)) + cookieTail); // first-touch source
|
||||
return serveJoinPage(res, tok, ang ? angle : '', ang, set);
|
||||
}
|
||||
// -- legacy bridge: /from/faucetwave | /from/tieroneads [?seg=advertiser]. Same squeeze page
|
||||
// with brand copy, NO sponsor (the sponsor cookie is cleared so they land in the holding
|
||||
// tank for adoption), the angle remembered so the welcome-back grant fires at signup, and
|
||||
// a forced first-touch source so link stats / admin can see the legacy arrivals.
|
||||
m = /^\/from\/(faucetwave|tieroneads)$/.exec(p);
|
||||
if (m && (req.method === 'GET' || req.method === 'HEAD')) {
|
||||
const brand = m[1];
|
||||
const key = (brand === 'faucetwave' ? 'fw' : 't1') + (String(u.searchParams.get('seg') || '').toLowerCase().startsWith('adv') ? '-adv' : '-earn');
|
||||
const cookieTail = `; Path=/; SameSite=Lax; Max-Age=${30 * 24 * 3600}${IS_PROD ? '; Secure' : ''}`;
|
||||
const set = ['iap.sponsor=; Path=/; SameSite=Lax; Max-Age=0' + (IS_PROD ? '; Secure' : ''), 'iap.angle=' + key + cookieTail, 'iap.ref=' + encodeURIComponent('legacy:' + brand) + cookieTail];
|
||||
return serveJoinPage(res, '', key, JOIN_ANGLES[key], set);
|
||||
}
|
||||
if (p === '/unsubscribe' && req.method === 'GET') {
|
||||
const r = await drip.unsubscribe(u.searchParams.get('e'), u.searchParams.get('t'));
|
||||
const msg = r.error ? r.error : 'Done. You will not get any more follow-up emails from InstantAdPay. Your account is unchanged.';
|
||||
@@ -784,6 +824,11 @@ const server = http.createServer(async (req, res) => {
|
||||
await auth.logout(req);
|
||||
}
|
||||
if (r.created) { sendWelcome(e, ref).catch(() => {}); } // sponsor notified at username set (/api/my/profile)
|
||||
// legacy bridge: a listed former Faucet Wave / Tier One Ads member gets welcome-back credits once
|
||||
if (r.created && /^(fw|t1)-(adv|earn)$/.test(via)) {
|
||||
try { const g = legacy.grant(e, siteConfig()); if (g) { await ads.addEarned(e, g.credits); console.log('legacy grant', g.brand, g.seg, g.credits, e); } }
|
||||
catch (err) { console.error('legacy grant', err.message); }
|
||||
}
|
||||
if (r.created && b.newsletter) sendy.subscribe(r.account.email, r.account.username || '').catch(() => {}); // pre-checked opt-in, silent, new joins only
|
||||
let memberId = 0;
|
||||
if (r.account.address) { try { memberId = await chain.memberIdByAccount(r.account.address); } catch (err) {} }
|
||||
@@ -886,7 +931,13 @@ const server = http.createServer(async (req, res) => {
|
||||
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,
|
||||
let tankWaiting = null; // top of every Overview: people waiting for a sponsor (Marty, 2026-09-12)
|
||||
try {
|
||||
if (!tankWaitCache || Date.now() - tankWaitCache.ts > 60000) tankWaitCache = { ts: Date.now(), list: await tank.waiting() };
|
||||
const tw = tankWaitCache.list;
|
||||
tankWaiting = { count: tw.length, names: tw.slice(0, 6).map(w => w.name), eligible: !!(await tank.eligibility(s.email)).ok };
|
||||
} catch (e) {}
|
||||
const out = { memberId, tankWaiting, email: s.email || (acct && acct.email) || null,
|
||||
address: s.address || (acct && acct.address) || null,
|
||||
username: (acct && acct.username) || null,
|
||||
refCode: (acct && acct.code) || null, credits: 0, buyerCount: 0,
|
||||
|
||||
Reference in New Issue
Block a user