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:
+2
-1
@@ -89,7 +89,8 @@ FACTS:
|
|||||||
- LINKED POSITIONS AND BADGES (2026-09-14): qualifying buyers on a member's linked positions count toward their achievement badges (Spark/Surge/Circuit/Nexus) and light their chip on their sponsor's line, and a badge once earned never regresses. CONTRACT RULE: levels 2 and 3 are paid to the MAIN wallet only when that wallet's own count of qualifying buyers reaches 2 and 5; buyers registered under a linked position count for that position, not the main wallet. Qualified Start positions buy UNDER the main wallet, so those buys raise the main count; a member whose old wallet was swapped out (its buyers stay with the old position) needs new $20+ buyers under the current main wallet before level 2 pays it. The contract still pays levels 2 and 3 to each position based on that position's OWN qualifying buyers, so the Qualifying buyers tile shows the main position's count with the linked positions' count underneath.
|
- LINKED POSITIONS AND BADGES (2026-09-14): qualifying buyers on a member's linked positions count toward their achievement badges (Spark/Surge/Circuit/Nexus) and light their chip on their sponsor's line, and a badge once earned never regresses. CONTRACT RULE: levels 2 and 3 are paid to the MAIN wallet only when that wallet's own count of qualifying buyers reaches 2 and 5; buyers registered under a linked position count for that position, not the main wallet. Qualified Start positions buy UNDER the main wallet, so those buys raise the main count; a member whose old wallet was swapped out (its buyers stay with the old position) needs new $20+ buyers under the current main wallet before level 2 pays it. The contract still pays levels 2 and 3 to each position based on that position's OWN qualifying buyers, so the Qualifying buyers tile shows the main position's count with the linked positions' count underneath.
|
||||||
- CREDITS VS POL (2026-09-15, Marty's stance): EVERY member earns CREDITS (viewing, videos, visits, inbox, sign-in bonus, badge bonuses); credits are ad delivery, not money. POL is paid by the contract to a member's wallet when referrals buy packages. ALWAYS describe activation as 'the $20 starter package plus payouts switched on' and lead every earning answer with it; do NOT volunteer that level 1 can pay without a purchase. If someone asks point-blank whether a purchase is required for level-1 payouts, answer truthfully in one sentence: the contract pays level 1 to any member whose payouts are switched on, and the $20 package is what makes them a qualifying buyer for their sponsor, funds their own ads, and unlocks badges, tools, the holding tank and wall positions. A $20+ purchase by a referral makes that referral a qualifying buyer for their sponsor; 2 and 5 open levels 2 and 3. Never claim the payout itself is gated by a purchase.
|
- CREDITS VS POL (2026-09-15, Marty's stance): EVERY member earns CREDITS (viewing, videos, visits, inbox, sign-in bonus, badge bonuses); credits are ad delivery, not money. POL is paid by the contract to a member's wallet when referrals buy packages. ALWAYS describe activation as 'the $20 starter package plus payouts switched on' and lead every earning answer with it; do NOT volunteer that level 1 can pay without a purchase. If someone asks point-blank whether a purchase is required for level-1 payouts, answer truthfully in one sentence: the contract pays level 1 to any member whose payouts are switched on, and the $20 package is what makes them a qualifying buyer for their sponsor, funds their own ads, and unlocks badges, tools, the holding tank and wall positions. A $20+ purchase by a referral makes that referral a qualifying buyer for their sponsor; 2 and 5 open levels 2 and 3. Never claim the payout itself is gated by a purchase.
|
||||||
- PIPELINE (coming soon, built 2026-09-15; opens when the site setting pipelineMode is on): a follow-up board on the dashboard, tab 'Pipeline' between My line and Buy packages. Columns: Talking to, Joined, Wallet linked, Payouts on, Bought, Building, Later. Prospects (from the My line prospect list) and directs are placed automatically from what they have actually done (coaching rung, on-chain buys); nobody drags cards. The sponsor adds a note, a follow-up date and a tag (hot, later, no response, not interested); a 'Follow up today' strip lists due cards; stalled cards (quiet 3+ days) are flagged; each card carries the message for its exact stage with 'Open chat with this message' (member chat) or copy. Until it opens, the tab shows a coming-soon card with the roadmap date.
|
- PIPELINE (coming soon, built 2026-09-15; opens when the site setting pipelineMode is on): a follow-up board on the dashboard, tab 'Pipeline' between My line and Buy packages. Columns: Talking to, Joined, Wallet linked, Payouts on, Bought, Building, Later. Prospects (from the My line prospect list) and directs are placed automatically from what they have actually done (coaching rung, on-chain buys); nobody drags cards. The sponsor adds a note, a follow-up date and a tag (hot, later, no response, not interested); a 'Follow up today' strip lists due cards; stalled cards (quiet 3+ days) are flagged; each card carries the message for its exact stage with 'Open chat with this message' (member chat) or copy. Until it opens, the tab shows a coming-soon card with the roadmap date.
|
||||||
- SPONSOR NUDGE ON A HELD BUY (2026-09-15): when a referral's purchase is held because the sponsor has not switched on payouts, the sponsor is emailed and gets an on-site message the same minute (once an hour at most) telling them who is waiting and to link a wallet / switch on payouts on the Wallet tab; nothing is charged to the buyer, who can simply try again once the sponsor is ready.
|
- UNACTIVATED SPONSOR = WALK UP, NEVER HOLD (2026-09-16, Marty): if a buyer's sponsor has not linked a wallet or switched on payouts, the purchase is NOT held. The site walks the sponsor line upward to the first upline who is activated with payouts on and the buy goes through under them (on-chain sponsorship for that buyer is then theirs, permanently); if nobody in the line is activated it goes under the company catch position (#1). The buyer sees a one-line notice and proceeds; the skipped sponsor gets an email + on-site message saying the sale routed past them and how to switch on payouts to catch the next one; the referral MOVES to whoever got paid (the account's stored sponsor is re-pointed when the position is created on-chain), so the buyer leaves the skipped sponsor's My line for good; the skipped sponsor can never earn from that wallet. Only a transient chain error ('rpc') still pauses a buy ("try again in a minute").
|
||||||
|
- (SUPERSEDED, kept for history) SPONSOR NUDGE ON A HELD BUY (2026-09-15): when a referral's purchase is held because the sponsor has not switched on payouts, the sponsor is emailed and gets an on-site message the same minute (once an hour at most) telling them who is waiting and to link a wallet / switch on payouts on the Wallet tab; nothing is charged to the buyer, who can simply try again once the sponsor is ready.
|
||||||
- DRILL DOWN FROM THE OVERVIEW (2026-09-15): every name chip in 'Your line at a glance' on the Overview is clickable; it jumps to My line with that person's Activity panel already open and the row highlighted.
|
- DRILL DOWN FROM THE OVERVIEW (2026-09-15): every name chip in 'Your line at a glance' on the Overview is clickable; it jumps to My line with that person's Activity panel already open and the row highlighted.
|
||||||
- VISIT PACKS ARE PAID UP FRONT (2026-09-15, Michael's question): a verified-visits campaign charges the whole pack when it is created (3 credits per visit, so 1,000 visits = 3,000 credits) and then delivers over time as members complete verified visits (each member can do at most 20 a day, so packs fill gradually: tens per day at today's size). The Campaigns row shows 'paid up front' and 'N of M visits delivered'; the credits are not being consumed again, they were reserved once. Same for featured-day bookings: a featured link is a flat buy (featuredPerDay credits per day, whole run charged when booked), its row shows 'paid up front', the day count and 'runs through <date>', and it ends on its last day ('run ended'). FEATURED CANNOT TAKE ADDED CREDITS (2026-09-16, Hugh and Michael both tried): the row offers 'Extend run' which books more days after the current run (or from today if it ended) and charges them right away; there is no 'Buy more views' on a featured row any more. Visit packs offer 'Buy more visits' (charged now, adds to the pack). Credits added to a campaign are set aside immediately (Available drops at once) and whatever is unspent returns to Available when the campaign ends.
|
- VISIT PACKS ARE PAID UP FRONT (2026-09-15, Michael's question): a verified-visits campaign charges the whole pack when it is created (3 credits per visit, so 1,000 visits = 3,000 credits) and then delivers over time as members complete verified visits (each member can do at most 20 a day, so packs fill gradually: tens per day at today's size). The Campaigns row shows 'paid up front' and 'N of M visits delivered'; the credits are not being consumed again, they were reserved once. Same for featured-day bookings: a featured link is a flat buy (featuredPerDay credits per day, whole run charged when booked), its row shows 'paid up front', the day count and 'runs through <date>', and it ends on its last day ('run ended'). FEATURED CANNOT TAKE ADDED CREDITS (2026-09-16, Hugh and Michael both tried): the row offers 'Extend run' which books more days after the current run (or from today if it ended) and charges them right away; there is no 'Buy more views' on a featured row any more. Visit packs offer 'Buy more visits' (charged now, adds to the pack). Credits added to a campaign are set aside immediately (Available drops at once) and whatever is unspent returns to Available when the campaign ends.
|
||||||
- CREDIT ACTIVITY LEDGER (2026-09-16): Members > Campaigns has a 'Credit activity' list (last 40 lines, newest first; also linked from the Earn credits pane) showing every real credit movement with a reason: welcome credits, sign-in bonus with streak, daily claim, inbox reads, video watches, verified visits, milestone and leaderboard bonuses, partner codes, admin/team grants, AI Copy Engine charges and refunds, flat buys (featured runs, extensions, visit packs, more visits), login-ad days, and metered delivery rolled up as one line per campaign per day ('Ad views: NAME', 'Inbox deliveries: NAME', 'Video views: NAME'); lines marked '(purchased credits)' spent on-chain purchased credits rather than the earned pool. Reservations (credits set aside for a live campaign) are NOT ledger lines; they show as 'in live campaigns'. Earned credits are spent before purchased ones, so an earn followed by a buy appears as two lines. The ledger starts 2026-09-16; nothing earlier is listed.
|
- CREDIT ACTIVITY LEDGER (2026-09-16): Members > Campaigns has a 'Credit activity' list (last 40 lines, newest first; also linked from the Earn credits pane) showing every real credit movement with a reason: welcome credits, sign-in bonus with streak, daily claim, inbox reads, video watches, verified visits, milestone and leaderboard bonuses, partner codes, admin/team grants, AI Copy Engine charges and refunds, flat buys (featured runs, extensions, visit packs, more visits), login-ad days, and metered delivery rolled up as one line per campaign per day ('Ad views: NAME', 'Inbox deliveries: NAME', 'Video views: NAME'); lines marked '(purchased credits)' spent on-chain purchased credits rather than the earned pool. Reservations (credits set aside for a live campaign) are NOT ledger lines; they show as 'in live campaigns'. Earned credits are spent before purchased ones, so an earn followed by a buy appears as two lines. The ledger starts 2026-09-16; nothing earlier is listed.
|
||||||
|
|||||||
@@ -2069,6 +2069,7 @@
|
|||||||
}
|
}
|
||||||
const spNow = fromPos ? { sponsorId: meNow.memberId } : await jretry('/api/sponsor');
|
const spNow = fromPos ? { sponsorId: meNow.memberId } : await jretry('/api/sponsor');
|
||||||
if (!fromPos && !meNow.memberId && spNow.sponsorBlocked) { IAP.status(sponsorHoldText(spNow), 'bad'); return; } // first activation must not hand a sponsored member to the company
|
if (!fromPos && !meNow.memberId && spNow.sponsorBlocked) { IAP.status(sponsorHoldText(spNow), 'bad'); return; } // first activation must not hand a sponsored member to the company
|
||||||
|
if (!fromPos && !meNow.memberId && spNow.sponsorRouted) IAP.status((spNow.sponsorRouted.from || 'Your sponsor') + ' has not switched on payouts, so this purchase credits ' + spNow.sponsorRouted.to + ' instead. You are not held up; carrying on.', 'ok');
|
||||||
// pre-flight: stop early if the POL is not there. Trust Wallet also hard-blocks any
|
// pre-flight: stop early if the POL is not there. Trust Wallet also hard-blocks any
|
||||||
// transaction that spends most of the balance ("drain your wallet"), so Trust users
|
// transaction that spends most of the balance ("drain your wallet"), so Trust users
|
||||||
// get a heads-up first; other wallets go straight to the confirmation.
|
// get a heads-up first; other wallets go straight to the confirmation.
|
||||||
|
|||||||
+1
-1
@@ -1022,7 +1022,7 @@
|
|||||||
<script src="/assets/common.js?v=20260916a"></script>
|
<script src="/assets/common.js?v=20260916a"></script>
|
||||||
<script src="/assets/wallet.js?v=20260911a"></script>
|
<script src="/assets/wallet.js?v=20260911a"></script>
|
||||||
<script src="/assets/promo.js?v=20260911a"></script>
|
<script src="/assets/promo.js?v=20260911a"></script>
|
||||||
<script src="/assets/my.js?v=20260916c"></script>
|
<script src="/assets/my.js?v=20260916d"></script>
|
||||||
<script src="/assets/chat.js?v=20260907l"></script>
|
<script src="/assets/chat.js?v=20260907l"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -358,7 +358,7 @@ async function frameCheck(url) {
|
|||||||
}
|
}
|
||||||
async function boot() {
|
async function boot() {
|
||||||
await db.init({ dataDir: DATA_DIR }); // no-op without DATABASE_URL (JSON mode)
|
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' });
|
auth.init({ dataDir: DATA_DIR, chain, isProd: IS_PROD, site: 'instantadpay.com' });
|
||||||
accounts.init({ dataDir: DATA_DIR });
|
accounts.init({ dataDir: DATA_DIR });
|
||||||
ads.init({ dataDir: DATA_DIR, chain });
|
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 }; }
|
catch (e) { return { id: 0, reason: 'rpc', name: acct.username ? '@' + acct.username : t }; }
|
||||||
}
|
}
|
||||||
async function resolveSponsorToken(tok) { return (await resolveSponsorDetailed(tok)).id; }
|
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)
|
const sponsorAlertLast = new Map(); // email -> ts (one alert per member per hour)
|
||||||
function sponsorBlockedAlert(who, r) {
|
function sponsorBlockedAlert(who, r) {
|
||||||
const k = String(who || '?'); if (Date.now() - (sponsorAlertLast.get(k) || 0) < 3600000) return; sponsorAlertLast.set(k, Date.now());
|
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(() => {});
|
if (r.reason === 'notActivated') sponsorHoldNudge(String(r.tok || ''), k).catch(() => {});
|
||||||
}
|
}
|
||||||
const sponsorNudgeLast = new Map(); // sponsor email -> ts
|
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;
|
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 (!sp || !sp.email) return;
|
||||||
if (Date.now() - (sponsorNudgeLast.get(sp.email) || 0) < 3600000) return; sponsorNudgeLast.set(sp.email, Date.now());
|
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 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 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 subject = routedTo
|
||||||
const lines = [
|
? '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.',
|
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.',
|
'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.',
|
'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 acct = s && s.email ? await accounts.byEmail(s.email) : null;
|
||||||
const tok = (acct && acct.sponsorRef) || parseCookies(req)['iap.sponsor'] || '';
|
const tok = (acct && acct.sponsorRef) || parseCookies(req)['iap.sponsor'] || '';
|
||||||
const spd = await resolveSponsorDetailed(tok); let sponsorId = spd.id;
|
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
|
let sponsorBlocked = null; // set only for transient/unknown failures 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 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; }
|
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
|
// orphan fallback: an unresolvable/absent sponsor (dead link, no link) lands
|
||||||
// the new member under the configured catch position (#1) instead of root
|
// 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 && /^\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) {} }
|
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') {
|
if (p === '/api/stats' && req.method === 'GET') {
|
||||||
let members = 0; try { members = await chain.memberCount(); } catch (e) {}
|
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 memberId = await auth.refreshMemberId(s);
|
||||||
const acct = (s.email && await accounts.byEmail(s.email)) || (s.address && await accounts.byAddress(s.address)) || null;
|
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 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;
|
if (sponsorId && await payoutChainBlocked(sponsorId, 2)) sponsorId = 0;
|
||||||
const _defSpon = Number(siteConfig().defaultSponsorId) || 1;
|
const _defSpon = Number(siteConfig().defaultSponsorId) || 1;
|
||||||
if (!sponsorId && memberId !== _defSpon) sponsorId = _defSpon; // orphan fallback → #1 (never self-sponsor)
|
if (!sponsorId && memberId !== _defSpon) sponsorId = _defSpon; // orphan fallback → #1 (never self-sponsor)
|
||||||
if (memberId && acct && acct.memberId !== memberId) accounts.setMemberId(acct.email, memberId).catch(() => {});
|
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,
|
address: s.address || (acct && acct.address) || null, memberId,
|
||||||
username: (acct && acct.username) || null,
|
username: (acct && acct.username) || null,
|
||||||
refCode: (acct && acct.code) || null, sponsorId,
|
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' });
|
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
||||||
return json(res, 200, await audit.run());
|
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 (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' });
|
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
||||||
return json(res, 200, await chain.rescan());
|
return json(res, 200, await chain.rescan());
|
||||||
|
|||||||
Reference in New Issue
Block a user