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:
@@ -79,6 +79,8 @@ FACTS:
|
||||
- WALLETS + BUYING POL (Training > Wallets and buying POL, /wallets, members only): preferred MetaMask (recommended; extra accounts for Qualified Start), Phantom, SafePal, Coinbase Wallet; Trust works but blocks buys spending most of its POL (keep ~2x). MoonPay flow: connect wallet, Buy packages > "Buy POL with a card" opens MoonPay with POL on Polygon + the member's address prefilled; card/Apple Pay/Google Pay; first-time ID check; minimum order ~$30; buy package cost + 2-3 POL for fees; POL arrives in minutes; then buy. Exchanges: withdraw POL on the Polygon network. Never MATIC on Ethereum, never share the recovery phrase.
|
||||
- INTRO VIDEO ON THE WALL: Profile > social links has an "Intro video" field (YouTube, Vimeo or direct .mp4 link). It embeds on the member's public wall page (/wall/<username>) right under their bio, above the three-level line and the join button.
|
||||
- HOLDING TANK (Members > My line > Holding tank card): free members who joined with no sponsor wait there; a member who has switched on payouts AND bought their own $20+ package can Adopt one (first come, max 2 open adoptions, 7-day window; if the person never links a wallet or buys, they fall back into the tank; a person can be adopted twice at most). Adopting sets the sponsor, opens a chat and emails the member; their first purchase then binds to the adopter on-chain. Members can also "Release to tank" one of their own free referrals (pay it forward). Admin sees the tank under Members.
|
||||
- HOLDING TANK ALERTS: when new members land in the tank, a note at the top of every member's Overview names them (usernames) and a post goes to the team's Telegram payments topic; adopt from My line > Holding tank (your own $20 package required).
|
||||
- LEGACY WELCOME (former Faucet Wave / Tier One Ads members): they join through instantadpay.com/from/faucetwave or instantadpay.com/from/tieroneads and, if their email is on the legacy list, welcome-back credits are added automatically at signup (former advertisers 500, former earners 150; once per person; credits, not POL). They land in the holding tank like any member who joins without a sponsor.
|
||||
- DORMANT-LEAD RESCUE: a FREE referral (no wallet, no purchase) with no message from their sponsor for 10 days triggers a warning email + dashboard flag to the sponsor ("unreached, tank in N days"); at 14 days (warning at least 4 days old) the lead moves to the holding tank and the sponsor is told. Sponsor resets the clock with a chat, a Nudge, or the "Contacted them" button (for phone/text contact). Leads whose sponsor link resolves to nobody go to the tank after a day. Nothing on-chain moves; anyone bound by a purchase never moves.
|
||||
- PIF (pay it forward) button: on a free direct or an adopted member who has linked a wallet, the sponsor taps PIF, enters an amount (suggested: the $20 package plus fees), and their OWN wallet app opens with the member's address prefilled; the POL goes wallet to wallet. The site never touches the funds; it only logs the transaction and tells the recipient with a Polygonscan link. The gift is theirs; nothing forces a purchase.
|
||||
- FOUNDING WEEK / PRE-LAUNCH (Training > Founding week checklist, /launch, members only): eight items read live from the account: username, wallet linked, payouts on, level 2 qualified (2 buyers of $20+, or Qualified Start with 2 linked positions), the leader play = level 3 (5 qualifying buyers, up to 5 linked positions; then buy from the main wallet), line banner, links + play chosen (self-marked), first two placed. Reason: unqualified levels pass up, so leaders qualify BEFORE their teams' teams buy. Countdown shows when admin sets launchAt. Never call the site 'pre-launch' publicly: it is live and paying.
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// Legacy bridge: former Faucet Wave / Tier One Ads members (EvolutionScript sites Marty closed)
|
||||
// arrive via /from/<brand>; if their email is on the private legacy list (DATA_DIR/legacy.json,
|
||||
// email -> { b: 'faucetwave'|'tier1ads'|'both', s: 'a' (advertiser) | 'e' (earner) }) they get a
|
||||
// one-time welcome-back credit grant at account creation. Grants are recorded in
|
||||
// DATA_DIR/legacy-grants.json so a person is only ever credited once.
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let DATA_DIR = null, list = null, grants = null;
|
||||
|
||||
function init(opts) { DATA_DIR = opts.dataDir; list = null; grants = null; }
|
||||
function load() {
|
||||
if (list === null) { try { list = JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'legacy.json'), 'utf8')); } catch (e) { list = {}; } }
|
||||
if (grants === null) { try { grants = JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'legacy-grants.json'), 'utf8')); } catch (e) { grants = {}; } }
|
||||
}
|
||||
function reload() { list = null; grants = null; load(); }
|
||||
function lookup(email) { load(); return list[String(email || '').trim().toLowerCase()] || null; }
|
||||
// credits for this email, or null when not listed / already granted. Records the grant.
|
||||
function grant(email, cfg) {
|
||||
load();
|
||||
const e = String(email || '').trim().toLowerCase();
|
||||
const rec = list[e];
|
||||
if (!rec || grants[e]) return null;
|
||||
const credits = rec.s === 'a' ? (Number(cfg.legacyCreditsAdvertiser) || 500) : (Number(cfg.legacyCreditsEarner) || 150);
|
||||
grants[e] = { credits, seg: rec.s, brand: rec.b, at: Date.now() };
|
||||
fs.writeFileSync(path.join(DATA_DIR, 'legacy-grants.json'), JSON.stringify(grants));
|
||||
return { credits, seg: rec.s, brand: rec.b };
|
||||
}
|
||||
function stats() {
|
||||
load();
|
||||
const g = Object.values(grants);
|
||||
return { listed: Object.keys(list).length, granted: g.length, credits: g.reduce((a, x) => a + (x.credits || 0), 0) };
|
||||
}
|
||||
module.exports = { init, reload, lookup, grant, stats };
|
||||
@@ -485,7 +485,7 @@
|
||||
}));
|
||||
|
||||
// site settings: key / value rows; booleans as checkboxes, numbers stay numbers
|
||||
const SITE_META = { siteName: 'Site name', tagline: 'Tagline', rehearsal: 'Testnet rehearsal banner', defaultSponsorId: 'Fallback sponsor (member #)', walletConnectProjectId: 'WalletConnect project id', moonpayPublicKey: 'MoonPay public key', telegramBotToken: 'Telegram proof feed: bot token', telegramChatId: 'Telegram proof feed: chat id', telegramTopicId: 'Telegram proof feed: topic id (optional)', telegramEvents: 'Telegram proof feed: events (payouts | payouts+purchases | all)', telegramCtaUrl: 'Telegram proof feed: join link under each post', telegramEchoChatId: 'Telegram echo (shared payments topic): chat id', telegramEchoTopicId: 'Telegram echo: topic id', telegramEchoEvents: 'Telegram echo: events (payouts | payouts+purchases | all)', pnlFixedMonthlyUsd: 'P&L: fixed monthly cost (USD)' };
|
||||
const SITE_META = { siteName: 'Site name', tagline: 'Tagline', rehearsal: 'Testnet rehearsal banner', defaultSponsorId: 'Fallback sponsor (member #)', walletConnectProjectId: 'WalletConnect project id', moonpayPublicKey: 'MoonPay public key', telegramBotToken: 'Telegram proof feed: bot token', telegramChatId: 'Telegram proof feed: chat id', telegramTopicId: 'Telegram proof feed: topic id (optional)', telegramEvents: 'Telegram proof feed: events (payouts | payouts+purchases | all)', telegramCtaUrl: 'Telegram proof feed: join link under each post', telegramEchoChatId: 'Telegram echo (shared payments topic): chat id', telegramEchoTopicId: 'Telegram echo: topic id', telegramEchoEvents: 'Telegram echo: events (payouts | payouts+purchases | all)', legacyCreditsAdvertiser: 'Legacy welcome credits: former advertisers', legacyCreditsEarner: 'Legacy welcome credits: former earners', pnlFixedMonthlyUsd: 'P&L: fixed monthly cost (USD)' };
|
||||
function drawSite() {
|
||||
const wrap = $('siteForm');
|
||||
wrap.innerHTML = Object.entries(siteObj).map(([k, v]) => '<div class="kv-row"><span class="k" title="' + esc(k) + '">' + esc(SITE_META[k] || humanize(k)) + '</span>'
|
||||
|
||||
+13
-2
@@ -4,7 +4,17 @@
|
||||
// set by the server when this page was served.
|
||||
(function () {
|
||||
const $ = id => document.getElementById(id);
|
||||
const LEG = (brand, seg) => seg === 'adv'
|
||||
? { eyebrow: 'For former ' + brand + ' advertisers', h: 'Your next ad budget <em>pays you back.</em>',
|
||||
lead: brand + ' is closed. The people who bought ads there are exactly who InstantAdPay was built for: real ad packages from $5, seven formats, and every package in your line paid out by a verified contract on Polygon in the same transaction.',
|
||||
points: ['Welcome-back credits land the moment your account exists: enough to run a real banner or text campaign today, on us.', 'Seven formats: banners, text ads, login ads, solo ads to member inboxes, video, featured links and verified visits. Views are timed on the server, so a real person saw your ad.', 'When anyone in your line buys ads, the contract pays you in that same transaction. Public on Polygonscan, nothing held, nothing to withdraw.'],
|
||||
cta: 'Claim your welcome-back credits', sub: 'Free account by email. No password, no wallet today. Use the email you had on ' + brand + ': the credits are tied to it.', video: false }
|
||||
: { eyebrow: 'For former ' + brand + ' members', h: 'Same daily habit. <em>Real payouts on-chain.</em>',
|
||||
lead: 'You viewed ads on ' + brand + '. Here you view ads to earn credits, run your own campaign with them for free, and when anyone in your line buys ads you are paid in POL to your own wallet in the same transaction.',
|
||||
points: ['Welcome-back credits on day one, so your first campaign runs before you have viewed a single ad.', 'Join with just an email. No password, no wallet today. Link a wallet later, only when you want payouts switched on.', 'Every payout is a public transaction on Polygon. Nothing is held, so there is nothing to withdraw and nothing to wait for.'],
|
||||
cta: 'Claim your welcome-back credits', sub: 'Free account by email. Use the email you had on ' + brand + ': the credits are tied to it.', video: false };
|
||||
const ANGLES = {
|
||||
'fw-adv': LEG('Faucet Wave', 'adv'), 'fw-earn': LEG('Faucet Wave', 'earn'), 't1-adv': LEG('Tier One Ads', 'adv'), 't1-earn': LEG('Tier One Ads', 'earn'),
|
||||
instant: { eyebrow: 'Same-transaction payouts', h: 'Paid before the page <em>reloads.</em>', lead: 'What if your commission landed before the thank-you page finished loading? On InstantAdPay that is not a metaphor. A smart contract on Polygon splits every ad package the moment it sells.',
|
||||
points: ['A verified contract splits every package in the same transaction it sells: 50 percent to the sponsor, 20 and 10 up the line, 20 to the platform.', 'It lands in your own wallet in seconds. There is no balance to withdraw because nothing is ever held.', 'Every payment is public on Polygonscan, so you can check the claim before you spend a dollar.'],
|
||||
cta: 'See a payout land in seconds', sub: 'Free account by email. No password, no wallet today. Your invite link is live the moment you are in.' },
|
||||
@@ -21,7 +31,7 @@
|
||||
points: ['Every direct buyer pays you 50 percent from their very first package.', 'Two qualifying buyers open level two at 20 percent. Five open level three at 10 percent. Constants in a verified contract.', 'Until a level opens, its share climbs to the next qualified member above, so the plan rewards the people who build.'],
|
||||
cta: 'Start your line. Two buyers is the target.', sub: 'Free account by email. Your invite link and the team-building plays are waiting inside.' }
|
||||
};
|
||||
const v = new URLSearchParams(location.search).get('v');
|
||||
const v = new URLSearchParams(location.search).get('v') || (document.cookie.match(/(?:^|; )iap\.angle=([^;]+)/) || [])[1] || '';
|
||||
const a = v && ANGLES[v];
|
||||
if (a) {
|
||||
document.body.classList.add('squeeze'); // server sets it too; this covers cached HTML
|
||||
@@ -29,7 +39,8 @@
|
||||
if (a.cta) { $('jnCapH').textContent = a.cta; $('jnCapSub').textContent = a.sub || ''; }
|
||||
// the matching hook video + this angle's three points replace the worked example
|
||||
const VID = 'https://coolify-saasytop.nyc3.digitaloceanspaces.com/promo/';
|
||||
const vid = $('jnVideo'); vid.src = VID + v + '.mp4'; vid.poster = VID + v + '.jpg';
|
||||
const vid = $('jnVideo');
|
||||
if (a.video === false) vid.hidden = true; else { vid.src = VID + v + '.mp4'; vid.poster = VID + v + '.jpg'; }
|
||||
$('jnPoints').innerHTML = (a.points || []).map(t => '<li>' + t.replace(/[&<>]/g, c => ({ '&': '&', '<': '<', '>': '>' }[c])) + '</li>').join('');
|
||||
$('jnMock').hidden = true; $('jnAngle').hidden = false; $('jnPoints').hidden = false;
|
||||
}
|
||||
|
||||
@@ -429,6 +429,16 @@
|
||||
const lm = $('launchMark');
|
||||
if (lm) { lm.hidden = !show; lm.innerHTML = show ? '<b>Launch ready: ' + done + ' of ' + items.length + '.</b> ' + (at && Date.now() < at ? 'Doors open ' + new Date(at).toLocaleString([], { weekday: 'short', hour: 'numeric', minute: '2-digit' }) + '. ' : '') + '<a href="/launch">Open the founding-week checklist</a>' : ''; }
|
||||
} catch (e) {}
|
||||
// people waiting for a sponsor in the holding tank (Marty, 2026-09-12): every Overview sees it
|
||||
try {
|
||||
const tw = d.tankWaiting, tn = $('tankNotice');
|
||||
if (tn) {
|
||||
tn.hidden = !(tw && tw.count);
|
||||
if (tw && tw.count) tn.innerHTML = '<b>' + tw.count + (tw.count === 1 ? ' person is' : ' people are') + ' waiting for a sponsor in the holding tank</b> \u00b7 '
|
||||
+ tw.names.map(esc).join(', ') + (tw.count > tw.names.length ? ' and more' : '') + '. <a href="#line">Adopt them from My line</a>'
|
||||
+ (tw.eligible ? '.' : ' (you need your own $20 package first).');
|
||||
}
|
||||
} catch (e) {}
|
||||
if (d.username) { // wall link rides the username
|
||||
const wl = location.origin + '/wall/' + d.username;
|
||||
$('wallLine').textContent = wl;
|
||||
|
||||
@@ -229,6 +229,7 @@
|
||||
</div>
|
||||
<div class="nc-steps" id="ncSteps"></div>
|
||||
<p class="small" id="launchMark" hidden style="margin:10px 0 0"></p>
|
||||
<p class="small" id="tankNotice" hidden style="margin:10px 0 0"></p>
|
||||
</div>
|
||||
<div class="card" id="lineTreeCard">
|
||||
<div class="card-head"><h3>Your line at a glance</h3><span class="sub" id="treeSub"></span></div>
|
||||
|
||||
@@ -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