diff --git a/chatbot.js b/chatbot.js index cf0414f..bb3e23c 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. +- 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. - GETTING STARTED CARD (2026-09-14): the top of every new member's Overview shows four steps (username, link wallet, switch on payouts, first package) with the current one explained and one button that opens the right tab and highlights the control (Wallet tab > Link wallet; then Activate payouts). Encouragement only: it can be hidden, and members who only want to view ads and earn credits are never blocked. It disappears once all four are done. diff --git a/public/assets/admin.js b/public/assets/admin.js index 69bd9c1..c7b45d9 100644 --- a/public/assets/admin.js +++ b/public/assets/admin.js @@ -757,7 +757,7 @@ })); // site settings: key / value rows; booleans as checkboxes, numbers stay numbers - const SITE_META = { siteName: 'Site name', tagline: 'Tagline', rehearsal: 'Testnet rehearsal banner', defaultSponsorId: 'Fallback sponsor (member #)', walletConnectProjectId: 'WalletConnect project id', moonpayPublicKey: 'MoonPay public key', telegramBotToken: 'Telegram proof feed: bot token', telegramChatId: 'Telegram proof feed: chat id', telegramTopicId: 'Telegram proof feed: topic id (optional)', telegramEvents: 'Telegram proof feed: events (payouts | payouts+purchases | all)', telegramCtaUrl: 'Telegram proof feed: join link under each post', aiCreditsPerGen: 'AI Copy Engine: credits per generation after the free allowance', aiFreeSurge: 'AI Copy Engine: free generations a month at Surge', aiFreeCircuit: 'AI Copy Engine: free generations a month at Circuit', aiFreeNexus: 'AI Copy Engine: free generations a month at Nexus', memberWeeklyEmail: 'Weekly member email to everyone active (1) or only sponsors with a line (0)', leaderboardWeeklyPrize: 'Leaderboard: weekly prize text (optional; blank shows the credit ladder)', leaderboardMonthlyPrize: 'Leaderboard: monthly prize text (optional)', leaderboardWeeklyCredits: 'Leaderboard: weekly credits for 1st,2nd,3rd… (e.g. 1000,500,250; blank = none)', leaderboardMonthlyCredits: 'Leaderboard: monthly credits for 1st,2nd,3rd… (e.g. 5000,2500,1000)', leaderboardAnnounceGeneral: 'Leaderboard: announce winners in the main group too (1/0)', telegramEchoChatId: 'Telegram echo (shared payments topic): chat id', telegramEchoTopicId: 'Telegram echo: topic id', telegramEchoEvents: 'Telegram echo: events (payouts | payouts+purchases | all)', legacyCreditsAdvertiser: 'Legacy welcome credits: former advertisers', legacyCreditsEarner: 'Legacy welcome credits: former earners', pnlFixedMonthlyUsd: 'P&L: fixed monthly cost (USD)' }; + const SITE_META = { noPayoutIds: 'No-payout positions (member #s, comma): linkage only, no buys from them, no joins routed under them', siteName: 'Site name', tagline: 'Tagline', rehearsal: 'Testnet rehearsal banner', defaultSponsorId: 'Fallback sponsor (member #)', walletConnectProjectId: 'WalletConnect project id', moonpayPublicKey: 'MoonPay public key', telegramBotToken: 'Telegram proof feed: bot token', telegramChatId: 'Telegram proof feed: chat id', telegramTopicId: 'Telegram proof feed: topic id (optional)', telegramEvents: 'Telegram proof feed: events (payouts | payouts+purchases | all)', telegramCtaUrl: 'Telegram proof feed: join link under each post', aiCreditsPerGen: 'AI Copy Engine: credits per generation after the free allowance', aiFreeSurge: 'AI Copy Engine: free generations a month at Surge', aiFreeCircuit: 'AI Copy Engine: free generations a month at Circuit', aiFreeNexus: 'AI Copy Engine: free generations a month at Nexus', memberWeeklyEmail: 'Weekly member email to everyone active (1) or only sponsors with a line (0)', leaderboardWeeklyPrize: 'Leaderboard: weekly prize text (optional; blank shows the credit ladder)', leaderboardMonthlyPrize: 'Leaderboard: monthly prize text (optional)', leaderboardWeeklyCredits: 'Leaderboard: weekly credits for 1st,2nd,3rd… (e.g. 1000,500,250; blank = none)', leaderboardMonthlyCredits: 'Leaderboard: monthly credits for 1st,2nd,3rd… (e.g. 5000,2500,1000)', leaderboardAnnounceGeneral: 'Leaderboard: announce winners in the main group too (1/0)', telegramEchoChatId: 'Telegram echo (shared payments topic): chat id', telegramEchoTopicId: 'Telegram echo: topic id', telegramEchoEvents: 'Telegram echo: events (payouts | payouts+purchases | all)', legacyCreditsAdvertiser: 'Legacy welcome credits: former advertisers', legacyCreditsEarner: 'Legacy welcome credits: former earners', pnlFixedMonthlyUsd: 'P&L: fixed monthly cost (USD)' }; function drawSite() { const wrap = $('siteForm'); wrap.innerHTML = Object.entries(siteObj).map(([k, v]) => '
' + esc(SITE_META[k] || humanize(k)) + '' diff --git a/public/assets/my.js b/public/assets/my.js index 3aa40f9..2054d81 100644 --- a/public/assets/my.js +++ b/public/assets/my.js @@ -1822,6 +1822,7 @@ } // ── linked positions (Qualified Start) ── const short = a => a ? a.slice(0, 6) + '…' + a.slice(-4) : ''; + let noPayoutAddrs = new Set(); // positions the site refuses to buy from (see siteConfig.noPayoutIds) async function loadPositions(me) { try { const r = await (await fetch('/api/my/positions')).json(); @@ -1829,7 +1830,7 @@ const list = r.positions || []; const rows = list.map((p, i) => '
Position ' + (i + 2) + ' ' + short(p.address) + '' + '' + (p.memberId ? '#' + p.memberId : 'not on-chain yet') + '' - + '' + (p.counted ? 'counts as a qualifying buyer' : p.memberId ? 'registered, buy $20+ to count' : 'buy a $20+ package to register it') + '' + + '' + (p.noPayout ? 'linkage only: no purchases from this position (payouts would reach a retired wallet)' : p.counted ? 'counts as a qualifying buyer' : p.memberId ? 'registered, buy $20+ to count' : 'buy a $20+ package to register it') + '' + '' + (p.credits || 0).toLocaleString() + ' credits' + (p.balanceWei != null ? ' · ' + (Number(BigInt(p.balanceWei) / 10n ** 14n) / 10000).toLocaleString(undefined, { maximumFractionDigits: 2 }) + ' POL' : '') + '' + (!p.memberId ? '' : '') + '
').join(''); @@ -1857,7 +1858,8 @@ if (sel) { const keep = sel.value; sel.innerHTML = '' - + list.map((p, i) => '').join(''); + + list.map((p, i) => '').join(''); + noPayoutAddrs = new Set(list.filter(p => p.noPayout).map(p => p.address.toLowerCase())); if (r.main && r.main.noPayout && r.main.address) noPayoutAddrs.add(r.main.address.toLowerCase()); if (keep && [...sel.options].some(o => o.value === keep)) sel.value = keep; $('buyFromWrap').hidden = !list.length; } @@ -1915,6 +1917,7 @@ const fromSel = $('buyFrom'); const fromPos = (fromSel && !$('buyFromWrap').hidden && fromSel.value && fromSel.value !== 'main') ? fromSel.value.toLowerCase() : null; if (fromPos && !meNow.memberId) { IAP.status('Switch on payouts for your main wallet first (Wallet tab), so this position can register under you.', 'bad'); return; } + if (noPayoutAddrs.has(fromPos || String(meNow.address || '').toLowerCase())) { IAP.status('This position is kept for linkage only. A purchase from it would pay a retired wallet. Buy from your main wallet or another position.', 'bad'); return; } if (!fromPos && !meNow.address) { IAP.status('Link your wallet first — one quick signature…'); await IAPWallet.signIn(); diff --git a/public/my.html b/public/my.html index 5964b66..e4a9208 100644 --- a/public/my.html +++ b/public/my.html @@ -984,7 +984,7 @@ - + diff --git a/server.js b/server.js index 5c82fb5..f7abb98 100644 --- a/server.js +++ b/server.js @@ -411,6 +411,7 @@ function siteConfig() { launchAt: '', // public launch moment, ISO 8601 with offset (e.g. 2026-09-18T19:00:00-05:00): countdown on /launch + dashboard mark aiCreditsPerGen: 10, aiFreeSurge: 20, aiFreeCircuit: 60, aiFreeNexus: 150, // AI Copy Engine: free generations per month by badge, then credits per generation memberWeeklyEmail: '0', // 1 = the weekly 'Your week on InstantAdPay' email goes to every active member, not only sponsors with a line + noPayoutIds: '25', // positions kept for linkage only (compromised wallets): no buys signed from them, no new joins routed under them or their upline chain (Marty, 2026-09-14) leaderboardWeeklyPrize: '', leaderboardMonthlyPrize: '', leaderboardWeeklyCredits: '1000,500,250', leaderboardMonthlyCredits: '5000,2500,1000', leaderboardAnnounceGeneral: '1', // referral contest prizes (text shown on /leaderboard; credits granted to the winner automatically at rollover) legacyCreditsAdvertiser: 500, legacyCreditsEarner: 150, // welcome-back credits for listed Faucet Wave / Tier One Ads emails arriving via /from/ geoTier1: '', // comma-separated ISO country codes; empty = built-in default (US, CA, GB, AU, NZ, IE, DE, FR, NL, SE, NO, DK, FI, CH, AT, BE) @@ -494,6 +495,17 @@ async function attachNames(evts) { // A sponsor token is a numeric chain id or a site share code. Codes resolve // to the referrer's CURRENT chain id, so activation any time before the // referral's first purchase still locks the line to them. +// No-payout positions (siteConfig.noPayoutIds): a member id whose on-chain wallet must never be paid again. +// A purchase pays the buyer's three uplines, so a buy from `id` is blocked when id itself or any of its three +// uplines is listed; a join under `id` is blocked when id or its two uplines is listed (the new member's buys +// would reach the listed wallet at tier 2 or 3). Returns the listed id that would be hit, else 0. +function noPayoutSet() { return new Set(String(siteConfig().noPayoutIds || '').split(/[,\s]+/).map(Number).filter(n => n > 0)); } +async function payoutChainBlocked(id, hops) { + const bad = noPayoutSet(); if (!bad.size || !id) return 0; + let cur = Number(id); + for (let i = 0; i <= hops && cur; i++) { if (bad.has(cur)) return cur; let m = null; try { m = await chain.member(cur); } catch (e) { break; } cur = m ? Number(m.sponsorId) || 0 : 0; } + return 0; +} async function resolveSponsorToken(tok) { const t = String(tok || '').trim().toLowerCase(); if (!t) return 0; @@ -828,6 +840,7 @@ const server = http.createServer(async (req, res) => { const acct = s && s.email ? await accounts.byEmail(s.email) : null; const tok = (acct && acct.sponsorRef) || parseCookies(req)['iap.sponsor'] || ''; let sponsorId = await resolveSponsorToken(tok); + if (sponsorId && await payoutChainBlocked(sponsorId, 2)) { console.log('sponsor routed away from no-payout chain', sponsorId); sponsorId = 0; } // orphan fallback: an unresolvable/absent sponsor (dead link, no link) lands // the new member under the configured catch position (#1) instead of root if (!sponsorId && (!acct || acct.memberId !== (Number(siteConfig().defaultSponsorId) || 1))) sponsorId = Number(siteConfig().defaultSponsorId) || 1; @@ -1012,6 +1025,7 @@ const server = http.createServer(async (req, res) => { const memberId = await auth.refreshMemberId(s); const acct = (s.email && await accounts.byEmail(s.email)) || (s.address && await accounts.byAddress(s.address)) || null; let sponsorId = await resolveSponsorToken((acct && acct.sponsorRef) || parseCookies(req)['iap.sponsor']); + if (sponsorId && await payoutChainBlocked(sponsorId, 2)) sponsorId = 0; const _defSpon = Number(siteConfig().defaultSponsorId) || 1; if (!sponsorId && memberId !== _defSpon) sponsorId = _defSpon; // orphan fallback → #1 (never self-sponsor) if (memberId && acct && acct.memberId !== memberId) accounts.setMemberId(acct.email, memberId).catch(() => {}); @@ -1142,6 +1156,7 @@ const server = http.createServer(async (req, res) => { if (id) { try { const mm = await chain.member(id); row.buyerCount = mm.buyerCount; row.counted = mm.countedAsBuyer; row.sponsorId = mm.sponsorId; } catch (e) {} row.credits = balPer[id] != null ? balPer[id] : 0; + row.noPayout = await payoutChainBlocked(id, 3); // linkage only: a buy from here would pay a listed wallet } out.push(row); } @@ -1149,6 +1164,7 @@ const server = http.createServer(async (req, res) => { if (mainId) { main.credits = balPer[mainId] != null ? balPer[mainId] : 0; try { main.buyerCount = (await chain.member(mainId)).buyerCount; } catch (e) {} + main.noPayout = await payoutChainBlocked(mainId, 3); } // live POL balances (the link signature proved the wallet is theirs) + a POL/USD rate from the catalog const bal = async a => { try { return BigInt(await chain.rpc('eth_getBalance', [a, 'latest'])).toString(); } catch (e) { return null; } };