From 9f8898d2bd6f1835de397df07d54bf2a585d52c7 Mon Sep 17 00:00:00 2001 From: martbost Date: Thu, 13 Aug 2026 14:35:05 -0500 Subject: [PATCH] Add public member dashboard at /my/:id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Members enter their on-chain ID (or follow a /my/46-style link) and see their own position: tier/level/directs/earnings facts, their subtree pyramid with downline rollups and open slots, classified payment history, and lineage to root — all chain-derived data only, none of the admin operational config. Public endpoint /api/public/member is cached (120s) and rate-limited (20/min/IP). Linked from the public footers. Co-Authored-By: Claude Fable 5 --- chain.js | 51 ++++++++++++++++++++++++++++++++++- public/index.html | 2 +- public/my.html | 16 +++++++++++ public/my.js | 69 +++++++++++++++++++++++++++++++++++++++++++++++ public/start.html | 2 +- server.js | 23 +++++++++++++++- 6 files changed, 159 insertions(+), 4 deletions(-) create mode 100644 public/my.html create mode 100644 public/my.js diff --git a/chain.js b/chain.js index 9ef7680..866fbc5 100644 --- a/chain.js +++ b/chain.js @@ -400,4 +400,53 @@ function getMatrixTree() { return { ready: true, snapshotAt: state.snapshotAt, memberCount: Object.keys(state.members).length, root, unplaced: unplaced.length ? unplaced : undefined }; } -module.exports = { startIndexer, getPayoutsPublic, verifyMember, memberLookup, getMatrixTree, isInTeam, CONTRACT }; +// subtree rooted at `id` from the snapshot: nodes to `showDepth`, rollups from the FULL subtree +function getSubtree(id, showDepth = 3) { + if (!state || !state.members[id]) return null; + const seen = new Set(); + function node(nid, depth) { + if (!nid || seen.has(nid)) return null; + seen.add(nid); + const m = state.members[nid]; + if (!m) return null; + const l = node(m.l, depth + 1), r = node(m.r, depth + 1); + const n = { + id: nid, tier: m.tier, level: m.level, levelName: levelName(m.level || 1), + directCount: m.directCount || 0, earnedPol: m.earnedPol || 0, + downCount: 0, downPol: 0 + }; + for (const c of [l, r]) if (c) { n.downCount += 1 + c.downCount; n.downPol = +(n.downPol + c.earnedPol + c.downPol).toFixed(2); } + if (depth < showDepth) { n.left = l; n.right = r; } + return n; + } + return node(id, 0); +} + +// everything a member may see about their own position — chain data only +async function memberPublic(id) { + const m = await fetchMember(id); + if (!m) return { registered: false, id }; + await fetchCosts().catch(() => {}); + const out = { + registered: true, id, account: m.account, joinedAt: m.joinedAt, + tier: m.tier, tierName: tierName(m.tier), level: m.level, levelName: levelName(m.level), + directCount: m.directCount, totalEarnedPol: m.totalEarnedPol, totalPaidPol: m.totalPaidPol, + referrerId: m.referrerId, uplineId: m.uplineId + }; + try { + const inc = await fetchIncome(id); + out.income = inc.map(p => ({ fromId: p.fromId, pol: p.pol, ts: p.ts, desc: describeIncome(p.fromTier, p.level, p.pol) })).reverse().slice(0, 50); + } catch (e) { out.income = []; } + out.subtree = getSubtree(id, 3); + const chain = []; + const seen = new Set([id]); + let cur = m.uplineId; + for (let i = 0; i < 40 && cur && !seen.has(cur); i++) { + seen.add(cur); chain.push(cur); + cur = state && state.members[cur] ? state.members[cur].uplineId : 0; + } + out.uplineChain = chain; + return out; +} + +module.exports = { startIndexer, getPayoutsPublic, verifyMember, memberLookup, memberPublic, getMatrixTree, isInTeam, CONTRACT }; diff --git a/public/index.html b/public/index.html index caab151..e30cb62 100644 --- a/public/index.html +++ b/public/index.html @@ -13,5 +13,5 @@
Depth over width

2 → 4 → 8 → 16 → 32

The first major team milestone is 30 correctly placed positions across the first four generations: 2 + 4 + 8 + 16.

2
4
8
16
Team principle: once your two are in place, do not keep adding more directs to the same qualified link. Help the next positions become qualified so the matrix develops depth instead of extra shallow legs.
Live payment proof

Real payouts, straight from the blockchain.

Every payment in this program happens on a public smart contract on Polygon — nobody can fake, hide, or edit it. Below are the latest member payouts, read live from the contract. Tap any row to verify the transaction yourself on Polygonscan.

Reading the blockchain…
Data is read directly from the RM Circle smart contract (0x33Bd…2DAF) on Polygon Mainnet. Member numbers are on-chain IDs, not names. Past payouts are not a promise of future results.
Ready to start?

See the current team placement.

The onboarding page automatically shows the sponsor position the team is currently helping. Always use the sponsor shown there instead of an old screenshot or saved link.

Open Getting Started Instructions →
-
This independent team page is educational and is not an earnings guarantee or investment advice. Cryptocurrency and smart-contract participation involve risk, including possible loss of funds. Never use funds you cannot afford to lose. Results depend on actual participation, qualification, upgrades, smart-contract behavior, and the market value of POL.
+
This independent team page is educational and is not an earnings guarantee or investment advice. Cryptocurrency and smart-contract participation involve risk, including possible loss of funds. Never use funds you cannot afford to lose. Results depend on actual participation, qualification, upgrades, smart-contract behavior, and the market value of POL.
diff --git a/public/my.html b/public/my.html new file mode 100644 index 0000000..5862507 --- /dev/null +++ b/public/my.html @@ -0,0 +1,16 @@ +My Team Dashboard | Crypto Team Build + + +
+ + +
+
All figures are read live from the RM Circle smart contract on Polygon and are historical facts, not a promise of future results. Participation involves cryptocurrency and smart-contract risk. Never use funds you cannot afford to lose.
+ diff --git a/public/my.js b/public/my.js new file mode 100644 index 0000000..e9f0885 --- /dev/null +++ b/public/my.js @@ -0,0 +1,69 @@ +(function(){ + const esc=s=>String(s??'').replace(/[&<>'"]/g,c=>({'&':'&','<':'<','>':'>',"'":''','"':'"'}[c])); + const fmt=n=>Number(n||0).toLocaleString(undefined,{maximumFractionDigits:2}); + const date=ts=>ts?new Date(ts*1000).toLocaleDateString(undefined,{year:'numeric',month:'short',day:'numeric'}):'—'; + const prompt=document.getElementById('idPrompt'),dash=document.getElementById('dash'); + + function pathId(){const m=location.pathname.match(/^\/my\/(\d+)$/);if(m)return m[1];const q=new URLSearchParams(location.search).get('id');if(q&&/^\d+$/.test(q))return q;try{return localStorage.getItem('ctb.myId')||''}catch(e){return ''}} + + function card(n,focus){ + if(!n)return '
open
slot
'; + const badge=n.tier===2?'P':'S'; + const down=n.downCount?`⬇ ${n.downCount} below · ${fmt(n.downPol)} POL`:'no downline yet'; + return `
${badge} #${n.id}${esc(n.levelName)}${n.directCount}/2 · ${fmt(n.earnedPol)} POL${down}
`; + } + function renderTree(root){ + if(!root){document.getElementById('dTree').innerHTML='
Team view is still indexing — check back in a few minutes.
';return} + const rows=[[root]]; + for(let g=1;g<4;g++)rows.push(rows[g-1].flatMap(n=>n?[n.left||null,n.right||null]:[null,null])); + if(!rows[3].some(Boolean))rows.pop(); + document.getElementById('dTree').innerHTML=`
${rows.map((r,i)=>`
${r.map(n=>card(n,i===0&&n&&true)).join('')}
`).join('')}
`; + } + function render(d){ + prompt.classList.add('hidden');dash.classList.remove('hidden'); + document.getElementById('dTitle').textContent='#'+d.id; + document.getElementById('dFacts').innerHTML= + `
Tier${esc(d.tierName)}
`+ + `
Level${esc(d.levelName)}
`+ + `
Directs${d.directCount}/2${d.directCount>=2?' ✓ qualified':''}
`+ + `
Total received${fmt(d.totalEarnedPol)} POL
`+ + `
Team below you${d.subtree?`${d.subtree.downCount} member${d.subtree.downCount===1?'':'s'}`:'—'}
`+ + `
Member since${date(d.joinedAt)}
`; + renderTree(d.subtree); + const inc=d.income||[]; + document.getElementById('dIncome').innerHTML=inc.length + ?`
${inc.map(p=>``).join('')}
WhenFrom memberForAmount
${date(p.ts)}#${p.fromId}${esc(p.desc)}${fmt(p.pol)} POL
` + :'
No payments yet — they appear here the moment they land on-chain.
'; + document.getElementById('dLineage').innerHTML=d.uplineChain&&d.uplineChain.length + ?`#${d.id} → ${d.uplineChain.map(i=>'#'+i).join(' → ')} (root)` + :'You are at the top of your line.'; + } + async function load(id){ + try{ + const r=await fetch('/api/public/member?id='+id); + const d=await r.json(); + if(!r.ok)throw new Error(d.error||'Lookup failed'); + if(!d.registered)throw new Error(`ID ${id} isn't registered on the smart contract — double-check the number.`); + try{localStorage.setItem('ctb.myId',String(id))}catch(e){} + if(!/^\/my\/\d+$/.test(location.pathname))history.replaceState(null,'','/my/'+id); + render(d); + }catch(x){ + prompt.classList.remove('hidden');dash.classList.add('hidden'); + document.getElementById('idError').textContent=x.message; + } + } + document.getElementById('idForm').addEventListener('submit',e=>{ + e.preventDefault(); + const v=document.getElementById('memberId').value.trim(); + if(!/^\d{1,15}$/.test(v)){document.getElementById('idError').textContent='Numbers only — the ID from the RM Circle dApp.';return} + load(v); + }); + document.getElementById('switchId').addEventListener('click',()=>{ + try{localStorage.removeItem('ctb.myId')}catch(e){} + history.replaceState(null,'','/my'); + dash.classList.add('hidden');prompt.classList.remove('hidden'); + document.getElementById('memberId').value=''; + }); + const id=pathId(); + if(id)load(id); +})(); diff --git a/public/start.html b/public/start.html index 9caa75d..f858e97 100644 --- a/public/start.html +++ b/public/start.html @@ -15,5 +15,5 @@
What happens next: get exactly 2 directs, retire your qualified referral link, help your 2 get their 2, and upgrade with earned POL when practical. The goal is depth and duplication—not endless directs on one link.
Risk reminder: participation involves cryptocurrency and smart-contract risk. No income is guaranteed. Use only funds you can afford to lose.
RM Circle Premium Team Build Roadmap — core strategy, step-by-step guide, premium levels, and duplication formula
The RM Circle is a team build project of the Crypto Team Build Network. This roadmap is the plan every member follows — tap to view full size.
-
This is an independent Crypto Team Build onboarding resource, not an owner/principal page. Always confirm transaction details in your wallet before signing. Never disclose your Secret Recovery Phrase.
+
This is an independent Crypto Team Build onboarding resource, not an owner/principal page. Always confirm transaction details in your wallet before signing. Never disclose your Secret Recovery Phrase.
diff --git a/server.js b/server.js index f306b0a..7e40913 100644 --- a/server.js +++ b/server.js @@ -50,6 +50,13 @@ RULES: } const SUBMISSIONS_FILE = path.join(DATA_DIR, 'submissions.json'); if (!fs.existsSync(SUBMISSIONS_FILE)) fs.writeFileSync(SUBMISSIONS_FILE, '[]'); +const memberCache = new Map(); +const lookupHits = new Map(); +function memberLookupLimited(ip) { + const now = Date.now(), rec = lookupHits.get(ip); + if (!rec || now > rec.reset) { lookupHits.set(ip, { count: 1, reset: now + 60000 }); return false; } + rec.count++; return rec.count > 20; +} const submitHits = new Map(); function submitRateLimited(ip) { const now = Date.now(), rec = submitHits.get(ip); @@ -250,6 +257,20 @@ async function handleApi(req,res,pathname){ if(req.method==='GET'&&pathname==='/api/public/config'){ const c=getConfig();return json(res,200,{siteName:c.siteName,programName:c.programName,bridgeHeadline:c.bridgeHeadline,bridgeSubheadline:c.bridgeSubheadline,premiumEntryPol:c.premiumEntryPol,telegramUrl:c.telegramUrl,supportLabel:c.supportLabel,showQueueProgress:c.showQueueProgress}); } + if(req.method==='GET'&&pathname==='/api/public/member'){ + const ip=String(req.headers['x-forwarded-for']||req.socket.remoteAddress||'').split(',')[0].trim(); + if(memberLookupLimited(ip))return json(res,429,{error:'Too many lookups — give it a minute.'}); + const id=Number(new URL(req.url,'http://x').searchParams.get('id')||0); + if(!Number.isInteger(id)||id<1||id>281474976710655)return json(res,400,{error:'Enter a numeric member ID.'}); + const cached=memberCache.get(id); + if(cached&&Date.now()-cached.ts<120000)return json(res,200,cached.data,{'Cache-Control':'public, max-age=60'}); + try{ + const r=await Promise.race([chain.memberPublic(id),new Promise((_,rej)=>setTimeout(()=>rej(new Error('Blockchain lookup timed out — try again.')),20000))]); + memberCache.set(id,{data:r,ts:Date.now()}); + if(memberCache.size>500)memberCache.delete(memberCache.keys().next().value); + return json(res,200,r,{'Cache-Control':'public, max-age=60'}); + }catch(e){return json(res,502,{error:e.message||'Lookup failed'})} + } if(req.method==='GET'&&pathname==='/api/public/payouts'){ return json(res,200,chain.getPayoutsPublic(),{'Cache-Control':'public, max-age=20'}); } @@ -330,7 +351,7 @@ const server=http.createServer(async(req,res)=>{ if(pathname==='/health'||pathname.startsWith('/api/'))return await handleApi(req,res,pathname); if(req.method!=='GET'&&req.method!=='HEAD')return send(res,405,'Method Not Allowed',{'Content-Type':'text/plain; charset=utf-8'}); let file; - if(pathname==='/')file=path.join(PUBLIC_DIR,'index.html');else if(pathname==='/start'||pathname==='/start/')file=path.join(PUBLIC_DIR,'start.html');else if(pathname==='/training'||pathname==='/training/')file=path.join(PUBLIC_DIR,'training.html');else if(pathname==='/admin'||pathname==='/admin/')file=path.join(PUBLIC_DIR,'admin.html');else{ + if(pathname==='/')file=path.join(PUBLIC_DIR,'index.html');else if(pathname==='/start'||pathname==='/start/')file=path.join(PUBLIC_DIR,'start.html');else if(pathname==='/training'||pathname==='/training/')file=path.join(PUBLIC_DIR,'training.html');else if(pathname==='/admin'||pathname==='/admin/')file=path.join(PUBLIC_DIR,'admin.html');else if(pathname==='/my'||pathname==='/my/'||/^\/my\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'my.html');else{ const safe=path.normalize(pathname).replace(/^([.][.][/\\])+/, '').replace(/^[/\\]+/,'');file=path.join(PUBLIC_DIR,safe);if(!file.startsWith(PUBLIC_DIR))file=''; } if(file&&staticFile(req,res,file))return;return staticFile(req,res,path.join(PUBLIC_DIR,'404.html'),404);