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 <noreply@anthropic.com>
This commit is contained in:
@@ -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/<username>/<badge>, 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/<username>), 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=<username>&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.
|
||||
|
||||
+1
-1
@@ -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');
|
||||
|
||||
+2
-2
@@ -183,7 +183,7 @@
|
||||
</div>
|
||||
<div class="statx c-violet">
|
||||
<div class="pl"><svg viewBox="0 0 24 24"><circle cx="12" cy="7" r="3.2"/><circle cx="5" cy="17" r="2.6"/><circle cx="19" cy="17" r="2.6"/><path d="M12 10v3M12 13l-5 2M12 13l5 2"/></svg></div>
|
||||
<div><div class="nv" id="dbBuyers">0</div><div class="lb">Qualifying buyers</div><span class="chip" id="dbBuyersChip" hidden></span></div>
|
||||
<div><div class="nv" id="dbBuyers">0</div><div class="lb">Qualifying buyers</div><span class="chip" id="dbBuyersChip" hidden></span><span class="small muted" id="dbBuyersSub" style="display:block;margin-top:4px"></span></div>
|
||||
</div>
|
||||
<div class="statx c-amber">
|
||||
<div class="pl"><svg viewBox="0 0 24 24"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M19 8v6M22 11h-6"/></svg></div>
|
||||
@@ -984,7 +984,7 @@
|
||||
<script src="/assets/common.js?v=20260914a"></script>
|
||||
<script src="/assets/wallet.js?v=20260911a"></script>
|
||||
<script src="/assets/promo.js?v=20260911a"></script>
|
||||
<script src="/assets/my.js?v=20260914j"></script>
|
||||
<script src="/assets/my.js?v=20260914k"></script>
|
||||
<script src="/assets/chat.js?v=20260907l"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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: [] });
|
||||
|
||||
Reference in New Issue
Block a user