Disclaimer page, self-service member alerts, and admin login lockout

- /disclaimer: independent-resource, affiliate, earnings, risk, and
  not-advice disclosures; linked from all footers.
- Self-service alerts: members opt in with their email on /my/:id to get
  "you've been paid" + "upgrade needed" emails for their own position
  (same watcher as the owner alerts, extended). Signed unsubscribe link
  (/unsubscribe?id=&t=HMAC), on-chain-registration check, rate-limited,
  masked-email status, confirmation email.
- Admin login: timing-safe compare + per-IP lockout (8 fails -> 15 min,
  escalating). Previously unlimited/brute-forceable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-08-14 14:45:57 -05:00
parent 8b3e82d960
commit 0f5630e6d0
9 changed files with 159 additions and 28 deletions
+1 -1
View File
@@ -27,5 +27,5 @@
<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> <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> </div></section>
</main> </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> <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><a href="/disclaimer">Disclaimers</a></div></footer>
<script src="/track.js"></script><script src="/chat.js" defer></script></body></html> <script src="/track.js"></script><script src="/chat.js" defer></script></body></html>
+28
View File
@@ -0,0 +1,28 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="all"><meta name="description" content="Disclaimers for this independent RM Circle team resource — affiliate, earnings, and risk disclosures. Informational and educational only; not affiliated with the contract owners; no income is promised."><title>Disclaimers | Crypto Team Build</title>
<link rel="canonical" href="https://rmcircle.saasy.top/disclaimer">
<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>Disclaimers</small></span></a><div class="nav-actions"><a class="btn btn-secondary hide-mobile" href="/">← Home</a><a class="btn btn-primary" href="/start">Get Started</a></div></header>
<main>
<section class="section"><div class="wrap" style="max-width:820px">
<div class="section-head"><div class="eyebrow">Please read</div><h1 style="font-size:clamp(30px,5vw,46px);margin:8px 0 10px">Disclaimers & Disclosures</h1><p>This page governs your use of this website. By using it you acknowledge you've read and understood the following. It is written plainly and on purpose — nothing here is hidden.</p></div>
<div class="card" style="margin-bottom:16px"><h2 style="margin:0 0 10px">Informational & educational only</h2><p style="color:var(--muted);line-height:1.7;margin:0">This is an <strong style="color:var(--text)">independent team resource</strong> for members and prospective members of the RM Circle Premium team build. Everything on it is provided for <strong style="color:var(--text)">informational and educational purposes only</strong>. The live figures, dashboards, payment feeds, matrix views, and member pages display <strong style="color:var(--text)">public information read directly from the Polygon blockchain</strong> — data that is already public and that anyone can view independently on a blockchain explorer. We present it in a more readable form; we do not create, control, or guarantee it.</p></div>
<div class="card" style="margin-bottom:16px"><h2 style="margin:0 0 10px">Not affiliated — we don't own the contract or the blockchain</h2><p style="color:var(--muted);line-height:1.7;margin:0">This website is <strong style="color:var(--text)">not operated by, endorsed by, or officially affiliated with</strong> the creators, owners, or operators of the RM Circle smart contract, the RM Circle dApp (thermcircle.com), the Crypto Team Build Network, Polygon, or any blockchain. We do not own or control the smart contract, the blockchain, your wallet, or your funds. We are independent participants who built these tools to help our own team understand and follow the program. The smart contract is a public, third-party program that operates on its own, exactly as its code dictates — see our plain-language review at <a href="/contract" style="color:var(--teal)">/contract</a>.</p></div>
<div class="card" style="margin-bottom:16px"><h2 style="margin:0 0 10px">Earnings disclaimer — no income is promised</h2><p style="color:var(--muted);line-height:1.7;margin:0 0 10px"><strong style="color:var(--text)">Nothing here is a promise, projection, or guarantee of income.</strong> Any amounts shown — including historical payments and "pipeline" or "incoming" figures — are either records of past on-chain events or amounts that <em>would</em> be paid by the contract's rules <em>if</em> specific future actions occur. They are not predictions of what you will earn, and they are not typical results.</p><p style="color:var(--muted);line-height:1.7;margin:0"><strong style="color:var(--text)">The majority of participants in programs like this do not profit, and many lose their entire contribution.</strong> Whether anyone earns anything depends entirely on their own effort, the actions of others, the smart contract's rules, and the market value of POL — all outside our control and yours. Past results never indicate future results. Do not participate expecting to make money.</p></div>
<div class="card" style="margin-bottom:16px"><h2 style="margin:0 0 10px">Affiliate & material-connection disclosure</h2><p style="color:var(--muted);line-height:1.7;margin:0">The people who operate this website <strong style="color:var(--text)">participate in the RM Circle program themselves and hold positions in it.</strong> We may benefit financially — through the smart contract's referral and matrix payments — when others join or upgrade, including through links, QR codes, or the sponsor rotation on this site. In other words, we have a direct financial interest in your participation. We disclose this openly so you can weigh it. Assume any link that leads to joining benefits a member of our team.</p></div>
<div class="card" style="margin-bottom:16px"><h2 style="margin:0 0 10px">Cryptocurrency & smart-contract risk</h2><p style="color:var(--muted);line-height:1.7;margin:0">Participation requires sending cryptocurrency (POL) to a smart contract on a public blockchain. This is <strong style="color:var(--text)">high-risk</strong>. Blockchain transactions are irreversible. Smart contracts can contain bugs or behave in unexpected ways. Token values are volatile and can fall sharply. You can lose some or all of the funds you commit. <strong style="color:var(--text)">Never send funds you cannot afford to lose entirely</strong>, and never share your wallet's Secret Recovery Phrase or private keys with anyone — including anyone claiming to represent this team.</p></div>
<div class="card" style="margin-bottom:16px"><h2 style="margin:0 0 10px">Not financial, legal, or tax advice</h2><p style="color:var(--muted);line-height:1.7;margin:0">Nothing on this website is financial, investment, legal, or tax advice, and nothing here is a solicitation or offer of a security. Cryptocurrency and program participation may be regulated or restricted where you live, and rules vary by country and state. <strong style="color:var(--text)">You are responsible for determining whether participation is legal for you and for your own compliance and taxes.</strong> Consult your own qualified, independent professional advisors before participating. Do not participate if it is not permitted in your jurisdiction.</p></div>
<div class="card" style="margin-bottom:16px"><h2 style="margin:0 0 10px">No warranty; accuracy</h2><p style="color:var(--muted);line-height:1.7;margin:0">This site is provided "as is," with no warranties of any kind. Although data is read from the blockchain, we don't guarantee it is complete, current, or error-free — blockchain nodes can lag, and displays can contain mistakes. <strong style="color:var(--text)">Always verify anything important yourself directly on the blockchain</strong> (<a href="https://polygonscan.com/address/0x33BdAEEfd6d17D80aE53816c916dFb26c4fB2DAF" target="_blank" rel="noopener noreferrer" style="color:var(--teal)">Polygonscan ↗</a>) before acting. To the fullest extent permitted by law, the operators of this site are not liable for any loss arising from your use of it or your participation in any program.</p></div>
<p class="micro" style="margin-top:8px">By continuing to use this website you accept these disclaimers. If you do not agree, please do not use the site. Questions can go to the team's Telegram group linked on the site.</p>
</div></section>
</main>
<footer class="wrap disclaimer">Independent, unaffiliated team resource. Informational and educational only. Not an offer, solicitation, or guarantee of income. Cryptocurrency participation carries risk of total loss.<div class="footer-links"><a href="/">Home</a><a href="/contract">Contract Security</a><a href="/training">Training</a><a href="/start">Getting Started</a></div></footer>
<script src="/track.js"></script><script src="/chat.js" defer></script></body></html>
+1 -1
View File
@@ -13,5 +13,5 @@
<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"><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. <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" 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> <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="/contract">Contract Security</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><a href="/disclaimer">Disclaimers</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> <script src="/track.js"></script><script src="/bridge.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" 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" 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> <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> </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><a href="/disclaimer">Disclaimers</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> <script src="/track.js"></script><script src="/join.js"></script><script src="/payouts.js" defer></script><script src="/chat.js" defer></script></body></html>
+2 -1
View File
@@ -9,6 +9,7 @@
<div id="dFacts" class="facts" style="grid-template-columns:repeat(3,1fr);margin:18px 0"></div> <div id="dFacts" class="facts" style="grid-template-columns:repeat(3,1fr);margin:18px 0"></div>
<div class="table-card" style="margin-bottom:18px"><div style="display:flex;justify-content:space-between;gap:12px;align-items:flex-start;flex-wrap:wrap"><div><h2 style="margin:0 0 4px">Your team</h2><p style="color:var(--muted);font-size:13px;margin:0 0 14px">Your position's matrix — the rollup line on each card counts everyone underneath, all the way down. Click a position to drill into that leg. Open slots are where the next placements land.</p></div><button id="dTreeToggle" class="btn btn-secondary btn-sm">List view</button></div><div id="dTreeNav" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:12px"></div><div id="dTree"></div><p class="micro" style="margin:14px 0 0"><span class="mtp-qmark" style="position:static;display:inline-grid;vertical-align:middle">✓</span> qualified (2/2 directs) · <span class="mt-badge mt-prem">P</span> Premium · <span class="mt-badge">S</span> Standard · ⬇ everyone below that position (all generations) and the POL they've earned · <span style="color:var(--teal)">↧ spillover</span> = placed there by upline activity; only members who join with a position's own ID count toward its 2/2</p><div id="dSpillNote" class="hidden"></div></div> <div class="table-card" style="margin-bottom:18px"><div style="display:flex;justify-content:space-between;gap:12px;align-items:flex-start;flex-wrap:wrap"><div><h2 style="margin:0 0 4px">Your team</h2><p style="color:var(--muted);font-size:13px;margin:0 0 14px">Your position's matrix — the rollup line on each card counts everyone underneath, all the way down. Click a position to drill into that leg. Open slots are where the next placements land.</p></div><button id="dTreeToggle" class="btn btn-secondary btn-sm">List view</button></div><div id="dTreeNav" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:12px"></div><div id="dTree"></div><p class="micro" style="margin:14px 0 0"><span class="mtp-qmark" style="position:static;display:inline-grid;vertical-align:middle">✓</span> qualified (2/2 directs) · <span class="mt-badge mt-prem">P</span> Premium · <span class="mt-badge">S</span> Standard · ⬇ everyone below that position (all generations) and the POL they've earned · <span style="color:var(--teal)">↧ spillover</span> = placed there by upline activity; only members who join with a position's own ID count toward its 2/2</p><div id="dSpillNote" class="hidden"></div></div>
<div class="table-card" style="margin-bottom:18px;border-color:rgba(123,224,161,.4)"><h2 style="margin:0 0 4px">Your pipeline</h2><p style="color:var(--muted);font-size:13px;margin:0 0 12px">Money forming below you. Each generation in your leg pays your position at exactly one level — when a member's level catches up to their depth, their <em>next</em> upgrade comes to you.</p><div id="dPipeline"></div></div> <div class="table-card" style="margin-bottom:18px;border-color:rgba(123,224,161,.4)"><h2 style="margin:0 0 4px">Your pipeline</h2><p style="color:var(--muted);font-size:13px;margin:0 0 12px">Money forming below you. Each generation in your leg pays your position at exactly one level — when a member's level catches up to their depth, their <em>next</em> upgrade comes to you.</p><div id="dPipeline"></div></div>
<div class="table-card" style="margin-bottom:18px;border-color:rgba(78,214,203,.35)"><h2 style="margin:0 0 4px">Email me my alerts</h2><p style="color:var(--muted);font-size:13px;margin:0 0 12px">Get an email the moment this position is <strong>paid</strong>, and when it <strong>needs an upgrade</strong> to catch incoming pay — so you never miss one. Opt in with your email; unsubscribe anytime.</p><div id="dAlerts"></div></div>
<div class="table-card" style="margin-bottom:18px"><h2 style="margin:0 0 4px">Share this position</h2><div id="dShare"></div></div> <div class="table-card" style="margin-bottom:18px"><h2 style="margin:0 0 4px">Share this position</h2><div id="dShare"></div></div>
<div class="table-card" style="margin-bottom:18px;border-color:var(--gold)"><h2 style="margin:0 0 4px">Just joined under this position?</h2><p style="color:var(--muted);font-size:13px;margin:0 0 12px">Welcome to the team! Enter the <strong>new member ID</strong> the RM Circle dApp gave you — we'll verify it on the blockchain and let the team know you're in.</p><form id="dJoinForm" style="display:grid;gap:8px;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" required><button class="btn btn-primary">Submit My ID →</button></form><div id="dJoinMsg" style="margin-top:10px;font-size:14px"></div></div> <div class="table-card" style="margin-bottom:18px;border-color:var(--gold)"><h2 style="margin:0 0 4px">Just joined under this position?</h2><p style="color:var(--muted);font-size:13px;margin:0 0 12px">Welcome to the team! Enter the <strong>new member ID</strong> the RM Circle dApp gave you — we'll verify it on the blockchain and let the team know you're in.</p><form id="dJoinForm" style="display:grid;gap:8px;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" required><button class="btn btn-primary">Submit My ID →</button></form><div id="dJoinMsg" style="margin-top:10px;font-size:14px"></div></div>
<div class="table-card" style="margin-bottom:18px"><h2 style="margin:0 0 4px">Payments received</h2><p style="color:var(--muted);font-size:13px;margin:0 0 10px">Every payment your position has received, straight from the smart contract.</p><div id="dIncome"></div></div> <div class="table-card" style="margin-bottom:18px"><h2 style="margin:0 0 4px">Payments received</h2><p style="color:var(--muted);font-size:13px;margin:0 0 10px">Every payment your position has received, straight from the smart contract.</p><div id="dIncome"></div></div>
@@ -16,5 +17,5 @@
<div class="notice" style="margin-top:18px"><strong>Team reminder:</strong> once your two directs are placed, retire your link and help your two get their two — always send new members through the <a href="/start" style="color:var(--gold)">current team sponsor page</a>.</div> <div class="notice" style="margin-top:18px"><strong>Team reminder:</strong> once your two directs are placed, retire your link and help your two get their two — always send new members through the <a href="/start" style="color:var(--gold)">current team sponsor page</a>.</div>
</section> </section>
</main> </main>
<footer class="wrap disclaimer">All figures are read live from the RM Circle smart contract on Polygon and are historical facts, not a promise of future results. Participation involves cryptocurrency and smart-contract risk. Never use funds you cannot afford to lose.</footer> <footer class="wrap disclaimer">All figures are read live from the RM Circle smart contract on Polygon and are historical facts, not a promise of future results. Participation involves cryptocurrency and smart-contract risk. Never use funds you cannot afford to lose.<div class="footer-links"><a href="/">Home</a><a href="/contract">Contract Security</a><a href="/disclaimer">Disclaimers</a></div></footer>
<script src="/track.js"></script><script src="/qrlib.js"></script><script src="/my.js"></script><script src="/payouts.js" defer></script><script src="/chat.js" defer></script></body></html> <script src="/track.js"></script><script src="/qrlib.js"></script><script src="/my.js"></script><script src="/payouts.js" defer></script><script src="/chat.js" defer></script></body></html>
+29
View File
@@ -100,6 +100,7 @@
?`<div class="table-wrap"><table class="table" style="min-width:520px"><thead><tr><th>When</th><th>From member</th><th>For</th><th>Amount</th></tr></thead><tbody>${inc.map(p=>`<tr><td>${date(p.ts)}</td><td>#${p.fromId}</td><td>${esc(p.desc)}</td><td><strong>${fmt(p.pol)} POL</strong></td></tr>`).join('')}</tbody></table></div>` ?`<div class="table-wrap"><table class="table" style="min-width:520px"><thead><tr><th>When</th><th>From member</th><th>For</th><th>Amount</th></tr></thead><tbody>${inc.map(p=>`<tr><td>${date(p.ts)}</td><td>#${p.fromId}</td><td>${esc(p.desc)}</td><td><strong>${fmt(p.pol)} POL</strong></td></tr>`).join('')}</tbody></table></div>`
:'<div class="empty">No payments yet — they appear here the moment they land on-chain.</div>'; :'<div class="empty">No payments yet — they appear here the moment they land on-chain.</div>';
renderPipeline(d); renderPipeline(d);
renderAlerts(d);
renderShare(d); renderShare(d);
document.getElementById('dLineage').innerHTML=d.uplineChain&&d.uplineChain.length document.getElementById('dLineage').innerHTML=d.uplineChain&&d.uplineChain.length
?`<strong style="color:var(--text)">#${d.id}</strong> → ${d.uplineChain.map(i=>'#'+i).join(' → ')} <span style="color:#8498aa">(root)</span>` ?`<strong style="color:var(--text)">#${d.id}</strong> → ${d.uplineChain.map(i=>'#'+i).join(' → ')} <span style="color:#8498aa">(root)</span>`
@@ -204,6 +205,34 @@
L.push(`See it live and get a red alert the moment you'd miss one: rmcircle.saasy.top/my/${d.id}`); L.push(`See it live and get a red alert the moment you'd miss one: rmcircle.saasy.top/my/${d.id}`);
return L.join('\n'); return L.join('\n');
} }
async function renderAlerts(d){
const el=document.getElementById('dAlerts');
if(!el)return;
let st={subscribed:false};
try{st=await (await fetch('/api/public/alert-status?id='+d.id)).json();}catch(e){}
function subscribedView(email){
el.innerHTML=`<p style="margin:0 0 10px"><span class="live-badge"><span class="dot"></span> Alerts ON</span> for <strong>${esc(email||'your email')}</strong></p><button id="alertOff" class="btn btn-secondary btn-sm">Turn off alerts</button><span id="alertMsg" class="micro" style="margin-left:8px"></span>`;
document.getElementById('alertOff').addEventListener('click',async()=>{
try{await fetch('/api/public/alert-signup',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:d.id,email:''})});renderAlerts(d);}catch(e){document.getElementById('alertMsg').textContent='Try again.';}
});
}
function offView(){
el.innerHTML=`<form id="alertForm" style="display:flex;gap:8px;flex-wrap:wrap;max-width:460px"><input name="email" class="input" type="email" placeholder="you@example.com" required style="flex:1;min-width:200px"><button class="btn btn-primary btn-sm">Turn on alerts</button></form><div id="alertMsg" class="micro" style="margin-top:8px"></div>`;
document.getElementById('alertForm').addEventListener('submit',async e=>{
e.preventDefault();
const email=e.currentTarget.elements.email.value.trim(),msg=document.getElementById('alertMsg');
msg.style.color='var(--muted)';msg.textContent='Turning on…';
try{
const r=await fetch('/api/public/alert-signup',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:d.id,email})});
const j=await r.json();
if(!r.ok)throw new Error(j.error||'Failed');
msg.style.color='var(--ok)';msg.textContent='Done — check your inbox for a confirmation.';
setTimeout(()=>renderAlerts(d),1200);
}catch(x){msg.style.color='var(--danger)';msg.textContent=x.message;}
});
}
if(st.subscribed)subscribedView(st.email);else offView();
}
function renderShare(d){ function renderShare(d){
const el=document.getElementById('dShare'); const el=document.getElementById('dShare');
if(!el)return; if(!el)return;
+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" 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> <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> <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><a href="/contract">Contract Security</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><a href="/disclaimer">Disclaimers</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> <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 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> </div></section>
</main> </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="/contract">Contract Security</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><a href="/disclaimer">Disclaimers</a></div></footer>
<script src="/track.js"></script><script src="/training.js"></script><script src="/chat.js" defer></script></body></html> <script src="/track.js"></script><script src="/training.js"></script><script src="/chat.js" defer></script></body></html>
+95 -22
View File
@@ -58,6 +58,26 @@ function memberLookupLimited(ip) {
if (!rec || now > rec.reset) { lookupHits.set(ip, { count: 1, reset: now + 60000 }); return false; } if (!rec || now > rec.reset) { lookupHits.set(ip, { count: 1, reset: now + 60000 }); return false; }
rec.count++; return rec.count > 20; rec.count++; return rec.count > 20;
} }
// Admin login brute-force gate: after 8 failures from an IP, lock it out for
// 15 minutes (escalating). Timing-safe password compare above. In-memory —
// a restart clears it, which is fine (attacker loses their progress too).
const loginHits = new Map();
const LOGIN_MAX = 8, LOGIN_LOCK_MS = 15 * 60 * 1000;
function loginGate(ip) {
const r = loginHits.get(ip);
if (r && r.until > Date.now()) return { locked: true, mins: Math.ceil((r.until - Date.now()) / 60000) };
return { locked: false };
}
function loginFail(ip) {
const now = Date.now();
let r = loginHits.get(ip);
if (!r || (r.until && r.until < now && r.count >= LOGIN_MAX)) r = { count: 0, until: 0 };
r.count++;
if (r.count >= LOGIN_MAX) { r.until = now + LOGIN_LOCK_MS * Math.min(8, r.count - LOGIN_MAX + 1); loginHits.set(ip, r); return { locked: true, mins: Math.ceil((r.until - now) / 60000) }; }
loginHits.set(ip, r);
return { locked: false, left: LOGIN_MAX - r.count };
}
function loginReset(ip) { loginHits.delete(ip); }
const submitHits = new Map(); const submitHits = new Map();
function submitRateLimited(ip) { function submitRateLimited(ip) {
const now = Date.now(), rec = submitHits.get(ip); const now = Date.now(), rec = submitHits.get(ip);
@@ -100,10 +120,11 @@ function sendEmailRaw(toEmail, subject, text) {
}).then(r => { if (r.status >= 300) r.text().then(t => console.error('sendgrid status', r.status, t.slice(0, 200))); }) }).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)); .catch(e => console.error('sendgrid error', e.message));
} }
function sendPaidEmail(toEmail, memberName, evt) { function sendPaidEmail(toEmail, memberName, evt, unsub) {
const kindLine = evt.kind === 'upline' ? `an upgrade pass-up from member #${evt.fromId}` : `a referral reward from member #${evt.fromId}'s entry`; 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 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.`; const foot = unsub ? `\n\nStop these alerts: ${unsub}` : `\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.`;
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/my/${evt.toId}\n\n— The RM Circle Team${foot}`;
sendEmailRaw(toEmail, `Your RM Circle position #${evt.toId} just got paid ${evt.pol.toFixed(2)} POL`, text); sendEmailRaw(toEmail, `Your RM Circle position #${evt.toId} just got paid ${evt.pol.toFixed(2)} POL`, text);
} }
function firePostback(clickid, txid, source) { function firePostback(clickid, txid, source) {
@@ -337,6 +358,27 @@ async function handleApi(req,res,pathname){
if(req.method==='GET'&&pathname==='/api/public/payouts'){ if(req.method==='GET'&&pathname==='/api/public/payouts'){
return json(res,200,chain.getPayoutsPublic(),{'Cache-Control':'public, max-age=20'}); return json(res,200,chain.getPayoutsPublic(),{'Cache-Control':'public, max-age=20'});
} }
if(req.method==='GET'&&pathname==='/api/public/alert-status'){
const id=Number(new URL(req.url,'http://x').searchParams.get('id')||0);
const rec=getMemberAlerts()[id];
return json(res,200,{subscribed:!!(rec&&rec.email),email:rec&&rec.email?maskEmail(rec.email):null});
}
if(req.method==='POST'&&pathname==='/api/public/alert-signup'){
const ip=String(req.headers['x-forwarded-for']||req.socket.remoteAddress||'').split(',')[0].trim();
if(submitRateLimited(ip))return json(res,429,{error:'Too many requests — wait a few minutes.'});
const b=await bodyJson(req).catch(()=>null); if(!b)return json(res,400,{error:'Invalid request.'});
const id=Number(b.id); if(!Number.isInteger(id)||id<1||id>281474976710655)return json(res,400,{error:'Enter your numeric member ID.'});
const email=String(b.email||'').trim();
const ma=getMemberAlerts();
if(!email){ if(ma[id]){delete ma[id];saveMemberAlerts(ma);} return json(res,200,{ok:true,subscribed:false}); }
if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)||email.length>120)return json(res,400,{error:'Enter a valid email address.'});
let onchain=null; try{ onchain=await Promise.race([chain.verifyMember(id),new Promise((_,rej)=>setTimeout(()=>rej(new Error('t')),6000))]); }catch(e){ onchain=null; }
if(onchain&&!onchain.registered)return json(res,400,{error:`ID ${id} isn't registered on the smart contract — double-check the number.`});
ma[id]={email:email.slice(0,120),ts:new Date().toISOString()};
saveMemberAlerts(ma);
sendEmailRaw(email,`Alerts on for RM Circle position #${id}`,`You're now subscribed to alerts for RM Circle position #${id}.\n\nYou'll get an email when this position is paid, and when it needs an upgrade to catch incoming pay.\n\nSee your position anytime: https://rmcircle.saasy.top/my/${id}\nStop these alerts: ${unsubUrl(id)}\n\n— The RM Circle Team`);
return json(res,200,{ok:true,subscribed:true,email:maskEmail(email)});
}
if(req.method==='GET'&&pathname==='/api/public/current-sponsor'){ 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.'}); 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.'}); 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.'});
@@ -353,7 +395,13 @@ async function handleApi(req,res,pathname){
const b=await bodyJson(req).catch(()=>({}));recordEvent(b.event,b.source);return json(res,200,{ok:true}); 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'){ 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 ip=String(req.headers['x-forwarded-for']||req.socket.remoteAddress||'').split(',')[0].trim();
const gate=loginGate(ip);
if(gate.locked)return json(res,429,{error:`Too many attempts. Try again in ${gate.mins} minute${gate.mins===1?'':'s'}.`});
const b=await bodyJson(req).catch(e=>null);if(!b)return json(res,400,{error:'Invalid request'});
const ok=typeof b.password==='string'&&b.password.length===ADMIN_PASSWORD.length&&crypto.timingSafeEqual(Buffer.from(b.password),Buffer.from(ADMIN_PASSWORD));
if(!ok){const g=loginFail(ip);return json(res,401,{error:g.locked?`Too many attempts. Locked for ${g.mins} minutes.`:`Invalid password.${g.left<=3?` ${g.left} attempt${g.left===1?'':'s'} left before lockout.`:''}`});}
loginReset(ip);
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}); 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'){ if(req.method==='POST'&&pathname==='/api/admin/logout'){
@@ -434,8 +482,16 @@ const server=http.createServer(async(req,res)=>{
const u=new URL(req.url,`http://${req.headers.host||'localhost'}`),pathname=decodeURIComponent(u.pathname); 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(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'}); if(req.method!=='GET'&&req.method!=='HEAD')return send(res,405,'Method Not Allowed',{'Content-Type':'text/plain; charset=utf-8'});
if(pathname==='/unsubscribe'){
const id=u.searchParams.get('id')||'', t=u.searchParams.get('t')||'';
let ok=false;
if(/^\d{1,15}$/.test(id)&&t&&t===unsubToken(id)){ const ma=getMemberAlerts(); if(ma[id]){delete ma[id];saveMemberAlerts(ma);} ok=true; }
const safeId=/^\d{1,15}$/.test(id)?id:'';
const body=`<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex"><title>${ok?'Unsubscribed':'Invalid link'}</title><link rel="stylesheet" href="/styles.css"></head><body class="admin-bg"><div class="login-panel" style="margin:12vh auto;text-align:center"><h1>${ok?'You are unsubscribed ✓':'Invalid or expired link'}</h1><p style="color:var(--muted)">${ok?`Position #${safeId} will no longer receive alert emails. You can turn them back on anytime from your dashboard.`:'We could not process that unsubscribe link. You can manage alerts from your dashboard.'}</p><a class="btn btn-primary" href="/my/${safeId}">Open dashboard</a></div></body></html>`;
res.writeHead(200,securityHeaders({'Content-Type':'text/html; charset=utf-8','Cache-Control':'no-store'}));return res.end(body);
}
let file; 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{ 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(pathname==='/disclaimer'||pathname==='/disclaimer/')file=path.join(PUBLIC_DIR,'disclaimer.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=''; 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); if(file&&staticFile(req,res,file))return;return staticFile(req,res,path.join(PUBLIC_DIR,'404.html'),404);
@@ -449,35 +505,49 @@ server.listen(PORT,()=>{console.log(`Crypto Team Build sponsor router running on
const OWNER_ALERTS_FILE = path.join(DATA_DIR, 'owner-alerts.json'); const OWNER_ALERTS_FILE = path.join(DATA_DIR, 'owner-alerts.json');
function loadOwnerAlerts(){ try{ return new Set(readJson(OWNER_ALERTS_FILE)); }catch(e){ return new Set(); } } function loadOwnerAlerts(){ try{ return new Set(readJson(OWNER_ALERTS_FILE)); }catch(e){ return new Set(); } }
function parseOwnerIds(){ return [...new Set(String(getConfig().ownerIds||'').split(',').map(s=>parseInt(String(s).trim(),10)).filter(n=>Number.isInteger(n)&&n>0))].slice(0,12); } function parseOwnerIds(){ return [...new Set(String(getConfig().ownerIds||'').split(',').map(s=>parseInt(String(s).trim(),10)).filter(n=>Number.isInteger(n)&&n>0))].slice(0,12); }
function checkOwnerUpgrades(){ // Self-service member alert subscriptions: memberId -> { email, ts }
const MEMBER_ALERTS_FILE = path.join(DATA_DIR, 'member-alerts.json');
if (!fs.existsSync(MEMBER_ALERTS_FILE)) fs.writeFileSync(MEMBER_ALERTS_FILE, '{}');
function getMemberAlerts(){ try{ return readJson(MEMBER_ALERTS_FILE)||{}; }catch(e){ return {}; } }
function saveMemberAlerts(o){ writeJson(MEMBER_ALERTS_FILE,o); }
const ALERT_SECRET = crypto.createHash('sha256').update('rmc-alerts::'+ADMIN_PASSWORD).digest('hex');
function unsubToken(id){ return crypto.createHmac('sha256',ALERT_SECRET).update('unsub:'+String(id)).digest('hex').slice(0,24); }
function unsubUrl(id){ return `https://rmcircle.saasy.top/unsubscribe?id=${id}&t=${unsubToken(id)}`; }
function maskEmail(e){ const i=String(e).indexOf('@'); if(i<1)return '•••'; return e[0]+'•••'+e.slice(i); }
// Upgrade-need alerts for both owner positions (email+Telegram) and any member
// who opted in via their dashboard (email only). Runs every 5 min from state.
function checkUpgradeAlerts(){
try{ try{
const c=getConfig(); const ids=parseOwnerIds(); const c=getConfig();
const watch={}; // id -> [{email, owner}]
if(c.ownerAlertEmail) for(const id of parseOwnerIds()){(watch[id]=watch[id]||[]).push({email:c.ownerAlertEmail,owner:true});}
const ma=getMemberAlerts();
for(const [idStr,rec] of Object.entries(ma)) if(rec&&rec.email){const id=Number(idStr);(watch[id]=watch[id]||[]).push({email:rec.email,owner:false});}
const ids=Object.keys(watch).map(Number);
if(!ids.length) return; if(!ids.length) return;
const res=chain.getOwnerUpgradeNeeds(ids); const res=chain.getOwnerUpgradeNeeds(ids);
if(!res.ready) return; if(!res.ready) return;
const alerted=loadOwnerAlerts(); const active=new Set(); const alerted=loadOwnerAlerts(); const active=new Set(); const tgDone=new Set();
for(const n of res.needs){ for(const n of res.needs){
const key=`${n.id}:${n.reason}:${n.neededLevel}`; active.add(key);
if(alerted.has(key)) continue;
alerted.add(key);
const who=n.members.map(m=>'#'+m).join(', '); const who=n.members.map(m=>'#'+m).join(', ');
const action=n.reason==='qualify' const action=n.reason==='qualify'?`Position #${n.id} needs its 2 directs to catch this.`:`Upgrade position #${n.id} (now ${n.levelName}) to ${n.neededLevelName} to catch it.`;
? `Position #${n.id} needs its 2 directs to catch this.` for(const w of (watch[n.id]||[])){
: `Upgrade position #${n.id} (now ${n.levelName}) to ${n.neededLevelName} to catch it.`; const key=`${w.email}:${n.id}:${n.reason}:${n.neededLevel}`; active.add(key);
if(c.ownerAlertEmail){ if(alerted.has(key)) continue; alerted.add(key);
sendEmailRaw(c.ownerAlertEmail, const unsub=w.owner?'':`\n\nStop these alerts: ${unsubUrl(n.id)}`;
sendEmailRaw(w.email,
`RM Circle: upgrade #${n.id} to ${n.neededLevelName} — ${n.amountAtRisk} POL incoming`, `RM Circle: upgrade #${n.id} to ${n.neededLevelName} — ${n.amountAtRisk} POL incoming`,
`Heads up — one of your positions has money about to arrive that it can't catch yet.\n\nPosition #${n.id} is at ${n.levelName}. Member(s) ${who} are ONE upgrade away from paying #${n.id} about ${n.amountAtRisk} POL — but that payment only stops at #${n.id} if it's at ${n.neededLevelName} and qualified.\n\n${action}\n\nDo it before they upgrade, or the payment passes to the next eligible position above you (it doesn't come back). Your positions: https://rmcircle.saasy.top/admin\n\n— RM Circle auto-watch`); `Heads up — position #${n.id} has money about to arrive it can't catch yet.\n\n#${n.id} is at ${n.levelName}. ${who} ${n.members.length===1?'is':'are'} ONE upgrade away from paying #${n.id} about ${n.amountAtRisk} POL — but that only stops at #${n.id} if it's at ${n.neededLevelName} and qualified.\n\n${action}\n\nDo it before they upgrade, or the payment passes to the next eligible position above (it doesn't come back).${unsub}\n\n— RM Circle auto-watch`);
if(w.owner&&!tgDone.has(n.id+':'+n.neededLevel)){ tgDone.add(n.id+':'+n.neededLevel); sendTelegram(`⏫ UPGRADE #${n.id} SOON: ${who} one upgrade from paying ~${n.amountAtRisk} POL. #${n.id} is ${n.levelName} — needs ${n.neededLevelName}${n.reason==='qualify'?' + 2 directs':''}. Upgrade before they do.`); }
} }
sendTelegram(`⏫ UPGRADE #${n.id} SOON: ${who} one upgrade from paying ~${n.amountAtRisk} POL. #${n.id} is ${n.levelName} — needs ${n.neededLevelName}${n.reason==='qualify'?' + 2 directs':''}. Upgrade before they do.`);
} }
let changed=false; let changed=false;
for(const k of [...alerted]) if(!active.has(k)){ alerted.delete(k); changed=true; } for(const k of [...alerted]) if(!active.has(k)){ alerted.delete(k); changed=true; }
if(changed||active.size) writeJson(OWNER_ALERTS_FILE,[...alerted]); if(changed||active.size) writeJson(OWNER_ALERTS_FILE,[...alerted]);
}catch(e){ console.error('owner upgrade check', e.message); } }catch(e){ console.error('upgrade alert check', e.message); }
} }
setInterval(checkOwnerUpgrades, 5*60*1000).unref(); setInterval(checkUpgradeAlerts, 5*60*1000).unref();
setTimeout(checkOwnerUpgrades, 30000).unref(); setTimeout(checkUpgradeAlerts, 30000).unref();
chain.startIndexer(evt=>{ chain.startIndexer(evt=>{
try{ try{
const c=getConfig(); const c=getConfig();
@@ -500,11 +570,14 @@ chain.startIndexer(evt=>{
} }
} }
}catch(e){console.error('team alert error',e.message)} }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) // "you've been paid" email — sponsor-record contact, and self-service subscribers
try{ try{
if(evt.type==='payout'){ if(evt.type==='payout'){
const sent=new Set();
const sp=getSponsors().find(x=>String(x.id)===String(evt.toId)); const sp=getSponsors().find(x=>String(x.id)===String(evt.toId));
if(sp&&sp.email)sendPaidEmail(sp.email,sp.name,evt); if(sp&&sp.email){sendPaidEmail(sp.email,sp.name,evt);sent.add(sp.email.toLowerCase());}
const rec=getMemberAlerts()[evt.toId];
if(rec&&rec.email&&!sent.has(rec.email.toLowerCase()))sendPaidEmail(rec.email,'there',evt,unsubUrl(evt.toId));
} }
}catch(e){console.error('paid email error',e.message)} }catch(e){console.error('paid email error',e.message)}
// Auto-count directs (Marty 2026-08-14, "prevent manual effort"): a new // Auto-count directs (Marty 2026-08-14, "prevent manual effort"): a new