diff --git a/public/chat.js b/public/chat.js index b6dd8f5..fa5491d 100644 --- a/public/chat.js +++ b/public/chat.js @@ -29,17 +29,17 @@ {k:['qualified','qualification','2 directs','two directs','directs','retire'], a:()=>`A position is qualified once it has 2 directs. The team recommendation: retire your link at that point and help your 2 get their 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 rmcircle.saasy.top/contract: 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.`}, + 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 rmcircle.team/contract: 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:['disappear','disappears','creator','creators','abandon','abandoned','walk away','goes away','shut down','shuts down','still work','keep running','keeps running','autonomous','dies','lost keys','without them','stop working','turned off','if they leave','what if they','forever','over time'], - a:()=>`Great question — and we checked it on-chain, not just in theory. The contract needs no one to keep it running: joins, upgrades, placement and every payout are fully automatic — no button anyone has to press, no expiry. If the creators walked away, lost their keys, or vanished, member payments keep flowing exactly as coded. We also verified that the founder, development and fee wallets are ordinary wallets, not programs — an ordinary wallet always accepts an incoming payment even if its key is lost forever, so a dead admin wallet can't jam a single member payment (at worst the project's own fee sits there uncollected). And the contract holds no stored balance — every payment is delivered in the same transaction. Full write-up in section 6 of rmcircle.saasy.top/contract.`}, + a:()=>`Great question — and we checked it on-chain, not just in theory. The contract needs no one to keep it running: joins, upgrades, placement and every payout are fully automatic — no button anyone has to press, no expiry. If the creators walked away, lost their keys, or vanished, member payments keep flowing exactly as coded. We also verified that the founder, development and fee wallets are ordinary wallets, not programs — an ordinary wallet always accepts an incoming payment even if its key is lost forever, so a dead admin wallet can't jam a single member payment (at worst the project's own fee sits there uncollected). And the contract holds no stored balance — every payment is delivered in the same transaction. Full write-up in section 6 of rmcircle.team/contract.`}, {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:()=>`Spillover 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 not count toward your qualification: only people who join using your 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 Member Dashboard — spillover placements are tagged there.`}, {k:['how do i get paid','how do i earn','commission','commissions','how payments work','how does the money','payout','payouts','pay flow','when do i get paid','how do i make money','income','two ways','how you get paid'], - a:()=>`Two ways money reaches your position — the full diagram is at rmcircle.saasy.top/how-pay-works. (1) Entry rewards: when a direct joins under your link you get their entry reward (about 326 POL at Premium) and you keep it — on every direct you bring. (2) Upgrade payments: each person below you pays you once, at the ONE level that matches how far below you they sit — someone 1 layer below pays you when they hit Ascensus, 2 layers below pays you at Fabrica, 3 at Culmen, and so on. To catch each, you must be at that level yourself and qualified, so stay one level ahead of your team. No income is guaranteed.`}, + a:()=>`Two ways money reaches your position — the full diagram is at rmcircle.team/how-pay-works. (1) Entry rewards: when a direct joins under your link you get their entry reward (about 326 POL at Premium) and you keep it — on every direct you bring. (2) Upgrade payments: each person below you pays you once, at the ONE level that matches how far below you they sit — someone 1 layer below pays you when they hit Ascensus, 2 layers below pays you at Fabrica, 3 at Culmen, and so on. To catch each, you must be at that level yourself and qualified, so stay one level ahead of your team. No income is guaranteed.`}, {k:['tier','standard','premium tier','standard tier','premium vs standard','which tier','half','smaller payment','less than expected','why is my payment'], a:()=>`There are two tiers. Premium is what our whole team builds at (${pol()} POL entry) — full payments. Standard costs about half and pays/earns half at every level. So if a payment ever comes in smaller than expected, it usually came from a Standard-tier position below you. Your tier is set when you join and can't be changed later (upgrading advances your level, not your tier), so always join Premium and make sure the people you bring on do too. Full breakdown: how-pay-works.`}, {k:['dashboard','my dashboard','my page','my position','my team','check my','see my','pipeline','my stats','alerts','notify me','email me','get notified'], - a:()=>`Your Member Dashboard is at rmcircle.saasy.top/my — enter your ID to see your position, your team, your payments, your pipeline (money forming below you), and any spillover under you. You can also turn on email alerts there to be notified the moment you're paid or need to upgrade. To share, use your personal invite page: rmcircle.saasy.top/join/<your ID>.`}, + a:()=>`Your Member Dashboard is at rmcircle.team/my — enter your ID to see your position, your team, your payments, your pipeline (money forming below you), and any spillover under you. You can also turn on email alerts there to be notified the moment you're paid or need to upgrade. To share, use your personal invite page: rmcircle.team/join/<your ID>.`}, {k:['level','levels','upgrade','scintilla','ascensus','fabrica','culmen','apex','fastigium','vertex','corona','8 levels'], a:()=>`There are 8 Premium levels: Scintilla, Ascensus, Fabrica, Culmen, Apex, Fastigium, Vertex, Corona. Everyone starts at Scintilla. Upgrade as quickly as practical — ideally using earned POL — because the first two payments at each level are designed to help fund your next upgrade. Stay aware of your active downline's levels so you don't fall behind.`}, {k:['30 positions','goal','milestone','matrix','how many people','team size'], @@ -83,7 +83,7 @@ const msgs=panel.querySelector('.chat-msgs'),chipsEl=panel.querySelector('.chat-chips'),form=panel.querySelector('.chat-input'),input=form.querySelector('input'); function addMsg(html,who){const d=document.createElement('div');d.className=`chat-msg ${who}`;if(who==='user'){d.textContent=html}else{d.innerHTML=html}msgs.appendChild(d);msgs.scrollTop=msgs.scrollHeight;return d} - function linkify(text){let h=esc(text);h=h.replace(/(https?:\/\/[^\s&<]+[^\s&<.,)])/g,u=>{const site=u.startsWith('https://rmcircle.saasy.top');const path=site?u.replace('https://rmcircle.saasy.top',''):u;return `${u}`});return h.replace(/\n/g,'
')} + function linkify(text){let h=esc(text);h=h.replace(/(https?:\/\/[^\s&<]+[^\s&<.,)])/g,u=>{const site=u.startsWith('https://rmcircle.team');const path=site?u.replace('https://rmcircle.team',''):u;return `${u}`});return h.replace(/\n/g,'
')} const history=[];let aiDown=false; function stripHtml(h){const d=document.createElement('div');d.innerHTML=h;return d.textContent||''} async function ask(q){ diff --git a/public/contract.html b/public/contract.html index 0e4af9a..a6e242b 100644 --- a/public/contract.html +++ b/public/contract.html @@ -1,33 +1,33 @@ -Smart Contract Security | Crypto Team Build - - - - - -
-
-
Plain-language code review

Can the bottom fall out of this? We read the code.

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.

- -
NetworkPolygon Mainnet
Source verified (Sourcify)Exact match ↗
- -

1. The code can never change.

Some crypto projects use "upgradeable" contracts — the operator can swap in new code after you've joined, changing the rules underneath you. This contract has none of that machinery. 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 exact bytecode match, the code we reviewed is provably the code that's running — not a copy, not a claim.

Verified on both major registries. The source is published and exact-match verified in two independent places: Sourcify ↗ (the open-source registry, verified the day of deployment) and Polygonscan ↗ (Exact Match, contract name TRUSTCorona, Solidity 0.8.28). Two registries, independently confirming the same thing: the readable source you can inspect is byte-for-byte the code that's running.

- -

2. Member money never sits in the contract.

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 in the same transaction — 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 payment proof feed: money in, money out, same block.

- -

3. The prices and payout rules are locked.

Entry and upgrade costs for all 8 levels were fixed the moment the contract deployed — there is no function to change them. 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, 100% goes to the eligible upline — the project takes nothing. 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.

- -

4. What the operator CAN do — the honest list.

A fair review reports the levers, not just the locks. The contract owner can do exactly four things:

  • Raise or lower the entry fee percentage (currently 5%, hard-capped at 50% in the code). This affects the price of future joins only — it can't touch anyone's earnings, and any change is instantly visible on the blockchain.
  • Redirect the project's own fee wallets (founder/development shares) — their revenue, never member payouts.
  • Sweep out coins accidentally sent to the contract address — member payment flows never leave a balance behind.
  • Transfer ownership of those same limited powers to someone else.

What the owner cannot 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.

- -

5. Where "missed" payments go.

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 get your 2 directs and keep your level current — eligible positions catch payments; ineligible positions watch them pass by. The Member Dashboard shows exactly this happening in real time.

- -

6. What if the creators disappear?

A fair question about any on-chain program: if the people who launched it walked away, lost their keys, or simply vanished — does it quietly break over time? For this contract the answer is no, and we verified it directly on the blockchain — not just in theory.

Nothing needs an admin to keep running. Joins, upgrades, matrix placement, and every payout are fully automatic. There is no periodic "keepalive," no button anyone has to press to keep payments flowing, and no expiry date anywhere in the code. Whether the creator is present or gone changes nothing about what members receive.

A lost or abandoned admin wallet can't jam the machine. Every payout requires its recipient to accept the coins, so we checked what those admin wallets actually are: the founder, development, and fee-receiver wallets are all ordinary wallets, not programs. An ordinary wallet always accepts an incoming payment — even if its key is lost forever and no human is behind it. So even in the worst case, the coins simply arrive and sit there uncollected; the member on the other side of that transaction still gets paid, every time. The only thing that could ever go uncollected is the project's own fee — never a member's earnings.

And if the owner's key were lost, the four limited powers from section 4 would simply freeze in place forever — the fee percentage and wallet addresses could never change again. Members would never notice, because members never depended on those powers. The contract also keeps no stored balance — every payment is delivered in the same transaction it arrives in — so there's nothing sitting inside that could be stranded. In short: this system's survival does not depend on any person staying reachable.

- -

Check it yourself — please.

- -
What this page is not: code security is not an income guarantee. This review says the rules can't change underneath you — 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.
- -

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.

-
-
- - +Smart Contract Security | Crypto Team Build + + + + + +
+
+
Plain-language code review

Can the bottom fall out of this? We read the code.

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.

+ +
NetworkPolygon Mainnet
Source verified (Sourcify)Exact match ↗
+ +

1. The code can never change.

Some crypto projects use "upgradeable" contracts — the operator can swap in new code after you've joined, changing the rules underneath you. This contract has none of that machinery. 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 exact bytecode match, the code we reviewed is provably the code that's running — not a copy, not a claim.

Verified on both major registries. The source is published and exact-match verified in two independent places: Sourcify ↗ (the open-source registry, verified the day of deployment) and Polygonscan ↗ (Exact Match, contract name TRUSTCorona, Solidity 0.8.28). Two registries, independently confirming the same thing: the readable source you can inspect is byte-for-byte the code that's running.

+ +

2. Member money never sits in the contract.

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 in the same transaction — 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 payment proof feed: money in, money out, same block.

+ +

3. The prices and payout rules are locked.

Entry and upgrade costs for all 8 levels were fixed the moment the contract deployed — there is no function to change them. 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, 100% goes to the eligible upline — the project takes nothing. 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.

+ +

4. What the operator CAN do — the honest list.

A fair review reports the levers, not just the locks. The contract owner can do exactly four things:

  • Raise or lower the entry fee percentage (currently 5%, hard-capped at 50% in the code). This affects the price of future joins only — it can't touch anyone's earnings, and any change is instantly visible on the blockchain.
  • Redirect the project's own fee wallets (founder/development shares) — their revenue, never member payouts.
  • Sweep out coins accidentally sent to the contract address — member payment flows never leave a balance behind.
  • Transfer ownership of those same limited powers to someone else.

What the owner cannot 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.

+ +

5. Where "missed" payments go.

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 get your 2 directs and keep your level current — eligible positions catch payments; ineligible positions watch them pass by. The Member Dashboard shows exactly this happening in real time.

+ +

6. What if the creators disappear?

A fair question about any on-chain program: if the people who launched it walked away, lost their keys, or simply vanished — does it quietly break over time? For this contract the answer is no, and we verified it directly on the blockchain — not just in theory.

Nothing needs an admin to keep running. Joins, upgrades, matrix placement, and every payout are fully automatic. There is no periodic "keepalive," no button anyone has to press to keep payments flowing, and no expiry date anywhere in the code. Whether the creator is present or gone changes nothing about what members receive.

A lost or abandoned admin wallet can't jam the machine. Every payout requires its recipient to accept the coins, so we checked what those admin wallets actually are: the founder, development, and fee-receiver wallets are all ordinary wallets, not programs. An ordinary wallet always accepts an incoming payment — even if its key is lost forever and no human is behind it. So even in the worst case, the coins simply arrive and sit there uncollected; the member on the other side of that transaction still gets paid, every time. The only thing that could ever go uncollected is the project's own fee — never a member's earnings.

And if the owner's key were lost, the four limited powers from section 4 would simply freeze in place forever — the fee percentage and wallet addresses could never change again. Members would never notice, because members never depended on those powers. The contract also keeps no stored balance — every payment is delivered in the same transaction it arrives in — so there's nothing sitting inside that could be stranded. In short: this system's survival does not depend on any person staying reachable.

+ +

Check it yourself — please.

+ +
What this page is not: code security is not an income guarantee. This review says the rules can't change underneath you — 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.
+ +

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.

+
+
+ + diff --git a/public/disclaimer.html b/public/disclaimer.html index 37463b2..9b122dc 100644 --- a/public/disclaimer.html +++ b/public/disclaimer.html @@ -1,28 +1,28 @@ -Disclaimers | Crypto Team Build - - - - -
-
-
Please read

Disclaimers & Disclosures

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.

- -

Informational & educational only

This is an independent team resource for members and prospective members of the RM Circle Premium team build. Everything on it is provided for informational and educational purposes only. The live figures, dashboards, payment feeds, matrix views, and member pages display public information read directly from the Polygon blockchain — 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.

- -

Not affiliated — we don't own the contract or the blockchain

This website is not operated by, endorsed by, or officially affiliated with 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 /contract.

- -

Earnings disclaimer — no income is promised

Nothing here is a promise, projection, or guarantee of income. Any amounts shown — including historical payments and "pipeline" or "incoming" figures — are either records of past on-chain events or amounts that would be paid by the contract's rules if specific future actions occur. They are not predictions of what you will earn, and they are not typical results.

The majority of participants in programs like this do not profit, and many lose their entire contribution. 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.

- -

Affiliate & material-connection disclosure

The people who operate this website participate in the RM Circle program themselves and hold positions in it. 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.

- -

Cryptocurrency & smart-contract risk

Participation requires sending cryptocurrency (POL) to a smart contract on a public blockchain. This is high-risk. 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. Never send funds you cannot afford to lose entirely, and never share your wallet's Secret Recovery Phrase or private keys with anyone — including anyone claiming to represent this team.

- -

Not financial, legal, or tax advice

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. You are responsible for determining whether participation is legal for you and for your own compliance and taxes. Consult your own qualified, independent professional advisors before participating. Do not participate if it is not permitted in your jurisdiction.

- -

No warranty; accuracy

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. Always verify anything important yourself directly on the blockchain (Polygonscan ↗) 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.

- -

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.

-
-
- - +Disclaimers | Crypto Team Build + + + + +
+
+
Please read

Disclaimers & Disclosures

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.

+ +

Informational & educational only

This is an independent team resource for members and prospective members of the RM Circle Premium team build. Everything on it is provided for informational and educational purposes only. The live figures, dashboards, payment feeds, matrix views, and member pages display public information read directly from the Polygon blockchain — 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.

+ +

Not affiliated — we don't own the contract or the blockchain

This website is not operated by, endorsed by, or officially affiliated with 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 /contract.

+ +

Earnings disclaimer — no income is promised

Nothing here is a promise, projection, or guarantee of income. Any amounts shown — including historical payments and "pipeline" or "incoming" figures — are either records of past on-chain events or amounts that would be paid by the contract's rules if specific future actions occur. They are not predictions of what you will earn, and they are not typical results.

The majority of participants in programs like this do not profit, and many lose their entire contribution. 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.

+ +

Affiliate & material-connection disclosure

The people who operate this website participate in the RM Circle program themselves and hold positions in it. 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.

+ +

Cryptocurrency & smart-contract risk

Participation requires sending cryptocurrency (POL) to a smart contract on a public blockchain. This is high-risk. 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. Never send funds you cannot afford to lose entirely, and never share your wallet's Secret Recovery Phrase or private keys with anyone — including anyone claiming to represent this team.

+ +

Not financial, legal, or tax advice

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. You are responsible for determining whether participation is legal for you and for your own compliance and taxes. Consult your own qualified, independent professional advisors before participating. Do not participate if it is not permitted in your jurisdiction.

+ +

No warranty; accuracy

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. Always verify anything important yourself directly on the blockchain (Polygonscan ↗) 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.

+ +

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.

+
+
+ + diff --git a/public/how-pay-works.html b/public/how-pay-works.html index f667b31..20ff891 100644 --- a/public/how-pay-works.html +++ b/public/how-pay-works.html @@ -1,77 +1,77 @@ -How You Get Paid | Crypto Team Build - - - - - - -
-
-
The pay flow

How you get paid — two ways.

There are exactly two ways money reaches your position. Once you see them, the whole plan clicks. No income is guaranteed — this shows the mechanics, not a promise of earnings.

- -
Stream 1 · you keep these

Entry rewards — from your own directs

-
YOUR
POSITION
+326 POL↑
Someone joins
under your link
-

When someone joins the program using your link, you receive their entry reward (326 POL at Premium) — directly, and you keep it. You earn this on every direct you ever bring, not just the first two. Two is simply the number that qualifies you.

- -
Stream 2 · the depth ladder

Upgrade payments — each layer pays you at ONE level

-

This is the part people ask about. When your team upgrades, each payment travels up the matrix to the position at the matching depth. Each person below you pays you exactly once — at the single level that matches how far below you they sit.

-
- - - - - - - - YOUR POSITION - every payment below flows here - Each layer of your team pays you once — - at the ONE level that matches its depth below you. - (POL amounts shown at Premium tier.) - - - deeper in your team ↓ - - - Layer 1 · directs - Layer 2 - Layer 3 - Layer 4 - - - - - - - - 1st upgrade → Ascensus +621 - 2nd upgrade → Fabrica +1,243 - 3rd upgrade → Culmen +2,486 - 4th upgrade → Apex +4,971 - - - - - - - - - -
-

To catch each one, you must be at that level yourself (and qualified). Layer 3's Culmen payment only stops at you if you're at Culmen — otherwise it passes up to the next eligible position. That's why the rule is: stay one level ahead of your team's deepest active layer.

- -
Good to know · tiers

Premium vs. Standard — why an amount can come in smaller

-

Every figure on this page is shown at the Premium tier — the tier our whole team builds at. The contract also has a Standard tier that costs about half to enter and, in exchange, pays and earns half as much at every level. So if a payment ever lands smaller than the diagram shows, it's almost always because it came from a Standard-tier position below you.

-
- - - - -
PaymentPremiumStandard
Entry reward (a direct joins)326 POL163 POL
1st upgrade → Ascensus621 POL311 POL
2nd upgrade → Fabrica1,243 POL621 POL
3rd upgrade → Culmen2,486 POL1,243 POL
-

Two things to remember: your tier is set when you join and can't be changed later — upgrading advances your level, never your tier. And a Standard position in your team pays everyone above it half, at every level. So the team play is simple: join Premium, and make sure the people you bring on do too, so your whole leg pays full value up the line.

-
The one line to remember: You keep your directs' entry money. For upgrades, each person below you pays you at exactly one level — the one that matches their depth beneath you. Twos are for building; depth-matched levels are for paying.
-

Every figure is a contract rule, verifiable on-chain, not a promise of income. Most participants may not profit. Full disclaimers → · How the contract works →

-
-
- - +How You Get Paid | Crypto Team Build + + + + + + +
+
+
The pay flow

How you get paid — two ways.

There are exactly two ways money reaches your position. Once you see them, the whole plan clicks. No income is guaranteed — this shows the mechanics, not a promise of earnings.

+ +
Stream 1 · you keep these

Entry rewards — from your own directs

+
YOUR
POSITION
+326 POL↑
Someone joins
under your link
+

When someone joins the program using your link, you receive their entry reward (326 POL at Premium) — directly, and you keep it. You earn this on every direct you ever bring, not just the first two. Two is simply the number that qualifies you.

+ +
Stream 2 · the depth ladder

Upgrade payments — each layer pays you at ONE level

+

This is the part people ask about. When your team upgrades, each payment travels up the matrix to the position at the matching depth. Each person below you pays you exactly once — at the single level that matches how far below you they sit.

+
+ + + + + + + + YOUR POSITION + every payment below flows here + Each layer of your team pays you once — + at the ONE level that matches its depth below you. + (POL amounts shown at Premium tier.) + + + deeper in your team ↓ + + + Layer 1 · directs + Layer 2 + Layer 3 + Layer 4 + + + + + + + + 1st upgrade → Ascensus +621 + 2nd upgrade → Fabrica +1,243 + 3rd upgrade → Culmen +2,486 + 4th upgrade → Apex +4,971 + + + + + + + + + +
+

To catch each one, you must be at that level yourself (and qualified). Layer 3's Culmen payment only stops at you if you're at Culmen — otherwise it passes up to the next eligible position. That's why the rule is: stay one level ahead of your team's deepest active layer.

+ +
Good to know · tiers

Premium vs. Standard — why an amount can come in smaller

+

Every figure on this page is shown at the Premium tier — the tier our whole team builds at. The contract also has a Standard tier that costs about half to enter and, in exchange, pays and earns half as much at every level. So if a payment ever lands smaller than the diagram shows, it's almost always because it came from a Standard-tier position below you.

+
+ + + + +
PaymentPremiumStandard
Entry reward (a direct joins)326 POL163 POL
1st upgrade → Ascensus621 POL311 POL
2nd upgrade → Fabrica1,243 POL621 POL
3rd upgrade → Culmen2,486 POL1,243 POL
+

Two things to remember: your tier is set when you join and can't be changed later — upgrading advances your level, never your tier. And a Standard position in your team pays everyone above it half, at every level. So the team play is simple: join Premium, and make sure the people you bring on do too, so your whole leg pays full value up the line.

+
The one line to remember: You keep your directs' entry money. For upgrades, each person below you pays you at exactly one level — the one that matches their depth beneath you. Twos are for building; depth-matched levels are for paying.
+

Every figure is a contract rule, verifiable on-chain, not a promise of income. Most participants may not profit. Full disclaimers → · How the contract works →

+
+
+ + diff --git a/public/index.html b/public/index.html index 8591a64..6c3aed8 100644 --- a/public/index.html +++ b/public/index.html @@ -1,24 +1,24 @@ -Crypto Team Build | RM Circle Premium - - - - - -
-
Premium • Polygon • Team Duplication

Get Your 2.
Help Your 2 Get Their 2.

A team-first strategy built around qualification, depth, and duplication.

Show Me the Current Team Placement →
QR code — scan to open the getting-started page with the current team sponsorScan to get started
opens the current team placement
See the Strategy
Independent team training resource • Participation involves risk • No income is guaranteed
-
RM Circle Premium Team Build Roadmap — core strategy, step-by-step guide, premium levels, and duplication formula
The RM Circle is a team build project of the Crypto Team Build Network. This roadmap is the plan every member follows — tap to view full size.
-
Our team by the numbers

Why it pays to be on a winning team.

Every figure below is read live from the Polygon blockchain — anyone can verify it. These are historical on-chain facts, not a promise of future income.

-
Reading the blockchain…
-
↧

Spillover works for you

When your team keeps recruiting, the overflow spills down into your matrix — filling your structure with members you didn't personally enroll. A bigger, more active team means more spillover landing under you.

↑

Depth pays as it climbs

Your income comes from your team upgrading through the levels, and every upgrade travels up the matrix. The deeper and more active your side, the more upgrade payments flow toward your position — if you stay qualified and keep your level ahead.

★

You never build alone

The team rotation points everyone's effort at helping the next person get their two. Join the momentum and the team works to qualify you — instead of grinding solo.

-

Blockchain data can lag or contain display errors — always verify on-chain. Participation involves cryptocurrency risk and no income is guaranteed; most participants may not profit. Full disclaimers →

-
-
Watch first

See how simple this is.

A quick walkthrough of the team build in action. Watch this before you dive into the strategy below.

-
The strategy

Simple enough to duplicate.

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.

2

Get exactly two

Use your position's link until two direct positions are placed beneath you and your position is qualified.

↘

Move the effort down

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.

↑

Upgrade responsibly

Use earned POL to advance when practical. Active builders may also choose to self-fund, but only within their own risk tolerance.

-
Moving-link workflow

Qualify. Then the effort moves down.

Step 1Use link

Share the current position's referral link.

Step 2Get 2

Place exactly two direct positions.

Step 3Retire link

Late signups still spill down — bonus depth, not a problem.

Step 4Help your 2

Shift the team effort to their links.

Step 5Repeat

Keep the qualification wave moving down.

-
Depth over width

2 → 4 → 8 → 16 → 32 → 64 → 128 → 256

Each generation doubles. A full eight-generation team is 510 positions (2+4+8+16+32+64+128+256) — but the first milestone that matters is the 30 positions in your first four generations: 2 + 4 + 8 + 16.

2
4
8
16
32
64
128
256
Team principle: once your two are in place, retire your link and shift to helping them get their 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.
-
Live payment proof

Real payouts, straight from the blockchain.

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

Reading the blockchain…
Data is read directly from the RM Circle smart contract (0x33Bd…2DAF) on Polygon Mainnet. Member numbers are on-chain IDs, not names. Past payouts are not a promise of future results. How the contract works — and why the rules can't change →
-
Still have doubts?

Good — you should ask hard questions.

Before you risk anything, it's smart to ask: Can the rules change after I join? Can anyone take my money? What happens if the people who built this walk away? We read the complete, verified smart-contract code and answered every one of those — in plain language, with links so you can check each claim on the blockchain yourself.

The short version: the code can't be changed, member money never sits in the contract, and the whole thing keeps running on its own — even if its creators disappear.

See How the Contract Works →
- -
Ready to start?

See the current team placement.

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

Open Getting Started Instructions →
-
- +Crypto Team Build | RM Circle Premium + + + + + +
+
Premium • Polygon • Team Duplication

Get Your 2.
Help Your 2 Get Their 2.

A team-first strategy built around qualification, depth, and duplication.

Show Me the Current Team Placement →
QR code — scan to open the getting-started page with the current team sponsorScan to get started
opens the current team placement
See the Strategy
Independent team training resource • Participation involves risk • No income is guaranteed
+
RM Circle Premium Team Build Roadmap — core strategy, step-by-step guide, premium levels, and duplication formula
The RM Circle is a team build project of the Crypto Team Build Network. This roadmap is the plan every member follows — tap to view full size.
+
Our team by the numbers

Why it pays to be on a winning team.

Every figure below is read live from the Polygon blockchain — anyone can verify it. These are historical on-chain facts, not a promise of future income.

+
Reading the blockchain…
+
↧

Spillover works for you

When your team keeps recruiting, the overflow spills down into your matrix — filling your structure with members you didn't personally enroll. A bigger, more active team means more spillover landing under you.

↑

Depth pays as it climbs

Your income comes from your team upgrading through the levels, and every upgrade travels up the matrix. The deeper and more active your side, the more upgrade payments flow toward your position — if you stay qualified and keep your level ahead.

★

You never build alone

The team rotation points everyone's effort at helping the next person get their two. Join the momentum and the team works to qualify you — instead of grinding solo.

+

Blockchain data can lag or contain display errors — always verify on-chain. Participation involves cryptocurrency risk and no income is guaranteed; most participants may not profit. Full disclaimers →

+
+
Watch first

See how simple this is.

A quick walkthrough of the team build in action. Watch this before you dive into the strategy below.

+
The strategy

Simple enough to duplicate.

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.

2

Get exactly two

Use your position's link until two direct positions are placed beneath you and your position is qualified.

↘

Move the effort down

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.

↑

Upgrade responsibly

Use earned POL to advance when practical. Active builders may also choose to self-fund, but only within their own risk tolerance.

+
Moving-link workflow

Qualify. Then the effort moves down.

Step 1Use link

Share the current position's referral link.

Step 2Get 2

Place exactly two direct positions.

Step 3Retire link

Late signups still spill down — bonus depth, not a problem.

Step 4Help your 2

Shift the team effort to their links.

Step 5Repeat

Keep the qualification wave moving down.

+
Depth over width

2 → 4 → 8 → 16 → 32 → 64 → 128 → 256

Each generation doubles. A full eight-generation team is 510 positions (2+4+8+16+32+64+128+256) — but the first milestone that matters is the 30 positions in your first four generations: 2 + 4 + 8 + 16.

2
4
8
16
32
64
128
256
Team principle: once your two are in place, retire your link and shift to helping them get their 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.
+
Live payment proof

Real payouts, straight from the blockchain.

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

Reading the blockchain…
Data is read directly from the RM Circle smart contract (0x33Bd…2DAF) on Polygon Mainnet. Member numbers are on-chain IDs, not names. Past payouts are not a promise of future results. How the contract works — and why the rules can't change →
+
Still have doubts?

Good — you should ask hard questions.

Before you risk anything, it's smart to ask: Can the rules change after I join? Can anyone take my money? What happens if the people who built this walk away? We read the complete, verified smart-contract code and answered every one of those — in plain language, with links so you can check each claim on the blockchain yourself.

The short version: the code can't be changed, member money never sits in the contract, and the whole thing keeps running on its own — even if its creators disappear.

See How the Contract Works →
+ +
Ready to start?

See the current team placement.

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

Open Getting Started Instructions →
+
+ diff --git a/public/join.html b/public/join.html index 0a2e425..c7a2f8b 100644 --- a/public/join.html +++ b/public/join.html @@ -1,30 +1,30 @@ -You're Invited | Crypto Team Build - - - - -
- -
Personal invitation

You've been invited by
Member #—.

This page is their real position on the team — every number on it is read live from the blockchain.

-

Your sponsor's position Verified on-chain

Reading the blockchain…
- -
Independent team training resource • Participation involves risk • No income is guaranteed
- -
Watch first

See how simple this is.

A quick walkthrough of the team build in action. Watch this before you dive into the strategy below.

- -
The strategy

Simple enough to duplicate.

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.

2

Get exactly two

Use your position's link until two direct positions are placed beneath you and your position is qualified.

↘

Move the effort down

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.

↑

Upgrade responsibly

Use earned POL to advance when practical. Active builders may also choose to self-fund, but only within their own risk tolerance.

- -
Depth over width

2 → 4 → 8 → 16 → 32

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

2
4
8
16
Team principle: once your two are in place, retire your link and shift to helping them get their 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.
- -
Live payment proof

Real payouts, straight from the blockchain.

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

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

Join under Member #—.

Your invite carries your sponsor automatically — no codes to remember. Here's the whole process, start to finish.

-
1

Install or open MetaMask

Use the official MetaMask website or your device's official app store. Never install a wallet from a link sent by a stranger.

-
2

Use Polygon Mainnet, funded with POL

Premium entry is 362 POL plus a little extra for network gas. Polygon Mainnet is chain ID 137. New to wallets? The training videos walk through every step.

-
3

Join with your sponsor's link

The button below opens the official RM Circle dApp with Member #— already set as your sponsor. Confirm the sponsor ID matches before signing, and never share your Secret Recovery Phrase with anyone.

Join Under #— →
-
4

Submit your NEW RM Circle ID here

After your purchase confirms, the RM Circle dApp shows your new member ID. Type it below — we verify it on the blockchain, your sponsor gets notified, and the team rotation starts working on your 2.

-
What happens next: 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.
-
Risk reminder: participation involves cryptocurrency and smart-contract risk. No income is guaranteed. Use only funds you can afford to lose.
- -
- +You're Invited | Crypto Team Build + + + + +
+ +
Personal invitation

You've been invited by
Member #—.

This page is their real position on the team — every number on it is read live from the blockchain.

+

Your sponsor's position Verified on-chain

Reading the blockchain…
+ +
Independent team training resource • Participation involves risk • No income is guaranteed
+ +
Watch first

See how simple this is.

A quick walkthrough of the team build in action. Watch this before you dive into the strategy below.

+ +
The strategy

Simple enough to duplicate.

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.

2

Get exactly two

Use your position's link until two direct positions are placed beneath you and your position is qualified.

↘

Move the effort down

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.

↑

Upgrade responsibly

Use earned POL to advance when practical. Active builders may also choose to self-fund, but only within their own risk tolerance.

+ +
Depth over width

2 → 4 → 8 → 16 → 32

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

2
4
8
16
Team principle: once your two are in place, retire your link and shift to helping them get their 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.
+ +
Live payment proof

Real payouts, straight from the blockchain.

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

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

Join under Member #—.

Your invite carries your sponsor automatically — no codes to remember. Here's the whole process, start to finish.

+
1

Install or open MetaMask

Use the official MetaMask website or your device's official app store. Never install a wallet from a link sent by a stranger.

+
2

Use Polygon Mainnet, funded with POL

Premium entry is 362 POL plus a little extra for network gas. Polygon Mainnet is chain ID 137. New to wallets? The training videos walk through every step.

+
3

Join with your sponsor's link

The button below opens the official RM Circle dApp with Member #— already set as your sponsor. Confirm the sponsor ID matches before signing, and never share your Secret Recovery Phrase with anyone.

Join Under #— →
+
4

Submit your NEW RM Circle ID here

After your purchase confirms, the RM Circle dApp shows your new member ID. Type it below — we verify it on the blockchain, your sponsor gets notified, and the team rotation starts working on your 2.

+
What happens next: 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.
+
Risk reminder: participation involves cryptocurrency and smart-contract risk. No income is guaranteed. Use only funds you can afford to lose.
+ +
+ diff --git a/public/my.js b/public/my.js index 439b2f6..e851edc 100644 --- a/public/my.js +++ b/public/my.js @@ -202,7 +202,7 @@ else if(gap) L.push(`YOUR NEXT MOVE: your next uncovered layer is layer ${gap} — to catch their ${LV[gap]||'top'} upgrades you'll need to be at ${LV[gap-1]} (${myNext?money(myNext)+' POL':''}). They're still climbing toward it${gapClose?', and getting close — worth upgrading soon':", so no rush — just get there before they do"}. You're already eligible for everything shallower.`); else if(myNext) L.push(`YOUR NEXT MOVE: you're currently eligible for every active layer. As your team goes deeper, upgrade to ${nextName} (${money(myNext)} POL) before that new layer climbs to its pay-you level.`); L.push(''); - 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.team/my/${d.id}`); return L.join('\n'); } async function renderAlerts(d){ @@ -236,11 +236,11 @@ function renderShare(d){ const el=document.getElementById('dShare'); if(!el)return; - const dashUrl=`https://rmcircle.saasy.top/join/${d.id}`; + const dashUrl=`https://rmcircle.team/join/${d.id}`; if(d.directCount>=2){ const nx=d.nextInLine; if(nx){ - const nxDash=`https://rmcircle.saasy.top/join/${nx.id}`; + const nxDash=`https://rmcircle.team/join/${nx.id}`; el.innerHTML=`

★ Qualified The team play now moves down. Next in line: #${nx.id} (${nx.directCount}/2 directs). Help them get their 2 — share their invite page (the full team-build pitch, personalized to them) or send prospects the join button, which carries #${nx.id} as sponsor. (If someone still joins through your own link, that's a bonus — the entry reward is yours and they spill down your leg — but only #${nx.id}'s own directs can qualify them.)

Join under #${nx.id} →

${esc(nxDash)}

When #${nx.id} reaches 2/2 this rotates to the next position in your leg — the qualification wave keeps moving down. General traffic can still go through the team rotation.

`; const qr=document.getElementById('dQr');if(qr)qr.innerHTML=qrSvg(nxDash); const cp=document.getElementById('copyShare'); diff --git a/public/start.html b/public/start.html index 2f0dfb8..5aee472 100644 --- a/public/start.html +++ b/public/start.html @@ -1,19 +1,19 @@ -Get Started | Crypto Team Build - - - - - -
Current placement

Use the sponsor shown on this page.

Team placements rotate as positions become qualified. Do not use an old screenshot or saved referral link.

- -
RM Circle Premium Team Build Roadmap — core strategy, step-by-step guide, premium levels, and duplication formula
The RM Circle is a team build project of the Crypto Team Build Network. This roadmap is the plan every member follows — tap to view full size.
- - +Get Started | Crypto Team Build + + + + + +
Current placement

Use the sponsor shown on this page.

Team placements rotate as positions become qualified. Do not use an old screenshot or saved referral link.

+ +
RM Circle Premium Team Build Roadmap — core strategy, step-by-step guide, premium levels, and duplication formula
The RM Circle is a team build project of the Crypto Team Build Network. This roadmap is the plan every member follows — tap to view full size.
+ + diff --git a/public/training.html b/public/training.html index 344e0c0..fa088b2 100644 --- a/public/training.html +++ b/public/training.html @@ -1,30 +1,30 @@ -Training | Crypto Team Build - - - - - - -
-
Team training

Watch. Learn. Duplicate.

Four short videos take you from zero to your Premium position — in order. Watch them all before you join, and share this page with your two directs when it's their turn.

- -
Video 1 · 14 min

How the team build works

The full picture: the 2-direct strategy, qualification, the moving link, and how depth pays the team.

- -
Video 2 · 3 min

Create your MetaMask wallet

Install MetaMask safely and set up your wallet. Never share your Secret Recovery Phrase with anyone.

- -
Video 3 · 3 min

Funding your wallet

Get POL onto Polygon Mainnet — enough for the 362 POL Premium entry plus a little extra for gas.

- -
Video 4 · 5 min

Buying the Premium position

Connect to the RM Circle dApp with the current team sponsor and complete your Premium entry.

- -
Must-know · 3 min read

Understanding spillover — and getting paid instead of passed

-

What spillover is. When a sponsor already has both matrix slots filled, the smart contract places the next new member in the first open slot further down the team — left to right, generation by generation. That member "spills over" into someone else's matrix. If your upline is recruiting, people can land under you that you never spoke to.

-

Why you can have people under you and still not be qualified. Qualification counts your directs — people who joined using your ID — not who sits in your matrix. Spillover fills your structure, but only your own two recruits qualify your position. Your Member Dashboard tags every spillover placement so you can see the difference at a glance.

-

Why spillover is still money in your future. Upgrade payments travel up the matrix, not the referral chain. When a member below you — spillover or not — buys a level, that payment stops at the first upline position that is qualified (2 directs) and already at that level. If that's you, you get paid by someone you never recruited. If you're missing a direct or a level, the payment passes you by and lands with the next eligible person above you — and you can watch that happen, on the blockchain, in real time.

-

What if someone joins through my link after I'm qualified? No harm done — it's a bonus. The team's recommendation is to retire your link at 2/2 and put your effort into helping your two, but the contract never punishes a late signup: it still pays your position the full entry reward, and the new member spills downward into the first open slot in your leg, adding depth. The member whose slot gets filled earns nothing at that moment (the entry reward came to you); their income starts when that spilled member upgrades — and only if they're qualified and at the required level to catch it. So a stray extra referral is genuinely good news — it just can never do your downline's most important job for them: only their own directs qualify their positions.

-

The takeaway: spillover is free potential income sitting under you — and qualification plus staying upgraded is how you claim it. Get your 2, keep your level ahead of your team's wave, help your people get their own 2, and the matrix pays your position as it grows.

-
Visual: see exactly how the money reaches your position — the pay-flow diagram →
Ready? Head to the Getting Started page to see the current team sponsor before you buy — placements rotate as positions qualify.
-
Risk reminder: participation involves cryptocurrency and smart-contract risk. No income is guaranteed. Use only funds you can afford to lose.
-
-
- - +Training | Crypto Team Build + + + + + + +
+
Team training

Watch. Learn. Duplicate.

Four short videos take you from zero to your Premium position — in order. Watch them all before you join, and share this page with your two directs when it's their turn.

+ +
Video 1 · 14 min

How the team build works

The full picture: the 2-direct strategy, qualification, the moving link, and how depth pays the team.

+ +
Video 2 · 3 min

Create your MetaMask wallet

Install MetaMask safely and set up your wallet. Never share your Secret Recovery Phrase with anyone.

+ +
Video 3 · 3 min

Funding your wallet

Get POL onto Polygon Mainnet — enough for the 362 POL Premium entry plus a little extra for gas.

+ +
Video 4 · 5 min

Buying the Premium position

Connect to the RM Circle dApp with the current team sponsor and complete your Premium entry.

+ +
Must-know · 3 min read

Understanding spillover — and getting paid instead of passed

+

What spillover is. When a sponsor already has both matrix slots filled, the smart contract places the next new member in the first open slot further down the team — left to right, generation by generation. That member "spills over" into someone else's matrix. If your upline is recruiting, people can land under you that you never spoke to.

+

Why you can have people under you and still not be qualified. Qualification counts your directs — people who joined using your ID — not who sits in your matrix. Spillover fills your structure, but only your own two recruits qualify your position. Your Member Dashboard tags every spillover placement so you can see the difference at a glance.

+

Why spillover is still money in your future. Upgrade payments travel up the matrix, not the referral chain. When a member below you — spillover or not — buys a level, that payment stops at the first upline position that is qualified (2 directs) and already at that level. If that's you, you get paid by someone you never recruited. If you're missing a direct or a level, the payment passes you by and lands with the next eligible person above you — and you can watch that happen, on the blockchain, in real time.

+

What if someone joins through my link after I'm qualified? No harm done — it's a bonus. The team's recommendation is to retire your link at 2/2 and put your effort into helping your two, but the contract never punishes a late signup: it still pays your position the full entry reward, and the new member spills downward into the first open slot in your leg, adding depth. The member whose slot gets filled earns nothing at that moment (the entry reward came to you); their income starts when that spilled member upgrades — and only if they're qualified and at the required level to catch it. So a stray extra referral is genuinely good news — it just can never do your downline's most important job for them: only their own directs qualify their positions.

+

The takeaway: spillover is free potential income sitting under you — and qualification plus staying upgraded is how you claim it. Get your 2, keep your level ahead of your team's wave, help your people get their own 2, and the matrix pays your position as it grows.

+
Visual: see exactly how the money reaches your position — the pay-flow diagram →
Ready? Head to the Getting Started page to see the current team sponsor before you buy — placements rotate as positions qualify.
+
Risk reminder: participation involves cryptocurrency and smart-contract risk. No income is guaranteed. Use only funds you can afford to lose.
+
+
+ + diff --git a/server.js b/server.js index 649dcf2..a3f4ab1 100644 --- a/server.js +++ b/server.js @@ -1,692 +1,692 @@ -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 tweet = require('./tweet'); - -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 -- PAY FLOW (full diagram at https://rmcircle.saasy.top/how-pay-works): TWO income streams. (1) Entry rewards — when a direct joins under a member's link, that member gets the entry reward (~326 POL at Premium) and keeps it, on EVERY direct (not just the first two). (2) Upgrade payments — each person below you pays you ONCE, at the single level matching how far below you they sit: someone 1 layer down pays you at their Ascensus upgrade, 2 layers down at Fabrica, 3 at Culmen, 4 at Apex, and so on. To catch each one you must be at that level yourself AND qualified — so stay one level ahead of your team's deepest active layer. This is why a member can be "skipped" on a payment: it was reserved for the position at the matching depth, or they weren't leveled up in time. -- TIERS: Premium (the tier the whole team builds at, ${c.premiumEntryPol || 362} POL entry) pays and earns FULL amounts; Standard costs about half and pays/earns HALF at every level. A smaller-than-expected payment almost always came from a Standard-tier position below. Tier is fixed at registration and CANNOT be changed later (upgrading advances your LEVEL, not your tier). Always recommend joining Premium and having recruits do the same. Amount comparison at /how-pay-works. -- MEMBER DASHBOARD & ALERTS: each member has a live dashboard at https://rmcircle.saasy.top/my (enter your ID) showing position, team, payments, pipeline (incoming money forming below), spillover tags, and qualification badges. Members can turn on opt-in EMAIL ALERTS there (notified when paid, and when they need to upgrade to catch incoming pay). A member's personal invite page to share is https://rmcircle.saasy.top/join/. -- RESILIENCE ("what if the creators disappear / owner loses keys / it falls apart over time"): the contract is autonomous and immutable — NO admin action, heartbeat, or living operator is required for joins, upgrades, matrix placement, or payouts; there is no pause switch and no expiry. Verified on-chain that the founder, development, and fee-receiver wallets are ordinary wallets (EOAs), NOT smart contracts — an ordinary wallet always accepts incoming POL even if its key is lost forever, so a dead or abandoned admin wallet cannot block any member payment (only the project's OWN uncollected fee would sit idle). The contract stores no balance (every payment is delivered in the same transaction). If the owner's key were lost, only the four limited admin powers freeze in place; members are unaffected. Details in section 6 of https://rmcircle.saasy.top/contract. -- 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 + live team stats), https://rmcircle.saasy.top/start (current sponsor + join steps), https://rmcircle.saasy.top/training (4 videos + spillover article), https://rmcircle.saasy.top/how-pay-works (the two income streams shown as a pay-flow diagram + Premium/Standard tier comparison), 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), https://rmcircle.saasy.top/my (member dashboard), https://rmcircle.saasy.top/disclaimer (affiliate/earnings/risk disclosures). -- 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; -} -// 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(); -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 '; } -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, unsub) { - 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 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); -} -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: a rotation join is one whose on-chain referrer is - // a rotation-queue sponsor that IS or HAS BEEN worked (status active or - // qualified) — not just the currently-active one. With auto-advance, the direct - // who completes a sponsor's 2/2 (and thus joined under it) submits their ID - // AFTER the rotation has already moved on, so keying off "active only" wrongly - // labeled them a leg join. A referrer that's a still-waiting queue position, or - // not in the queue at all, is a personal leg join. - const sponsorsNow = getSponsors(); - const active = activeSponsor(sponsorsNow); - let joinPath = 'unknown'; - if (onchain && onchain.registered) { - const refSp = sponsorsNow.find(s => String(s.id) === String(onchain.referrerId)); - joinPath = (refSp && (refSp.status === 'active' || refSp.status === 'qualified')) ? '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(onchain.referrerId), 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 under #${onchain.referrerId} ${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(Sponsor #${onchain.referrerId}'s direct count 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' 'unsafe-inline'; 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{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'){ - const q=new URL(req.url,'http://x').searchParams; - const offset=Number(q.get('offset')||0); - const limit=Number(q.get('limit')||40); - return json(res,200,chain.getPayoutsPublic(offset,limit),{'Cache-Control':'public, max-age=20'}); - } - if(req.method==='GET'&&pathname==='/api/public/org-stats'){ - const root=Number(getConfig().orgRootId||21); - const d=chain.getOrgShare(Number.isInteger(root)&&root>0?root:21); - // expose only the aggregate showcase numbers (all public on-chain data) - return json(res,200,{ready:d.ready,found:!!d.found,memberPct:d.memberPct,orgMembers:d.orgMembers,generations:d.generations,orgPol:d.orgPol,totalMembers:d.totalMembers},{'Cache-Control':'public, max-age=60'}); - } - 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'){ - 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 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}); - } - 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/org-share'){ - const raw=new URL(req.url,'http://x').searchParams.get('root'); - const root=Number(raw||parseOwnerIds()[0]||21); - if(!Number.isInteger(root)||root<1||root>281474976710655)return json(res,400,{error:'Enter a numeric root ID.'}); - return json(res,200,chain.getOrgShare(root)); - } - if(req.method==='GET'&&pathname==='/api/admin/income'){ - const raw=new URL(req.url,'http://x').searchParams.get('ids')||''; - const ids=[...new Set(raw.split(',').map(s=>parseInt(String(s).trim(),10)).filter(n=>Number.isInteger(n)&&n>0&&n<=281474976710655))].slice(0,12); - if(!ids.length)return json(res,400,{error:'Enter one or more numeric IDs (comma-separated).'}); - try{ - const results=await Promise.race([ - Promise.all(ids.map(id=>chain.getIncome(id).catch(()=>({registered:false,id})))), - new Promise((_,rej)=>setTimeout(()=>rej(new Error('Blockchain lookup timed out — try again.')),25000)) - ]); - const rows=[],perId={};let grand=0,grandListed=0; - for(const r of results){ - if(!r.registered){perId[r.id]={registered:false};continue;} - perId[r.id]={registered:true,levelName:r.levelName,tierName:r.tierName,totalEarnedPol:r.totalEarnedPol,count:r.income.length}; - grand+=r.totalEarnedPol; - for(const p of r.income){rows.push({toId:r.id,fromId:p.fromId,pol:p.pol,ts:p.ts,desc:p.desc});grandListed+=p.pol;} - } - rows.sort((a,b)=>(b.ts||0)-(a.ts||0)); - let upgradeNeeds=[];try{upgradeNeeds=chain.getOwnerUpgradeNeeds(ids).needs;}catch(e){} - let routing=null;try{routing=chain.getOrgRouting(Number(getConfig().orgRootId)||21,ids);}catch(e){} - return json(res,200,{ids,perId,rows:rows.slice(0,500),grandEarnedPol:+grand.toFixed(2),grandListedPol:+grandListed.toFixed(2),upgradeNeeds,routing}); - }catch(e){return json(res,502,{error:e.message||'Lookup failed'})} - } - 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','ownerIds','ownerAlertEmail','orgRootId','tweetEnabled','tweetCtaUrl','tweetHashtags','blotatoTwitterId'])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`, - ``, - ``, - ``, - ``, - ``, - `` - ].join(''); - html=html.replace(/]*>/gi,''); - html=html.replace(/(]*>)/i,`$1${og}`); - res.writeHead(200,securityHeaders({'Content-Type':'text/html; charset=utf-8','Cache-Control':'no-cache'})); - res.end(html); -} -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'}); - 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=`${ok?'Unsubscribed':'Invalid link'}`; - res.writeHead(200,securityHeaders({'Content-Type':'text/html; charset=utf-8','Cache-Control':'no-store'}));return res.end(body); - } - { let mj; - if((mj=pathname.match(/^\/my\/(\d{1,15})$/)))return serveMemberPage(req,res,path.join(PUBLIC_DIR,'my.html'),'my',mj[1]); - if((mj=pathname.match(/^\/join\/(\d{1,15})$/)))return serveMemberPage(req,res,path.join(PUBLIC_DIR,'join.html'),'join',mj[1]); - } - 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(pathname==='/disclaimer'||pathname==='/disclaimer/')file=path.join(PUBLIC_DIR,'disclaimer.html');else if(pathname==='/how-pay-works'||pathname==='/how-pay-works/')file=path.join(PUBLIC_DIR,'how-pay-works.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. -// Owner upgrade watcher: emails/Telegrams when an owned position (config.ownerIds) -// has a payment about to arrive it can't catch yet, so Marty can upgrade in time. -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 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); } -// 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{ - 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; - const res=chain.getOwnerUpgradeNeeds(ids); - if(!res.ready) return; - const alerted=loadOwnerAlerts(); const active=new Set(); const tgDone=new Set(); - for(const n of res.needs){ - const who=n.members.map(m=>'#'+m).join(', '); - 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.`; - for(const w of (watch[n.id]||[])){ - const key=`${w.email}:${n.id}:${n.reason}:${n.neededLevel}`; active.add(key); - if(alerted.has(key)) continue; alerted.add(key); - 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`, - `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.`); } - } - } - let changed=false; - for(const k of [...alerted]) if(!active.has(k)){ alerted.delete(k); changed=true; } - if(changed||active.size) writeJson(OWNER_ALERTS_FILE,[...alerted]); - }catch(e){ console.error('upgrade alert check', e.message); } -} -setInterval(checkUpgradeAlerts, 5*60*1000).unref(); -setTimeout(checkUpgradeAlerts, 30000).unref(); -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'?` (${evt.gen?`Gen ${evt.gen} `:''}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 — sponsor-record contact, and self-service subscribers - try{ - if(evt.type==='payout'){ - const sent=new Set(); - const sp=getSponsors().find(x=>String(x.id)===String(evt.toId)); - 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)} - // Auto-tweet on-chain payout proof to @cryptoteambuild via Blotato (Marty - // 2026-08-15). Org-gated to the #21 organization; OFF unless config.tweetEnabled. - try{ - if(evt.type==='payout'){ - const orgRoot=Number(getConfig().orgRootId)||21; - if(chain.isInTeam(evt.toId,orgRoot))tweet.queuePayoutTweet(evt,getConfig()); - } - }catch(e){console.error('tweet hook error',e.message)} - // Auto-count directs + AUTO-ADVANCE (Marty 2026-08-15): a new registration - // whose referrer sits in the rotation queue increments that sponsor's directs. - // When it reaches 2/2 the sponsor is auto-qualified and the next waiting - // position activates — no manual Qualify click (changed from the earlier - // manual design so the rotation never sits stuck at a 2/2 sponsor). - try{ - if(evt.type==='registered'&&evt.referrerId!=null){ - let 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){ - const newDirects=Math.min(2,(Number(sponsors[idx].directs)||0)+1); - const s=sponsors[idx]; - if(newDirects>=2){ - sponsors[idx]={...sponsors[idx],directs:2,status:'qualified'}; - sponsors=normalizeStatuses(sponsors); - saveSponsors(sponsors); - const next=activeSponsor(sponsors); - sendTelegram(`✅ AUTO-ADVANCE: queue sponsor #${s.id}${s.name?` (${s.name})`:''} reached 2/2 and is now QUALIFIED.\nRotation moved to ${next?`#${next.id}${next.name?` (${next.name})`:''}`:'— nobody waiting (add the next position to the queue)'}.`); - }else{ - sponsors[idx].directs=newDirects; - saveSponsors(sponsors); - sendTelegram(`🤖 AUTO-COUNT: #${evt.id} is a DIRECT for queue sponsor #${s.id}${s.name?` (${s.name})`:''} — now ${newDirects}/2.`); - } - } - } - }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 tweet = require('./tweet'); + +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.team), 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.team/my +- PAY FLOW (full diagram at https://rmcircle.team/how-pay-works): TWO income streams. (1) Entry rewards — when a direct joins under a member's link, that member gets the entry reward (~326 POL at Premium) and keeps it, on EVERY direct (not just the first two). (2) Upgrade payments — each person below you pays you ONCE, at the single level matching how far below you they sit: someone 1 layer down pays you at their Ascensus upgrade, 2 layers down at Fabrica, 3 at Culmen, 4 at Apex, and so on. To catch each one you must be at that level yourself AND qualified — so stay one level ahead of your team's deepest active layer. This is why a member can be "skipped" on a payment: it was reserved for the position at the matching depth, or they weren't leveled up in time. +- TIERS: Premium (the tier the whole team builds at, ${c.premiumEntryPol || 362} POL entry) pays and earns FULL amounts; Standard costs about half and pays/earns HALF at every level. A smaller-than-expected payment almost always came from a Standard-tier position below. Tier is fixed at registration and CANNOT be changed later (upgrading advances your LEVEL, not your tier). Always recommend joining Premium and having recruits do the same. Amount comparison at /how-pay-works. +- MEMBER DASHBOARD & ALERTS: each member has a live dashboard at https://rmcircle.team/my (enter your ID) showing position, team, payments, pipeline (incoming money forming below), spillover tags, and qualification badges. Members can turn on opt-in EMAIL ALERTS there (notified when paid, and when they need to upgrade to catch incoming pay). A member's personal invite page to share is https://rmcircle.team/join/. +- RESILIENCE ("what if the creators disappear / owner loses keys / it falls apart over time"): the contract is autonomous and immutable — NO admin action, heartbeat, or living operator is required for joins, upgrades, matrix placement, or payouts; there is no pause switch and no expiry. Verified on-chain that the founder, development, and fee-receiver wallets are ordinary wallets (EOAs), NOT smart contracts — an ordinary wallet always accepts incoming POL even if its key is lost forever, so a dead or abandoned admin wallet cannot block any member payment (only the project's OWN uncollected fee would sit idle). The contract stores no balance (every payment is delivered in the same transaction). If the owner's key were lost, only the four limited admin powers freeze in place; members are unaffected. Details in section 6 of https://rmcircle.team/contract. +- 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.team/start right before joining. +- Site pages: https://rmcircle.team/ (strategy overview + roadmap + live team stats), https://rmcircle.team/start (current sponsor + join steps), https://rmcircle.team/training (4 videos + spillover article), https://rmcircle.team/how-pay-works (the two income streams shown as a pay-flow diagram + Premium/Standard tier comparison), https://rmcircle.team/contract (plain-language security review of the verified smart contract — code can't change, no pooled funds, locked rules, honest list of operator powers), https://rmcircle.team/my (member dashboard), https://rmcircle.team/disclaimer (affiliate/earnings/risk disclosures). +- 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; +} +// 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(); +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 '; } +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, unsub) { + 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 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.team/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); +} +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: a rotation join is one whose on-chain referrer is + // a rotation-queue sponsor that IS or HAS BEEN worked (status active or + // qualified) — not just the currently-active one. With auto-advance, the direct + // who completes a sponsor's 2/2 (and thus joined under it) submits their ID + // AFTER the rotation has already moved on, so keying off "active only" wrongly + // labeled them a leg join. A referrer that's a still-waiting queue position, or + // not in the queue at all, is a personal leg join. + const sponsorsNow = getSponsors(); + const active = activeSponsor(sponsorsNow); + let joinPath = 'unknown'; + if (onchain && onchain.registered) { + const refSp = sponsorsNow.find(s => String(s.id) === String(onchain.referrerId)); + joinPath = (refSp && (refSp.status === 'active' || refSp.status === 'qualified')) ? '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(onchain.referrerId), 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 under #${onchain.referrerId} ${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(Sponsor #${onchain.referrerId}'s direct count 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.team', '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' 'unsafe-inline'; 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{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'){ + const q=new URL(req.url,'http://x').searchParams; + const offset=Number(q.get('offset')||0); + const limit=Number(q.get('limit')||40); + return json(res,200,chain.getPayoutsPublic(offset,limit),{'Cache-Control':'public, max-age=20'}); + } + if(req.method==='GET'&&pathname==='/api/public/org-stats'){ + const root=Number(getConfig().orgRootId||21); + const d=chain.getOrgShare(Number.isInteger(root)&&root>0?root:21); + // expose only the aggregate showcase numbers (all public on-chain data) + return json(res,200,{ready:d.ready,found:!!d.found,memberPct:d.memberPct,orgMembers:d.orgMembers,generations:d.generations,orgPol:d.orgPol,totalMembers:d.totalMembers},{'Cache-Control':'public, max-age=60'}); + } + 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.team/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'){ + 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 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}); + } + 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/org-share'){ + const raw=new URL(req.url,'http://x').searchParams.get('root'); + const root=Number(raw||parseOwnerIds()[0]||21); + if(!Number.isInteger(root)||root<1||root>281474976710655)return json(res,400,{error:'Enter a numeric root ID.'}); + return json(res,200,chain.getOrgShare(root)); + } + if(req.method==='GET'&&pathname==='/api/admin/income'){ + const raw=new URL(req.url,'http://x').searchParams.get('ids')||''; + const ids=[...new Set(raw.split(',').map(s=>parseInt(String(s).trim(),10)).filter(n=>Number.isInteger(n)&&n>0&&n<=281474976710655))].slice(0,12); + if(!ids.length)return json(res,400,{error:'Enter one or more numeric IDs (comma-separated).'}); + try{ + const results=await Promise.race([ + Promise.all(ids.map(id=>chain.getIncome(id).catch(()=>({registered:false,id})))), + new Promise((_,rej)=>setTimeout(()=>rej(new Error('Blockchain lookup timed out — try again.')),25000)) + ]); + const rows=[],perId={};let grand=0,grandListed=0; + for(const r of results){ + if(!r.registered){perId[r.id]={registered:false};continue;} + perId[r.id]={registered:true,levelName:r.levelName,tierName:r.tierName,totalEarnedPol:r.totalEarnedPol,count:r.income.length}; + grand+=r.totalEarnedPol; + for(const p of r.income){rows.push({toId:r.id,fromId:p.fromId,pol:p.pol,ts:p.ts,desc:p.desc});grandListed+=p.pol;} + } + rows.sort((a,b)=>(b.ts||0)-(a.ts||0)); + let upgradeNeeds=[];try{upgradeNeeds=chain.getOwnerUpgradeNeeds(ids).needs;}catch(e){} + let routing=null;try{routing=chain.getOrgRouting(Number(getConfig().orgRootId)||21,ids);}catch(e){} + return json(res,200,{ids,perId,rows:rows.slice(0,500),grandEarnedPol:+grand.toFixed(2),grandListedPol:+grandListed.toFixed(2),upgradeNeeds,routing}); + }catch(e){return json(res,502,{error:e.message||'Lookup failed'})} + } + 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','ownerIds','ownerAlertEmail','orgRootId','tweetEnabled','tweetCtaUrl','tweetHashtags','blotatoTwitterId'])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`, + ``, + ``, + ``, + ``, + ``, + `` + ].join(''); + html=html.replace(/]*>/gi,''); + html=html.replace(/(]*>)/i,`$1${og}`); + res.writeHead(200,securityHeaders({'Content-Type':'text/html; charset=utf-8','Cache-Control':'no-cache'})); + res.end(html); +} +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'}); + 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=`${ok?'Unsubscribed':'Invalid link'}`; + res.writeHead(200,securityHeaders({'Content-Type':'text/html; charset=utf-8','Cache-Control':'no-store'}));return res.end(body); + } + { let mj; + if((mj=pathname.match(/^\/my\/(\d{1,15})$/)))return serveMemberPage(req,res,path.join(PUBLIC_DIR,'my.html'),'my',mj[1]); + if((mj=pathname.match(/^\/join\/(\d{1,15})$/)))return serveMemberPage(req,res,path.join(PUBLIC_DIR,'join.html'),'join',mj[1]); + } + 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(pathname==='/disclaimer'||pathname==='/disclaimer/')file=path.join(PUBLIC_DIR,'disclaimer.html');else if(pathname==='/how-pay-works'||pathname==='/how-pay-works/')file=path.join(PUBLIC_DIR,'how-pay-works.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. +// Owner upgrade watcher: emails/Telegrams when an owned position (config.ownerIds) +// has a payment about to arrive it can't catch yet, so Marty can upgrade in time. +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 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); } +// 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.team/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{ + 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; + const res=chain.getOwnerUpgradeNeeds(ids); + if(!res.ready) return; + const alerted=loadOwnerAlerts(); const active=new Set(); const tgDone=new Set(); + for(const n of res.needs){ + const who=n.members.map(m=>'#'+m).join(', '); + 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.`; + for(const w of (watch[n.id]||[])){ + const key=`${w.email}:${n.id}:${n.reason}:${n.neededLevel}`; active.add(key); + if(alerted.has(key)) continue; alerted.add(key); + 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`, + `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.`); } + } + } + let changed=false; + for(const k of [...alerted]) if(!active.has(k)){ alerted.delete(k); changed=true; } + if(changed||active.size) writeJson(OWNER_ALERTS_FILE,[...alerted]); + }catch(e){ console.error('upgrade alert check', e.message); } +} +setInterval(checkUpgradeAlerts, 5*60*1000).unref(); +setTimeout(checkUpgradeAlerts, 30000).unref(); +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'?` (${evt.gen?`Gen ${evt.gen} `:''}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 — sponsor-record contact, and self-service subscribers + try{ + if(evt.type==='payout'){ + const sent=new Set(); + const sp=getSponsors().find(x=>String(x.id)===String(evt.toId)); + 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)} + // Auto-tweet on-chain payout proof to @cryptoteambuild via Blotato (Marty + // 2026-08-15). Org-gated to the #21 organization; OFF unless config.tweetEnabled. + try{ + if(evt.type==='payout'){ + const orgRoot=Number(getConfig().orgRootId)||21; + if(chain.isInTeam(evt.toId,orgRoot))tweet.queuePayoutTweet(evt,getConfig()); + } + }catch(e){console.error('tweet hook error',e.message)} + // Auto-count directs + AUTO-ADVANCE (Marty 2026-08-15): a new registration + // whose referrer sits in the rotation queue increments that sponsor's directs. + // When it reaches 2/2 the sponsor is auto-qualified and the next waiting + // position activates — no manual Qualify click (changed from the earlier + // manual design so the rotation never sits stuck at a 2/2 sponsor). + try{ + if(evt.type==='registered'&&evt.referrerId!=null){ + let 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){ + const newDirects=Math.min(2,(Number(sponsors[idx].directs)||0)+1); + const s=sponsors[idx]; + if(newDirects>=2){ + sponsors[idx]={...sponsors[idx],directs:2,status:'qualified'}; + sponsors=normalizeStatuses(sponsors); + saveSponsors(sponsors); + const next=activeSponsor(sponsors); + sendTelegram(`✅ AUTO-ADVANCE: queue sponsor #${s.id}${s.name?` (${s.name})`:''} reached 2/2 and is now QUALIFIED.\nRotation moved to ${next?`#${next.id}${next.name?` (${next.name})`:''}`:'— nobody waiting (add the next position to the queue)'}.`); + }else{ + sponsors[idx].directs=newDirects; + saveSponsors(sponsors); + sendTelegram(`🤖 AUTO-COUNT: #${evt.id} is a DIRECT for queue sponsor #${s.id}${s.name?` (${s.name})`:''} — now ${newDirects}/2.`); + } + } + } + }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)} +});