Files
rm-circle-team-router/server.js
T
martbost 6b17419646 Add sponsor contact emails and Telegram team-build activity alerts
Sponsors can carry an optional contact email (Add Sponsor form, ✉ inline
edit in the queue, shown under the name). New config teamRootId: any NEW
on-chain event at or below that member ID — registration, upgrade, or
payout seen by the live tail — posts to the Telegram group topic, with
the tx link and the sponsor's contact email when one is on file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 12:57:51 -05:00

319 lines
27 KiB
JavaScript

const http = require('http');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { URL } = require('url');
const chain = require('./chain');
const PORT = Number(process.env.PORT || 3000);
const ROOT = __dirname;
const PUBLIC_DIR = path.join(ROOT, 'public');
const DATA_DIR = process.env.DATA_DIR || path.join(ROOT, 'data');
const SEED_DIR = path.join(ROOT, 'seed');
const SPONSORS_FILE = path.join(DATA_DIR, 'sponsors.json');
const CONFIG_FILE = path.join(DATA_DIR, 'config.json');
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'changeme';
const IS_PROD = process.env.NODE_ENV === 'production';
const SESSION_TTL = 8 * 60 * 60 * 1000;
const LEVELS = ['Scintilla','Ascensus','Fabrica','Culmen','Apex','Fastigium','Vertex','Corona'];
const OPENROUTER_MODEL = process.env.OPENROUTER_MODEL || 'deepseek/deepseek-v4-flash:nitro';
const OPENROUTER_KEY_FILE = path.join(DATA_DIR, 'openrouter.key');
function getOpenRouterKey() {
if (process.env.OPENROUTER_API_KEY) return process.env.OPENROUTER_API_KEY;
try { return fs.readFileSync(OPENROUTER_KEY_FILE, 'utf8').trim(); } catch (e) { return ''; }
}
const chatHits = new Map();
function chatRateLimited(ip) {
const now = Date.now(), rec = chatHits.get(ip);
if (!rec || now > rec.reset) { chatHits.set(ip, { count: 1, reset: now + 60000 }); return false; }
rec.count++; return rec.count > 10;
}
function chatSystemPrompt() {
const c = getConfig(), sponsors = getSponsors(), a = activeSponsor(sponsors);
const waiting = sponsors.filter(s=>s.status==='waiting').length;
return `You are "Team Help", the assistant on ${c.siteName || 'Crypto Team Build'} (https://rmcircle.saasy.top), the team site for the RM Circle Premium team build — a project of the Crypto Team Build Network.
FACTS:
- Strategy: enter RM Circle dApp at Premium tier (${c.premiumEntryPol || 362} POL on Polygon Mainnet, chain ID 137, POL is the gas token). Each member gets EXACTLY 2 directs, then the position is "qualified" and its referral link is retired. The team then helps those 2 get their 2 (moving-link strategy). Build depth, not width. Never add extra directs to a qualified link.
- 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.
- Current team sponsor: ${a ? `ID ${a.id}${c.showSponsorName && a.name ? ` (${a.name})` : ''}, ${a.directs}/2 directs` : 'shown on the start page'}. ${waiting} placement(s) waiting. Placements rotate as positions qualify — always verify on https://rmcircle.saasy.top/start right before joining.
- Site pages: https://rmcircle.saasy.top/ (strategy overview + roadmap), https://rmcircle.saasy.top/start (current sponsor + join steps), https://rmcircle.saasy.top/training (4 videos: 1. How the team build works, 2. Create your MetaMask wallet, 3. Funding your wallet, 4. Buying the Premium position).
- Telegram group for live team help: ${c.telegramUrl || 'https://t.me/cryptoteambuild'}
RULES:
- Keep answers short: 1-4 sentences, plain text, no markdown formatting. Include full URLs when pointing to a page.
- NEVER promise, estimate, or imply earnings or income. If asked about returns/profit, say results depend on team effort, duplication, upgrades, smart-contract rules and POL's market value, that no income is guaranteed, and to only use funds they can afford to lose.
- NEVER ask for or discuss handling anyone's Secret Recovery Phrase or private keys except to warn they must never share them with anyone.
- Only answer questions about this project, the site, wallets/POL as they relate to joining, and the team process. For anything else, or anything you are not sure about, say you're not sure and point them to the Telegram group: ${c.telegramUrl || 'https://t.me/cryptoteambuild'}
- Never give financial, legal, or tax advice.`;
}
const SUBMISSIONS_FILE = path.join(DATA_DIR, 'submissions.json');
if (!fs.existsSync(SUBMISSIONS_FILE)) fs.writeFileSync(SUBMISSIONS_FILE, '[]');
const 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));
}
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; }
subs.push({ newId, memberName, sponsorId, source: source||'(direct)', clickid, ts: new Date().toISOString(),
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);
const chainLine = onchain === null ? '⏳ On-chain check unavailable — verify manually in admin.'
: onchain.registered
? `✅ VERIFIED ON-CHAIN: ${onchain.tierName} tier, level ${onchain.levelName}, referred by ID ${onchain.referrerId}${String(onchain.referrerId)!==sponsorId?` ⚠ (submitted sponsor was ${sponsorId})`:''}`
: `❌ NOT FOUND ON-CHAIN — ID ${newId} has no registration on the contract yet.`;
sendTelegram(`🔔 RM Circle: NEW MEMBER CONFIRMED\nName: ${memberName}\nNew ID: ${newId}\nJoined under sponsor: ${sponsorId}\nSource: ${source||'(direct)'}\n${chainLine}\n→ Add ${memberName} (ID ${newId}) to the rotation queue.`);
return json(res, 200, { ok: true, 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 }); }
}
const sessions = new Map();
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'].includes(event)) return;
const s = String(source||'').toLowerCase().trim().replace(/[^a-z0-9.()\-_:/ ]/g,'').slice(0,80) || '(direct)';
const a = getAnalytics(); if (!a.sources) a.sources = {};
if (!a.sources[s]) { if (Object.keys(a.sources).length >= 500) return; a.sources[s] = { bridge:0, start:0, click:0 }; }
a.sources[s][event] = (a.sources[s][event]||0) + 1;
writeJson(ANALYTICS_FILE, a);
}
function normalizeStatuses(sponsors, preferredActiveId=null) {
const eligible=sponsors.filter(s=>s.status!=='qualified');
let activeId=preferredActiveId;
if(!activeId || !eligible.some(s=>s.id===activeId)){
const existing=eligible.find(s=>s.status==='active');
activeId=existing?existing.id:(eligible[0]?.id||null);
}
return sponsors.map(s=>s.status==='qualified'?s:{...s,status:s.id===activeId?'active':'waiting'});
}
function publicSponsorPayload(sponsor, config) {
if(!sponsor)return null;
return {id:sponsor.id,name:config.showSponsorName?sponsor.name:null,directs:sponsor.directs,goal:2,level:sponsor.level,referralUrl:`${config.dappReferralBaseUrl}${encodeURIComponent(sponsor.id)}`};
}
const CSP_BASE="default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; font-src 'self' data:; form-action 'self'; frame-src https://www.youtube-nocookie.com";
function securityHeaders(extra={}) {
// Public pages must render inside safelist / traffic-exchange iframes, so framing stays open here; admin.html re-locks it via ADMIN_FRAME_HEADERS.
return {
'X-Content-Type-Options':'nosniff','Referrer-Policy':'strict-origin-when-cross-origin',
'Permissions-Policy':'camera=(), microphone=(), geolocation=()',
'Content-Security-Policy':`${CSP_BASE}; frame-ancestors *`,
...extra
};
}
const ADMIN_FRAME_HEADERS={'X-Frame-Options':'DENY','Content-Security-Policy':`${CSP_BASE}; frame-ancestors 'none'`};
function send(res,status,body,headers={}) { res.writeHead(status,securityHeaders(headers));res.end(body); }
function json(res,status,obj,headers={}) { send(res,status,JSON.stringify(obj),{'Content-Type':'application/json; charset=utf-8',...headers}); }
function parseCookies(req){const out={};for(const p of (req.headers.cookie||'').split(';')){const i=p.indexOf('=');if(i>0)out[p.slice(0,i).trim()]=decodeURIComponent(p.slice(i+1).trim())}return out}
function getSession(req){const token=parseCookies(req)['ctb.sid'];if(!token)return null;const s=sessions.get(token);if(!s)return null;if(s.expires<Date.now()){sessions.delete(token);return null}return {token,...s}}
function requireAdmin(req,res){if(!getSession(req)){json(res,401,{error:'Unauthorized'});return false}return true}
async function bodyJson(req){return await new Promise((resolve,reject)=>{let data='';req.on('data',c=>{data+=c;if(data.length>100000){reject(new Error('Payload too large'));req.destroy()}});req.on('end',()=>{if(!data)return resolve({});try{resolve(JSON.parse(data))}catch(e){reject(new Error('Invalid JSON'))}});req.on('error',reject)})}
function contentType(file){const ext=path.extname(file);return ({'.html':'text/html; charset=utf-8','.css':'text/css; charset=utf-8','.js':'application/javascript; charset=utf-8','.json':'application/json; charset=utf-8','.png':'image/png','.jpg':'image/jpeg','.jpeg':'image/jpeg','.webp':'image/webp','.svg':'image/svg+xml','.ico':'image/x-icon','.mp4':'video/mp4','.webm':'video/webm'}[ext]||'application/octet-stream')}
function staticFile(req,res,file,status=200){
if(!fs.existsSync(file)||!fs.statSync(file).isFile())return false;
const size=fs.statSync(file).size;
const base={'Content-Type':contentType(file),'Accept-Ranges':'bytes','Cache-Control':['.html','.css','.js'].includes(path.extname(file))?'no-cache':'public, max-age=3600',...(path.basename(file)==='admin.html'?ADMIN_FRAME_HEADERS:{})};
const m=status===200&&req.headers.range?String(req.headers.range).match(/^bytes=(\d*)-(\d*)$/):null;
if(m&&(m[1]!==''||m[2]!=='')){
const start=m[1]===''?Math.max(0,size-Number(m[2])):Number(m[1]);
const end=(m[1]!==''&&m[2]!=='')?Math.min(Number(m[2]),size-1):size-1;
if(start>end||start>=size){res.writeHead(416,securityHeaders({'Content-Range':`bytes */${size}`}));res.end();return true}
res.writeHead(206,securityHeaders({...base,'Content-Range':`bytes ${start}-${end}/${size}`,'Content-Length':end-start+1}));
if(req.method==='HEAD')res.end();else fs.createReadStream(file,{start,end}).pipe(res);
return true;
}
res.writeHead(status,securityHeaders({...base,'Content-Length':size}));
if(req.method==='HEAD')res.end();else fs.createReadStream(file).pipe(res);
return true;
}
async function handleApi(req,res,pathname){
if(req.method==='GET'&&pathname==='/health') return json(res,200,{ok:true});
if(req.method==='GET'&&pathname==='/api/public/config'){
const c=getConfig();return json(res,200,{siteName:c.siteName,programName:c.programName,bridgeHeadline:c.bridgeHeadline,bridgeSubheadline:c.bridgeSubheadline,premiumEntryPol:c.premiumEntryPol,telegramUrl:c.telegramUrl,supportLabel:c.supportLabel,showQueueProgress:c.showQueueProgress});
}
if(req.method==='GET'&&pathname==='/api/public/payouts'){
return json(res,200,chain.getPayoutsPublic(),{'Cache-Control':'public, max-age=20'});
}
if(req.method==='GET'&&pathname==='/api/public/current-sponsor'){
const sponsors=getSponsors(),c=getConfig(),a=activeSponsor(sponsors);if(!a)return json(res,404,{error:'No active sponsor is currently assigned.'});
return json(res,200,{sponsor:publicSponsorPayload(a,c),waitingCount:sponsors.filter(s=>s.status==='waiting').length,message:'Always use the current sponsor shown on this page. Team placement rotates as members qualify.'});
}
if(req.method==='POST'&&pathname==='/api/public/join-click'){
const b=await bodyJson(req).catch(()=>({}));recordEvent('click',b.source);
const clickid=typeof b.clickid==='string'?b.clickid.trim().slice(0,80).replace(/[^A-Za-z0-9._-]/g,''):'';
firePostback(clickid,`join-${clickid}`,b.source);
let sponsors=getSponsors();const a=activeSponsor(sponsors);if(a){sponsors=sponsors.map(s=>s.id===a.id?{...s,clicks:(s.clicks||0)+1}:s);saveSponsors(sponsors)}return json(res,200,{ok:true});
}
if(req.method==='POST'&&pathname==='/api/public/chat')return await handleChat(req,res);
if(req.method==='POST'&&pathname==='/api/public/submit-id')return await handleSubmitId(req,res);
if(req.method==='POST'&&pathname==='/api/public/track'){
const b=await bodyJson(req).catch(()=>({}));recordEvent(b.event,b.source);return json(res,200,{ok:true});
}
if(req.method==='POST'&&pathname==='/api/admin/login'){
const b=await bodyJson(req).catch(e=>null);if(!b)return json(res,400,{error:'Invalid request'});if(typeof b.password!=='string'||b.password!==ADMIN_PASSWORD)return json(res,401,{error:'Invalid password'});
const token=crypto.randomBytes(32).toString('hex');sessions.set(token,{expires:Date.now()+SESSION_TTL});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);return json(res,200,{ok:true},{'Set-Cookie':'ctb.sid=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0'});
}
if(pathname.startsWith('/api/admin/')&&!requireAdmin(req,res))return;
if(req.method==='GET'&&pathname==='/api/admin/matrix-tree'){
return json(res,200,chain.getMatrixTree());
}
if(req.method==='GET'&&pathname==='/api/admin/member-lookup'){
const id=Number(new URL(req.url,'http://x').searchParams.get('id')||0);
if(!Number.isInteger(id)||id<1||id>281474976710655)return json(res,400,{error:'Enter a numeric member ID.'});
try{
const r=await Promise.race([chain.memberLookup(id),new Promise((_,rej)=>setTimeout(()=>rej(new Error('Chain RPC timeout — try again.')),25000))]);
return json(res,200,r);
}catch(e){return json(res,502,{error:e.message||'Lookup failed'})}
}
if(req.method==='GET'&&pathname==='/api/admin/state'){let subs=[];try{subs=readJson(SUBMISSIONS_FILE).slice(-50).reverse()}catch(e){}return json(res,200,{sponsors:getSponsors(),config:getConfig(),analytics:getAnalytics(),submissions:subs,aiChat:{configured:!!getOpenRouterKey(),model:OPENROUTER_MODEL}});}
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'])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].directs=Math.max(0,Math.min(2,Number(sponsors[idx].directs)||0));saveSponsors(sponsors);return json(res,200,{sponsors});}
if(req.method==='DELETE'&&!action){const wasActive=sponsors[idx].status==='active';sponsors.splice(idx,1);if(wasActive)sponsors=normalizeStatuses(sponsors);saveSponsors(sponsors);return json(res,200,{sponsors});}
if(req.method==='POST'&&action==='increment'){sponsors[idx].directs=Math.min(2,(Number(sponsors[idx].directs)||0)+1);saveSponsors(sponsors);return json(res,200,{sponsors});}
if(req.method==='POST'&&action==='activate'){if(sponsors[idx].status==='qualified')return json(res,400,{error:'Qualified sponsors cannot be activated until reset.'});sponsors=normalizeStatuses(sponsors,id);saveSponsors(sponsors);return json(res,200,{sponsors});}
if(req.method==='POST'&&action==='qualify'){sponsors[idx]={...sponsors[idx],directs:2,status:'qualified'};sponsors=normalizeStatuses(sponsors);saveSponsors(sponsors);return json(res,200,{sponsors,active:activeSponsor(sponsors)});}
if(req.method==='POST'&&action==='reset'){sponsors[idx]={...sponsors[idx],directs:0,status:'waiting'};sponsors=normalizeStatuses(sponsors);saveSponsors(sponsors);return json(res,200,{sponsors});}
if(req.method==='POST'&&action==='move'){const b=await bodyJson(req);const swap=b.direction==='up'?idx-1:idx+1;if(swap>=0&&swap<sponsors.length){const t=sponsors[idx].sortOrder;sponsors[idx].sortOrder=sponsors[swap].sortOrder;sponsors[swap].sortOrder=t;saveSponsors(sponsors)}return json(res,200,{sponsors:getSponsors()});}
}
return json(res,404,{error:'API endpoint not found'});
}
const server=http.createServer(async(req,res)=>{
try{
const u=new URL(req.url,`http://${req.headers.host||'localhost'}`),pathname=decodeURIComponent(u.pathname);
if(pathname==='/health'||pathname.startsWith('/api/'))return await handleApi(req,res,pathname);
if(req.method!=='GET'&&req.method!=='HEAD')return send(res,405,'Method Not Allowed',{'Content-Type':'text/plain; charset=utf-8'});
let file;
if(pathname==='/')file=path.join(PUBLIC_DIR,'index.html');else if(pathname==='/start'||pathname==='/start/')file=path.join(PUBLIC_DIR,'start.html');else if(pathname==='/training'||pathname==='/training/')file=path.join(PUBLIC_DIR,'training.html');else if(pathname==='/admin'||pathname==='/admin/')file=path.join(PUBLIC_DIR,'admin.html');else{
const safe=path.normalize(pathname).replace(/^([.][.][/\\])+/, '').replace(/^[/\\]+/,'');file=path.join(PUBLIC_DIR,safe);if(!file.startsWith(PUBLIC_DIR))file='';
}
if(file&&staticFile(req,res,file))return;return staticFile(req,res,path.join(PUBLIC_DIR,'404.html'),404);
}catch(e){console.error(e);json(res,500,{error:'Internal server error'});}
});
server.listen(PORT,()=>{console.log(`Crypto Team Build sponsor router running on http://localhost:${PORT}`);if(ADMIN_PASSWORD==='changeme')console.warn('WARNING: Set ADMIN_PASSWORD before production deployment.');});
// Team-activity alerts: any NEW on-chain event at/below config.teamRootId goes
// to the Telegram group topic, with the sponsor's contact email when we have it.
chain.startIndexer(evt=>{
try{
const c=getConfig();
const rootId=Number(c.teamRootId)||0;
if(!rootId)return;
const ids=evt.type==='payout'?[evt.toId,evt.fromId]:[evt.id];
if(!ids.some(i=>chain.isInTeam(i,rootId)))return;
const contact=id=>{const s=getSponsors().find(x=>String(x.id)===String(id));return s&&s.email?`\nContact: ${s.name?s.name+' — ':''}${s.email}`:''};
let text;
if(evt.type==='registered')text=`📈 TEAM BUILD: new position!\n#${evt.id} registered under #${evt.referrerId} (${evt.tierName}).`;
else if(evt.type==='upgraded')text=`🚀 TEAM BUILD: #${evt.id} upgraded to ${evt.levelName}.`;
else text=`💸 TEAM BUILD: #${evt.toId} just got PAID ${evt.pol.toFixed(2)} POL${evt.kind==='upline'?` (upgrade pass-up from #${evt.fromId})`:` (referral reward from #${evt.fromId})`}.${contact(evt.toId)}`;
if(evt.tx)text+=`\nhttps://polygonscan.com/tx/${evt.tx}`;
sendTelegram(text);
}catch(e){console.error('team alert error',e.message)}
});