From 0a19e77e7fca2f7f4b795acff359ca7d77168296 Mon Sep 17 00:00:00 2001 From: martbost Date: Mon, 14 Sep 2026 10:44:36 -0500 Subject: [PATCH] Badges count qualifying buyers across linked positions (never regress); line chips light on any of a member's positions; buyers tile notes linked-position buyers Co-Authored-By: Claude Fable 5.1 --- chatbot.js | 1 + public/assets/my.js | 2 +- public/my.html | 4 ++-- server.js | 17 +++++++++++++---- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/chatbot.js b/chatbot.js index bb3e23c..88d98a6 100644 --- a/chatbot.js +++ b/chatbot.js @@ -83,6 +83,7 @@ FACTS: - BLOG (public, instantadpay.com/blog): Marty's coaching and teaching articles on building a line, advertising that pays, and daily habits; each article has its own page and can be shared; RSS at /blog/feed.xml. Members who want to write their own articles: not offered today. - ACHIEVEMENT BADGES ON TELEGRAM (2026-09-13): when a member unlocks Spark/Surge/Circuit/Nexus, their personalised badge image (username on the ribbon) is posted automatically to the team's Telegram payments topic and the main group, once per badge; members cannot trigger posts themselves (the 'Post to Telegram' button is admin-only); 'Share' opens a picker (X, Facebook, Telegram, WhatsApp, LinkedIn, Massifly (copies the post and opens the feed composer), the phone's own share menu, copy link, save image) that shares the member's public badge page instantadpay.com/b//, which shows the badge and their join link. - PROMO TOOLKIT BY BADGE + AI COPY ENGINE (2026-09-14): Promo tools > AI Copy Engine shows the ladder: Free (links, posts, swipes, banners, wall, objections, shorts, badge pages), Spark (payouts on: one-tap campaign templates aimed at the member's link, printable handout with their QR at /handout/), Surge (first qualifying buyer; the AI Copy Engine is live: posts, DMs, follow-ups, objection replies, emails, story posts, team broadcasts in the member's name with their link, honesty rules built in; 20 free generations a month), Circuit (60 free; Video Maker renders every promo video and short with the member's own end card and QR, hosted for download; split tester compares join angles by views, joins, buyers), Nexus (150 free; Leader Ops: three-level team triage with stalled flags and one-click nudges, AI-drafted team broadcasts, credit grants from the leader's earned pool to anyone in their line, co-branded join page showing the sponsor's bio, and the member's own partner code: welcome credits funded from the leader's pool at each redemption, plus a partner kit page /partners?ref=&promo=CODE). After the free allowance each generation costs 10 ad credits from the earned pool. Credits are advertising, never money. +- 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. 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. - NO-PAYOUT POSITIONS (2026-09-14): admin can list member ids (Settings > No-payout positions) whose wallets must never be paid again (e.g. a compromised key). Those positions stay linked for history and counting only: the buy-from picker disables them, a purchase from them is refused, and invite links that would place a new member under them (or under their upline chain) route to the fallback sponsor instead. The contract itself cannot change a member's wallet. - MY LINE SPONSOR LINE (2026-09-14): every row on My line, all three levels, shows who sponsored that person ("sponsored by @name", or "you" for directs). - MY LINE ACTIVITY DROP-DOWN (2026-09-14): every row on My line, all three levels, has an Activity button that opens that person's activity: last seen, joined, stage and next step, ads viewed today and claim streak, campaigns active, link views in 30 days and joins, directs and qualifying buyers, wallet/payouts, badges, and a Working or Quiet verdict. Read-only, phone friendly. diff --git a/public/assets/my.js b/public/assets/my.js index 2054d81..1f1b5d1 100644 --- a/public/assets/my.js +++ b/public/assets/my.js @@ -526,7 +526,7 @@ $('dbCredits').textContent = ((d.credits || 0) + earnedAv).toLocaleString(); $('dbCreditsSub').textContent = (d.credits || 0).toLocaleString() + ' purchased' + (d.creditedCredits ? ' (' + d.creditedCredits.toLocaleString() + ' credited to you)' : '') + ' · ' + earnedAv.toLocaleString() + ' earned' + (inCamp ? ' · ' + inCamp.toLocaleString() + ' in campaigns' : ''); $('dbEarned').textContent = IAP.fmtPol(d.earnedWei || '0'); - $('dbBuyers').textContent = d.buyerCount || 0; + $('dbBuyers').textContent = d.buyerCount || 0; if (d.buyerCountPositions) { const sub = $('dbBuyersSub'); if (sub) sub.textContent = '+' + d.buyerCountPositions + ' on your linked positions (count toward badges; levels pay on your main position)'; } $('dbTeam').textContent = (d.referrals || []).length; // trend chips: only real, computable facts const ec = $('dbEarnedChip'); diff --git a/public/my.html b/public/my.html index e4a9208..8c384a8 100644 --- a/public/my.html +++ b/public/my.html @@ -183,7 +183,7 @@
-
0
Qualifying buyers
+
0
Qualifying buyers
@@ -984,7 +984,7 @@ - + diff --git a/server.js b/server.js index f7abb98..30c1d83 100644 --- a/server.js +++ b/server.js @@ -1099,7 +1099,12 @@ const server = http.createServer(async (req, res) => { try { const mm = await chain.member(memberId); out.buyerCount = mm.buyerCount; - const bal = await ads.balances((await myMemberIds(s)).ids, s.email); + const mine = await myMemberIds(s); + // linked positions are the member's own: their qualifying buyers count toward the account (Jim, 2026-09-14) + out.buyerCountMain = mm.buyerCount; out.buyerCountPositions = 0; + for (const id of mine.ids) if (id && id !== memberId) { try { out.buyerCountPositions += (await chain.member(id)).buyerCount || 0; } catch (e) {} } + out.buyerCountAll = out.buyerCountMain + out.buyerCountPositions; // badges count every position; levels the contract pays #main on depend on buyerCount alone + const bal = await ads.balances(mine.ids, s.email); out.credits = bal.total; out.creditedCredits = bal.credited; out.earnedCredits = bal.earned; out.inCampaigns = bal.inCampaigns; out.availableCredits = bal.available; } catch (e) { out.chainReadError = true; } let earned = 0n, n = 0; @@ -1114,12 +1119,13 @@ const server = http.createServer(async (req, res) => { // achievement milestones (same ladder as the Overview stepper) + one-time credit // bonuses — computed AFTER buyerCount is read from chain above (else always 0) { - const bc = out.buyerCount || 0; + const bc = out.buyerCountAll != null ? out.buyerCountAll : (out.buyerCount || 0); const reached = []; if (out.memberId) reached.push('payouts'); if (bc >= 1) reached.push('firstBuyer'); if (bc >= 2) reached.push('level2'); if (bc >= 5) reached.push('level3'); + try { for (const k of await ads.milestonesOf(out.email)) if (!reached.includes(k)) reached.push(k); } catch (e) {} // a badge once earned stays earned out.milestonesReached = reached; if (out.email && reached.length) out.milestonesGranted = await ads.grantMilestones(out.email, reached); } @@ -1677,6 +1683,9 @@ const server = http.createServer(async (req, res) => { const label = a => a.username ? '@' + a.username : (a.memberId ? 'member #' + a.memberId : 'member'); for (const a of [meAcct, ...levels.flatMap(L => L.members)].filter(Boolean)) { const n = label(a); if (a.username) byTok[String(a.username).toLowerCase()] = n; if (a.code) byTok[String(a.code).toLowerCase()] = n; if (a.memberId) byTok[String(a.memberId)] = n; } const sponsorOf = m => { const t = String(m.sponsorRef || '').toLowerCase(); if (!t) return null; if (meAcct && (t === String(meAcct.username || '').toLowerCase() || t === String(meAcct.code || '').toLowerCase() || t === String(meAcct.memberId || ''))) return 'you'; return byTok[t] || ('@' + t); }; + // a member's linked positions are theirs too: a qualifying buy from one of them lights their chip + const posIds = {}; for (const m of levels.flatMap(L => L.members)) { try { posIds[m.email] = (await accounts.positions(m.email)).map(p => p.memberId).filter(Boolean); } catch (e) { posIds[m.email] = []; } } + const idsOf = m => [m.memberId, ...(posIds[m.email] || [])].filter(Boolean); const out = levels.map(L => ({ level: L.level, members: L.members.map(m => ({ memberId: m.memberId || 0, sponsor: sponsorOf(m), @@ -1684,8 +1693,8 @@ const server = http.createServer(async (req, res) => { ref: m.code || null, // opens the activity drop-down (Clinton's ask, 2026-09-14): any of the three levels email: L.level === 1 ? m.email : null, // directs only joined: m.created, - qualified: !!(m.memberId && qualified.has(m.memberId)), - earnedWei: (m.memberId && earnedBy[m.memberId]) || '0' })) })); + qualified: idsOf(m).some(id => qualified.has(id)), + earnedWei: idsOf(m).reduce((n, id) => n + BigInt(earnedBy[id] || '0'), 0n).toString() })) })); // the member's own linked positions sit on level 1 too, labelled as theirs if (own.length) { if (!out.find(L => L.level === 1)) out.unshift({ level: 1, members: [] });