const http = require('http'); const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const { URL } = require('url'); const chain = require('./chain'); const PORT = Number(process.env.PORT || 3000); const ROOT = __dirname; const PUBLIC_DIR = path.join(ROOT, 'public'); const DATA_DIR = process.env.DATA_DIR || path.join(ROOT, 'data'); const SEED_DIR = path.join(ROOT, 'seed'); const SPONSORS_FILE = path.join(DATA_DIR, 'sponsors.json'); const CONFIG_FILE = path.join(DATA_DIR, 'config.json'); const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'changeme'; const IS_PROD = process.env.NODE_ENV === 'production'; const SESSION_TTL = 8 * 60 * 60 * 1000; const LEVELS = ['Scintilla','Ascensus','Fabrica','Culmen','Apex','Fastigium','Vertex','Corona']; const OPENROUTER_MODEL = process.env.OPENROUTER_MODEL || 'deepseek/deepseek-v4-flash:nitro'; const OPENROUTER_KEY_FILE = path.join(DATA_DIR, 'openrouter.key'); function getOpenRouterKey() { if (process.env.OPENROUTER_API_KEY) return process.env.OPENROUTER_API_KEY; try { return fs.readFileSync(OPENROUTER_KEY_FILE, 'utf8').trim(); } catch (e) { return ''; } } const chatHits = new Map(); function chatRateLimited(ip) { const now = Date.now(), rec = chatHits.get(ip); if (!rec || now > rec.reset) { chatHits.set(ip, { count: 1, reset: now + 60000 }); return false; } rec.count++; return rec.count > 10; } function chatSystemPrompt() { const c = getConfig(), sponsors = getSponsors(), a = activeSponsor(sponsors); const waiting = sponsors.filter(s=>s.status==='waiting').length; return `You are "Team Help", the assistant on ${c.siteName || 'Crypto Team Build'} (https://rmcircle.saasy.top), the team site for the RM Circle Premium team build — a project of the Crypto Team Build Network. FACTS: - Strategy: enter RM Circle dApp at Premium tier (${c.premiumEntryPol || 362} POL on Polygon Mainnet, chain ID 137, POL is the gas token). Each member gets 2 directs to become "qualified"; the team then recommends retiring that link and helping those 2 get their 2 with their OWN links (moving-link strategy) — a position only qualifies through its own directs. If an extra signup lands on a qualified link anyway, present it as a BONUS, not a rule violation: it still pays that member the full entry reward and spills downward into the first open slot in their leg (depth). Spillover never qualifies the people below. - First team goal: 30 properly placed positions (2+4+8+16), then 32, 64, 128 and beyond. - 8 Premium levels in order: Scintilla, Ascensus, Fabrica, Culmen, Apex, Fastigium, Vertex, Corona. Everyone starts at Scintilla. Upgrade as quickly as practical, ideally with earned POL; the first two payments at each level help fund the next upgrade. Stay aware of your active downline's levels so you don't fall behind and miss payments. - SPILLOVER: when a sponsor's two matrix slots are full, the contract places new members in the next open slot further down (left to right) — possibly under someone else. Spillover fills that member's matrix and sets up future upgrade income to their position, but does NOT count toward qualification: only people who join using a member's own ID are their directs. Upgrade payments travel up the MATRIX (not who-referred-whom) and only stop at positions that are qualified (2 directs) AND already at the level being bought — otherwise the payment passes them by to the next eligible upline. Members can see their own matrix, spillover tags, and payments at https://rmcircle.saasy.top/my - Current team sponsor: ${a ? `ID ${a.id}${c.showSponsorName && a.name ? ` (${a.name})` : ''}, ${a.directs}/2 directs` : 'shown on the start page'}. ${waiting} placement(s) waiting. Placements rotate as positions qualify — always verify on https://rmcircle.saasy.top/start right before joining. - Site pages: https://rmcircle.saasy.top/ (strategy overview + roadmap), https://rmcircle.saasy.top/start (current sponsor + join steps), https://rmcircle.saasy.top/training (4 videos: 1. How the team build works, 2. Create your MetaMask wallet, 3. Funding your wallet, 4. Buying the Premium position). - Telegram group for live team help: ${c.telegramUrl || 'https://t.me/cryptoteambuild'} RULES: - Keep answers short: 1-4 sentences, plain text, no markdown formatting. Include full URLs when pointing to a page. - NEVER promise, estimate, or imply earnings or income. If asked about returns/profit, say results depend on team effort, duplication, upgrades, smart-contract rules and POL's market value, that no income is guaranteed, and to only use funds they can afford to lose. - NEVER ask for or discuss handling anyone's Secret Recovery Phrase or private keys except to warn they must never share them with anyone. - Only answer questions about this project, the site, wallets/POL as they relate to joining, and the team process. For anything else, or anything you are not sure about, say you're not sure and point them to the Telegram group: ${c.telegramUrl || 'https://t.me/cryptoteambuild'} - Never give financial, legal, or tax advice.`; } const SUBMISSIONS_FILE = path.join(DATA_DIR, 'submissions.json'); if (!fs.existsSync(SUBMISSIONS_FILE)) fs.writeFileSync(SUBMISSIONS_FILE, '[]'); const memberCache = new Map(); const lookupHits = new Map(); function memberLookupLimited(ip) { const now = Date.now(), rec = lookupHits.get(ip); if (!rec || now > rec.reset) { lookupHits.set(ip, { count: 1, reset: now + 60000 }); return false; } rec.count++; return rec.count > 20; } const submitHits = new Map(); function submitRateLimited(ip) { const now = Date.now(), rec = submitHits.get(ip); if (!rec || now > rec.reset) { submitHits.set(ip, { count: 1, reset: now + 600000 }); return false; } rec.count++; return rec.count > 5; } function sendTelegram(text) { const c = getConfig(); if (!c.telegramBotToken || !c.telegramChatId) return; const payload = { chat_id: c.telegramChatId, text }; if (c.telegramTopicId && /^[0-9]+$/.test(String(c.telegramTopicId))) payload.message_thread_id = Number(c.telegramTopicId); fetch(`https://api.telegram.org/bot${c.telegramBotToken}/sendMessage`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }).then(async r=>{ if(!r.ok) console.error('telegram sendMessage status', r.status, (await r.text().catch(()=>'')).slice(0,200)); }) .catch(e=>console.error('telegram error', e.message)); } const SENDGRID_KEY_FILE = path.join(DATA_DIR, 'sendgrid.key'); function getSendgridKey() { if (process.env.SENDGRID_API_KEY) return process.env.SENDGRID_API_KEY; try { return fs.readFileSync(SENDGRID_KEY_FILE, 'utf8').trim(); } catch (e) { return ''; } } // SendGrid is domain-authenticated for marketingwithmarty.com and // mybrandedvoice.com — the from address must stay on one of those or DKIM fails. function emailFrom() { return getConfig().emailFrom || 'The RM Circle Team '; } function sendEmailRaw(toEmail, subject, text) { const key = getSendgridKey(); if (!key || !toEmail) return; const fromStr = emailFrom(); const m = fromStr.match(/^(.*)<([^>]+)>\s*$/); const from = m ? { email: m[2].trim(), name: m[1].trim() || undefined } : { email: fromStr.trim() }; fetch('https://api.sendgrid.com/v3/mail/send', { method: 'POST', headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ personalizations: [{ to: [{ email: toEmail }] }], from, subject, content: [{ type: 'text/plain', value: text }] }) }).then(r => { if (r.status >= 300) r.text().then(t => console.error('sendgrid status', r.status, t.slice(0, 200))); }) .catch(e => console.error('sendgrid error', e.message)); } function sendPaidEmail(toEmail, memberName, evt) { const kindLine = evt.kind === 'upline' ? `an upgrade pass-up from member #${evt.fromId}` : `a referral reward from member #${evt.fromId}'s entry`; const verify = evt.tx ? `\n\nVerify it yourself on the blockchain:\nhttps://polygonscan.com/tx/${evt.tx}` : ''; const text = `Hi ${memberName || 'there'},\n\nGood news — your RM Circle position #${evt.toId} just received ${evt.pol.toFixed(2)} POL (${kindLine}).${verify}\n\nKeep the momentum going: check your level so the next payment in your leg doesn't pass you by.\nhttps://rmcircle.saasy.top/training\n\n— The RM Circle Team\n\nYou're receiving this because your team admin has this address on file for team-build updates. Reply to this email to be removed.`; sendEmailRaw(toEmail, `Your RM Circle position #${evt.toId} just got paid ${evt.pol.toFixed(2)} POL`, text); } function firePostback(clickid, txid, source) { const pb = getConfig().bemobPostbackUrl; if (!clickid || !pb || !/^https:\/\/[a-z0-9.-]+\/postback/i.test(pb)) return; fetch(`${pb}${pb.includes('?')?'&':'?'}cid=${encodeURIComponent(clickid)}&payout=0&txid=${encodeURIComponent(txid)}`) .then(r=>{ if(r.ok) recordEvent('postback', source); else console.error('bemob postback status', r.status); }) .catch(e=>console.error('bemob postback error', e.message)); } async function handleSubmitId(req, res) { const ip = String(req.headers['x-forwarded-for']||req.socket.remoteAddress||'').split(',')[0].trim(); if (submitRateLimited(ip)) return json(res, 429, { error: 'Too many submissions — please wait a few minutes.' }); const b = await bodyJson(req).catch(()=>null); if (!b) return json(res, 400, { error: 'Invalid request.' }); const newId = String(b.newId||'').trim(); if (!/^[0-9]{1,10}$/.test(newId)) return json(res, 400, { error: 'Enter your numeric RM Circle ID (numbers only).' }); const memberName = String(b.memberName||'').replace(/[\u0000-\u001f\u007f]/g,'').trim().slice(0, 60); if (!memberName) return json(res, 400, { error: 'Add your name or Telegram handle so the team can reach you.' }); const sponsorId = String(b.sponsorId||'').trim().slice(0, 20).replace(/[^0-9A-Za-z._-]/g,'') || '?'; const source = typeof b.source==='string' ? b.source : ''; const clickid = typeof b.clickid==='string' ? b.clickid.trim().slice(0,80).replace(/[^A-Za-z0-9._-]/g,'') : ''; let subs = []; try { subs = readJson(SUBMISSIONS_FILE); } catch(e) {} if (subs.some(s=>s.newId===newId)) return json(res, 200, { ok: true, duplicate: true }); // on-chain verification: does this ID actually exist on the contract? let onchain = null; try { onchain = await Promise.race([ chain.verifyMember(Number(newId)), new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), 6000)) ]); } catch (e) { onchain = null; } // the chain decides the path: rotation join (referrer = active rotation sponsor) // vs leg join (someone's personal team build) — regardless of which page they used const active = activeSponsor(getSponsors()); let joinPath = 'unknown'; if (onchain && onchain.registered) joinPath = (active && String(onchain.referrerId) === String(active.id)) ? 'rotation' : 'leg'; else if (onchain && !onchain.registered) joinPath = 'notfound'; subs.push({ newId, memberName, sponsorId, source: source||'(direct)', clickid, ts: new Date().toISOString(), path: joinPath, onchain: onchain ? { registered: onchain.registered, tier: onchain.tierName, level: onchain.levelName, referrerId: onchain.referrerId, uplineId: onchain.uplineId } : undefined }); writeJson(SUBMISSIONS_FILE, subs.slice(-1000)); recordEvent('purchase', source); firePostback(clickid, `purchase-${clickid}`, source); let msg; if (joinPath === 'rotation') { 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→ +1 direct for ID ${active.id}; add ${memberName} (ID ${newId}) to the rotation queue.`; } 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'].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{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)}`; if(r.nextInLine)r.nextInLine.referralUrl=`${getConfig().dappReferralBaseUrl}${encodeURIComponent(r.nextInLine.id)}`; memberCache.set(id,{data:r,ts:Date.now()}); if(memberCache.size>500)memberCache.delete(memberCache.keys().next().value); return json(res,200,r,{'Cache-Control':'public, max-age=60'}); }catch(e){return json(res,502,{error:e.message||'Lookup failed'})} } if(req.method==='GET'&&pathname==='/api/public/payouts'){ return json(res,200,chain.getPayoutsPublic(),{'Cache-Control':'public, max-age=20'}); } if(req.method==='GET'&&pathname==='/api/public/current-sponsor'){ const sponsors=getSponsors(),c=getConfig(),a=activeSponsor(sponsors);if(!a)return json(res,404,{error:'No active sponsor is currently assigned.'}); return json(res,200,{sponsor:publicSponsorPayload(a,c),waitingCount:sponsors.filter(s=>s.status==='waiting').length,message:'Always use the current sponsor shown on this page. Team placement rotates as members qualify.'}); } if(req.method==='POST'&&pathname==='/api/public/join-click'){ const b=await bodyJson(req).catch(()=>({}));recordEvent('click',b.source); const clickid=typeof b.clickid==='string'?b.clickid.trim().slice(0,80).replace(/[^A-Za-z0-9._-]/g,''):''; firePostback(clickid,`join-${clickid}`,b.source); let sponsors=getSponsors();const a=activeSponsor(sponsors);if(a){sponsors=sponsors.map(s=>s.id===a.id?{...s,clicks:(s.clicks||0)+1}:s);saveSponsors(sponsors)}return json(res,200,{ok:true}); } if(req.method==='POST'&&pathname==='/api/public/chat')return await handleChat(req,res); if(req.method==='POST'&&pathname==='/api/public/submit-id')return await handleSubmitId(req,res); if(req.method==='POST'&&pathname==='/api/public/track'){ const b=await bodyJson(req).catch(()=>({}));recordEvent(b.event,b.source);return json(res,200,{ok:true}); } if(req.method==='POST'&&pathname==='/api/admin/login'){ const b=await bodyJson(req).catch(e=>null);if(!b)return json(res,400,{error:'Invalid request'});if(typeof b.password!=='string'||b.password!==ADMIN_PASSWORD)return json(res,401,{error:'Invalid password'}); const token=crypto.randomBytes(32).toString('hex');sessions.set(token,{expires:Date.now()+SESSION_TTL});saveSessions();const cookie=`ctb.sid=${encodeURIComponent(token)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${SESSION_TTL/1000}${IS_PROD?'; Secure':''}`;return json(res,200,{ok:true},{'Set-Cookie':cookie}); } if(req.method==='POST'&&pathname==='/api/admin/logout'){ const s=getSession(req);if(s){sessions.delete(s.token);saveSessions();}return json(res,200,{ok:true},{'Set-Cookie':'ctb.sid=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0'}); } if(pathname.startsWith('/api/admin/')&&!requireAdmin(req,res))return; if(req.method==='GET'&&pathname==='/api/admin/matrix-tree'){ return json(res,200,chain.getMatrixTree()); } if(req.method==='GET'&&pathname==='/api/admin/member-lookup'){ const id=Number(new URL(req.url,'http://x').searchParams.get('id')||0); if(!Number.isInteger(id)||id<1||id>281474976710655)return json(res,400,{error:'Enter a numeric member ID.'}); try{ const r=await Promise.race([chain.memberLookup(id),new Promise((_,rej)=>setTimeout(()=>rej(new Error('Chain RPC timeout — try again.')),25000))]); return json(res,200,r); }catch(e){return json(res,502,{error:e.message||'Lookup failed'})} } if(req.method==='GET'&&pathname==='/api/admin/state'){let subs=[];try{subs=readJson(SUBMISSIONS_FILE).slice(-50).reverse()}catch(e){}return json(res,200,{sponsors:getSponsors(),config:getConfig(),analytics:getAnalytics(),submissions:subs,aiChat:{configured:!!getOpenRouterKey(),model:OPENROUTER_MODEL},email:{configured:!!getSendgridKey(),from:emailFrom()}});} if(req.method==='POST'&&pathname==='/api/admin/sendgrid-key'){ const b=await bodyJson(req);const key=typeof b.key==='string'?b.key.trim():null; if(key===null)return json(res,400,{error:'Invalid request.'}); if(key===''){try{fs.unlinkSync(SENDGRID_KEY_FILE)}catch(e){}return json(res,200,{configured:!!getSendgridKey()});} if(!/^SG\./.test(key)||key.length<40||/\s/.test(key))return json(res,400,{error:'That does not look like a SendGrid API key (starts with SG.).'}); fs.writeFileSync(SENDGRID_KEY_FILE,key,{mode:0o600}); return json(res,200,{configured:true}); } if(req.method==='POST'&&pathname==='/api/admin/openrouter-key'){ const b=await bodyJson(req);const key=typeof b.key==='string'?b.key.trim():null; if(key===null)return json(res,400,{error:'Invalid request.'}); if(key===''){try{fs.unlinkSync(OPENROUTER_KEY_FILE)}catch(e){}return json(res,200,{configured:!!getOpenRouterKey()});} if(key.length<20||/\s/.test(key))return json(res,400,{error:'That does not look like a valid API key.'}); fs.writeFileSync(OPENROUTER_KEY_FILE,key,{mode:0o600}); return json(res,200,{configured:true}); } if(req.method==='POST'&&pathname==='/api/admin/sponsors'){ const b=await bodyJson(req);const {id,name,parentId='',level='Scintilla',notes='',email=''}=b;if(!id||!name)return json(res,400,{error:'ID and name are required.'});if(!LEVELS.includes(level))return json(res,400,{error:'Invalid level.'});if(email&&!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(email).trim()))return json(res,400,{error:'Invalid email address.'});let sponsors=getSponsors();if(sponsors.some(s=>String(s.id)===String(id)))return json(res,409,{error:'That sponsor ID already exists.'}); const maxOrder=sponsors.reduce((m,s)=>Math.max(m,s.sortOrder||0),0);sponsors.push({id:String(id).trim(),name:String(name).trim(),parentId:String(parentId||'').trim(),directs:0,level,status:sponsors.some(s=>s.status==='active')?'waiting':'active',sortOrder:maxOrder+10,clicks:0,notes:String(notes||'').trim(),email:String(email||'').trim().slice(0,120)});sponsors=normalizeStatuses(sponsors);saveSponsors(sponsors);return json(res,201,{sponsors}); } if(req.method==='PATCH'&&pathname==='/api/admin/config'){ const b=await bodyJson(req),cur=getConfig(),next={...cur};for(const k of ['siteName','programName','bridgeHeadline','bridgeSubheadline','premiumEntryPol','dappReferralBaseUrl','telegramUrl','supportLabel','showSponsorName','showQueueProgress','bemobPostbackUrl','telegramBotToken','telegramChatId','telegramTopicId','teamRootId','emailFrom','teamAlertEmail'])if(Object.prototype.hasOwnProperty.call(b,k))next[k]=b[k];next.premiumEntryPol=Number(next.premiumEntryPol)||362;next.updatedAt=new Date().toISOString();writeJson(CONFIG_FILE,next);return json(res,200,{config:next}); } const m=pathname.match(/^\/api\/admin\/sponsors\/([^/]+)(?:\/(increment|activate|qualify|reset|move))?$/); if(m){const id=decodeURIComponent(m[1]),action=m[2]||null;let sponsors=getSponsors(),idx=sponsors.findIndex(s=>s.id===id);if(idx<0)return json(res,404,{error:'Sponsor not found.'}); if(req.method==='PATCH'&&!action){const b=await bodyJson(req);if(Object.prototype.hasOwnProperty.call(b,'level')&&!LEVELS.includes(b.level))return json(res,400,{error:'Invalid level.'});if(Object.prototype.hasOwnProperty.call(b,'email')&&b.email&&!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(b.email).trim()))return json(res,400,{error:'Invalid email address.'});for(const k of ['name','parentId','directs','level','notes','email'])if(Object.prototype.hasOwnProperty.call(b,k))sponsors[idx][k]=k==='email'?String(b[k]||'').trim().slice(0,120):b[k];sponsors[idx].name=String(sponsors[idx].name||'').trim().slice(0,80)||sponsors[idx].name;sponsors[idx].directs=Math.max(0,Math.min(2,Number(sponsors[idx].directs)||0));saveSponsors(sponsors);return json(res,200,{sponsors});} if(req.method==='DELETE'&&!action){const wasActive=sponsors[idx].status==='active';sponsors.splice(idx,1);if(wasActive)sponsors=normalizeStatuses(sponsors);saveSponsors(sponsors);return json(res,200,{sponsors});} if(req.method==='POST'&&action==='increment'){sponsors[idx].directs=Math.min(2,(Number(sponsors[idx].directs)||0)+1);saveSponsors(sponsors);return json(res,200,{sponsors});} if(req.method==='POST'&&action==='activate'){if(sponsors[idx].status==='qualified')return json(res,400,{error:'Qualified sponsors cannot be activated until reset.'});sponsors=normalizeStatuses(sponsors,id);saveSponsors(sponsors);return json(res,200,{sponsors});} if(req.method==='POST'&&action==='qualify'){sponsors[idx]={...sponsors[idx],directs:2,status:'qualified'};sponsors=normalizeStatuses(sponsors);saveSponsors(sponsors);return json(res,200,{sponsors,active:activeSponsor(sponsors)});} if(req.method==='POST'&&action==='reset'){sponsors[idx]={...sponsors[idx],directs:0,status:'waiting'};sponsors=normalizeStatuses(sponsors);saveSponsors(sponsors);return json(res,200,{sponsors});} if(req.method==='POST'&&action==='move'){const b=await bodyJson(req);const swap=b.direction==='up'?idx-1:idx+1;if(swap>=0&&swap{ try{ const u=new URL(req.url,`http://${req.headers.host||'localhost'}`),pathname=decodeURIComponent(u.pathname); if(pathname==='/health'||pathname.startsWith('/api/'))return await handleApi(req,res,pathname); if(req.method!=='GET'&&req.method!=='HEAD')return send(res,405,'Method Not Allowed',{'Content-Type':'text/plain; charset=utf-8'}); let file; if(pathname==='/')file=path.join(PUBLIC_DIR,'index.html');else if(pathname==='/start'||pathname==='/start/')file=path.join(PUBLIC_DIR,'start.html');else if(pathname==='/training'||pathname==='/training/')file=path.join(PUBLIC_DIR,'training.html');else if(pathname==='/admin'||pathname==='/admin/')file=path.join(PUBLIC_DIR,'admin.html');else if(pathname==='/my'||pathname==='/my/'||/^\/my\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'my.html');else{ const safe=path.normalize(pathname).replace(/^([.][.][/\\])+/, '').replace(/^[/\\]+/,'');file=path.join(PUBLIC_DIR,safe);if(!file.startsWith(PUBLIC_DIR))file=''; } if(file&&staticFile(req,res,file))return;return staticFile(req,res,path.join(PUBLIC_DIR,'404.html'),404); }catch(e){console.error(e);json(res,500,{error:'Internal server error'});} }); server.listen(PORT,()=>{console.log(`Crypto Team Build sponsor router running on http://localhost:${PORT}`);if(ADMIN_PASSWORD==='changeme')console.warn('WARNING: Set ADMIN_PASSWORD before production deployment.');}); // Team-activity alerts: any NEW on-chain event at/below config.teamRootId goes // to the Telegram group topic, with the sponsor's contact email when we have it. chain.startIndexer(evt=>{ try{ const c=getConfig(); const rootId=Number(c.teamRootId)||0; const ids=evt.type==='payout'?[evt.toId,evt.fromId]:[evt.id]; if(rootId&&ids.some(i=>chain.isInTeam(i,rootId))){ const contact=id=>{const s=getSponsors().find(x=>String(x.id)===String(id));return s&&s.email?`\nContact: ${s.name?s.name+' — ':''}${s.email}`:''}; let text; if(evt.type==='registered')text=`📈 TEAM BUILD: new position!\n#${evt.id} registered under #${evt.referrerId} (${evt.tierName}).`; else if(evt.type==='upgraded')text=`🚀 TEAM BUILD: #${evt.id} upgraded to ${evt.levelName}.`; else text=`💸 TEAM BUILD: #${evt.toId} just got PAID ${evt.pol.toFixed(2)} POL${evt.kind==='upline'?` (upgrade pass-up from #${evt.fromId})`:` (referral reward from #${evt.fromId})`}.${contact(evt.toId)}`; if(evt.tx)text+=`\nhttps://polygonscan.com/tx/${evt.tx}`; sendTelegram(text); // admin email alert — same team-gated events, so deep-leg action still surfaces if(c.teamAlertEmail){ const subj=evt.type==='registered'?`RM Circle team build: #${evt.id} registered under #${evt.referrerId}` :evt.type==='upgraded'?`RM Circle team build: #${evt.id} upgraded to ${evt.levelName}` :`RM Circle team build: #${evt.toId} paid ${evt.pol.toFixed(2)} POL`; sendEmailRaw(c.teamAlertEmail,subj,text.replace(/^[^\s]+ /,'')); } } }catch(e){console.error('team alert error',e.message)} // "you've been paid" email — any payout whose recipient has a contact email on file (not subtree-gated) try{ if(evt.type==='payout'){ const sp=getSponsors().find(x=>String(x.id)===String(evt.toId)); if(sp&&sp.email)sendPaidEmail(sp.email,sp.name,evt); } }catch(e){console.error('paid email error',e.message)} });