Add /contract — plain-language security review of the smart contract

Shareable page from the 2026-08-13 source review: code immutability
(no proxy/delegatecall/selfdestruct, Sourcify exact match), same-tx
distribution (no pooled funds), locked prices and fee constants, the
honest list of owner powers with the 50% entry-fee cap, where unmatched
pass-ups go, and verify-yourself links. Linked from all public footers,
the payment-proof note, and the chatbot (canned + AI prompt).

Also: passed-over lists now exclude structurally-skipped uplines — a
level-N payment never checks the first N-1 uplines, so listing them as
"not eligible" was misleading.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-08-14 10:51:34 -05:00
parent 8d766161e8
commit d1aef1e3ff
8 changed files with 555 additions and 520 deletions
+3 -1
View File
@@ -247,7 +247,9 @@ async function processRange(fromBlock, toBlock) {
};
if (!isRef) {
if (txUpgrades[tx]) p.upgrade = txUpgrades[tx];
p.passed = passedOver(p.fromId, p.toId);
// a level-N payment structurally skips the first N-1 uplines (they're
// never checked) — only list uplines that failed the eligibility test
p.passed = passedOver(p.fromId, p.toId).slice(Math.max(0, p.level - 1));
}
// upgrade a snapshot-sourced twin in place, else append (twin = already known from snapshot, so not a NEW event)
const twinIdx = state.payouts.findIndex(x => !x.tx && samePayout(x, p));
+2
View File
@@ -28,6 +28,8 @@
a:()=>{const s=sponsorInfo&&sponsorInfo.sponsor;return s?`The current team sponsor is <strong>ID ${esc(s.id)}${s.name?' ('+esc(s.name)+')':''}</strong> — always verify on the <a href="/start">Getting Started page</a> right before you join, because placements rotate as positions qualify. Don't use an old screenshot or saved link.`:`The current sponsor is always shown live on the <a href="/start">Getting Started page</a> — check there right before joining, because placements rotate as positions qualify.`}},
{k:['qualified','qualification','2 directs','two directs','directs','retire'],
a:()=>`A position is <strong>qualified</strong> once it has 2 directs. The team recommendation: retire your link at that point and help your 2 get <em>their</em> 2 using their own links — that's the only thing that qualifies them. And if someone happens to join through your link after 2/2, it's a bonus, not a problem: the entry reward still pays your position and they spill downward, adding depth to your leg. Just remember spillover never qualifies the people below — their own directs do.`},
{k:['safe','scam','rug','rug pull','security','audit','trust','legit','contract code','smart contract','can they change','steal'],
a:()=>`Fair question — don't take anyone's word for it. We reviewed the complete verified source of the RM Circle smart contract and published a plain-language breakdown at <a href="/contract">rmcircle.saasy.top/contract</a>: the code can never change (no upgrade mechanism exists), member money never sits in the contract (every payment distributes in the same transaction), prices and payout rules are locked forever, and the operator's powers are limited to a short, honest list. Every claim links to the blockchain so you can verify it yourself. Code security isn't an income guarantee though — results depend on real participation, and POL's value moves.`},
{k:['spillover','spill over','spill','under me but not qualified','people under me','not my direct','passed over','missed payment','skipped','why am i not qualified'],
a:()=>`<strong>Spillover</strong> happens when someone's sponsor already has both matrix slots full — the smart contract then places the new member in the next open slot further down, which can be under YOU. Spillover fills your matrix and sets up future upgrade payments to your position, but it does <strong>not</strong> count toward your qualification: only people who join using <em>your</em> ID are your directs. That's why you can see 2 under you and still show 1/2. And important: upgrade payments only stop at positions that are qualified (2 directs) AND at the required level — otherwise the payment passes you by to the next eligible upline. Check your position on your <a href="/my">Member Dashboard</a> — spillover placements are tagged there.`},
{k:['level','levels','upgrade','scintilla','ascensus','fabrica','culmen','apex','fastigium','vertex','corona','8 levels'],
+31
View File
@@ -0,0 +1,31 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="description" content="How the RM Circle smart contract works — what's locked forever, what the operator can and can't do, and how to verify every claim yourself on the blockchain."><title>Smart Contract Security | Crypto Team Build</title>
<link rel="canonical" href="https://rmcircle.saasy.top/contract">
<meta property="og:type" content="website"><meta property="og:site_name" content="Crypto Team Build Network"><meta property="og:title" content="How the RM Circle Smart Contract Works — and Why the Bottom Can't Fall Out"><meta property="og:description" content="The code can never change, member money never sits in the contract, and every rule is public. A plain-language review of the verified source — with links to check everything yourself."><meta property="og:image" content="https://rmcircle.saasy.top/og-card.jpg"><meta property="og:image:width" content="1200"><meta property="og:image:height" content="630">
<meta name="twitter:card" content="summary_large_image"><meta name="twitter:title" content="RM Circle Smart Contract — plain-language security review"><meta name="twitter:description" content="What's locked forever, what the operator can and can't do, and how to verify it all yourself."><meta name="twitter:image" content="https://rmcircle.saasy.top/og-card.jpg"><link rel="stylesheet" href="/styles.css"></head>
<body>
<header class="wrap nav"><a class="brand" href="/"><div class="brand-mark">RM</div><span><span id="brandName">Crypto Team Build</span><small>Contract Security</small></span></a><div class="nav-actions"><a class="btn btn-secondary hide-mobile" href="/">← Strategy</a><a class="btn btn-primary" href="/start">Get Started</a></div></header>
<main>
<section class="section"><div class="wrap" style="max-width:880px">
<div class="section-head"><div class="eyebrow">Plain-language code review</div><h1 style="font-size:clamp(32px,5vw,50px);margin:8px 0 10px">Can the bottom fall out of this? <span class="gold">We read the code.</span></h1><p>Every payment in the RM Circle runs through one smart contract on the Polygon blockchain. We reviewed its complete, verified source line by line. Here's what it says — in plain language, with links so you can check every claim yourself. Nobody has to take our word for anything.</p></div>
<div class="facts" style="grid-template-columns:repeat(3,1fr);margin:18px 0 26px"><div class="fact"><small>Contract</small><strong style="font-size:13px"><a href="https://polygonscan.com/address/0x33BdAEEfd6d17D80aE53816c916dFb26c4fB2DAF" target="_blank" rel="noopener noreferrer" style="color:var(--teal)">0x33Bd…2DAF ↗</a></strong></div><div class="fact"><small>Network</small><strong>Polygon Mainnet</strong></div><div class="fact"><small>Source verified</small><strong style="font-size:13px"><a href="https://repo.sourcify.dev/contracts/full_match/137/0x33BdAEEfd6d17D80aE53816c916dFb26c4fB2DAF/" target="_blank" rel="noopener noreferrer" style="color:var(--teal)">Exact match ↗</a></strong></div></div>
<div class="card" style="margin-bottom:16px"><h2 style="margin:0 0 10px">1. The code can never change.</h2><p style="color:var(--muted);line-height:1.65;margin:0">Some crypto projects use "upgradeable" contracts — the operator can swap in new code after you've joined, changing the rules underneath you. <strong style="color:var(--text)">This contract has none of that machinery.</strong> No proxy, no delegatecall, no self-destruct — the three mechanisms that allow post-launch changes are simply absent from the code. What was deployed on May 27, 2026 is what runs today and what will run in ten years. And because the source is verified as an <em>exact bytecode match</em>, the code we reviewed is provably the code that's running — not a copy, not a claim.</p></div>
<div class="card" style="margin-bottom:16px"><h2 style="margin:0 0 10px">2. Member money never sits in the contract.</h2><p style="color:var(--muted);line-height:1.65;margin:0">There is no pool, no vault, no balance an operator could run away with. When someone joins or upgrades, the contract splits and delivers every coin <strong style="color:var(--text)">in the same transaction</strong> — sponsor reward, upline payment, fees — straight to member wallets. If any single transfer fails, the whole transaction reverses and nobody pays anything. You can watch this happen live on the <a href="/#proof" style="color:var(--teal)">payment proof feed</a>: money in, money out, same block.</p></div>
<div class="card" style="margin-bottom:16px"><h2 style="margin:0 0 10px">3. The prices and payout rules are locked.</h2><p style="color:var(--muted);line-height:1.65;margin:0">Entry and upgrade costs for all 8 levels were fixed the moment the contract deployed — <strong style="color:var(--text)">there is no function to change them.</strong> The payout split is compiled into the code as constants: on entries, the sponsor receives the slot cost minus a 5% project fee (founders 1% + 1%, development 3%); on upgrades, <strong style="color:var(--text)">100% goes to the eligible upline — the project takes nothing.</strong> There's also no pause switch: nobody can freeze joins, upgrades, or payouts. The eligibility rule is public too: an upgrade payment stops at the first upline who is qualified (2 directs) and already at that level — otherwise it passes them and continues up.</p></div>
<div class="card" style="margin-bottom:16px"><h2 style="margin:0 0 10px">4. What the operator CAN do — the honest list.</h2><p style="color:var(--muted);line-height:1.65;margin:0 0 10px">A fair review reports the levers, not just the locks. The contract owner can do exactly four things:</p><ul style="color:var(--muted);line-height:1.7;margin:0;padding-left:20px"><li>Raise or lower the <strong style="color:var(--text)">entry fee percentage</strong> (currently 5%, hard-capped at 50% in the code). This affects the price of <em>future</em> joins only — it can't touch anyone's earnings, and any change is instantly visible on the blockchain.</li><li>Redirect the <strong style="color:var(--text)">project's own fee wallets</strong> (founder/development shares) — their revenue, never member payouts.</li><li>Sweep out coins <strong style="color:var(--text)">accidentally sent</strong> to the contract address — member payment flows never leave a balance behind.</li><li>Transfer ownership of those same limited powers to someone else.</li></ul><p style="color:var(--muted);line-height:1.65;margin:10px 0 0">What the owner <strong style="color:var(--text)">cannot</strong> do: take member funds, change payout rules, change prices, reroute earnings, block withdrawals (there's nothing to withdraw — payments are instant), or replace the code. Even a fully hostile owner is limited to that list above.</p></div>
<div class="card" style="margin-bottom:16px"><h2 style="margin:0 0 10px">5. Where "missed" payments go.</h2><p style="color:var(--muted);line-height:1.65;margin:0">When an upgrade payment finds no eligible upline within 16 levels (or reaches the root), it goes to the project's fee wallet rather than vanishing. That's the strongest built-in reason to <strong style="color:var(--text)">get your 2 directs and keep your level current</strong> — eligible positions catch payments; ineligible positions watch them pass by. The <a href="/my" style="color:var(--teal)">Member Dashboard</a> shows exactly this happening in real time.</p></div>
<div class="card" style="margin-bottom:16px"><h2 style="margin:0 0 10px">Check it yourself — please.</h2><ul style="color:var(--muted);line-height:1.8;margin:0;padding-left:20px"><li><a href="https://polygonscan.com/address/0x33BdAEEfd6d17D80aE53816c916dFb26c4fB2DAF" target="_blank" rel="noopener noreferrer" style="color:var(--teal)">The contract on Polygonscan ↗</a> — every transaction, ever, public.</li><li><a href="https://repo.sourcify.dev/contracts/full_match/137/0x33BdAEEfd6d17D80aE53816c916dFb26c4fB2DAF/" target="_blank" rel="noopener noreferrer" style="color:var(--teal)">The verified source code ↗</a> — the exact code that's running, independently verified.</li><li><a href="/#proof" style="color:var(--teal)">The live payment feed</a> — every payout on this site links to its blockchain transaction.</li><li>Ask any developer you trust to read the source against this page. It's ~400 lines.</li></ul></div>
<div class="callout warning"><strong>What this page is not:</strong> code security is not an income guarantee. This review says the <em>rules can't change underneath you</em> — it does not promise the program grows, that positions fill, or that POL holds its value. Results depend on real participation and duplication, and POL's market price moves. Never use funds you can't afford to lose.</div>
<p class="micro" style="margin-top:18px">Review method: complete verified source (Sourcify exact-match, creation + runtime bytecode) read against the deployed contract on 2026-08-13; payout math cross-checked against live on-chain transactions. Reviewed independently by this team's tooling — not by the contract's developers.</p>
</div></section>
</main>
<footer class="wrap disclaimer">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.<div class="footer-links"><a href="/">Strategy</a><a href="/start">Getting Started</a><a href="/training">Training</a><a href="/my">Member Dashboard</a></div></footer>
<script src="/track.js"></script><script src="/chat.js" defer></script></body></html>
+2 -2
View File
@@ -11,7 +11,7 @@
<section id="strategy" class="section"><div class="wrap"><div class="section-head"><div class="eyebrow">The strategy</div><h2>Simple enough to duplicate.</h2><p>Each position gets two directs to qualify, then retires its link and helps the next two positions repeat the process. Late signups on a qualified link are a welcome bonus — they pay that member and spill downward as depth.</p></div><div class="grid-3"><article class="card"><div class="card-icon">2</div><h3>Get exactly two</h3><p>Use your position's link until two direct positions are placed beneath you and your position is qualified.</p></article><article class="card"><div class="card-icon">↘</div><h3>Move the effort down</h3><p>Retire your link and help each of your two directs use their own links until they each have two — that's what qualifies them. If someone still joins through your link anyway, it's a bonus: it pays you and spills down as depth.</p></article><article class="card"><div class="card-icon">↑</div><h3>Upgrade responsibly</h3><p>Use earned POL to advance when practical. Active builders may also choose to self-fund, but only within their own risk tolerance.</p></article></div></div></section>
<section class="section"><div class="wrap"><div class="section-head"><div class="eyebrow">Moving-link workflow</div><h2>Qualify. Then the effort moves down.</h2></div><div class="flow"><div class="flow-step"><b>Step 1</b><strong>Use link</strong><p>Share the current position's referral link.</p></div><div class="flow-step"><b>Step 2</b><strong>Get 2</strong><p>Place exactly two direct positions.</p></div><div class="flow-step"><b>Step 3</b><strong>Retire link</strong><p>Late signups still spill down — bonus depth, not a problem.</p></div><div class="flow-step"><b>Step 4</b><strong>Help your 2</strong><p>Shift the team effort to their links.</p></div><div class="flow-step"><b>Step 5</b><strong>Repeat</strong><p>Keep the qualification wave moving down.</p></div></div></div></section>
<section class="section"><div class="wrap"><div class="section-head"><div class="eyebrow">Depth over width</div><h2>2 → 4 → 8 → 16 → 32</h2><p>The first major team milestone is 30 correctly placed positions across the first four generations: 2 + 4 + 8 + 16.</p></div><div class="matrix" aria-label="Matrix growth illustration"><div class="matrix-group"><div class="people"><span class="person"></span><span class="person"></span></div><b>2</b></div><div class="matrix-group"><div class="people"><span class="person"></span><span class="person"></span><span class="person"></span><span class="person"></span></div><b>4</b></div><div class="matrix-group"><div class="people" id="p8"></div><b>8</b></div><div class="matrix-group"><div class="people" id="p16"></div><b>16</b></div></div><div class="notice"><strong>Team principle:</strong> once your two are in place, retire your link and shift to helping them get <em>their</em> two — their own directs are the only thing that qualifies their positions to catch payments. And if an extra signup comes through your link anyway, it's a bonus, not a problem: it still pays your position the entry reward and spills downward to fill an open slot in your leg. Spillover never qualifies the people below, though — so the team effort always moves down.</div></div></section>
<section class="section" id="proof"><div class="wrap"><div class="section-head"><div class="eyebrow">Live payment proof</div><h2>Real payouts, straight from the blockchain.</h2><p>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.</p></div><div id="payoutTotals" class="pp-totals"></div><div id="payoutFeed" class="pp-feed"><div class="empty">Reading the blockchain…</div></div><div class="pp-note">Data is read directly from the RM Circle smart contract (<a href="https://polygonscan.com/address/0x33BdAEEfd6d17D80aE53816c916dFb26c4fB2DAF" target="_blank" rel="noopener noreferrer" style="color:var(--teal)">0x33Bd…2DAF</a>) on Polygon Mainnet. Member numbers are on-chain IDs, not names. Past payouts are not a promise of future results.</div></div></section>
<section class="section" id="proof"><div class="wrap"><div class="section-head"><div class="eyebrow">Live payment proof</div><h2>Real payouts, straight from the blockchain.</h2><p>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.</p></div><div id="payoutTotals" class="pp-totals"></div><div id="payoutFeed" class="pp-feed"><div class="empty">Reading the blockchain…</div></div><div class="pp-note">Data is read directly from the RM Circle smart contract (<a href="https://polygonscan.com/address/0x33BdAEEfd6d17D80aE53816c916dFb26c4fB2DAF" target="_blank" rel="noopener noreferrer" style="color:var(--teal)">0x33Bd…2DAF</a>) on Polygon Mainnet. Member numbers are on-chain IDs, not names. Past payouts are not a promise of future results. <a href="/contract" style="color:var(--gold)">How the contract works — and why the rules can't change →</a></div></div></section>
<section class="section"><div class="wrap"><div class="card" style="text-align:center;padding:34px"><div class="eyebrow">Ready to start?</div><h2 style="font-size:38px;margin:10px 0">See the current team placement.</h2><p style="max-width:680px;margin:0 auto 20px;color:var(--muted)">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.</p><a class="btn btn-primary" href="/start">Open Getting Started Instructions →</a></div></div></section>
</main><footer class="wrap disclaimer">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.<div class="footer-links"><a href="/training">Training</a><a href="/start">Getting Started</a><a href="/my">Member Dashboard</a><a href="/admin">Team Admin</a></div></footer>
</main><footer class="wrap disclaimer">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.<div class="footer-links"><a href="/training">Training</a><a href="/start">Getting Started</a><a href="/my">Member Dashboard</a><a href="/contract">Contract Security</a><a href="/admin">Team Admin</a></div></footer>
<script src="/track.js"></script><script src="/bridge.js"></script><script src="/payouts.js" defer></script><script src="/chat.js" defer></script></body></html>
+30 -30
View File
@@ -1,30 +1,30 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex"><meta name="description" content="You've been personally invited to the RM Circle Premium team build — see the strategy, the live on-chain payouts, and your sponsor's real position."><title>You're Invited | Crypto Team Build</title>
<meta property="og:type" content="website"><meta property="og:site_name" content="Crypto Team Build Network"><meta property="og:title" content="You're invited to the RM Circle Premium Team Build"><meta property="og:description" content="A team-first strategy built around qualification, depth, and duplication — with every payout verified on the blockchain. Your invite carries your sponsor automatically."><meta property="og:image" content="https://rmcircle.saasy.top/og-card.jpg"><meta property="og:image:width" content="1200"><meta property="og:image:height" content="630"><meta property="og:image:alt" content="The RM Circle Premium Team Build roadmap card — Crypto Team Build Network">
<meta name="twitter:card" content="summary_large_image"><meta name="twitter:title" content="You're invited — RM Circle Premium Team Build"><meta name="twitter:description" content="Get your 2. Help your 2 get their 2. Your invite carries your sponsor automatically."><meta name="twitter:image" content="https://rmcircle.saasy.top/og-card.jpg"><link rel="stylesheet" href="/styles.css"></head>
<body>
<header class="wrap nav"><a class="brand" href="/"><div class="brand-mark">RM</div><span><span id="brandName">Crypto Team Build</span><small id="brandProgram">RM Circle Premium</small></span></a><div class="nav-actions"><a class="btn btn-secondary hide-mobile" href="#strategy">How it works</a><a class="btn btn-secondary" href="/training">Training</a><a class="btn btn-primary" href="#join">Join the Team</a></div></header>
<main>
<section class="hero wrap" style="padding-bottom:12px"><div class="eyebrow">Personal invitation</div><h1>You've been invited by<br><span class="gold">Member <span id="invId">#—</span>.</span></h1><p id="invSub">This page is their real position on the team — every number on it is read live from the blockchain.</p>
<div id="invCard" class="table-card" style="max-width:680px;margin:20px 0 6px"><div style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap"><h2 style="margin:0">Your sponsor's position <span class="live-badge" style="vertical-align:middle;margin-left:6px"><span class="dot"></span> Verified on-chain</span></h2><span id="invQualified"></span></div><div id="invFacts" class="facts" style="grid-template-columns:repeat(3,1fr);margin:14px 0 0"><div class="empty" style="grid-column:1/-1">Reading the blockchain…</div></div><div id="invQualNote"></div></div>
<div class="hero-actions" style="margin-top:18px"><a class="btn btn-primary" href="#join">Join Under <span id="invIdBtn">This Member</span> →</a><a class="btn btn-secondary" href="#strategy">See the Strategy First</a></div>
<div class="micro">Independent team training resource • Participation involves risk • No income is guaranteed</div></section>
<section class="section" style="padding-bottom:0"><div class="wrap"><div class="section-head"><div class="eyebrow">Watch first</div><h2>See how simple this is.</h2><p>A quick walkthrough of the team build in action. Watch this before you dive into the strategy below.</p></div><div class="video-card" style="max-width:880px;margin:0 auto"><iframe class="video-frame" style="display:block;width:100%;height:auto;aspect-ratio:16/9" width="880" height="495" src="https://www.youtube-nocookie.com/embed/VOhwBiAO7to" title="RM Circle Premium team build walkthrough video" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></div></div></section>
<section id="strategy" class="section"><div class="wrap"><div class="section-head"><div class="eyebrow">The strategy</div><h2>Simple enough to duplicate.</h2><p>Each position gets two directs to qualify, then retires its link and helps the next two positions repeat the process. Late signups on a qualified link are a welcome bonus — they pay that member and spill downward as depth.</p></div><div class="grid-3"><article class="card"><div class="card-icon">2</div><h3>Get exactly two</h3><p>Use your position's link until two direct positions are placed beneath you and your position is qualified.</p></article><article class="card"><div class="card-icon">↘</div><h3>Move the effort down</h3><p>Retire your link and help each of your two directs use their own links until they each have two — that's what qualifies them. If someone still joins through your link anyway, it's a bonus: it pays you and spills down as depth.</p></article><article class="card"><div class="card-icon">↑</div><h3>Upgrade responsibly</h3><p>Use earned POL to advance when practical. Active builders may also choose to self-fund, but only within their own risk tolerance.</p></article></div></div></section>
<section class="section"><div class="wrap"><div class="section-head"><div class="eyebrow">Depth over width</div><h2>2 → 4 → 8 → 16 → 32</h2><p>The first major team milestone is 30 correctly placed positions across the first four generations: 2 + 4 + 8 + 16.</p></div><div class="matrix" aria-label="Matrix growth illustration"><div class="matrix-group"><div class="people"><span class="person"></span><span class="person"></span></div><b>2</b></div><div class="matrix-group"><div class="people"><span class="person"></span><span class="person"></span><span class="person"></span><span class="person"></span></div><b>4</b></div><div class="matrix-group"><div class="people" id="p8"></div><b>8</b></div><div class="matrix-group"><div class="people" id="p16"></div><b>16</b></div></div><div class="notice"><strong>Team principle:</strong> once your two are in place, retire your link and shift to helping them get <em>their</em> two — their own directs are the only thing that qualifies their positions to catch payments. Spillover grows your team but never qualifies anyone, so the effort always moves down.</div></div></section>
<section class="section" id="proof"><div class="wrap"><div class="section-head"><div class="eyebrow">Live payment proof</div><h2>Real payouts, straight from the blockchain.</h2><p>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.</p></div><div id="payoutTotals" class="pp-totals"></div><div id="payoutFeed" class="pp-feed"><div class="empty">Reading the blockchain…</div></div><div class="pp-note">Data is read directly from the RM Circle smart contract (<a href="https://polygonscan.com/address/0x33BdAEEfd6d17D80aE53816c916dFb26c4fB2DAF" target="_blank" rel="noopener noreferrer" style="color:var(--teal)">0x33Bd…2DAF</a>) on Polygon Mainnet. Member numbers are on-chain IDs, not names. Past payouts are not a promise of future results.</div></div></section>
<section class="section" id="join"><div class="wrap"><div class="section-head"><div class="eyebrow">Ready to join?</div><h2>Join under Member <span class="gold" id="invIdJoin">#—</span>.</h2><p>Your invite carries your sponsor automatically — no codes to remember. Here's the whole process, start to finish.</p></div>
<div class="instruction-list" style="max-width:880px;margin:0 auto"><article class="instruction"><div class="num">1</div><div><h3>Install or open MetaMask</h3><p>Use the official MetaMask website or your device's official app store. Never install a wallet from a link sent by a stranger.</p><div style="margin-top:10px"><a class="btn btn-secondary btn-sm" href="https://metamask.io/" target="_blank" rel="noopener noreferrer">Official MetaMask Site ↗</a></div></div></article>
<article class="instruction"><div class="num">2</div><div><h3>Use Polygon Mainnet, funded with POL</h3><p>Premium entry is <strong>362 POL</strong> plus a little extra for network gas. Polygon Mainnet is chain ID <strong>137</strong>. New to wallets? <a href="/training" style="color:var(--teal)">The training videos</a> walk through every step.</p></div></article>
<article class="instruction"><div class="num">3</div><div><h3>Join with your sponsor's link</h3><p>The button below opens the official RM Circle dApp with <strong>Member <span class="inv-id-inline">#—</span></strong> already set as your sponsor. Confirm the sponsor ID matches before signing, and never share your Secret Recovery Phrase with anyone.</p><div style="margin-top:10px;display:flex;gap:8px;flex-wrap:wrap"><a id="joinBtn" class="btn btn-primary" href="#" target="_blank" rel="noopener noreferrer">Join Under <span class="inv-id-inline">#—</span> →</a><button id="copySponsor" class="btn btn-secondary">Copy Sponsor ID</button></div></div></article>
<article class="instruction" style="border-color:var(--gold);background:rgba(243,190,67,.05)"><div class="num" style="background:var(--gold);color:#071421;border-color:var(--gold)">4</div><div><h3>Submit your NEW RM Circle ID here</h3><p>After your purchase confirms, the RM Circle dApp shows your <strong>new member ID</strong>. Type it below — we verify it on the blockchain, your sponsor gets notified, and the team rotation starts working on <strong>your</strong> 2.</p><form id="joinForm" style="display:grid;gap:8px;margin-top:12px;max-width:480px"><input name="newId" class="input" inputmode="numeric" pattern="[0-9]{1,10}" maxlength="10" placeholder="Your NEW RM Circle ID (numbers only)" required><input name="memberName" class="input" maxlength="60" placeholder="Your name or Telegram @handle (so the team can reach you)" required><button class="btn btn-primary">Submit My ID →</button></form><div id="joinMsg" style="margin-top:10px;font-size:14px"></div></div></article></div>
<div class="callout" style="margin-top:16px;max-width:880px;margin-left:auto;margin-right:auto"><strong>What happens next:</strong> get your 2 directs to qualify, then retire your link and help your 2 get their 2 — that's the whole system. You'll get your own invite page just like this one the moment you're in.</div>
<div class="callout warning" style="margin-top:12px;max-width:880px;margin-left:auto;margin-right:auto"><strong>Risk reminder:</strong> participation involves cryptocurrency and smart-contract risk. No income is guaranteed. Use only funds you can afford to lose.</div></div></section>
</main><footer class="wrap disclaimer">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.<div class="footer-links"><a href="/training">Training</a><a href="/my">Member Dashboard</a></div></footer>
<script src="/track.js"></script><script src="/join.js"></script><script src="/payouts.js" defer></script><script src="/chat.js" defer></script></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex"><meta name="description" content="You've been personally invited to the RM Circle Premium team build — see the strategy, the live on-chain payouts, and your sponsor's real position."><title>You're Invited | Crypto Team Build</title>
<meta property="og:type" content="website"><meta property="og:site_name" content="Crypto Team Build Network"><meta property="og:title" content="You're invited to the RM Circle Premium Team Build"><meta property="og:description" content="A team-first strategy built around qualification, depth, and duplication — with every payout verified on the blockchain. Your invite carries your sponsor automatically."><meta property="og:image" content="https://rmcircle.saasy.top/og-card.jpg"><meta property="og:image:width" content="1200"><meta property="og:image:height" content="630"><meta property="og:image:alt" content="The RM Circle Premium Team Build roadmap card — Crypto Team Build Network">
<meta name="twitter:card" content="summary_large_image"><meta name="twitter:title" content="You're invited — RM Circle Premium Team Build"><meta name="twitter:description" content="Get your 2. Help your 2 get their 2. Your invite carries your sponsor automatically."><meta name="twitter:image" content="https://rmcircle.saasy.top/og-card.jpg"><link rel="stylesheet" href="/styles.css"></head>
<body>
<header class="wrap nav"><a class="brand" href="/"><div class="brand-mark">RM</div><span><span id="brandName">Crypto Team Build</span><small id="brandProgram">RM Circle Premium</small></span></a><div class="nav-actions"><a class="btn btn-secondary hide-mobile" href="#strategy">How it works</a><a class="btn btn-secondary" href="/training">Training</a><a class="btn btn-primary" href="#join">Join the Team</a></div></header>
<main>
<section class="hero wrap" style="padding-bottom:12px"><div class="eyebrow">Personal invitation</div><h1>You've been invited by<br><span class="gold">Member <span id="invId">#—</span>.</span></h1><p id="invSub">This page is their real position on the team — every number on it is read live from the blockchain.</p>
<div id="invCard" class="table-card" style="max-width:680px;margin:20px 0 6px"><div style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap"><h2 style="margin:0">Your sponsor's position <span class="live-badge" style="vertical-align:middle;margin-left:6px"><span class="dot"></span> Verified on-chain</span></h2><span id="invQualified"></span></div><div id="invFacts" class="facts" style="grid-template-columns:repeat(3,1fr);margin:14px 0 0"><div class="empty" style="grid-column:1/-1">Reading the blockchain…</div></div><div id="invQualNote"></div></div>
<div class="hero-actions" style="margin-top:18px"><a class="btn btn-primary" href="#join">Join Under <span id="invIdBtn">This Member</span> →</a><a class="btn btn-secondary" href="#strategy">See the Strategy First</a></div>
<div class="micro">Independent team training resource • Participation involves risk • No income is guaranteed</div></section>
<section class="section" style="padding-bottom:0"><div class="wrap"><div class="section-head"><div class="eyebrow">Watch first</div><h2>See how simple this is.</h2><p>A quick walkthrough of the team build in action. Watch this before you dive into the strategy below.</p></div><div class="video-card" style="max-width:880px;margin:0 auto"><iframe class="video-frame" style="display:block;width:100%;height:auto;aspect-ratio:16/9" width="880" height="495" src="https://www.youtube-nocookie.com/embed/VOhwBiAO7to" title="RM Circle Premium team build walkthrough video" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></div></div></section>
<section id="strategy" class="section"><div class="wrap"><div class="section-head"><div class="eyebrow">The strategy</div><h2>Simple enough to duplicate.</h2><p>Each position gets two directs to qualify, then retires its link and helps the next two positions repeat the process. Late signups on a qualified link are a welcome bonus — they pay that member and spill downward as depth.</p></div><div class="grid-3"><article class="card"><div class="card-icon">2</div><h3>Get exactly two</h3><p>Use your position's link until two direct positions are placed beneath you and your position is qualified.</p></article><article class="card"><div class="card-icon">↘</div><h3>Move the effort down</h3><p>Retire your link and help each of your two directs use their own links until they each have two — that's what qualifies them. If someone still joins through your link anyway, it's a bonus: it pays you and spills down as depth.</p></article><article class="card"><div class="card-icon">↑</div><h3>Upgrade responsibly</h3><p>Use earned POL to advance when practical. Active builders may also choose to self-fund, but only within their own risk tolerance.</p></article></div></div></section>
<section class="section"><div class="wrap"><div class="section-head"><div class="eyebrow">Depth over width</div><h2>2 → 4 → 8 → 16 → 32</h2><p>The first major team milestone is 30 correctly placed positions across the first four generations: 2 + 4 + 8 + 16.</p></div><div class="matrix" aria-label="Matrix growth illustration"><div class="matrix-group"><div class="people"><span class="person"></span><span class="person"></span></div><b>2</b></div><div class="matrix-group"><div class="people"><span class="person"></span><span class="person"></span><span class="person"></span><span class="person"></span></div><b>4</b></div><div class="matrix-group"><div class="people" id="p8"></div><b>8</b></div><div class="matrix-group"><div class="people" id="p16"></div><b>16</b></div></div><div class="notice"><strong>Team principle:</strong> once your two are in place, retire your link and shift to helping them get <em>their</em> two — their own directs are the only thing that qualifies their positions to catch payments. Spillover grows your team but never qualifies anyone, so the effort always moves down.</div></div></section>
<section class="section" id="proof"><div class="wrap"><div class="section-head"><div class="eyebrow">Live payment proof</div><h2>Real payouts, straight from the blockchain.</h2><p>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.</p></div><div id="payoutTotals" class="pp-totals"></div><div id="payoutFeed" class="pp-feed"><div class="empty">Reading the blockchain…</div></div><div class="pp-note">Data is read directly from the RM Circle smart contract (<a href="https://polygonscan.com/address/0x33BdAEEfd6d17D80aE53816c916dFb26c4fB2DAF" target="_blank" rel="noopener noreferrer" style="color:var(--teal)">0x33Bd…2DAF</a>) on Polygon Mainnet. Member numbers are on-chain IDs, not names. Past payouts are not a promise of future results.</div></div></section>
<section class="section" id="join"><div class="wrap"><div class="section-head"><div class="eyebrow">Ready to join?</div><h2>Join under Member <span class="gold" id="invIdJoin">#—</span>.</h2><p>Your invite carries your sponsor automatically — no codes to remember. Here's the whole process, start to finish.</p></div>
<div class="instruction-list" style="max-width:880px;margin:0 auto"><article class="instruction"><div class="num">1</div><div><h3>Install or open MetaMask</h3><p>Use the official MetaMask website or your device's official app store. Never install a wallet from a link sent by a stranger.</p><div style="margin-top:10px"><a class="btn btn-secondary btn-sm" href="https://metamask.io/" target="_blank" rel="noopener noreferrer">Official MetaMask Site ↗</a></div></div></article>
<article class="instruction"><div class="num">2</div><div><h3>Use Polygon Mainnet, funded with POL</h3><p>Premium entry is <strong>362 POL</strong> plus a little extra for network gas. Polygon Mainnet is chain ID <strong>137</strong>. New to wallets? <a href="/training" style="color:var(--teal)">The training videos</a> walk through every step.</p></div></article>
<article class="instruction"><div class="num">3</div><div><h3>Join with your sponsor's link</h3><p>The button below opens the official RM Circle dApp with <strong>Member <span class="inv-id-inline">#—</span></strong> already set as your sponsor. Confirm the sponsor ID matches before signing, and never share your Secret Recovery Phrase with anyone.</p><div style="margin-top:10px;display:flex;gap:8px;flex-wrap:wrap"><a id="joinBtn" class="btn btn-primary" href="#" target="_blank" rel="noopener noreferrer">Join Under <span class="inv-id-inline">#—</span> →</a><button id="copySponsor" class="btn btn-secondary">Copy Sponsor ID</button></div></div></article>
<article class="instruction" style="border-color:var(--gold);background:rgba(243,190,67,.05)"><div class="num" style="background:var(--gold);color:#071421;border-color:var(--gold)">4</div><div><h3>Submit your NEW RM Circle ID here</h3><p>After your purchase confirms, the RM Circle dApp shows your <strong>new member ID</strong>. Type it below — we verify it on the blockchain, your sponsor gets notified, and the team rotation starts working on <strong>your</strong> 2.</p><form id="joinForm" style="display:grid;gap:8px;margin-top:12px;max-width:480px"><input name="newId" class="input" inputmode="numeric" pattern="[0-9]{1,10}" maxlength="10" placeholder="Your NEW RM Circle ID (numbers only)" required><input name="memberName" class="input" maxlength="60" placeholder="Your name or Telegram @handle (so the team can reach you)" required><button class="btn btn-primary">Submit My ID →</button></form><div id="joinMsg" style="margin-top:10px;font-size:14px"></div></div></article></div>
<div class="callout" style="margin-top:16px;max-width:880px;margin-left:auto;margin-right:auto"><strong>What happens next:</strong> get your 2 directs to qualify, then retire your link and help your 2 get their 2 — that's the whole system. You'll get your own invite page just like this one the moment you're in.</div>
<div class="callout warning" style="margin-top:12px;max-width:880px;margin-left:auto;margin-right:auto"><strong>Risk reminder:</strong> participation involves cryptocurrency and smart-contract risk. No income is guaranteed. Use only funds you can afford to lose.</div></div></section>
</main><footer class="wrap disclaimer">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.<div class="footer-links"><a href="/training">Training</a><a href="/my">Member Dashboard</a><a href="/contract">Contract Security</a></div></footer>
<script src="/track.js"></script><script src="/join.js"></script><script src="/payouts.js" defer></script><script src="/chat.js" defer></script></body></html>
+1 -1
View File
@@ -15,5 +15,5 @@
<div class="callout" style="margin-top:16px"><strong>What happens next:</strong> get your 2 directs to qualify, then retire your link and help your 2 get their 2 — their own directs are what qualify them. If an extra referral happens anyway, it's a bonus: it still pays you and spills down your leg. Upgrade with earned POL when practical, and stay ahead of your team's levels so payments never pass you by.</div>
<div class="callout warning" style="margin-top:12px"><strong>Risk reminder:</strong> participation involves cryptocurrency and smart-contract risk. No income is guaranteed. Use only funds you can afford to lose.</div><div id="supportBox" class="notice" style="margin-top:12px"></div><div id="supportLinkWrap" class="hidden" style="margin-top:10px"><a id="supportLink" class="btn btn-secondary" target="_blank" rel="noopener noreferrer">Open Team Support ↗</a></div></section></div>
<figure class="roadmap-figure"><a href="/roadmap.webp" target="_blank" rel="noopener"><img src="/roadmap.webp" alt="RM Circle Premium Team Build Roadmap — core strategy, step-by-step guide, premium levels, and duplication formula" width="1149" height="1369" loading="lazy"></a><figcaption>The RM Circle is a team build project of the <strong>Crypto Team Build Network</strong>. This roadmap is the plan every member follows — tap to view full size.</figcaption></figure></div></main>
<footer class="wrap disclaimer">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.<div class="footer-links"><a href="/my">Already joined? Open your Member Dashboard →</a></div></footer>
<footer class="wrap disclaimer">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.<div class="footer-links"><a href="/my">Already joined? Open your Member Dashboard →</a><a href="/contract">Contract Security</a></div></footer>
<script src="/track.js"></script><script src="/start.js"></script><script src="/payouts.js" defer></script><script src="/chat.js" defer></script></body></html>
+1 -1
View File
@@ -26,5 +26,5 @@
<div class="callout warning" style="max-width:880px;margin:14px auto 0"><strong>Risk reminder:</strong> participation involves cryptocurrency and smart-contract risk. No income is guaranteed. Use only funds you can afford to lose.</div>
</div></section>
</main>
<footer class="wrap disclaimer">This is an independent Crypto Team Build training resource. Always confirm transaction details in your wallet before signing. Never disclose your Secret Recovery Phrase.<div class="footer-links"><a href="/">Strategy</a><a href="/start">Getting Started</a><a href="/admin">Team Admin</a></div></footer>
<footer class="wrap disclaimer">This is an independent Crypto Team Build training resource. Always confirm transaction details in your wallet before signing. Never disclose your Secret Recovery Phrase.<div class="footer-links"><a href="/">Strategy</a><a href="/start">Getting Started</a><a href="/contract">Contract Security</a><a href="/admin">Team Admin</a></div></footer>
<script src="/track.js"></script><script src="/training.js"></script><script src="/chat.js" defer></script></body></html>
+485 -485
View File
@@ -1,485 +1,485 @@
const http = require('http');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { URL } = require('url');
const chain = require('./chain');
const PORT = Number(process.env.PORT || 3000);
const ROOT = __dirname;
const PUBLIC_DIR = path.join(ROOT, 'public');
const DATA_DIR = process.env.DATA_DIR || path.join(ROOT, 'data');
const SEED_DIR = path.join(ROOT, 'seed');
const SPONSORS_FILE = path.join(DATA_DIR, 'sponsors.json');
const CONFIG_FILE = path.join(DATA_DIR, 'config.json');
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'changeme';
const IS_PROD = process.env.NODE_ENV === 'production';
const SESSION_TTL = 8 * 60 * 60 * 1000;
const LEVELS = ['Scintilla','Ascensus','Fabrica','Culmen','Apex','Fastigium','Vertex','Corona'];
const OPENROUTER_MODEL = process.env.OPENROUTER_MODEL || 'deepseek/deepseek-v4-flash:nitro';
const OPENROUTER_KEY_FILE = path.join(DATA_DIR, 'openrouter.key');
function getOpenRouterKey() {
if (process.env.OPENROUTER_API_KEY) return process.env.OPENROUTER_API_KEY;
try { return fs.readFileSync(OPENROUTER_KEY_FILE, 'utf8').trim(); } catch (e) { return ''; }
}
const chatHits = new Map();
function chatRateLimited(ip) {
const now = Date.now(), rec = chatHits.get(ip);
if (!rec || now > rec.reset) { chatHits.set(ip, { count: 1, reset: now + 60000 }); return false; }
rec.count++; return rec.count > 10;
}
function chatSystemPrompt() {
const c = getConfig(), sponsors = getSponsors(), a = activeSponsor(sponsors);
const waiting = sponsors.filter(s=>s.status==='waiting').length;
return `You are "Team Help", the assistant on ${c.siteName || 'Crypto Team Build'} (https://rmcircle.saasy.top), the team site for the RM Circle Premium team build — a project of the Crypto Team Build Network.
FACTS:
- Strategy: enter RM Circle dApp at Premium tier (${c.premiumEntryPol || 362} POL on Polygon Mainnet, chain ID 137, POL is the gas token). Each member gets 2 directs to become "qualified"; the team then recommends retiring that link and helping those 2 get their 2 with their OWN links (moving-link strategy) — a position only qualifies through its own directs. If an extra signup lands on a qualified link anyway, present it as a BONUS, not a rule violation: it still pays that member the full entry reward and spills downward into the first open slot in their leg (depth). Spillover never qualifies the people below.
- First team goal: 30 properly placed positions (2+4+8+16), then 32, 64, 128 and beyond.
- 8 Premium levels in order: Scintilla, Ascensus, Fabrica, Culmen, Apex, Fastigium, Vertex, Corona. Everyone starts at Scintilla. Upgrade as quickly as practical, ideally with earned POL; the first two payments at each level help fund the next upgrade. Stay aware of your active downline's levels so you don't fall behind and miss payments.
- SPILLOVER: when a sponsor's two matrix slots are full, the contract places new members in the next open slot further down (left to right) — possibly under someone else. Spillover fills that member's matrix and sets up future upgrade income to their position, but does NOT count toward qualification: only people who join using a member's own ID are their directs. Upgrade payments travel up the MATRIX (not who-referred-whom) and only stop at positions that are qualified (2 directs) AND already at the level being bought — otherwise the payment passes them by to the next eligible upline. Members can see their own matrix, spillover tags, and payments at https://rmcircle.saasy.top/my
- Current team sponsor: ${a ? `ID ${a.id}${c.showSponsorName && a.name ? ` (${a.name})` : ''}, ${a.directs}/2 directs` : 'shown on the start page'}. ${waiting} placement(s) waiting. Placements rotate as positions qualify — always verify on https://rmcircle.saasy.top/start right before joining.
- Site pages: https://rmcircle.saasy.top/ (strategy overview + roadmap), https://rmcircle.saasy.top/start (current sponsor + join steps), https://rmcircle.saasy.top/training (4 videos: 1. How the team build works, 2. Create your MetaMask wallet, 3. Funding your wallet, 4. Buying the Premium position).
- Telegram group for live team help: ${c.telegramUrl || 'https://t.me/cryptoteambuild'}
RULES:
- Keep answers short: 1-4 sentences, plain text, no markdown formatting. Include full URLs when pointing to a page.
- NEVER promise, estimate, or imply earnings or income. If asked about returns/profit, say results depend on team effort, duplication, upgrades, smart-contract rules and POL's market value, that no income is guaranteed, and to only use funds they can afford to lose.
- NEVER ask for or discuss handling anyone's Secret Recovery Phrase or private keys except to warn they must never share them with anyone.
- Only answer questions about this project, the site, wallets/POL as they relate to joining, and the team process. For anything else, or anything you are not sure about, say you're not sure and point them to the Telegram group: ${c.telegramUrl || 'https://t.me/cryptoteambuild'}
- Never give financial, legal, or tax advice.`;
}
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);
if (!rec || now > rec.reset) { submitHits.set(ip, { count: 1, reset: now + 600000 }); return false; }
rec.count++; return rec.count > 5;
}
function sendTelegram(text) {
const c = getConfig();
if (!c.telegramBotToken || !c.telegramChatId) return;
const payload = { chat_id: c.telegramChatId, text };
if (c.telegramTopicId && /^[0-9]+$/.test(String(c.telegramTopicId))) payload.message_thread_id = Number(c.telegramTopicId);
fetch(`https://api.telegram.org/bot${c.telegramBotToken}/sendMessage`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
}).then(async r=>{ if(!r.ok) console.error('telegram sendMessage status', r.status, (await r.text().catch(()=>'')).slice(0,200)); })
.catch(e=>console.error('telegram error', e.message));
}
const SENDGRID_KEY_FILE = path.join(DATA_DIR, 'sendgrid.key');
function getSendgridKey() {
if (process.env.SENDGRID_API_KEY) return process.env.SENDGRID_API_KEY;
try { return fs.readFileSync(SENDGRID_KEY_FILE, 'utf8').trim(); } catch (e) { return ''; }
}
// SendGrid is domain-authenticated for marketingwithmarty.com and
// mybrandedvoice.com — the from address must stay on one of those or DKIM fails.
function emailFrom() { return getConfig().emailFrom || 'The RM Circle Team <no-reply@marketingwithmarty.com>'; }
function sendEmailRaw(toEmail, subject, text) {
const key = getSendgridKey();
if (!key || !toEmail) return;
const fromStr = emailFrom();
const m = fromStr.match(/^(.*)<([^>]+)>\s*$/);
const from = m ? { email: m[2].trim(), name: m[1].trim() || undefined } : { email: fromStr.trim() };
fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
personalizations: [{ to: [{ email: toEmail }] }],
from, subject,
content: [{ type: 'text/plain', value: text }]
})
}).then(r => { if (r.status >= 300) r.text().then(t => console.error('sendgrid status', r.status, t.slice(0, 200))); })
.catch(e => console.error('sendgrid error', e.message));
}
function sendPaidEmail(toEmail, memberName, evt) {
const kindLine = evt.kind === 'upline' ? `an upgrade pass-up from member #${evt.fromId}` : `a referral reward from member #${evt.fromId}'s entry`;
const verify = evt.tx ? `\n\nVerify it yourself on the blockchain:\nhttps://polygonscan.com/tx/${evt.tx}` : '';
const text = `Hi ${memberName || 'there'},\n\nGood news — your RM Circle position #${evt.toId} just received ${evt.pol.toFixed(2)} POL (${kindLine}).${verify}\n\nKeep the momentum going: check your level so the next payment in your leg doesn't pass you by.\nhttps://rmcircle.saasy.top/training\n\n— The RM Circle Team\n\nYou're receiving this because your team admin has this address on file for team-build updates. Reply to this email to be removed.`;
sendEmailRaw(toEmail, `Your RM Circle position #${evt.toId} just got paid ${evt.pol.toFixed(2)} POL`, text);
}
function firePostback(clickid, txid, source) {
const pb = getConfig().bemobPostbackUrl;
if (!clickid || !pb || !/^https:\/\/[a-z0-9.-]+\/postback/i.test(pb)) return;
fetch(`${pb}${pb.includes('?')?'&':'?'}cid=${encodeURIComponent(clickid)}&payout=0&txid=${encodeURIComponent(txid)}`)
.then(r=>{ if(r.ok) recordEvent('postback', source); else console.error('bemob postback status', r.status); })
.catch(e=>console.error('bemob postback error', e.message));
}
async function handleSubmitId(req, res) {
const ip = String(req.headers['x-forwarded-for']||req.socket.remoteAddress||'').split(',')[0].trim();
if (submitRateLimited(ip)) return json(res, 429, { error: 'Too many submissions — please wait a few minutes.' });
const b = await bodyJson(req).catch(()=>null);
if (!b) return json(res, 400, { error: 'Invalid request.' });
const newId = String(b.newId||'').trim();
if (!/^[0-9]{1,10}$/.test(newId)) return json(res, 400, { error: 'Enter your numeric RM Circle ID (numbers only).' });
const memberName = String(b.memberName||'').replace(/[\u0000-\u001f\u007f]/g,'').trim().slice(0, 60);
if (!memberName) return json(res, 400, { error: 'Add your name or Telegram handle so the team can reach you.' });
const sponsorId = String(b.sponsorId||'').trim().slice(0, 20).replace(/[^0-9A-Za-z._-]/g,'') || '?';
const source = typeof b.source==='string' ? b.source : '';
const clickid = typeof b.clickid==='string' ? b.clickid.trim().slice(0,80).replace(/[^A-Za-z0-9._-]/g,'') : '';
let subs = []; try { subs = readJson(SUBMISSIONS_FILE); } catch(e) {}
if (subs.some(s=>s.newId===newId)) return json(res, 200, { ok: true, duplicate: true });
// on-chain verification: does this ID actually exist on the contract?
let onchain = null;
try {
onchain = await Promise.race([
chain.verifyMember(Number(newId)),
new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), 6000))
]);
} catch (e) { onchain = null; }
// the chain decides the path: rotation join (referrer = active rotation sponsor)
// vs leg join (someone's personal team build) — regardless of which page they used
const active = activeSponsor(getSponsors());
let joinPath = 'unknown';
if (onchain && onchain.registered) joinPath = (active && String(onchain.referrerId) === String(active.id)) ? 'rotation' : 'leg';
else if (onchain && !onchain.registered) joinPath = 'notfound';
subs.push({ newId, memberName, sponsorId, source: source||'(direct)', clickid, ts: new Date().toISOString(), path: joinPath,
onchain: onchain ? { registered: onchain.registered, tier: onchain.tierName, level: onchain.levelName, referrerId: onchain.referrerId, uplineId: onchain.uplineId } : undefined });
writeJson(SUBMISSIONS_FILE, subs.slice(-1000));
recordEvent('purchase', source);
firePostback(clickid, `purchase-${clickid}`, source);
let msg;
if (joinPath === 'rotation') {
// rotation joins go straight into the queue as waiting positions — no manual step
let queueNote = '';
try {
let sponsors = getSponsors();
if (sponsors.some(s => String(s.id) === String(newId))) {
queueNote = 'Already in the rotation queue.';
} else {
const maxOrder = sponsors.reduce((m, s) => Math.max(m, s.sortOrder || 0), 0);
sponsors.push({ id: String(newId), name: memberName, parentId: String(active.id), directs: 0, level: onchain.levelName || 'Scintilla', status: sponsors.some(s => s.status === 'active') ? 'waiting' : 'active', sortOrder: maxOrder + 10, clicks: 0, notes: `auto-added: rotation join ${new Date().toISOString().slice(0, 10)}` });
sponsors = normalizeStatuses(sponsors);
saveSponsors(sponsors);
const waitingAhead = sponsors.filter(s => s.status === 'waiting' && (s.sortOrder || 0) < maxOrder + 10).length;
queueNote = `Auto-added to the rotation queue (${waitingAhead} waiting ahead of them).`;
}
} catch (e) { queueNote = `⚠ Auto-add to queue failed (${e.message}) — add manually.`; console.error('queue auto-add', e.message); }
msg = `🔔 RM Circle: ROTATION JOIN CONFIRMED ✅\nName: ${memberName}\nNew ID: ${newId} (${onchain.tierName}, verified on-chain)\nJoined under rotation sponsor: #${onchain.referrerId}\nSource: ${source||'(direct)'}\n✅ ${queueNote}\n(+1 direct for ID ${active.id} syncs from the chain automatically.)`;
} else if (joinPath === 'leg') {
msg = `🌱 RM Circle: TEAM-BUILD JOIN (not rotation)\nName: ${memberName}\nNew ID: ${newId} (${onchain.tierName}, verified on-chain)\nActual sponsor on-chain: #${onchain.referrerId}${sponsorId!=='?'&&String(onchain.referrerId)!==sponsorId?` (form said ${sponsorId})`:''}\nSource: ${source||'(direct)'}\n→ Leg growth under #${onchain.referrerId} — no rotation action needed. Add them to the rotation queue only if they want the team effort.`;
} else if (joinPath === 'notfound') {
msg = `🔔 RM Circle: ID SUBMITTED — ❌ NOT FOUND ON-CHAIN\nName: ${memberName}\nNew ID: ${newId}\nClaimed sponsor: ${sponsorId}\nSource: ${source||'(direct)'}\n→ ID has no registration on the contract — double-check with them before any queue action.`;
} else {
msg = `🔔 RM Circle: NEW MEMBER SUBMITTED\nName: ${memberName}\nNew ID: ${newId}\nClaimed sponsor: ${sponsorId}\nSource: ${source||'(direct)'}\n⏳ On-chain check unavailable — verify manually in admin (member lookup).`;
}
sendTelegram(msg);
return json(res, 200, { ok: true, path: joinPath, onchain: onchain ? { registered: onchain.registered, tier: onchain.tierName, level: onchain.levelName, referrerId: onchain.referrerId } : null });
}
async function handleChat(req, res) {
const ip = String(req.headers['x-forwarded-for']||req.socket.remoteAddress||'').split(',')[0].trim();
if (chatRateLimited(ip)) return json(res, 429, { error: 'Too many messages — give it a minute.' });
const apiKey = getOpenRouterKey();
if (!apiKey) return json(res, 200, { fallback: true });
const b = await bodyJson(req).catch(()=>null);
if (!b || !Array.isArray(b.messages)) return json(res, 400, { error: 'Invalid request' });
const msgs = b.messages.slice(-8)
.filter(m=>m&&(m.role==='user'||m.role==='assistant')&&typeof m.content==='string')
.map(m=>({ role: m.role, content: m.content.slice(0, 500) }));
if (!msgs.length || msgs[msgs.length-1].role !== 'user') return json(res, 400, { error: 'Invalid request' });
try {
const ctrl = new AbortController(); const timer = setTimeout(()=>ctrl.abort(), 20000);
const r = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST', signal: ctrl.signal,
headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', 'HTTP-Referer': 'https://rmcircle.saasy.top', 'X-Title': 'RM Circle Team Help' },
body: JSON.stringify({ model: OPENROUTER_MODEL, max_tokens: 350, temperature: 0.3, messages: [{ role: 'system', content: chatSystemPrompt() }, ...msgs] })
});
clearTimeout(timer);
if (!r.ok) { console.error('openrouter status', r.status); return json(res, 200, { fallback: true }); }
const d = await r.json();
const reply = d && d.choices && d.choices[0] && d.choices[0].message && d.choices[0].message.content;
if (!reply) return json(res, 200, { fallback: true });
return json(res, 200, { reply: String(reply).trim().slice(0, 2000) });
} catch (e) { console.error('openrouter error', e.message); return json(res, 200, { fallback: true }); }
}
// Sessions persist in the data volume so redeploys stop logging the admin out.
const SESSIONS_FILE = path.join(DATA_DIR, 'sessions.json');
const sessions = new Map();
try {
const saved = JSON.parse(fs.readFileSync(SESSIONS_FILE, 'utf8'));
const now = Date.now();
for (const [t, s] of Object.entries(saved)) if (s && s.expires > now) sessions.set(t, s);
} catch (e) {}
function saveSessions() {
try {
const now = Date.now();
for (const [t, s] of sessions) if (s.expires <= now) sessions.delete(t);
const tmp = SESSIONS_FILE + '.tmp';
fs.writeFileSync(tmp, JSON.stringify(Object.fromEntries(sessions)), { mode: 0o600 });
fs.renameSync(tmp, SESSIONS_FILE);
} catch (e) { console.error('session save failed', e.message); }
}
function ensureDataFile(name) {
fs.mkdirSync(DATA_DIR, { recursive: true });
const target = path.join(DATA_DIR, name);
if (!fs.existsSync(target)) fs.copyFileSync(path.join(SEED_DIR, name), target);
}
ensureDataFile('sponsors.json');
ensureDataFile('config.json');
const ANALYTICS_FILE = path.join(DATA_DIR, 'analytics.json');
if (!fs.existsSync(ANALYTICS_FILE)) fs.writeFileSync(ANALYTICS_FILE, JSON.stringify({ sources: {} }, null, 2));
function readJson(file) { return JSON.parse(fs.readFileSync(file, 'utf8')); }
function writeJson(file, data) {
const temp = `${file}.${crypto.randomUUID()}.tmp`;
fs.writeFileSync(temp, JSON.stringify(data, null, 2));
fs.renameSync(temp, file);
}
function getSponsors() { return readJson(SPONSORS_FILE).sort((a,b)=>(a.sortOrder||0)-(b.sortOrder||0)); }
function saveSponsors(s) { writeJson(SPONSORS_FILE, s); }
function getConfig() { return readJson(CONFIG_FILE); }
function activeSponsor(sponsors) { return sponsors.find(s=>s.status==='active') || sponsors.find(s=>s.status==='waiting') || null; }
function getAnalytics() { try { return readJson(ANALYTICS_FILE); } catch (e) { return { sources: {} }; } }
function recordEvent(event, source) {
if (!['bridge','start','click','training','postback','purchase','join'].includes(event)) return;
const s = String(source||'').toLowerCase().trim().replace(/[^a-z0-9.()\-_:/ ]/g,'').slice(0,80) || '(direct)';
const a = getAnalytics(); if (!a.sources) a.sources = {};
if (!a.sources[s]) { if (Object.keys(a.sources).length >= 500) return; a.sources[s] = { bridge:0, start:0, click:0 }; }
a.sources[s][event] = (a.sources[s][event]||0) + 1;
writeJson(ANALYTICS_FILE, a);
}
function normalizeStatuses(sponsors, preferredActiveId=null) {
const eligible=sponsors.filter(s=>s.status!=='qualified');
let activeId=preferredActiveId;
if(!activeId || !eligible.some(s=>s.id===activeId)){
const existing=eligible.find(s=>s.status==='active');
activeId=existing?existing.id:(eligible[0]?.id||null);
}
return sponsors.map(s=>s.status==='qualified'?s:{...s,status:s.id===activeId?'active':'waiting'});
}
function publicSponsorPayload(sponsor, config) {
if(!sponsor)return null;
return {id:sponsor.id,name:config.showSponsorName?sponsor.name:null,directs:sponsor.directs,goal:2,level:sponsor.level,referralUrl:`${config.dappReferralBaseUrl}${encodeURIComponent(sponsor.id)}`};
}
const CSP_BASE="default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; font-src 'self' data:; form-action 'self'; frame-src https://www.youtube-nocookie.com";
function securityHeaders(extra={}) {
// Public pages must render inside safelist / traffic-exchange iframes, so framing stays open here; admin.html re-locks it via ADMIN_FRAME_HEADERS.
return {
'X-Content-Type-Options':'nosniff','Referrer-Policy':'strict-origin-when-cross-origin',
'Permissions-Policy':'camera=(), microphone=(), geolocation=()',
'Content-Security-Policy':`${CSP_BASE}; frame-ancestors *`,
...extra
};
}
const ADMIN_FRAME_HEADERS={'X-Frame-Options':'DENY','Content-Security-Policy':`${CSP_BASE}; frame-ancestors 'none'`};
function send(res,status,body,headers={}) { res.writeHead(status,securityHeaders(headers));res.end(body); }
function json(res,status,obj,headers={}) { send(res,status,JSON.stringify(obj),{'Content-Type':'application/json; charset=utf-8',...headers}); }
function parseCookies(req){const out={};for(const p of (req.headers.cookie||'').split(';')){const i=p.indexOf('=');if(i>0)out[p.slice(0,i).trim()]=decodeURIComponent(p.slice(i+1).trim())}return out}
function getSession(req){const token=parseCookies(req)['ctb.sid'];if(!token)return null;const s=sessions.get(token);if(!s)return null;if(s.expires<Date.now()){sessions.delete(token);return null}return {token,...s}}
function requireAdmin(req,res){if(!getSession(req)){json(res,401,{error:'Unauthorized'});return false}return true}
async function bodyJson(req){return await new Promise((resolve,reject)=>{let data='';req.on('data',c=>{data+=c;if(data.length>100000){reject(new Error('Payload too large'));req.destroy()}});req.on('end',()=>{if(!data)return resolve({});try{resolve(JSON.parse(data))}catch(e){reject(new Error('Invalid JSON'))}});req.on('error',reject)})}
function contentType(file){const ext=path.extname(file);return ({'.html':'text/html; charset=utf-8','.css':'text/css; charset=utf-8','.js':'application/javascript; charset=utf-8','.json':'application/json; charset=utf-8','.png':'image/png','.jpg':'image/jpeg','.jpeg':'image/jpeg','.webp':'image/webp','.svg':'image/svg+xml','.ico':'image/x-icon','.mp4':'video/mp4','.webm':'video/webm'}[ext]||'application/octet-stream')}
function staticFile(req,res,file,status=200){
if(!fs.existsSync(file)||!fs.statSync(file).isFile())return false;
const size=fs.statSync(file).size;
const base={'Content-Type':contentType(file),'Accept-Ranges':'bytes','Cache-Control':['.html','.css','.js'].includes(path.extname(file))?'no-cache':'public, max-age=3600',...(path.basename(file)==='admin.html'?ADMIN_FRAME_HEADERS:{})};
const m=status===200&&req.headers.range?String(req.headers.range).match(/^bytes=(\d*)-(\d*)$/):null;
if(m&&(m[1]!==''||m[2]!=='')){
const start=m[1]===''?Math.max(0,size-Number(m[2])):Number(m[1]);
const end=(m[1]!==''&&m[2]!=='')?Math.min(Number(m[2]),size-1):size-1;
if(start>end||start>=size){res.writeHead(416,securityHeaders({'Content-Range':`bytes */${size}`}));res.end();return true}
res.writeHead(206,securityHeaders({...base,'Content-Range':`bytes ${start}-${end}/${size}`,'Content-Length':end-start+1}));
if(req.method==='HEAD')res.end();else fs.createReadStream(file,{start,end}).pipe(res);
return true;
}
res.writeHead(status,securityHeaders({...base,'Content-Length':size}));
if(req.method==='HEAD')res.end();else fs.createReadStream(file).pipe(res);
return true;
}
async function handleApi(req,res,pathname){
if(req.method==='GET'&&pathname==='/health') return json(res,200,{ok:true});
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))]);
if(r.registered)r.referralUrl=`${getConfig().dappReferralBaseUrl}${encodeURIComponent(id)}`;
// align next-in-line with the human-curated rotation: prefer the ACTIVE
// rotation sponsor when they sit in this member's leg and need directs;
// else the first chain-order position that's a queue participant; else
// keep the chain's pure structural pick (covers legs outside the queue).
if(r.registered&&r.subtree){
try{
const sponsors=getSponsors();
const act=activeSponsor(sponsors);
const participants=new Set(sponsors.filter(s=>s.status!=='qualified').map(s=>String(s.id)));
const bfs=[];const q=[r.subtree.left,r.subtree.right].filter(Boolean);
while(q.length){const n=q.shift();bfs.push(n);if(n.left)q.push(n.left);if(n.right)q.push(n.right);}
let pick=null;
if(act)pick=bfs.find(n=>String(n.id)===String(act.id)&&(n.directCount||0)<2);
if(!pick)pick=bfs.find(n=>participants.has(String(n.id))&&(n.directCount||0)<2);
if(pick)r.nextInLine={id:pick.id,directCount:pick.directCount||0,levelName:pick.levelName};
}catch(e){}
}
if(r.nextInLine)r.nextInLine.referralUrl=`${getConfig().dappReferralBaseUrl}${encodeURIComponent(r.nextInLine.id)}`;
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'});
}
if(req.method==='GET'&&pathname==='/api/public/current-sponsor'){
const sponsors=getSponsors(),c=getConfig(),a=activeSponsor(sponsors);if(!a)return json(res,404,{error:'No active sponsor is currently assigned.'});
return json(res,200,{sponsor:publicSponsorPayload(a,c),waitingCount:sponsors.filter(s=>s.status==='waiting').length,message:'Always use the current sponsor shown on this page. Team placement rotates as members qualify.'});
}
if(req.method==='POST'&&pathname==='/api/public/join-click'){
const b=await bodyJson(req).catch(()=>({}));recordEvent('click',b.source);
const clickid=typeof b.clickid==='string'?b.clickid.trim().slice(0,80).replace(/[^A-Za-z0-9._-]/g,''):'';
firePostback(clickid,`join-${clickid}`,b.source);
let sponsors=getSponsors();const a=activeSponsor(sponsors);if(a){sponsors=sponsors.map(s=>s.id===a.id?{...s,clicks:(s.clicks||0)+1}:s);saveSponsors(sponsors)}return json(res,200,{ok:true});
}
if(req.method==='POST'&&pathname==='/api/public/chat')return await handleChat(req,res);
if(req.method==='POST'&&pathname==='/api/public/submit-id')return await handleSubmitId(req,res);
if(req.method==='POST'&&pathname==='/api/public/track'){
const b=await bodyJson(req).catch(()=>({}));recordEvent(b.event,b.source);return json(res,200,{ok:true});
}
if(req.method==='POST'&&pathname==='/api/admin/login'){
const b=await bodyJson(req).catch(e=>null);if(!b)return json(res,400,{error:'Invalid request'});if(typeof b.password!=='string'||b.password!==ADMIN_PASSWORD)return json(res,401,{error:'Invalid password'});
const token=crypto.randomBytes(32).toString('hex');sessions.set(token,{expires:Date.now()+SESSION_TTL});saveSessions();const cookie=`ctb.sid=${encodeURIComponent(token)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${SESSION_TTL/1000}${IS_PROD?'; Secure':''}`;return json(res,200,{ok:true},{'Set-Cookie':cookie});
}
if(req.method==='POST'&&pathname==='/api/admin/logout'){
const s=getSession(req);if(s){sessions.delete(s.token);saveSessions();}return json(res,200,{ok:true},{'Set-Cookie':'ctb.sid=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0'});
}
if(pathname.startsWith('/api/admin/')&&!requireAdmin(req,res))return;
if(req.method==='GET'&&pathname==='/api/admin/matrix-tree'){
return json(res,200,chain.getMatrixTree());
}
if(req.method==='GET'&&pathname==='/api/admin/member-lookup'){
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.'});
try{
const r=await Promise.race([chain.memberLookup(id),new Promise((_,rej)=>setTimeout(()=>rej(new Error('Chain RPC timeout — try again.')),25000))]);
return json(res,200,r);
}catch(e){return json(res,502,{error:e.message||'Lookup failed'})}
}
if(req.method==='GET'&&pathname==='/api/admin/state'){let subs=[];try{subs=readJson(SUBMISSIONS_FILE).slice(-50).reverse()}catch(e){}return json(res,200,{sponsors:getSponsors(),config:getConfig(),analytics:getAnalytics(),submissions:subs,aiChat:{configured:!!getOpenRouterKey(),model:OPENROUTER_MODEL},email:{configured:!!getSendgridKey(),from:emailFrom()}});}
if(req.method==='POST'&&pathname==='/api/admin/sendgrid-key'){
const b=await bodyJson(req);const key=typeof b.key==='string'?b.key.trim():null;
if(key===null)return json(res,400,{error:'Invalid request.'});
if(key===''){try{fs.unlinkSync(SENDGRID_KEY_FILE)}catch(e){}return json(res,200,{configured:!!getSendgridKey()});}
if(!/^SG\./.test(key)||key.length<40||/\s/.test(key))return json(res,400,{error:'That does not look like a SendGrid API key (starts with SG.).'});
fs.writeFileSync(SENDGRID_KEY_FILE,key,{mode:0o600});
return json(res,200,{configured:true});
}
if(req.method==='POST'&&pathname==='/api/admin/openrouter-key'){
const b=await bodyJson(req);const key=typeof b.key==='string'?b.key.trim():null;
if(key===null)return json(res,400,{error:'Invalid request.'});
if(key===''){try{fs.unlinkSync(OPENROUTER_KEY_FILE)}catch(e){}return json(res,200,{configured:!!getOpenRouterKey()});}
if(key.length<20||/\s/.test(key))return json(res,400,{error:'That does not look like a valid API key.'});
fs.writeFileSync(OPENROUTER_KEY_FILE,key,{mode:0o600});
return json(res,200,{configured:true});
}
if(req.method==='POST'&&pathname==='/api/admin/sponsors'){
const b=await bodyJson(req);const {id,name,parentId='',level='Scintilla',notes='',email=''}=b;if(!id||!name)return json(res,400,{error:'ID and name are required.'});if(!LEVELS.includes(level))return json(res,400,{error:'Invalid level.'});if(email&&!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(email).trim()))return json(res,400,{error:'Invalid email address.'});let sponsors=getSponsors();if(sponsors.some(s=>String(s.id)===String(id)))return json(res,409,{error:'That sponsor ID already exists.'});
const maxOrder=sponsors.reduce((m,s)=>Math.max(m,s.sortOrder||0),0);sponsors.push({id:String(id).trim(),name:String(name).trim(),parentId:String(parentId||'').trim(),directs:0,level,status:sponsors.some(s=>s.status==='active')?'waiting':'active',sortOrder:maxOrder+10,clicks:0,notes:String(notes||'').trim(),email:String(email||'').trim().slice(0,120)});sponsors=normalizeStatuses(sponsors);saveSponsors(sponsors);return json(res,201,{sponsors});
}
if(req.method==='PATCH'&&pathname==='/api/admin/config'){
const b=await bodyJson(req),cur=getConfig(),next={...cur};for(const k of ['siteName','programName','bridgeHeadline','bridgeSubheadline','premiumEntryPol','dappReferralBaseUrl','telegramUrl','supportLabel','showSponsorName','showQueueProgress','bemobPostbackUrl','telegramBotToken','telegramChatId','telegramTopicId','teamRootId','emailFrom','teamAlertEmail'])if(Object.prototype.hasOwnProperty.call(b,k))next[k]=b[k];next.premiumEntryPol=Number(next.premiumEntryPol)||362;next.updatedAt=new Date().toISOString();writeJson(CONFIG_FILE,next);return json(res,200,{config:next});
}
const m=pathname.match(/^\/api\/admin\/sponsors\/([^/]+)(?:\/(increment|activate|qualify|reset|move))?$/);
if(m){const id=decodeURIComponent(m[1]),action=m[2]||null;let sponsors=getSponsors(),idx=sponsors.findIndex(s=>s.id===id);if(idx<0)return json(res,404,{error:'Sponsor not found.'});
if(req.method==='PATCH'&&!action){const b=await bodyJson(req);if(Object.prototype.hasOwnProperty.call(b,'level')&&!LEVELS.includes(b.level))return json(res,400,{error:'Invalid level.'});if(Object.prototype.hasOwnProperty.call(b,'email')&&b.email&&!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(b.email).trim()))return json(res,400,{error:'Invalid email address.'});for(const k of ['name','parentId','directs','level','notes','email'])if(Object.prototype.hasOwnProperty.call(b,k))sponsors[idx][k]=k==='email'?String(b[k]||'').trim().slice(0,120):b[k];sponsors[idx].name=String(sponsors[idx].name||'').trim().slice(0,80)||sponsors[idx].name;sponsors[idx].directs=Math.max(0,Math.min(2,Number(sponsors[idx].directs)||0));saveSponsors(sponsors);return json(res,200,{sponsors});}
if(req.method==='DELETE'&&!action){const wasActive=sponsors[idx].status==='active';sponsors.splice(idx,1);if(wasActive)sponsors=normalizeStatuses(sponsors);saveSponsors(sponsors);return json(res,200,{sponsors});}
if(req.method==='POST'&&action==='increment'){sponsors[idx].directs=Math.min(2,(Number(sponsors[idx].directs)||0)+1);saveSponsors(sponsors);return json(res,200,{sponsors});}
if(req.method==='POST'&&action==='activate'){if(sponsors[idx].status==='qualified')return json(res,400,{error:'Qualified sponsors cannot be activated until reset.'});sponsors=normalizeStatuses(sponsors,id);saveSponsors(sponsors);return json(res,200,{sponsors});}
if(req.method==='POST'&&action==='qualify'){sponsors[idx]={...sponsors[idx],directs:2,status:'qualified'};sponsors=normalizeStatuses(sponsors);saveSponsors(sponsors);return json(res,200,{sponsors,active:activeSponsor(sponsors)});}
if(req.method==='POST'&&action==='reset'){sponsors[idx]={...sponsors[idx],directs:0,status:'waiting'};sponsors=normalizeStatuses(sponsors);saveSponsors(sponsors);return json(res,200,{sponsors});}
if(req.method==='POST'&&action==='move'){const b=await bodyJson(req);const swap=b.direction==='up'?idx-1:idx+1;if(swap>=0&&swap<sponsors.length){const t=sponsors[idx].sortOrder;sponsors[idx].sortOrder=sponsors[swap].sortOrder;sponsors[swap].sortOrder=t;saveSponsors(sponsors)}return json(res,200,{sponsors:getSponsors()});}
}
return json(res,404,{error:'API endpoint not found'});
}
const server=http.createServer(async(req,res)=>{
try{
const u=new URL(req.url,`http://${req.headers.host||'localhost'}`),pathname=decodeURIComponent(u.pathname);
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==='/my'||pathname==='/my/'||/^\/my\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'my.html');else if(/^\/join\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'join.html');else if(pathname==='/join'||pathname==='/join/'){res.writeHead(302,{Location:'/start'});return res.end();}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);
}catch(e){console.error(e);json(res,500,{error:'Internal server error'});}
});
server.listen(PORT,()=>{console.log(`Crypto Team Build sponsor router running on http://localhost:${PORT}`);if(ADMIN_PASSWORD==='changeme')console.warn('WARNING: Set ADMIN_PASSWORD before production deployment.');});
// Team-activity alerts: any NEW on-chain event at/below config.teamRootId goes
// to the Telegram group topic, with the sponsor's contact email when we have it.
chain.startIndexer(evt=>{
try{
const c=getConfig();
const rootId=Number(c.teamRootId)||0;
const ids=evt.type==='payout'?[evt.toId,evt.fromId]:[evt.id];
if(rootId&&ids.some(i=>chain.isInTeam(i,rootId))){
const contact=id=>{const s=getSponsors().find(x=>String(x.id)===String(id));return s&&s.email?`\nContact: ${s.name?s.name+' — ':''}${s.email}`:''};
let text;
if(evt.type==='registered')text=`📈 TEAM BUILD: new position!\n#${evt.id} registered under #${evt.referrerId} (${evt.tierName}).`;
else if(evt.type==='upgraded')text=`🚀 TEAM BUILD: #${evt.id} upgraded to ${evt.levelName}.`;
else text=`💸 TEAM BUILD: #${evt.toId} just got PAID ${evt.pol.toFixed(2)} POL${evt.kind==='upline'?` (upgrade pass-up from #${evt.fromId})`:` (referral reward from #${evt.fromId})`}.${contact(evt.toId)}`;
if(evt.tx)text+=`\nhttps://polygonscan.com/tx/${evt.tx}`;
sendTelegram(text);
// admin email alert — same team-gated events, so deep-leg action still surfaces
if(c.teamAlertEmail){
const subj=evt.type==='registered'?`RM Circle team build: #${evt.id} registered under #${evt.referrerId}`
:evt.type==='upgraded'?`RM Circle team build: #${evt.id} upgraded to ${evt.levelName}`
:`RM Circle team build: #${evt.toId} paid ${evt.pol.toFixed(2)} POL`;
sendEmailRaw(c.teamAlertEmail,subj,text.replace(/^[^\s]+ /,''));
}
}
}catch(e){console.error('team alert error',e.message)}
// "you've been paid" email — any payout whose recipient has a contact email on file (not subtree-gated)
try{
if(evt.type==='payout'){
const sp=getSponsors().find(x=>String(x.id)===String(evt.toId));
if(sp&&sp.email)sendPaidEmail(sp.email,sp.name,evt);
}
}catch(e){console.error('paid email error',e.message)}
// Auto-count directs (Marty 2026-08-14, "prevent manual effort"): a new
// registration whose referrer sits in the rotation queue increments that
// sponsor's directs automatically — same operation as the admin ⊕ button.
// Deliberately does NOT auto-qualify at 2/2: rotating the team focus stays
// a human call, so it alerts instead.
try{
if(evt.type==='registered'&&evt.referrerId!=null){
const sponsors=getSponsors();
const idx=sponsors.findIndex(x=>String(x.id)===String(evt.referrerId));
if(idx>=0&&sponsors[idx].status!=='qualified'&&(Number(sponsors[idx].directs)||0)<2){
sponsors[idx].directs=Math.min(2,(Number(sponsors[idx].directs)||0)+1);
saveSponsors(sponsors);
const s=sponsors[idx];
sendTelegram(`🤖 AUTO-COUNT: #${evt.id} is a DIRECT for queue sponsor #${s.id}${s.name?` (${s.name})`:''} — now ${s.directs}/2.${s.directs>=2?'\n🎯 2/2 reached — open /admin and hit Qualify when you want the rotation to move.':''}`);
}
}
}catch(e){console.error('auto-direct error',e.message)}
// Queue level sync: when a queue member upgrades on-chain, their Level in the
// rotation queue follows automatically (same op as the admin level dropdown).
try{
if(evt.type==='upgraded'&&evt.id!=null){
const sponsors=getSponsors();
const idx=sponsors.findIndex(x=>String(x.id)===String(evt.id));
if(idx>=0&&LEVELS.includes(evt.levelName)&&sponsors[idx].level!==evt.levelName){
sponsors[idx].level=evt.levelName;
saveSponsors(sponsors);
sendTelegram(`🤖 AUTO-LEVEL: queue sponsor #${sponsors[idx].id}${sponsors[idx].name?` (${sponsors[idx].name})`:''} upgraded on-chain — queue level updated to ${evt.levelName}.`);
}
}
}catch(e){console.error('auto-level error',e.message)}
});
const http = require('http');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { URL } = require('url');
const chain = require('./chain');
const PORT = Number(process.env.PORT || 3000);
const ROOT = __dirname;
const PUBLIC_DIR = path.join(ROOT, 'public');
const DATA_DIR = process.env.DATA_DIR || path.join(ROOT, 'data');
const SEED_DIR = path.join(ROOT, 'seed');
const SPONSORS_FILE = path.join(DATA_DIR, 'sponsors.json');
const CONFIG_FILE = path.join(DATA_DIR, 'config.json');
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'changeme';
const IS_PROD = process.env.NODE_ENV === 'production';
const SESSION_TTL = 8 * 60 * 60 * 1000;
const LEVELS = ['Scintilla','Ascensus','Fabrica','Culmen','Apex','Fastigium','Vertex','Corona'];
const OPENROUTER_MODEL = process.env.OPENROUTER_MODEL || 'deepseek/deepseek-v4-flash:nitro';
const OPENROUTER_KEY_FILE = path.join(DATA_DIR, 'openrouter.key');
function getOpenRouterKey() {
if (process.env.OPENROUTER_API_KEY) return process.env.OPENROUTER_API_KEY;
try { return fs.readFileSync(OPENROUTER_KEY_FILE, 'utf8').trim(); } catch (e) { return ''; }
}
const chatHits = new Map();
function chatRateLimited(ip) {
const now = Date.now(), rec = chatHits.get(ip);
if (!rec || now > rec.reset) { chatHits.set(ip, { count: 1, reset: now + 60000 }); return false; }
rec.count++; return rec.count > 10;
}
function chatSystemPrompt() {
const c = getConfig(), sponsors = getSponsors(), a = activeSponsor(sponsors);
const waiting = sponsors.filter(s=>s.status==='waiting').length;
return `You are "Team Help", the assistant on ${c.siteName || 'Crypto Team Build'} (https://rmcircle.saasy.top), the team site for the RM Circle Premium team build — a project of the Crypto Team Build Network.
FACTS:
- Strategy: enter RM Circle dApp at Premium tier (${c.premiumEntryPol || 362} POL on Polygon Mainnet, chain ID 137, POL is the gas token). Each member gets 2 directs to become "qualified"; the team then recommends retiring that link and helping those 2 get their 2 with their OWN links (moving-link strategy) — a position only qualifies through its own directs. If an extra signup lands on a qualified link anyway, present it as a BONUS, not a rule violation: it still pays that member the full entry reward and spills downward into the first open slot in their leg (depth). Spillover never qualifies the people below.
- First team goal: 30 properly placed positions (2+4+8+16), then 32, 64, 128 and beyond.
- 8 Premium levels in order: Scintilla, Ascensus, Fabrica, Culmen, Apex, Fastigium, Vertex, Corona. Everyone starts at Scintilla. Upgrade as quickly as practical, ideally with earned POL; the first two payments at each level help fund the next upgrade. Stay aware of your active downline's levels so you don't fall behind and miss payments.
- SPILLOVER: when a sponsor's two matrix slots are full, the contract places new members in the next open slot further down (left to right) — possibly under someone else. Spillover fills that member's matrix and sets up future upgrade income to their position, but does NOT count toward qualification: only people who join using a member's own ID are their directs. Upgrade payments travel up the MATRIX (not who-referred-whom) and only stop at positions that are qualified (2 directs) AND already at the level being bought — otherwise the payment passes them by to the next eligible upline. Members can see their own matrix, spillover tags, and payments at https://rmcircle.saasy.top/my
- Current team sponsor: ${a ? `ID ${a.id}${c.showSponsorName && a.name ? ` (${a.name})` : ''}, ${a.directs}/2 directs` : 'shown on the start page'}. ${waiting} placement(s) waiting. Placements rotate as positions qualify — always verify on https://rmcircle.saasy.top/start right before joining.
- Site pages: https://rmcircle.saasy.top/ (strategy overview + roadmap), https://rmcircle.saasy.top/start (current sponsor + join steps), https://rmcircle.saasy.top/training (4 videos: 1. How the team build works, 2. Create your MetaMask wallet, 3. Funding your wallet, 4. Buying the Premium position), https://rmcircle.saasy.top/contract (plain-language security review of the verified smart contract — code can't change, no pooled funds, locked rules, honest list of operator powers).
- Telegram group for live team help: ${c.telegramUrl || 'https://t.me/cryptoteambuild'}
RULES:
- Keep answers short: 1-4 sentences, plain text, no markdown formatting. Include full URLs when pointing to a page.
- NEVER promise, estimate, or imply earnings or income. If asked about returns/profit, say results depend on team effort, duplication, upgrades, smart-contract rules and POL's market value, that no income is guaranteed, and to only use funds they can afford to lose.
- NEVER ask for or discuss handling anyone's Secret Recovery Phrase or private keys except to warn they must never share them with anyone.
- Only answer questions about this project, the site, wallets/POL as they relate to joining, and the team process. For anything else, or anything you are not sure about, say you're not sure and point them to the Telegram group: ${c.telegramUrl || 'https://t.me/cryptoteambuild'}
- Never give financial, legal, or tax advice.`;
}
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);
if (!rec || now > rec.reset) { submitHits.set(ip, { count: 1, reset: now + 600000 }); return false; }
rec.count++; return rec.count > 5;
}
function sendTelegram(text) {
const c = getConfig();
if (!c.telegramBotToken || !c.telegramChatId) return;
const payload = { chat_id: c.telegramChatId, text };
if (c.telegramTopicId && /^[0-9]+$/.test(String(c.telegramTopicId))) payload.message_thread_id = Number(c.telegramTopicId);
fetch(`https://api.telegram.org/bot${c.telegramBotToken}/sendMessage`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
}).then(async r=>{ if(!r.ok) console.error('telegram sendMessage status', r.status, (await r.text().catch(()=>'')).slice(0,200)); })
.catch(e=>console.error('telegram error', e.message));
}
const SENDGRID_KEY_FILE = path.join(DATA_DIR, 'sendgrid.key');
function getSendgridKey() {
if (process.env.SENDGRID_API_KEY) return process.env.SENDGRID_API_KEY;
try { return fs.readFileSync(SENDGRID_KEY_FILE, 'utf8').trim(); } catch (e) { return ''; }
}
// SendGrid is domain-authenticated for marketingwithmarty.com and
// mybrandedvoice.com — the from address must stay on one of those or DKIM fails.
function emailFrom() { return getConfig().emailFrom || 'The RM Circle Team <no-reply@marketingwithmarty.com>'; }
function sendEmailRaw(toEmail, subject, text) {
const key = getSendgridKey();
if (!key || !toEmail) return;
const fromStr = emailFrom();
const m = fromStr.match(/^(.*)<([^>]+)>\s*$/);
const from = m ? { email: m[2].trim(), name: m[1].trim() || undefined } : { email: fromStr.trim() };
fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
personalizations: [{ to: [{ email: toEmail }] }],
from, subject,
content: [{ type: 'text/plain', value: text }]
})
}).then(r => { if (r.status >= 300) r.text().then(t => console.error('sendgrid status', r.status, t.slice(0, 200))); })
.catch(e => console.error('sendgrid error', e.message));
}
function sendPaidEmail(toEmail, memberName, evt) {
const kindLine = evt.kind === 'upline' ? `an upgrade pass-up from member #${evt.fromId}` : `a referral reward from member #${evt.fromId}'s entry`;
const verify = evt.tx ? `\n\nVerify it yourself on the blockchain:\nhttps://polygonscan.com/tx/${evt.tx}` : '';
const text = `Hi ${memberName || 'there'},\n\nGood news — your RM Circle position #${evt.toId} just received ${evt.pol.toFixed(2)} POL (${kindLine}).${verify}\n\nKeep the momentum going: check your level so the next payment in your leg doesn't pass you by.\nhttps://rmcircle.saasy.top/training\n\n— The RM Circle Team\n\nYou're receiving this because your team admin has this address on file for team-build updates. Reply to this email to be removed.`;
sendEmailRaw(toEmail, `Your RM Circle position #${evt.toId} just got paid ${evt.pol.toFixed(2)} POL`, text);
}
function firePostback(clickid, txid, source) {
const pb = getConfig().bemobPostbackUrl;
if (!clickid || !pb || !/^https:\/\/[a-z0-9.-]+\/postback/i.test(pb)) return;
fetch(`${pb}${pb.includes('?')?'&':'?'}cid=${encodeURIComponent(clickid)}&payout=0&txid=${encodeURIComponent(txid)}`)
.then(r=>{ if(r.ok) recordEvent('postback', source); else console.error('bemob postback status', r.status); })
.catch(e=>console.error('bemob postback error', e.message));
}
async function handleSubmitId(req, res) {
const ip = String(req.headers['x-forwarded-for']||req.socket.remoteAddress||'').split(',')[0].trim();
if (submitRateLimited(ip)) return json(res, 429, { error: 'Too many submissions — please wait a few minutes.' });
const b = await bodyJson(req).catch(()=>null);
if (!b) return json(res, 400, { error: 'Invalid request.' });
const newId = String(b.newId||'').trim();
if (!/^[0-9]{1,10}$/.test(newId)) return json(res, 400, { error: 'Enter your numeric RM Circle ID (numbers only).' });
const memberName = String(b.memberName||'').replace(/[\u0000-\u001f\u007f]/g,'').trim().slice(0, 60);
if (!memberName) return json(res, 400, { error: 'Add your name or Telegram handle so the team can reach you.' });
const sponsorId = String(b.sponsorId||'').trim().slice(0, 20).replace(/[^0-9A-Za-z._-]/g,'') || '?';
const source = typeof b.source==='string' ? b.source : '';
const clickid = typeof b.clickid==='string' ? b.clickid.trim().slice(0,80).replace(/[^A-Za-z0-9._-]/g,'') : '';
let subs = []; try { subs = readJson(SUBMISSIONS_FILE); } catch(e) {}
if (subs.some(s=>s.newId===newId)) return json(res, 200, { ok: true, duplicate: true });
// on-chain verification: does this ID actually exist on the contract?
let onchain = null;
try {
onchain = await Promise.race([
chain.verifyMember(Number(newId)),
new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), 6000))
]);
} catch (e) { onchain = null; }
// the chain decides the path: rotation join (referrer = active rotation sponsor)
// vs leg join (someone's personal team build) — regardless of which page they used
const active = activeSponsor(getSponsors());
let joinPath = 'unknown';
if (onchain && onchain.registered) joinPath = (active && String(onchain.referrerId) === String(active.id)) ? 'rotation' : 'leg';
else if (onchain && !onchain.registered) joinPath = 'notfound';
subs.push({ newId, memberName, sponsorId, source: source||'(direct)', clickid, ts: new Date().toISOString(), path: joinPath,
onchain: onchain ? { registered: onchain.registered, tier: onchain.tierName, level: onchain.levelName, referrerId: onchain.referrerId, uplineId: onchain.uplineId } : undefined });
writeJson(SUBMISSIONS_FILE, subs.slice(-1000));
recordEvent('purchase', source);
firePostback(clickid, `purchase-${clickid}`, source);
let msg;
if (joinPath === 'rotation') {
// rotation joins go straight into the queue as waiting positions — no manual step
let queueNote = '';
try {
let sponsors = getSponsors();
if (sponsors.some(s => String(s.id) === String(newId))) {
queueNote = 'Already in the rotation queue.';
} else {
const maxOrder = sponsors.reduce((m, s) => Math.max(m, s.sortOrder || 0), 0);
sponsors.push({ id: String(newId), name: memberName, parentId: String(active.id), directs: 0, level: onchain.levelName || 'Scintilla', status: sponsors.some(s => s.status === 'active') ? 'waiting' : 'active', sortOrder: maxOrder + 10, clicks: 0, notes: `auto-added: rotation join ${new Date().toISOString().slice(0, 10)}` });
sponsors = normalizeStatuses(sponsors);
saveSponsors(sponsors);
const waitingAhead = sponsors.filter(s => s.status === 'waiting' && (s.sortOrder || 0) < maxOrder + 10).length;
queueNote = `Auto-added to the rotation queue (${waitingAhead} waiting ahead of them).`;
}
} catch (e) { queueNote = `⚠ Auto-add to queue failed (${e.message}) — add manually.`; console.error('queue auto-add', e.message); }
msg = `🔔 RM Circle: ROTATION JOIN CONFIRMED ✅\nName: ${memberName}\nNew ID: ${newId} (${onchain.tierName}, verified on-chain)\nJoined under rotation sponsor: #${onchain.referrerId}\nSource: ${source||'(direct)'}\n✅ ${queueNote}\n(+1 direct for ID ${active.id} syncs from the chain automatically.)`;
} else if (joinPath === 'leg') {
msg = `🌱 RM Circle: TEAM-BUILD JOIN (not rotation)\nName: ${memberName}\nNew ID: ${newId} (${onchain.tierName}, verified on-chain)\nActual sponsor on-chain: #${onchain.referrerId}${sponsorId!=='?'&&String(onchain.referrerId)!==sponsorId?` (form said ${sponsorId})`:''}\nSource: ${source||'(direct)'}\n→ Leg growth under #${onchain.referrerId} — no rotation action needed. Add them to the rotation queue only if they want the team effort.`;
} else if (joinPath === 'notfound') {
msg = `🔔 RM Circle: ID SUBMITTED — ❌ NOT FOUND ON-CHAIN\nName: ${memberName}\nNew ID: ${newId}\nClaimed sponsor: ${sponsorId}\nSource: ${source||'(direct)'}\n→ ID has no registration on the contract — double-check with them before any queue action.`;
} else {
msg = `🔔 RM Circle: NEW MEMBER SUBMITTED\nName: ${memberName}\nNew ID: ${newId}\nClaimed sponsor: ${sponsorId}\nSource: ${source||'(direct)'}\n⏳ On-chain check unavailable — verify manually in admin (member lookup).`;
}
sendTelegram(msg);
return json(res, 200, { ok: true, path: joinPath, onchain: onchain ? { registered: onchain.registered, tier: onchain.tierName, level: onchain.levelName, referrerId: onchain.referrerId } : null });
}
async function handleChat(req, res) {
const ip = String(req.headers['x-forwarded-for']||req.socket.remoteAddress||'').split(',')[0].trim();
if (chatRateLimited(ip)) return json(res, 429, { error: 'Too many messages — give it a minute.' });
const apiKey = getOpenRouterKey();
if (!apiKey) return json(res, 200, { fallback: true });
const b = await bodyJson(req).catch(()=>null);
if (!b || !Array.isArray(b.messages)) return json(res, 400, { error: 'Invalid request' });
const msgs = b.messages.slice(-8)
.filter(m=>m&&(m.role==='user'||m.role==='assistant')&&typeof m.content==='string')
.map(m=>({ role: m.role, content: m.content.slice(0, 500) }));
if (!msgs.length || msgs[msgs.length-1].role !== 'user') return json(res, 400, { error: 'Invalid request' });
try {
const ctrl = new AbortController(); const timer = setTimeout(()=>ctrl.abort(), 20000);
const r = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST', signal: ctrl.signal,
headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', 'HTTP-Referer': 'https://rmcircle.saasy.top', 'X-Title': 'RM Circle Team Help' },
body: JSON.stringify({ model: OPENROUTER_MODEL, max_tokens: 350, temperature: 0.3, messages: [{ role: 'system', content: chatSystemPrompt() }, ...msgs] })
});
clearTimeout(timer);
if (!r.ok) { console.error('openrouter status', r.status); return json(res, 200, { fallback: true }); }
const d = await r.json();
const reply = d && d.choices && d.choices[0] && d.choices[0].message && d.choices[0].message.content;
if (!reply) return json(res, 200, { fallback: true });
return json(res, 200, { reply: String(reply).trim().slice(0, 2000) });
} catch (e) { console.error('openrouter error', e.message); return json(res, 200, { fallback: true }); }
}
// Sessions persist in the data volume so redeploys stop logging the admin out.
const SESSIONS_FILE = path.join(DATA_DIR, 'sessions.json');
const sessions = new Map();
try {
const saved = JSON.parse(fs.readFileSync(SESSIONS_FILE, 'utf8'));
const now = Date.now();
for (const [t, s] of Object.entries(saved)) if (s && s.expires > now) sessions.set(t, s);
} catch (e) {}
function saveSessions() {
try {
const now = Date.now();
for (const [t, s] of sessions) if (s.expires <= now) sessions.delete(t);
const tmp = SESSIONS_FILE + '.tmp';
fs.writeFileSync(tmp, JSON.stringify(Object.fromEntries(sessions)), { mode: 0o600 });
fs.renameSync(tmp, SESSIONS_FILE);
} catch (e) { console.error('session save failed', e.message); }
}
function ensureDataFile(name) {
fs.mkdirSync(DATA_DIR, { recursive: true });
const target = path.join(DATA_DIR, name);
if (!fs.existsSync(target)) fs.copyFileSync(path.join(SEED_DIR, name), target);
}
ensureDataFile('sponsors.json');
ensureDataFile('config.json');
const ANALYTICS_FILE = path.join(DATA_DIR, 'analytics.json');
if (!fs.existsSync(ANALYTICS_FILE)) fs.writeFileSync(ANALYTICS_FILE, JSON.stringify({ sources: {} }, null, 2));
function readJson(file) { return JSON.parse(fs.readFileSync(file, 'utf8')); }
function writeJson(file, data) {
const temp = `${file}.${crypto.randomUUID()}.tmp`;
fs.writeFileSync(temp, JSON.stringify(data, null, 2));
fs.renameSync(temp, file);
}
function getSponsors() { return readJson(SPONSORS_FILE).sort((a,b)=>(a.sortOrder||0)-(b.sortOrder||0)); }
function saveSponsors(s) { writeJson(SPONSORS_FILE, s); }
function getConfig() { return readJson(CONFIG_FILE); }
function activeSponsor(sponsors) { return sponsors.find(s=>s.status==='active') || sponsors.find(s=>s.status==='waiting') || null; }
function getAnalytics() { try { return readJson(ANALYTICS_FILE); } catch (e) { return { sources: {} }; } }
function recordEvent(event, source) {
if (!['bridge','start','click','training','postback','purchase','join'].includes(event)) return;
const s = String(source||'').toLowerCase().trim().replace(/[^a-z0-9.()\-_:/ ]/g,'').slice(0,80) || '(direct)';
const a = getAnalytics(); if (!a.sources) a.sources = {};
if (!a.sources[s]) { if (Object.keys(a.sources).length >= 500) return; a.sources[s] = { bridge:0, start:0, click:0 }; }
a.sources[s][event] = (a.sources[s][event]||0) + 1;
writeJson(ANALYTICS_FILE, a);
}
function normalizeStatuses(sponsors, preferredActiveId=null) {
const eligible=sponsors.filter(s=>s.status!=='qualified');
let activeId=preferredActiveId;
if(!activeId || !eligible.some(s=>s.id===activeId)){
const existing=eligible.find(s=>s.status==='active');
activeId=existing?existing.id:(eligible[0]?.id||null);
}
return sponsors.map(s=>s.status==='qualified'?s:{...s,status:s.id===activeId?'active':'waiting'});
}
function publicSponsorPayload(sponsor, config) {
if(!sponsor)return null;
return {id:sponsor.id,name:config.showSponsorName?sponsor.name:null,directs:sponsor.directs,goal:2,level:sponsor.level,referralUrl:`${config.dappReferralBaseUrl}${encodeURIComponent(sponsor.id)}`};
}
const CSP_BASE="default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; font-src 'self' data:; form-action 'self'; frame-src https://www.youtube-nocookie.com";
function securityHeaders(extra={}) {
// Public pages must render inside safelist / traffic-exchange iframes, so framing stays open here; admin.html re-locks it via ADMIN_FRAME_HEADERS.
return {
'X-Content-Type-Options':'nosniff','Referrer-Policy':'strict-origin-when-cross-origin',
'Permissions-Policy':'camera=(), microphone=(), geolocation=()',
'Content-Security-Policy':`${CSP_BASE}; frame-ancestors *`,
...extra
};
}
const ADMIN_FRAME_HEADERS={'X-Frame-Options':'DENY','Content-Security-Policy':`${CSP_BASE}; frame-ancestors 'none'`};
function send(res,status,body,headers={}) { res.writeHead(status,securityHeaders(headers));res.end(body); }
function json(res,status,obj,headers={}) { send(res,status,JSON.stringify(obj),{'Content-Type':'application/json; charset=utf-8',...headers}); }
function parseCookies(req){const out={};for(const p of (req.headers.cookie||'').split(';')){const i=p.indexOf('=');if(i>0)out[p.slice(0,i).trim()]=decodeURIComponent(p.slice(i+1).trim())}return out}
function getSession(req){const token=parseCookies(req)['ctb.sid'];if(!token)return null;const s=sessions.get(token);if(!s)return null;if(s.expires<Date.now()){sessions.delete(token);return null}return {token,...s}}
function requireAdmin(req,res){if(!getSession(req)){json(res,401,{error:'Unauthorized'});return false}return true}
async function bodyJson(req){return await new Promise((resolve,reject)=>{let data='';req.on('data',c=>{data+=c;if(data.length>100000){reject(new Error('Payload too large'));req.destroy()}});req.on('end',()=>{if(!data)return resolve({});try{resolve(JSON.parse(data))}catch(e){reject(new Error('Invalid JSON'))}});req.on('error',reject)})}
function contentType(file){const ext=path.extname(file);return ({'.html':'text/html; charset=utf-8','.css':'text/css; charset=utf-8','.js':'application/javascript; charset=utf-8','.json':'application/json; charset=utf-8','.png':'image/png','.jpg':'image/jpeg','.jpeg':'image/jpeg','.webp':'image/webp','.svg':'image/svg+xml','.ico':'image/x-icon','.mp4':'video/mp4','.webm':'video/webm'}[ext]||'application/octet-stream')}
function staticFile(req,res,file,status=200){
if(!fs.existsSync(file)||!fs.statSync(file).isFile())return false;
const size=fs.statSync(file).size;
const base={'Content-Type':contentType(file),'Accept-Ranges':'bytes','Cache-Control':['.html','.css','.js'].includes(path.extname(file))?'no-cache':'public, max-age=3600',...(path.basename(file)==='admin.html'?ADMIN_FRAME_HEADERS:{})};
const m=status===200&&req.headers.range?String(req.headers.range).match(/^bytes=(\d*)-(\d*)$/):null;
if(m&&(m[1]!==''||m[2]!=='')){
const start=m[1]===''?Math.max(0,size-Number(m[2])):Number(m[1]);
const end=(m[1]!==''&&m[2]!=='')?Math.min(Number(m[2]),size-1):size-1;
if(start>end||start>=size){res.writeHead(416,securityHeaders({'Content-Range':`bytes */${size}`}));res.end();return true}
res.writeHead(206,securityHeaders({...base,'Content-Range':`bytes ${start}-${end}/${size}`,'Content-Length':end-start+1}));
if(req.method==='HEAD')res.end();else fs.createReadStream(file,{start,end}).pipe(res);
return true;
}
res.writeHead(status,securityHeaders({...base,'Content-Length':size}));
if(req.method==='HEAD')res.end();else fs.createReadStream(file).pipe(res);
return true;
}
async function handleApi(req,res,pathname){
if(req.method==='GET'&&pathname==='/health') return json(res,200,{ok:true});
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))]);
if(r.registered)r.referralUrl=`${getConfig().dappReferralBaseUrl}${encodeURIComponent(id)}`;
// align next-in-line with the human-curated rotation: prefer the ACTIVE
// rotation sponsor when they sit in this member's leg and need directs;
// else the first chain-order position that's a queue participant; else
// keep the chain's pure structural pick (covers legs outside the queue).
if(r.registered&&r.subtree){
try{
const sponsors=getSponsors();
const act=activeSponsor(sponsors);
const participants=new Set(sponsors.filter(s=>s.status!=='qualified').map(s=>String(s.id)));
const bfs=[];const q=[r.subtree.left,r.subtree.right].filter(Boolean);
while(q.length){const n=q.shift();bfs.push(n);if(n.left)q.push(n.left);if(n.right)q.push(n.right);}
let pick=null;
if(act)pick=bfs.find(n=>String(n.id)===String(act.id)&&(n.directCount||0)<2);
if(!pick)pick=bfs.find(n=>participants.has(String(n.id))&&(n.directCount||0)<2);
if(pick)r.nextInLine={id:pick.id,directCount:pick.directCount||0,levelName:pick.levelName};
}catch(e){}
}
if(r.nextInLine)r.nextInLine.referralUrl=`${getConfig().dappReferralBaseUrl}${encodeURIComponent(r.nextInLine.id)}`;
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'});
}
if(req.method==='GET'&&pathname==='/api/public/current-sponsor'){
const sponsors=getSponsors(),c=getConfig(),a=activeSponsor(sponsors);if(!a)return json(res,404,{error:'No active sponsor is currently assigned.'});
return json(res,200,{sponsor:publicSponsorPayload(a,c),waitingCount:sponsors.filter(s=>s.status==='waiting').length,message:'Always use the current sponsor shown on this page. Team placement rotates as members qualify.'});
}
if(req.method==='POST'&&pathname==='/api/public/join-click'){
const b=await bodyJson(req).catch(()=>({}));recordEvent('click',b.source);
const clickid=typeof b.clickid==='string'?b.clickid.trim().slice(0,80).replace(/[^A-Za-z0-9._-]/g,''):'';
firePostback(clickid,`join-${clickid}`,b.source);
let sponsors=getSponsors();const a=activeSponsor(sponsors);if(a){sponsors=sponsors.map(s=>s.id===a.id?{...s,clicks:(s.clicks||0)+1}:s);saveSponsors(sponsors)}return json(res,200,{ok:true});
}
if(req.method==='POST'&&pathname==='/api/public/chat')return await handleChat(req,res);
if(req.method==='POST'&&pathname==='/api/public/submit-id')return await handleSubmitId(req,res);
if(req.method==='POST'&&pathname==='/api/public/track'){
const b=await bodyJson(req).catch(()=>({}));recordEvent(b.event,b.source);return json(res,200,{ok:true});
}
if(req.method==='POST'&&pathname==='/api/admin/login'){
const b=await bodyJson(req).catch(e=>null);if(!b)return json(res,400,{error:'Invalid request'});if(typeof b.password!=='string'||b.password!==ADMIN_PASSWORD)return json(res,401,{error:'Invalid password'});
const token=crypto.randomBytes(32).toString('hex');sessions.set(token,{expires:Date.now()+SESSION_TTL});saveSessions();const cookie=`ctb.sid=${encodeURIComponent(token)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${SESSION_TTL/1000}${IS_PROD?'; Secure':''}`;return json(res,200,{ok:true},{'Set-Cookie':cookie});
}
if(req.method==='POST'&&pathname==='/api/admin/logout'){
const s=getSession(req);if(s){sessions.delete(s.token);saveSessions();}return json(res,200,{ok:true},{'Set-Cookie':'ctb.sid=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0'});
}
if(pathname.startsWith('/api/admin/')&&!requireAdmin(req,res))return;
if(req.method==='GET'&&pathname==='/api/admin/matrix-tree'){
return json(res,200,chain.getMatrixTree());
}
if(req.method==='GET'&&pathname==='/api/admin/member-lookup'){
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.'});
try{
const r=await Promise.race([chain.memberLookup(id),new Promise((_,rej)=>setTimeout(()=>rej(new Error('Chain RPC timeout — try again.')),25000))]);
return json(res,200,r);
}catch(e){return json(res,502,{error:e.message||'Lookup failed'})}
}
if(req.method==='GET'&&pathname==='/api/admin/state'){let subs=[];try{subs=readJson(SUBMISSIONS_FILE).slice(-50).reverse()}catch(e){}return json(res,200,{sponsors:getSponsors(),config:getConfig(),analytics:getAnalytics(),submissions:subs,aiChat:{configured:!!getOpenRouterKey(),model:OPENROUTER_MODEL},email:{configured:!!getSendgridKey(),from:emailFrom()}});}
if(req.method==='POST'&&pathname==='/api/admin/sendgrid-key'){
const b=await bodyJson(req);const key=typeof b.key==='string'?b.key.trim():null;
if(key===null)return json(res,400,{error:'Invalid request.'});
if(key===''){try{fs.unlinkSync(SENDGRID_KEY_FILE)}catch(e){}return json(res,200,{configured:!!getSendgridKey()});}
if(!/^SG\./.test(key)||key.length<40||/\s/.test(key))return json(res,400,{error:'That does not look like a SendGrid API key (starts with SG.).'});
fs.writeFileSync(SENDGRID_KEY_FILE,key,{mode:0o600});
return json(res,200,{configured:true});
}
if(req.method==='POST'&&pathname==='/api/admin/openrouter-key'){
const b=await bodyJson(req);const key=typeof b.key==='string'?b.key.trim():null;
if(key===null)return json(res,400,{error:'Invalid request.'});
if(key===''){try{fs.unlinkSync(OPENROUTER_KEY_FILE)}catch(e){}return json(res,200,{configured:!!getOpenRouterKey()});}
if(key.length<20||/\s/.test(key))return json(res,400,{error:'That does not look like a valid API key.'});
fs.writeFileSync(OPENROUTER_KEY_FILE,key,{mode:0o600});
return json(res,200,{configured:true});
}
if(req.method==='POST'&&pathname==='/api/admin/sponsors'){
const b=await bodyJson(req);const {id,name,parentId='',level='Scintilla',notes='',email=''}=b;if(!id||!name)return json(res,400,{error:'ID and name are required.'});if(!LEVELS.includes(level))return json(res,400,{error:'Invalid level.'});if(email&&!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(email).trim()))return json(res,400,{error:'Invalid email address.'});let sponsors=getSponsors();if(sponsors.some(s=>String(s.id)===String(id)))return json(res,409,{error:'That sponsor ID already exists.'});
const maxOrder=sponsors.reduce((m,s)=>Math.max(m,s.sortOrder||0),0);sponsors.push({id:String(id).trim(),name:String(name).trim(),parentId:String(parentId||'').trim(),directs:0,level,status:sponsors.some(s=>s.status==='active')?'waiting':'active',sortOrder:maxOrder+10,clicks:0,notes:String(notes||'').trim(),email:String(email||'').trim().slice(0,120)});sponsors=normalizeStatuses(sponsors);saveSponsors(sponsors);return json(res,201,{sponsors});
}
if(req.method==='PATCH'&&pathname==='/api/admin/config'){
const b=await bodyJson(req),cur=getConfig(),next={...cur};for(const k of ['siteName','programName','bridgeHeadline','bridgeSubheadline','premiumEntryPol','dappReferralBaseUrl','telegramUrl','supportLabel','showSponsorName','showQueueProgress','bemobPostbackUrl','telegramBotToken','telegramChatId','telegramTopicId','teamRootId','emailFrom','teamAlertEmail'])if(Object.prototype.hasOwnProperty.call(b,k))next[k]=b[k];next.premiumEntryPol=Number(next.premiumEntryPol)||362;next.updatedAt=new Date().toISOString();writeJson(CONFIG_FILE,next);return json(res,200,{config:next});
}
const m=pathname.match(/^\/api\/admin\/sponsors\/([^/]+)(?:\/(increment|activate|qualify|reset|move))?$/);
if(m){const id=decodeURIComponent(m[1]),action=m[2]||null;let sponsors=getSponsors(),idx=sponsors.findIndex(s=>s.id===id);if(idx<0)return json(res,404,{error:'Sponsor not found.'});
if(req.method==='PATCH'&&!action){const b=await bodyJson(req);if(Object.prototype.hasOwnProperty.call(b,'level')&&!LEVELS.includes(b.level))return json(res,400,{error:'Invalid level.'});if(Object.prototype.hasOwnProperty.call(b,'email')&&b.email&&!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(b.email).trim()))return json(res,400,{error:'Invalid email address.'});for(const k of ['name','parentId','directs','level','notes','email'])if(Object.prototype.hasOwnProperty.call(b,k))sponsors[idx][k]=k==='email'?String(b[k]||'').trim().slice(0,120):b[k];sponsors[idx].name=String(sponsors[idx].name||'').trim().slice(0,80)||sponsors[idx].name;sponsors[idx].directs=Math.max(0,Math.min(2,Number(sponsors[idx].directs)||0));saveSponsors(sponsors);return json(res,200,{sponsors});}
if(req.method==='DELETE'&&!action){const wasActive=sponsors[idx].status==='active';sponsors.splice(idx,1);if(wasActive)sponsors=normalizeStatuses(sponsors);saveSponsors(sponsors);return json(res,200,{sponsors});}
if(req.method==='POST'&&action==='increment'){sponsors[idx].directs=Math.min(2,(Number(sponsors[idx].directs)||0)+1);saveSponsors(sponsors);return json(res,200,{sponsors});}
if(req.method==='POST'&&action==='activate'){if(sponsors[idx].status==='qualified')return json(res,400,{error:'Qualified sponsors cannot be activated until reset.'});sponsors=normalizeStatuses(sponsors,id);saveSponsors(sponsors);return json(res,200,{sponsors});}
if(req.method==='POST'&&action==='qualify'){sponsors[idx]={...sponsors[idx],directs:2,status:'qualified'};sponsors=normalizeStatuses(sponsors);saveSponsors(sponsors);return json(res,200,{sponsors,active:activeSponsor(sponsors)});}
if(req.method==='POST'&&action==='reset'){sponsors[idx]={...sponsors[idx],directs:0,status:'waiting'};sponsors=normalizeStatuses(sponsors);saveSponsors(sponsors);return json(res,200,{sponsors});}
if(req.method==='POST'&&action==='move'){const b=await bodyJson(req);const swap=b.direction==='up'?idx-1:idx+1;if(swap>=0&&swap<sponsors.length){const t=sponsors[idx].sortOrder;sponsors[idx].sortOrder=sponsors[swap].sortOrder;sponsors[swap].sortOrder=t;saveSponsors(sponsors)}return json(res,200,{sponsors:getSponsors()});}
}
return json(res,404,{error:'API endpoint not found'});
}
const server=http.createServer(async(req,res)=>{
try{
const u=new URL(req.url,`http://${req.headers.host||'localhost'}`),pathname=decodeURIComponent(u.pathname);
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==='/my'||pathname==='/my/'||/^\/my\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'my.html');else if(pathname==='/contract'||pathname==='/contract/')file=path.join(PUBLIC_DIR,'contract.html');else if(/^\/join\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'join.html');else if(pathname==='/join'||pathname==='/join/'){res.writeHead(302,{Location:'/start'});return res.end();}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);
}catch(e){console.error(e);json(res,500,{error:'Internal server error'});}
});
server.listen(PORT,()=>{console.log(`Crypto Team Build sponsor router running on http://localhost:${PORT}`);if(ADMIN_PASSWORD==='changeme')console.warn('WARNING: Set ADMIN_PASSWORD before production deployment.');});
// Team-activity alerts: any NEW on-chain event at/below config.teamRootId goes
// to the Telegram group topic, with the sponsor's contact email when we have it.
chain.startIndexer(evt=>{
try{
const c=getConfig();
const rootId=Number(c.teamRootId)||0;
const ids=evt.type==='payout'?[evt.toId,evt.fromId]:[evt.id];
if(rootId&&ids.some(i=>chain.isInTeam(i,rootId))){
const contact=id=>{const s=getSponsors().find(x=>String(x.id)===String(id));return s&&s.email?`\nContact: ${s.name?s.name+' — ':''}${s.email}`:''};
let text;
if(evt.type==='registered')text=`📈 TEAM BUILD: new position!\n#${evt.id} registered under #${evt.referrerId} (${evt.tierName}).`;
else if(evt.type==='upgraded')text=`🚀 TEAM BUILD: #${evt.id} upgraded to ${evt.levelName}.`;
else text=`💸 TEAM BUILD: #${evt.toId} just got PAID ${evt.pol.toFixed(2)} POL${evt.kind==='upline'?` (upgrade pass-up from #${evt.fromId})`:` (referral reward from #${evt.fromId})`}.${contact(evt.toId)}`;
if(evt.tx)text+=`\nhttps://polygonscan.com/tx/${evt.tx}`;
sendTelegram(text);
// admin email alert — same team-gated events, so deep-leg action still surfaces
if(c.teamAlertEmail){
const subj=evt.type==='registered'?`RM Circle team build: #${evt.id} registered under #${evt.referrerId}`
:evt.type==='upgraded'?`RM Circle team build: #${evt.id} upgraded to ${evt.levelName}`
:`RM Circle team build: #${evt.toId} paid ${evt.pol.toFixed(2)} POL`;
sendEmailRaw(c.teamAlertEmail,subj,text.replace(/^[^\s]+ /,''));
}
}
}catch(e){console.error('team alert error',e.message)}
// "you've been paid" email — any payout whose recipient has a contact email on file (not subtree-gated)
try{
if(evt.type==='payout'){
const sp=getSponsors().find(x=>String(x.id)===String(evt.toId));
if(sp&&sp.email)sendPaidEmail(sp.email,sp.name,evt);
}
}catch(e){console.error('paid email error',e.message)}
// Auto-count directs (Marty 2026-08-14, "prevent manual effort"): a new
// registration whose referrer sits in the rotation queue increments that
// sponsor's directs automatically — same operation as the admin ⊕ button.
// Deliberately does NOT auto-qualify at 2/2: rotating the team focus stays
// a human call, so it alerts instead.
try{
if(evt.type==='registered'&&evt.referrerId!=null){
const sponsors=getSponsors();
const idx=sponsors.findIndex(x=>String(x.id)===String(evt.referrerId));
if(idx>=0&&sponsors[idx].status!=='qualified'&&(Number(sponsors[idx].directs)||0)<2){
sponsors[idx].directs=Math.min(2,(Number(sponsors[idx].directs)||0)+1);
saveSponsors(sponsors);
const s=sponsors[idx];
sendTelegram(`🤖 AUTO-COUNT: #${evt.id} is a DIRECT for queue sponsor #${s.id}${s.name?` (${s.name})`:''} — now ${s.directs}/2.${s.directs>=2?'\n🎯 2/2 reached — open /admin and hit Qualify when you want the rotation to move.':''}`);
}
}
}catch(e){console.error('auto-direct error',e.message)}
// Queue level sync: when a queue member upgrades on-chain, their Level in the
// rotation queue follows automatically (same op as the admin level dropdown).
try{
if(evt.type==='upgraded'&&evt.id!=null){
const sponsors=getSponsors();
const idx=sponsors.findIndex(x=>String(x.id)===String(evt.id));
if(idx>=0&&LEVELS.includes(evt.levelName)&&sponsors[idx].level!==evt.levelName){
sponsors[idx].level=evt.levelName;
saveSponsors(sponsors);
sendTelegram(`🤖 AUTO-LEVEL: queue sponsor #${sponsors[idx].id}${sponsors[idx].name?` (${sponsors[idx].name})`:''} upgraded on-chain — queue level updated to ${evt.levelName}.`);
}
}
}catch(e){console.error('auto-level error',e.message)}
});