Never hold a sale for an unactivated sponsor: walk up to the first payable upline, referral follows the money
Marty, 2026-09-16: a purchase must never wait on a sponsor who has not linked a wallet / switched on payouts. /api/sponsor and /me now walk the site's sponsor line upward to the first upline that is activated with payouts on (not on the no-payout list) and the buy proceeds under them; nobody activated -> the catch position (#1). The buyer sees a one-line notice and carries on; admin gets a heads-up. When the position is created on-chain (MemberActivated), sponsorSyncOnEvent re-points the buyer's stored sponsor to whoever was paid (#1 included), so the referral leaves the skipped sponsor's line for good, then sends the pointed notices: the skipped sponsor(s) are told they lost this referral permanently and how to switch on payouts; the sponsor who was paid is told the referral is theirs for good and to coach the one who missed it (teach-forward). Admin lookup: GET /api/admin/sponsor-resolve?ref=. Only a transient chain error still pauses a buy. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -358,7 +358,7 @@ async function frameCheck(url) {
|
||||
}
|
||||
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)); emailOnEvent(ev).catch(() => {}); telegramOnEvent(ev).catch(() => {}); } });
|
||||
chain.init({ onEvent: ev => { attachNames([ev]).then(a => pushFeed(a[0])).catch(() => pushFeed(ev)); emailOnEvent(ev).catch(() => {}); telegramOnEvent(ev).catch(() => {}); sponsorSyncOnEvent(ev).catch(e => console.error('sponsor sync', e.message)); } });
|
||||
auth.init({ dataDir: DATA_DIR, chain, isProd: IS_PROD, site: 'instantadpay.com' });
|
||||
accounts.init({ dataDir: DATA_DIR });
|
||||
ads.init({ dataDir: DATA_DIR, chain });
|
||||
@@ -542,6 +542,94 @@ async function resolveSponsorDetailed(tok) {
|
||||
catch (e) { return { id: 0, reason: 'rpc', name: acct.username ? '@' + acct.username : t }; }
|
||||
}
|
||||
async function resolveSponsorToken(tok) { return (await resolveSponsorDetailed(tok)).id; }
|
||||
// 2026-09-16 (Marty): a buyer is never held up because their sponsor has not activated. Walk the
|
||||
// site's sponsor line upward to the first upline who can be paid on-chain (activated, payouts on,
|
||||
// not on the no-payout list); none -> the catch position (#1, Marty's top). The referral then follows
|
||||
// the money: sponsorSyncOnEvent re-points the buyer's account to whoever was paid once the position exists.
|
||||
async function walkUpActivatedSponsor(tok) {
|
||||
const skipped = []; const seen = new Set();
|
||||
const t = String(tok || '').trim().toLowerCase();
|
||||
let acct = await accounts.byCode(t); if (!acct) acct = await accounts.byUsername(t);
|
||||
for (let hop = 0; acct && hop < 25 && !seen.has(acct.email); hop++) {
|
||||
seen.add(acct.email);
|
||||
const name = acct.username ? '@' + acct.username : acct.email.replace(/@.*/, '') + '@';
|
||||
let id = 0;
|
||||
if (acct.address) { try { id = await chain.memberIdByAccount(acct.address); } catch (e) { return { id: 0, name: null, skipped, reason: 'rpc' }; } }
|
||||
if (id && !(await payoutChainBlocked(id, 2))) return { id, name, skipped, reason: 'ok' };
|
||||
skipped.push({ email: acct.email, name });
|
||||
acct = await accounts.sponsorOf(acct.email);
|
||||
}
|
||||
return { id: 0, name: null, skipped, reason: 'none' };
|
||||
}
|
||||
// The referral follows the money (Marty, 2026-09-16): when a position is created on-chain under a
|
||||
// different sponsor than the account's stored referrer (walk-up past an unactivated sponsor, or the
|
||||
// catch-position fallback), move the account to the sponsor that actually got paid, #1 included.
|
||||
async function sponsorSyncOnEvent(ev) {
|
||||
if (!ev || ev.type !== 'MemberActivated' || !ev.account) return;
|
||||
const acct = await accounts.byAddress(ev.account); if (!acct) return;
|
||||
if (acct.address && acct.address.toLowerCase() !== String(ev.account).toLowerCase()) return; // a linked extra position, not the main wallet
|
||||
const onchain = Number(ev.sponsorId) || 0; if (!onchain) return;
|
||||
const spAcct = await accounts.byMemberId(onchain);
|
||||
if (spAcct && spAcct.email === acct.email) return; // never self-sponsor an account
|
||||
const cur = acct.sponsorRef ? (await resolveSponsorDetailed(acct.sponsorRef)).id : 0;
|
||||
if (cur === onchain) return;
|
||||
const ref = spAcct && spAcct.code ? spAcct.code : String(onchain);
|
||||
const old = acct.sponsorRef || '';
|
||||
await accounts.setSponsorRef(acct.email, ref);
|
||||
console.log('referral moved to where the pay landed:', acct.email, JSON.stringify(old), '->', ref, '(#' + onchain + ')');
|
||||
// Notices, now that it is a fact on the chain (Marty, 2026-09-16): everyone between the stored
|
||||
// referrer and the paid sponsor who was simply not activated is told they lost this referral for
|
||||
// good; the sponsor who was paid is told they gained it and should coach the one who missed it.
|
||||
try {
|
||||
const nameOf = a => a.username ? '@' + a.username : a.email.replace(/@.*/, '') + '@';
|
||||
const toName = spAcct ? nameOf(spAcct) : 'the company (#' + onchain + ')';
|
||||
const lost = []; const seen = new Set();
|
||||
let x = await refAccount(old);
|
||||
while (x && !seen.has(x.email) && (!spAcct || x.email !== spAcct.email) && lost.length < 25) {
|
||||
seen.add(x.email);
|
||||
let activated = false; if (x.address) { try { activated = !!(await chain.memberIdByAccount(x.address)); } catch (e) { activated = true; } }
|
||||
if (!activated) lost.push(x); // an activated-but-excluded (no-payout) sponsor is an admin matter, not a "switch on payouts" lesson
|
||||
x = await accounts.sponsorOf(x.email);
|
||||
}
|
||||
for (const l of lost) sponsorHoldNudge(l.email, acct.email, toName).catch(() => {});
|
||||
if (spAcct && lost.length) sponsorGainNudge(spAcct, nameOf(acct), lost.map(nameOf)).catch(() => {});
|
||||
} catch (e) { console.error('sponsor move notices', e.message); }
|
||||
}
|
||||
async function refAccount(ref) {
|
||||
const t = String(ref || '').trim().toLowerCase(); if (!t) return null;
|
||||
let a = await accounts.byCode(t); if (!a) a = await accounts.byUsername(t);
|
||||
if (!a && /^\d+$/.test(t)) a = await accounts.byMemberId(Number(t));
|
||||
return a || null;
|
||||
}
|
||||
const sponsorGainLast = new Map(); // sponsor email|buyer -> ts
|
||||
async function sponsorGainNudge(sp, buyerName, lostNames) {
|
||||
if (!sp || !sp.email) return;
|
||||
const key = sp.email + '|' + buyerName; if (sponsorGainLast.has(key)) return; sponsorGainLast.set(key, Date.now());
|
||||
const who = lostNames.length === 1 ? lostNames[0] : lostNames.slice(0, -1).join(', ') + ' and ' + lostNames[lostNames.length - 1];
|
||||
const subject = buyerName + ' is now in your line for good (' + who + ' had not switched on payouts)';
|
||||
const lines = [
|
||||
buyerName + ' just bought an ad package on InstantAdPay. They came in through ' + who + ', but ' + (lostNames.length === 1 ? who + ' had' : 'they had') + ' not linked a wallet and switched on payouts, so the contract could not pay ' + (lostNames.length === 1 ? 'them' : 'any of them') + '. It walked up your line and paid you at level 1 instead.',
|
||||
'That is permanent. The contract locks a buyer to the sponsor who was paid at their first purchase, so ' + buyerName + ' is in your line on the chain from now on: this purchase and every one after it pays you as their level 1. They now show under you in My line.',
|
||||
'The lesson to pass down: ' + who + ' just lost a referral they worked for because payouts were off. Take two minutes with them, help them link a wallet and switch on payouts (Wallet tab, one free signature and one small transaction), and show them how to check it. Every leader in this line who teaches that step keeps their people from losing the next one.',
|
||||
'My line: https://instantadpay.com/my#line'
|
||||
];
|
||||
const text = lines.join('\n\n');
|
||||
if (mailer.hasKey()) mailer.send(sp.email, subject, text + '\n\nInstantAdPay').catch(() => {});
|
||||
try {
|
||||
const html = lines.map(l => '<p>' + l.replace(/&/g, '&').replace(/</g, '<').replace(/(https:\/\/[^\s]+)/g, '<a href="$1" target="_blank" rel="noopener">$1</a>') + '</p>').join('');
|
||||
await messages.deliver(1, ADMIN_EMAIL || 'house@instantadpay.com', [sp.email], subject, html);
|
||||
} 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());
|
||||
const text = '\u2934\uFE0F InstantAdPay: purchase by ' + k.replace(/^(.{2}).*(@.*)$/, '$1***$2') + ' routed past ' + (spd.name || '?')
|
||||
+ ' (payouts not switched on) to ' + routed.to + '. The buyer was not held up.' + (skipped.length > 1 ? ' Skipped: ' + skipped.map(x => x.name).join(', ') + '.' : '');
|
||||
const sc = siteConfig();
|
||||
if (sc.telegramBotToken && sc.telegramAdminChatId) telegramSend(sc.telegramAdminChatId, text).catch(() => {});
|
||||
else if (ADMIN_EMAIL && mailer.hasKey()) mailer.send(ADMIN_EMAIL, 'InstantAdPay: purchase routed past an unactivated sponsor', text).catch(() => {});
|
||||
// the skipped sponsor and the one who gains are told by sponsorSyncOnEvent, once the position exists on the chain
|
||||
}
|
||||
const sponsorAlertLast = new Map(); // email -> ts (one alert per member per hour)
|
||||
function sponsorBlockedAlert(who, r) {
|
||||
const k = String(who || '?'); if (Date.now() - (sponsorAlertLast.get(k) || 0) < 3600000) return; sponsorAlertLast.set(k, Date.now());
|
||||
@@ -553,15 +641,23 @@ function sponsorBlockedAlert(who, r) {
|
||||
if (r.reason === 'notActivated') sponsorHoldNudge(String(r.tok || ''), k).catch(() => {});
|
||||
}
|
||||
const sponsorNudgeLast = new Map(); // sponsor email -> ts
|
||||
async function sponsorHoldNudge(tok, buyerEmail) {
|
||||
async function sponsorHoldNudge(tok, buyerEmail, routedTo) {
|
||||
const t = String(tok || '').trim().toLowerCase(); if (!t) return;
|
||||
let sp = await accounts.byCode(t); if (!sp) sp = await accounts.byUsername(t);
|
||||
let sp = t.includes('@') ? await accounts.byEmail(t) : null;
|
||||
if (!sp) sp = await accounts.byCode(t); if (!sp) sp = await accounts.byUsername(t);
|
||||
if (!sp || !sp.email) return;
|
||||
if (Date.now() - (sponsorNudgeLast.get(sp.email) || 0) < 3600000) return; sponsorNudgeLast.set(sp.email, Date.now());
|
||||
const buyer = await accounts.byEmail(buyerEmail); const bn = buyer && buyer.username ? '@' + buyer.username : 'One of your referrals';
|
||||
const step = !sp.address ? 'link a wallet and switch on payouts' : 'switch on payouts';
|
||||
const subject = bn + ' is trying to buy. ' + (sp.address ? 'Switch on payouts' : 'Link your wallet') + ' so it pays you';
|
||||
const lines = [
|
||||
const subject = routedTo
|
||||
? 'You lost ' + bn + ' to ' + routedTo + ' (payouts were not switched on)'
|
||||
: bn + ' is trying to buy. ' + (sp.address ? 'Switch on payouts' : 'Link your wallet') + ' so it pays you';
|
||||
const lines = routedTo ? [
|
||||
'You just lost a sale. ' + bn + ' is buying an ad package on InstantAdPay right now, and because payouts are not switched on for your account, the contract cannot pay you. The purchase is paying ' + routedTo + ' instead.',
|
||||
'This is permanent. The contract locks a buyer to the sponsor who was paid at their first purchase, so ' + bn + ' now belongs to ' + routedTo + ', has moved out of your line, and will never pay you from that wallet. Nothing can bring that referral back.',
|
||||
'The next one does not have to go the same way. Open the Wallet tab and ' + step + '. One free signature to link, one small transaction to switch on. Two minutes, and every purchase in your line from then on pays you the moment it happens.',
|
||||
'Wallet tab: https://instantadpay.com/my#wallet'
|
||||
] : [
|
||||
bn + ' just tried to buy an ad package on InstantAdPay, and the purchase is on hold because payouts are not switched on for your account yet. The contract pays your share in the same transaction, but only to a wallet that is switched on, so the site paused the buy instead of sending your 50 percent to someone else.',
|
||||
'To fix it: open the Wallet tab and ' + step + '. One free signature to link, one small transaction to switch on. Takes two minutes, and every purchase in your line from then on pays you the moment it happens.',
|
||||
'Until then their purchase waits, and they have been told why.',
|
||||
@@ -940,8 +1036,18 @@ const server = http.createServer(async (req, res) => {
|
||||
const acct = s && s.email ? await accounts.byEmail(s.email) : null;
|
||||
const tok = (acct && acct.sponsorRef) || parseCookies(req)['iap.sponsor'] || '';
|
||||
const spd = await resolveSponsorDetailed(tok); let sponsorId = spd.id;
|
||||
let sponsorBlocked = null; // set when the account itself names a sponsor that cannot be paid right now: the client refuses the transaction
|
||||
if (acct && acct.sponsorRef && !sponsorId && spd.reason !== 'none') { sponsorBlocked = spd.reason; sponsorBlockedAlert(acct.email, Object.assign({ tok }, spd)); }
|
||||
let sponsorBlocked = null; // set only for transient/unknown failures now: the client refuses the transaction
|
||||
let sponsorRouted = null; // set when an unactivated sponsor was walked past: the client tells the buyer, then proceeds
|
||||
if (acct && acct.sponsorRef && !sponsorId && spd.reason === 'notActivated') {
|
||||
const w = await walkUpActivatedSponsor(tok);
|
||||
if (w.reason === 'rpc') sponsorBlocked = 'rpc';
|
||||
else {
|
||||
const catchId = Number(siteConfig().defaultSponsorId) || 1;
|
||||
sponsorId = w.id;
|
||||
sponsorRouted = { from: spd.name || null, to: w.id ? w.name : 'the company (#' + catchId + ')', toId: w.id || catchId, skipped: w.skipped.map(x => x.name) };
|
||||
sponsorRoutedAlert(acct.email, spd, sponsorRouted, w.skipped);
|
||||
}
|
||||
} else if (acct && acct.sponsorRef && !sponsorId && spd.reason !== 'none') { sponsorBlocked = spd.reason; sponsorBlockedAlert(acct.email, Object.assign({ tok }, spd)); }
|
||||
if (sponsorId && await payoutChainBlocked(sponsorId, 2)) { console.log('sponsor routed away from no-payout chain', sponsorId); sponsorId = 0; }
|
||||
// orphan fallback: an unresolvable/absent sponsor (dead link, no link) lands
|
||||
// the new member under the configured catch position (#1) instead of root
|
||||
@@ -958,7 +1064,7 @@ const server = http.createServer(async (req, res) => {
|
||||
if (!a && /^\d+$/.test(nameTok)) a = await accounts.byMemberId(Number(nameTok));
|
||||
if (a) { name = a.username ? '@' + a.username : (a.memberId ? 'member #' + a.memberId : null); avatarUrl = a.avatarUrl || null; own = !!(acct && a.email === acct.email); var bio = null, cobrand = false; try { cobrand = (await ads.milestonesOf(a.email)).includes('level3'); if (cobrand) bio = a.bio ? String(a.bio).slice(0, 220) : null; } catch (e) {} }
|
||||
}
|
||||
return json(res, 200, { ref: tok, sponsorId, sponsorBlocked, sponsorName: spd.name || null, invited: !!(tok || showTok), name, avatarUrl, own, bio: typeof bio === 'undefined' ? null : bio, cobrand: typeof cobrand === 'undefined' ? false : cobrand });
|
||||
return json(res, 200, { ref: tok, sponsorId, sponsorBlocked, sponsorRouted, sponsorName: spd.name || null, invited: !!(tok || showTok), name, avatarUrl, own, bio: typeof bio === 'undefined' ? null : bio, cobrand: typeof cobrand === 'undefined' ? false : cobrand });
|
||||
}
|
||||
if (p === '/api/stats' && req.method === 'GET') {
|
||||
let members = 0; try { members = await chain.memberCount(); } catch (e) {}
|
||||
@@ -1127,12 +1233,18 @@ 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;
|
||||
const spdMe = await resolveSponsorDetailed((acct && acct.sponsorRef) || parseCookies(req)['iap.sponsor']); let sponsorId = spdMe.id;
|
||||
const sponsorBlocked = (acct && acct.sponsorRef && !sponsorId && spdMe.reason !== 'none') ? spdMe.reason : null;
|
||||
let sponsorBlocked = (acct && acct.sponsorRef && !sponsorId && spdMe.reason !== 'none') ? spdMe.reason : null;
|
||||
let sponsorRouted = null;
|
||||
if (sponsorBlocked === 'notActivated') { // never hold a buyer for an unactivated sponsor: walk up the line
|
||||
const w = await walkUpActivatedSponsor(acct.sponsorRef);
|
||||
if (w.reason === 'rpc') sponsorBlocked = 'rpc';
|
||||
else { sponsorBlocked = null; sponsorId = w.id; sponsorRouted = { from: spdMe.name || null, to: w.id ? w.name : 'the company', toId: w.id || (Number(siteConfig().defaultSponsorId) || 1) }; }
|
||||
}
|
||||
if (sponsorId && await payoutChainBlocked(sponsorId, 2)) sponsorId = 0;
|
||||
const _defSpon = Number(siteConfig().defaultSponsorId) || 1;
|
||||
if (!sponsorId && memberId !== _defSpon) sponsorId = _defSpon; // orphan fallback → #1 (never self-sponsor)
|
||||
if (memberId && acct && acct.memberId !== memberId) accounts.setMemberId(acct.email, memberId).catch(() => {});
|
||||
const out = { signedIn: true, sponsorBlocked, sponsorName: spdMe.name || null, email: s.email || (acct && acct.email) || null,
|
||||
const out = { signedIn: true, sponsorBlocked, sponsorRouted, sponsorName: spdMe.name || null, 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,
|
||||
@@ -2554,6 +2666,13 @@ const server = http.createServer(async (req, res) => {
|
||||
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
||||
return json(res, 200, await audit.run());
|
||||
}
|
||||
if (p === '/api/admin/sponsor-resolve' && req.method === 'GET') { // support: where would a buy under ?ref= land right now?
|
||||
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
||||
const ref = String(u.searchParams.get('ref') || '').trim();
|
||||
const detailed = await resolveSponsorDetailed(ref);
|
||||
const walk = detailed.reason === 'notActivated' ? await walkUpActivatedSponsor(ref) : null;
|
||||
return json(res, 200, { ref, detailed, walk, catchId: Number(siteConfig().defaultSponsorId) || 1 });
|
||||
}
|
||||
if (p === '/api/admin/chain/rescan' && req.method === 'POST') { // rebuild the full event history from the deploy block
|
||||
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
||||
return json(res, 200, await chain.rescan());
|
||||
|
||||
Reference in New Issue
Block a user