Payment trace: self-serve "who was paid, who was skipped and why" per purchase (Earnings tab + admin member card)
Marty, 2026-09-16, after the third "why didn't my sponsor get paid" thread of the day (livedreams / gracie25,
Morten / Terry's linked wallets). GET /api/my/trace?who=<#|username> (yourself, anyone up to 3 levels
below you, or your own 3 uplines) and GET /api/admin/trace?who= list every purchase the member was
part of, newest first: buyer, sponsor, package, then levels 1-3 with the recipient, the skipped
positions and the reason ("not qualified: had N of 2 qualifying buyers then"), the platform share, and
a verify link. Linked extra wallets are named after their owner. Earnings tab card "Trace a payment";
admin member card gets a "Payment trace" section; chatbot canned answer + AI prompt route the
question there.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,8 @@ const CANNED = [
|
|||||||
a: 'Every package splits the same way: 50 percent to the direct sponsor, 20 percent to level 2, 10 percent to level 3, 20 percent to the platform. Those numbers are constants in the contract and cannot be changed. Play with scenarios on the home page calculator. No income is promised; nobody earns unless real ad buying happens.' },
|
a: 'Every package splits the same way: 50 percent to the direct sponsor, 20 percent to level 2, 10 percent to level 3, 20 percent to the platform. Those numbers are constants in the contract and cannot be changed. Play with scenarios on the home page calculator. No income is promised; nobody earns unless real ad buying happens.' },
|
||||||
{ re: /(balance (went|going|keeps going) down|credits? (disappear|missing|gone|dropping|went down|ticking)|why (is|did) my (balance|credits)|in campaigns|set aside|reserved credits|available to spend)/i,
|
{ re: /(balance (went|going|keeps going) down|credits? (disappear|missing|gone|dropping|went down|ticking)|why (is|did) my (balance|credits)|in campaigns|set aside|reserved credits|available to spend)/i,
|
||||||
a: 'One rule: your balance is what is NOT committed to a live campaign. When you start a campaign its whole budget is set aside right then (earned credits first, then purchased), so your Available number drops once and then sits still while the ad runs. The budget spends down inside that campaign in Members > Campaigns, where you can see delivered views, network views and what is left. The dashboard shows Purchased available, Earned available and In live campaigns as three separate numbers; a paused campaign keeps its unspent budget set aside so it can resume. Nothing is lost when a balance reads low while a campaign is live. Purchased credits live on the blockchain; if we ever refund or credit purchased money it shows under Purchased as "credited to you" and spends like a purchase, login ads included. Earned credits fund every format except login ads. Two formats are flat buys paid in full when booked: featured links (sold by the day) and verified-visit packs. Adding credits to a featured link does nothing, so its row offers Extend run instead, which books more days at the daily rate; a visit pack offers Buy more visits. Every credit that comes in or goes out is listed with its reason under Members > Campaigns > Credit activity (also linked from Earn credits): sign-in bonus, daily claim, video watched, verified visit, campaign buys, views delivered per day, grants and refunds. Check it before assuming credits went missing.' },
|
a: 'One rule: your balance is what is NOT committed to a live campaign. When you start a campaign its whole budget is set aside right then (earned credits first, then purchased), so your Available number drops once and then sits still while the ad runs. The budget spends down inside that campaign in Members > Campaigns, where you can see delivered views, network views and what is left. The dashboard shows Purchased available, Earned available and In live campaigns as three separate numbers; a paused campaign keeps its unspent budget set aside so it can resume. Nothing is lost when a balance reads low while a campaign is live. Purchased credits live on the blockchain; if we ever refund or credit purchased money it shows under Purchased as "credited to you" and spends like a purchase, login ads included. Earned credits fund every format except login ads. Two formats are flat buys paid in full when booked: featured links (sold by the day) and verified-visit packs. Adding credits to a featured link does nothing, so its row offers Extend run instead, which books more days at the daily rate; a visit pack offers Buy more visits. Every credit that comes in or goes out is listed with its reason under Members > Campaigns > Credit activity (also linked from Earn credits): sign-in bonus, daily claim, video watched, verified visit, campaign buys, views delivered per day, grants and refunds. Check it before assuming credits went missing.' },
|
||||||
|
{ re: /(why (didn'?t|did not|hasn'?t|has not) (my sponsor|i|he|she|they|.{1,30}) (get|got|been) paid|didn'?t get paid|not get paid|missed (a |the )?(payment|commission)|trace (a )?payment|payment trace|who got paid)/i,
|
||||||
|
a: 'Every purchase and every share is on the chain, and you can read it yourself: open Members > Earnings > Trace a payment, enter the member number or username (yourself, anyone up to 3 levels below you, or your own uplines), and each purchase they were part of is laid out level by level: who was paid, who was skipped and why. The three usual answers: the person had not unlocked that level when the purchase settled (level 2 needs 2 qualifying $20+ buyers, level 3 needs 5, and qualification never applies backwards); the buyer used Qualified Start, so the purchase came from a linked wallet whose sponsor is the buyer\'s own main position, which means level 1 went to the buyer themselves; or the person\'s payouts were not switched on. Skipped shares pass up to the next qualified person, never to nobody.' },
|
||||||
{ re: /(qualif|unlock level|level 2|level 3|pass.?up)/i,
|
{ re: /(qualif|unlock level|level 2|level 3|pass.?up)/i,
|
||||||
a: 'Two different things are earned here. CREDITS: every member, free ones included, earns ad credits by viewing ads, videos, visits and the daily sign-in; credits buy your own ads and are never withdrawn. POL: real crypto paid to your wallet when your referrals buy packages, once you are activated: the $20 starter package plus payouts switched on from your wallet. Level 1 pays 50 percent of the packages your direct referrals buy, from the day you are activated. Bring 2 buyers of $20 or more and level 2 unlocks; 5 unlock level 3. When a level is not qualified, its share climbs the sponsor line, checking up to 25 positions, and pays the first qualified person. Qualification never expires and cannot be bought. Qualified Start: you may link extra wallets of your own as positions under your account (Buy packages > Qualified Start); each one that buys a $20+ package counts as a qualifying buyer, its credits pool with yours, and 50% of its purchase comes back to your main wallet. Your own money, your own wallets, a faster start, never an income promise.' },
|
a: 'Two different things are earned here. CREDITS: every member, free ones included, earns ad credits by viewing ads, videos, visits and the daily sign-in; credits buy your own ads and are never withdrawn. POL: real crypto paid to your wallet when your referrals buy packages, once you are activated: the $20 starter package plus payouts switched on from your wallet. Level 1 pays 50 percent of the packages your direct referrals buy, from the day you are activated. Bring 2 buyers of $20 or more and level 2 unlocks; 5 unlock level 3. When a level is not qualified, its share climbs the sponsor line, checking up to 25 positions, and pays the first qualified person. Qualification never expires and cannot be bought. Qualified Start: you may link extra wallets of your own as positions under your account (Buy packages > Qualified Start); each one that buys a $20+ package counts as a qualifying buyer, its credits pool with yours, and 50% of its purchase comes back to your main wallet. Your own money, your own wallets, a faster start, never an income promise.' },
|
||||||
{ re: /(free member|without (a )?wallet|do i (have to|need to) buy|earn (in )?credits|credits (vs|versus|or) pol|earn pol|get paid in)/i,
|
{ re: /(free member|without (a )?wallet|do i (have to|need to) buy|earn (in )?credits|credits (vs|versus|or) pol|earn pol|get paid in)/i,
|
||||||
@@ -93,6 +95,7 @@ FACTS:
|
|||||||
- (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.
|
- (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.
|
||||||
|
- PAYMENT TRACE (2026-09-16): Members > Earnings > "Trace a payment": enter a member number or @username (yourself, anyone up to 3 levels below you, or your own 3 uplines) and every purchase they were part of is listed tier by tier — who was paid at level 1/2/3 (50/20/10%), who was skipped and why ("not qualified: had N of 2 qualifying buyers then"), the platform's 20%, and a verify link to the transaction. Use it to answer "why didn't my sponsor / why didn't I get paid on X's purchase" — the usual answers are (a) the sponsor had not unlocked that level when the purchase settled (qualification never applies backwards), (b) the buyer used Qualified Start, so the purchase came from a linked wallet whose on-chain sponsor is the buyer's own main position (level 1 goes to the buyer themselves), (c) the sponsor's payouts were not switched on. Admins see the same trace under the member card in Admin > Members.
|
||||||
- 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.
|
||||||
- PAYMENT + MISSED-PAYMENT NOTICES (2026-09-15): every payout that lands (who bought, level, share in POL and dollars, transaction link) and every missed payout is delivered BOTH by email and as an on-site inbox message from the company account, shown in the login pop-up and the Messages card, so it is waiting when the member signs in. MISSED-PAYOUT detail: when a level-2 or level-3 share passes a member by because that level is not open on their account, the member gets an email the same minute: who bought, the POL and dollar amount they missed, how many qualifying buyers they have versus the 2 or 5 needed, and the two ways to close the gap (bring buyers, or Qualified Start). Qualified members whose wallet rejected a transfer get a different email telling them to link a regular wallet.
|
- PAYMENT + MISSED-PAYMENT NOTICES (2026-09-15): every payout that lands (who bought, level, share in POL and dollars, transaction link) and every missed payout is delivered BOTH by email and as an on-site inbox message from the company account, shown in the login pop-up and the Messages card, so it is waiting when the member signs in. MISSED-PAYOUT detail: when a level-2 or level-3 share passes a member by because that level is not open on their account, the member gets an email the same minute: who bought, the POL and dollar amount they missed, how many qualifying buyers they have versus the 2 or 5 needed, and the two ways to close the gap (bring buyers, or Qualified Start). Qualified members whose wallet rejected a transfer get a different email telling them to link a regular wallet.
|
||||||
- LAUNCH WEEK SWIPES (2026-09-15): the founding-week checklist page (/launch) ends with four promoter emails members send to their OWN lists, one a day toward Monday's opening, with the member's invite link and the FOUNDER code (500 credits for anyone who joins before Mon 2026-09-21 9 AM Central) filled in, each with a Copy button. These are swipes for members, not emails the site sends.
|
- LAUNCH WEEK SWIPES (2026-09-15): the founding-week checklist page (/launch) ends with four promoter emails members send to their OWN lists, one a day toward Monday's opening, with the member's invite link and the FOUNDER code (500 credits for anyone who joins before Mon 2026-09-21 9 AM Central) filled in, each with a Copy button. These are swipes for members, not emails the site sends.
|
||||||
|
|||||||
+1
-1
@@ -487,6 +487,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/assets/common.js?v=20260916a"></script>
|
<script src="/assets/common.js?v=20260916a"></script>
|
||||||
<script src="/assets/admin.js?v=20260916a"></script>
|
<script src="/assets/admin.js?v=20260916b"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -364,6 +364,31 @@
|
|||||||
+ (d.campaigns.length ? d.campaigns.map(c => '<tr><td>' + c.id + '</td><td>' + esc(c.type) + '</td><td>' + esc(c.status) + '</td><td>' + Number(c.budget || 0).toLocaleString() + '</td><td>' + Number(c.spent || 0).toLocaleString() + '</td><td>' + Number(c.views || 0).toLocaleString() + '</td><td>' + Number(c.clicks || 0).toLocaleString() + '</td><td class="small muted">' + when(c.created) + '</td></tr>').join('') : '<tr><td colspan="8" class="muted">No campaigns.</td></tr>') + '</table></div>';
|
+ (d.campaigns.length ? d.campaigns.map(c => '<tr><td>' + c.id + '</td><td>' + esc(c.type) + '</td><td>' + esc(c.status) + '</td><td>' + Number(c.budget || 0).toLocaleString() + '</td><td>' + Number(c.spent || 0).toLocaleString() + '</td><td>' + Number(c.views || 0).toLocaleString() + '</td><td>' + Number(c.clicks || 0).toLocaleString() + '</td><td class="small muted">' + when(c.created) + '</td></tr>').join('') : '<tr><td colspan="8" class="muted">No campaigns.</td></tr>') + '</table></div>';
|
||||||
$('mcBody').innerHTML = h;
|
$('mcBody').innerHTML = h;
|
||||||
if (location.hash !== '#members') history.replaceState(null, '', '#members');
|
if (location.hash !== '#members') history.replaceState(null, '', '#members');
|
||||||
|
loadTrace(d);
|
||||||
|
}
|
||||||
|
// payment trace under the member card (2026-09-16): per purchase, who was paid / skipped and why
|
||||||
|
async function loadTrace(d) {
|
||||||
|
const ch = d.chain || {}; const mid = ch.memberId || (d.account && d.account.memberId) || 0;
|
||||||
|
const box = document.createElement('div'); box.id = 'mcTrace'; box.innerHTML = '<h4 style="margin:18px 0 6px">Payment trace</h4><p class="small muted">Reading the chain…</p>';
|
||||||
|
$('mcBody').appendChild(box);
|
||||||
|
if (!mid) { box.innerHTML = '<h4 style="margin:18px 0 6px">Payment trace</h4><p class="small muted">Not activated on the chain, nothing to trace.</p>'; return; }
|
||||||
|
try {
|
||||||
|
const r = await api('/api/admin/trace?who=' + mid);
|
||||||
|
const nm = id => id ? ('#' + id + (r.names[id] ? ' @' + esc(r.names[id]) : '')) : 'nobody';
|
||||||
|
let h = '<h4 style="margin:18px 0 6px">Payment trace</h4><p class="small muted" style="margin:0 0 6px">' + r.buyersNow + ' qualifying buyer' + (r.buyersNow === 1 ? '' : 's') + ' · every purchase this member was part of, newest first.</p>';
|
||||||
|
if (!r.purchases.length) h += '<p class="small muted">No purchases involving them yet.</p>';
|
||||||
|
for (const p of r.purchases) {
|
||||||
|
h += '<div style="border:1px solid rgba(255,255,255,.1);border-radius:8px;padding:8px 10px;margin:0 0 6px"><div class="small"><b>' + when(p.ts) + '</b> · ' + nm(p.buyerId) + ' bought $' + (p.priceCents / 100).toFixed(0) + ' (' + polOf(p.paidWei) + ' POL)' + (p.sponsorId ? ' · sponsor ' + nm(p.sponsorId) : '') + ' · <span class="muted">' + esc(p.tx.slice(0, 12)) + '…</span></div>';
|
||||||
|
for (const t of p.tiers) {
|
||||||
|
let line = '<b>L' + t.tier + ' ' + t.pct + '%</b>: ';
|
||||||
|
for (const x of t.skipped) line += '<span style="color:#f0a742">' + nm(x.id) + ' skipped (' + (x.reason === 'unqualified' ? 'had ' + x.buyersThen + ' of ' + (t.tier === 2 ? 2 : 5) + ' buyers then' : esc(x.reason)) + ')</span> → ';
|
||||||
|
line += t.paidTo ? '<span style="color:' + (t.paidTo === mid ? '#3ecf7a' : 'inherit') + '">' + nm(t.paidTo) + ' ' + polOf(t.amountWei) + ' POL</span>' : '<span class="muted">no catcher → company</span>';
|
||||||
|
h += '<div class="small" style="margin-top:3px">' + line + '</div>';
|
||||||
|
}
|
||||||
|
h += '<div class="small muted" style="margin-top:3px">platform ' + polOf(p.adminWei) + ' POL</div></div>';
|
||||||
|
}
|
||||||
|
box.innerHTML = h;
|
||||||
|
} catch (e) { box.innerHTML = '<h4 style="margin:18px 0 6px">Payment trace</h4><p class="small bad">' + esc(e.message) + '</p>'; }
|
||||||
}
|
}
|
||||||
function closeMember() { $('memCard').hidden = true; document.querySelectorAll('#pane-members > .card').forEach(c => { c.hidden = false; }); }
|
function closeMember() { $('memCard').hidden = true; document.querySelectorAll('#pane-members > .card').forEach(c => { c.hidden = false; }); }
|
||||||
// live matches while typing: any part of the username, email, member #, share code or wallet
|
// live matches while typing: any part of the username, email, member #, share code or wallet
|
||||||
|
|||||||
@@ -777,6 +777,45 @@
|
|||||||
fill('refFeed', a.referrals, 'No referral activity yet. Share your invite link.');
|
fill('refFeed', a.referrals, 'No referral activity yet. Share your invite link.');
|
||||||
fill('buyFeed', a.purchases, 'No purchases from your wallet yet.');
|
fill('buyFeed', a.purchases, 'No purchases from your wallet yet.');
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
initTrace();
|
||||||
|
}
|
||||||
|
// payment trace (2026-09-16): who was paid, who was skipped and why, per purchase
|
||||||
|
let traceInit = false;
|
||||||
|
function initTrace() {
|
||||||
|
if (traceInit || !$('trGo')) return; traceInit = true;
|
||||||
|
const run = async () => {
|
||||||
|
const who = ($('trWho').value || '').trim(); const out = $('trOut'); const msg = $('trMsg');
|
||||||
|
msg.textContent = 'Reading the chain…'; out.innerHTML = '';
|
||||||
|
try {
|
||||||
|
const c = await IAP.getConfig();
|
||||||
|
const r = await (await fetch('/api/my/trace?who=' + encodeURIComponent(who))).json();
|
||||||
|
if (r.error) { msg.textContent = r.error; return; }
|
||||||
|
msg.textContent = '';
|
||||||
|
const nm = id => id ? ('#' + id + (r.names[id] ? ' @' + esc(r.names[id]) : '')) : 'nobody';
|
||||||
|
const pol = w => IAP.fmtPol(w) + ' POL';
|
||||||
|
const when = ts => new Date(ts).toLocaleString([], { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' });
|
||||||
|
let h = '<p class="small" style="margin:0 0 8px"><b>' + nm(r.memberId) + '</b> · ' + (r.activated ? 'activated under ' + nm(r.sponsorId) : 'not activated on the chain yet') + ' · ' + r.buyersNow + ' qualifying buyer' + (r.buyersNow === 1 ? '' : 's') + ' (level 2 needs 2, level 3 needs 5)</p>';
|
||||||
|
if (!r.purchases.length) { out.innerHTML = h + '<p class="muted small">No purchases involving ' + nm(r.memberId) + ' yet.</p>'; return; }
|
||||||
|
for (const p of r.purchases) {
|
||||||
|
const isBuyer = p.buyerId === r.memberId;
|
||||||
|
h += '<div class="card" style="padding:10px 12px;margin:0 0 8px">'
|
||||||
|
+ '<div class="small"><b>' + when(p.ts) + '</b> · ' + nm(p.buyerId) + ' bought a $' + (p.priceCents / 100).toFixed(0) + ' package (' + pol(p.paidWei) + ')' + (p.sponsorId ? ' · sponsor ' + nm(p.sponsorId) : '')
|
||||||
|
+ (c.explorer ? ' · <a target="_blank" rel="noopener" href="' + c.explorer + '/tx/' + p.tx + '">verify ↗</a>' : '') + '</div>';
|
||||||
|
for (const t of p.tiers) {
|
||||||
|
const me = t.paidTo === r.memberId, skippedMe = t.skipped.some(x => x.id === r.memberId);
|
||||||
|
let line = '<b>Level ' + t.tier + ' (' + t.pct + '%)</b>: ';
|
||||||
|
for (const x of t.skipped) line += '<span style="color:#f0a742">' + nm(x.id) + ' skipped (' + (x.reason === 'unqualified' ? 'not qualified: had ' + x.buyersThen + ' of ' + (t.tier === 2 ? 2 : 5) + ' qualifying buyers then' : esc(x.reason)) + ')</span> → ';
|
||||||
|
line += t.paidTo ? '<span style="color:' + (me ? '#3ecf7a' : 'inherit') + '">' + nm(t.paidTo) + ' paid ' + pol(t.amountWei) + '</span>' : '<span class="muted">no eligible catcher within reach → company</span>';
|
||||||
|
h += '<div class="small" style="margin-top:4px' + (me || skippedMe ? ';background:rgba(255,255,255,.04);border-radius:6px;padding:2px 6px' : '') + '">' + line + '</div>';
|
||||||
|
}
|
||||||
|
h += '<div class="small muted" style="margin-top:4px">Platform (20%): ' + pol(p.adminWei) + (isBuyer ? '' : '') + '</div></div>';
|
||||||
|
}
|
||||||
|
h += '<p class="muted small" style="margin:6px 0 0">A skipped level means that person had not unlocked it when the purchase settled. Qualification never applies backwards; the next purchase after they qualify pays them.</p>';
|
||||||
|
out.innerHTML = h;
|
||||||
|
} catch (e) { msg.textContent = 'Could not read the chain just now. Try again in a minute.'; }
|
||||||
|
};
|
||||||
|
$('trGo').addEventListener('click', run);
|
||||||
|
$('trWho').addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); run(); } });
|
||||||
}
|
}
|
||||||
|
|
||||||
// credit activity ledger under the campaign table (2026-09-16): every earn and spend with its reason
|
// credit activity ledger under the campaign table (2026-09-16): every earn and spend with its reason
|
||||||
|
|||||||
+11
-1
@@ -693,6 +693,16 @@
|
|||||||
<div class="feed" id="buyFeed"><div class="row muted">Nothing yet.</div></div>
|
<div class="feed" id="buyFeed"><div class="row muted">Nothing yet.</div></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card" id="traceCard">
|
||||||
|
<h3>Trace a payment</h3>
|
||||||
|
<p class="muted small" style="margin:0 0 8px">Wondering why someone did or did not get paid on a purchase? Enter a member number or username (yourself, anyone up to 3 levels below you, or your own uplines) and every purchase they were part of is laid out tier by tier: who was paid, who was skipped and why.</p>
|
||||||
|
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
|
||||||
|
<input id="trWho" placeholder="member # or @username" style="max-width:260px" autocomplete="off">
|
||||||
|
<button class="btn small" id="trGo" type="button">Trace</button>
|
||||||
|
<span class="muted small" id="trMsg"></span>
|
||||||
|
</div>
|
||||||
|
<div id="trOut" style="margin-top:10px"></div>
|
||||||
|
</div>
|
||||||
<div class="card" id="earnGate" hidden>
|
<div class="card" id="earnGate" hidden>
|
||||||
<h3>Your on-chain history starts with payouts</h3>
|
<h3>Your on-chain history starts with payouts</h3>
|
||||||
<p class="muted small">Once payouts are switched on, every payment, referral event, and purchase in
|
<p class="muted small">Once payouts are switched on, every payment, referral event, and purchase in
|
||||||
@@ -1022,7 +1032,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=20260916d"></script>
|
<script src="/assets/my.js?v=20260916e"></script>
|
||||||
<script src="/assets/chat.js?v=20260907l"></script>
|
<script src="/assets/chat.js?v=20260907l"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -502,6 +502,49 @@ function isAdmin(req) {
|
|||||||
return !!adminFromRequest(req); // /admin portal session
|
return !!adminFromRequest(req); // /admin portal session
|
||||||
}
|
}
|
||||||
// attach a memberId->username map to events so activity shows real people
|
// attach a memberId->username map to events so activity shows real people
|
||||||
|
// Payment trace (Marty, 2026-09-16): every purchase a member is involved in, tier by tier — who was
|
||||||
|
// paid, who was skipped and why (with how many qualifying buyers the skipped person had at that block)
|
||||||
|
// — so "why didn't X get paid on Y's buy" becomes a self-serve lookup instead of a support thread.
|
||||||
|
async function nameForMember(id) {
|
||||||
|
try { const a = await accounts.byMemberId(id); if (a && a.username) return a.username; } catch (e) {}
|
||||||
|
if (db.enabled()) { // a linked extra wallet: name it after its owner
|
||||||
|
try { const r = await db.q('SELECT email FROM positions WHERE member_id=? LIMIT 1', [Number(id)]); if (r.length) { const a = await accounts.byEmail(r[0].email); if (a && a.username) return a.username + ' (linked wallet)'; } } catch (e) {}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function sponsorMapFromIndex(evs) { const m = new Map(); for (const e of evs) if (e.type === 'MemberActivated' && e.id) m.set(e.id, Number(e.sponsorId) || 0); return m; }
|
||||||
|
function uplineIds(map, id, hops) { const out = []; let cur = map.get(Number(id)) || 0; for (let i = 0; i < hops && cur; i++) { out.push(cur); cur = map.get(cur) || 0; } return out; }
|
||||||
|
async function tracePurchases(targetId, limit) {
|
||||||
|
const id = Number(targetId) || 0; if (!id) return { error: 'Which member?' };
|
||||||
|
const evs = chain.recentEvents(1e9); // whole history, newest first
|
||||||
|
const byTx = new Map();
|
||||||
|
for (const e of evs) { if (!e.tx) continue; let g = byTx.get(e.tx); if (!g) { g = []; byTx.set(e.tx, g); } g.push(e); }
|
||||||
|
const counted = evs.filter(e => e.type === 'BuyerCounted');
|
||||||
|
const buyersBefore = (sid, block) => counted.filter(e => e.sponsorId === sid && e.block < block).length;
|
||||||
|
const out = [];
|
||||||
|
for (const e of evs) {
|
||||||
|
if (e.type !== 'Purchase') continue;
|
||||||
|
const g = byTx.get(e.tx) || [];
|
||||||
|
const paid = g.filter(x => x.type === 'TierPaid' && x.buyerId === e.buyerId);
|
||||||
|
const skipped = g.filter(x => x.type === 'PassedUp' && x.buyerId === e.buyerId);
|
||||||
|
const act = g.find(x => x.type === 'MemberActivated' && x.id === e.buyerId);
|
||||||
|
const admin = g.find(x => x.type === 'AdminPaid' && x.buyerId === e.buyerId);
|
||||||
|
const involved = e.buyerId === id || paid.some(x => x.recipientId === id) || skipped.some(x => x.skippedId === id) || (act && act.sponsorId === id);
|
||||||
|
if (!involved) continue;
|
||||||
|
const tiers = [1, 2, 3].map(t => {
|
||||||
|
const p = paid.find(x => x.tier === t); const sk = skipped.filter(x => x.tier === t);
|
||||||
|
return { tier: t, pct: [50, 20, 10][t - 1], paidTo: p ? p.recipientId : 0, amountWei: p ? p.amountWei : '0', hops: p ? p.hops : 0,
|
||||||
|
skipped: sk.map(x => ({ id: x.skippedId, reason: x.reason, buyersThen: x.reason === 'unqualified' ? buyersBefore(x.skippedId, e.block) : null })) };
|
||||||
|
});
|
||||||
|
out.push({ ts: e.ts, block: e.block, tx: e.tx, buyerId: e.buyerId, sponsorId: act ? act.sponsorId : null, priceCents: e.priceCents, paidWei: e.paidWei, tiers, adminWei: admin ? admin.amountWei : '0' });
|
||||||
|
if (out.length >= (limit || 30)) break;
|
||||||
|
}
|
||||||
|
const ids = new Set([id]);
|
||||||
|
for (const p of out) { ids.add(p.buyerId); if (p.sponsorId) ids.add(p.sponsorId); for (const t of p.tiers) { if (t.paidTo) ids.add(t.paidTo); for (const x of t.skipped) ids.add(x.id); } }
|
||||||
|
const names = {}; for (const i of ids) { const n = await nameForMember(i); if (n) names[i] = n; }
|
||||||
|
const smap = sponsorMapFromIndex(evs);
|
||||||
|
return { memberId: id, name: names[id] || null, activated: smap.has(id), sponsorId: smap.get(id) || 0, buyersNow: counted.filter(e => e.sponsorId === id).length, purchases: out, names };
|
||||||
|
}
|
||||||
async function attachNames(evts) {
|
async function attachNames(evts) {
|
||||||
try {
|
try {
|
||||||
const ids = [];
|
const ids = [];
|
||||||
@@ -2491,6 +2534,21 @@ const server = http.createServer(async (req, res) => {
|
|||||||
res.writeHead(302, baseHeaders({ Location: target }));
|
res.writeHead(302, baseHeaders({ Location: target }));
|
||||||
return res.end();
|
return res.end();
|
||||||
}
|
}
|
||||||
|
if (p === '/api/my/trace' && req.method === 'GET') { // payment trace for yourself or anyone within 3 levels of you, either direction
|
||||||
|
const s = await auth.fromRequest(req);
|
||||||
|
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
|
||||||
|
const mine = (await myMemberIds(s)).ids.filter(Boolean);
|
||||||
|
const who = String(u.searchParams.get('who') || '').trim().replace(/^[@#]/, '').toLowerCase();
|
||||||
|
let target = /^\d+$/.test(who) ? Number(who) : 0;
|
||||||
|
if (!target && who) { let a = await accounts.byUsername(who); if (!a) a = await accounts.byCode(who); if (a && a.memberId) target = a.memberId; }
|
||||||
|
if (!target && !who && mine.length) target = mine[0];
|
||||||
|
if (!target) return json(res, 400, { error: who ? 'No activated member called "' + who.slice(0, 40) + '". Use a member number or a username of someone who has bought a package.' : 'Enter a member number or username.' });
|
||||||
|
if (!mine.length) return json(res, 403, { error: 'Link a wallet and buy a package first; the trace works on chain positions.' });
|
||||||
|
const smap = sponsorMapFromIndex(chain.recentEvents(1e9));
|
||||||
|
const ok = mine.includes(target) || uplineIds(smap, target, 3).some(x => mine.includes(x)) || mine.some(m => uplineIds(smap, m, 3).includes(target));
|
||||||
|
if (!ok) return json(res, 403, { error: 'You can trace yourself, anyone up to 3 levels below you, and your own 3 uplines.' });
|
||||||
|
return json(res, 200, await tracePurchases(target, 30));
|
||||||
|
}
|
||||||
if (p === '/api/my/credits/activity' && req.method === 'GET') { // credit ledger: every earn and spend with a reason, newest first
|
if (p === '/api/my/credits/activity' && req.method === 'GET') { // credit ledger: every earn and spend with a reason, newest first
|
||||||
const s = await auth.fromRequest(req);
|
const s = await auth.fromRequest(req);
|
||||||
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
|
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
|
||||||
@@ -2673,6 +2731,14 @@ const server = http.createServer(async (req, res) => {
|
|||||||
const walk = detailed.reason === 'notActivated' ? await walkUpActivatedSponsor(ref) : null;
|
const walk = detailed.reason === 'notActivated' ? await walkUpActivatedSponsor(ref) : null;
|
||||||
return json(res, 200, { ref, detailed, walk, catchId: Number(siteConfig().defaultSponsorId) || 1 });
|
return json(res, 200, { ref, detailed, walk, catchId: Number(siteConfig().defaultSponsorId) || 1 });
|
||||||
}
|
}
|
||||||
|
if (p === '/api/admin/trace' && req.method === 'GET') { // payment trace for any member
|
||||||
|
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
||||||
|
const who = String(u.searchParams.get('who') || '').trim().replace(/^[@#]/, '').toLowerCase();
|
||||||
|
let target = /^\d+$/.test(who) ? Number(who) : 0;
|
||||||
|
if (!target && who) { let a = await accounts.byUsername(who); if (!a) a = await accounts.byCode(who); if (!a && who.includes('@')) a = await accounts.byEmail(who); if (a && a.memberId) target = a.memberId; }
|
||||||
|
if (!target) return json(res, 400, { error: 'No activated member matches "' + who.slice(0, 40) + '".' });
|
||||||
|
return json(res, 200, await tracePurchases(target, 60));
|
||||||
|
}
|
||||||
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