Payout alerts highlight unlocked tools: shared suite-tools level map drives the Telegram team feed, recruiting feed, member DM on upgrade, and X posts; only announces LIVE tiers and gated behind config.suiteToolsInAlerts (off until Suite launch)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
Stack trace:
|
||||
Frame Function Args
|
||||
0007FFFFBBB0 0002100603A4 (0000000000B0, 0007FFFFDC10, 0007FFFFC730, 0007FFFFDC10) msys-2.0.dll+0x203A4
|
||||
0007FFFFC490 0002100625FE (0007FFFFC730, 000000000000, 000000000180, 000000000000) msys-2.0.dll+0x225FE
|
||||
0007FFFFC490 0002100C1794 (0007FFFFC6F0, 000000000000, 000000000000, 0007FFFFC700) msys-2.0.dll+0x81794
|
||||
000000000006 0002100BD229 (7FFA00000346, 0007FFFFC700, 000000000000, 7FFA46EDAB50) msys-2.0.dll+0x7D229
|
||||
0007FFFFC848 0002100BD62A (0007FFFFC858, 000A00000000, 0000000000B5, 0000000000B5) msys-2.0.dll+0x7D62A
|
||||
0007FFFFC848 000210213B18 (000000000000, 000A00000030, 000000000000, 000000000000) msys-2.0.dll+0x1D3B18
|
||||
0007FFFFC848 00010042A1E5 (0002100A7AB3, 000000000000, 000A00000520, 0000000001F0) grep.exe+0x2A1E5
|
||||
0007FFFFCB80 000100404B7C (000000000014, 0007FFFFCB80, 000000000000, 000000000000) grep.exe+0x4B7C
|
||||
0007FFFFCB80 000100429678 (0002100455E0, 000000714950, 000000000148, 000000000000) grep.exe+0x29678
|
||||
0007FFFFCD30 000210047F11 (000000000000, 000000000000, 000000000000, 000000000000) msys-2.0.dll+0x7F11
|
||||
000000000000 000210045AC3 (000000000000, 000000000000, 000000000000, 000000000000) msys-2.0.dll+0x5AC3
|
||||
0007FFFFFFF0 000210045B74 (000000000000, 000000000000, 000000000000, 000000000000) msys-2.0.dll+0x5B74
|
||||
End of stack trace
|
||||
Loaded modules:
|
||||
000100400000 grep.exe
|
||||
7FFA46D60000 ntdll.dll
|
||||
7FFA045B0000 aswhook.dll
|
||||
7FFA46060000 KERNEL32.DLL
|
||||
7FFA44190000 KERNELBASE.dll
|
||||
000430B30000 msys-intl-8.dll
|
||||
0005603F0000 msys-iconv-2.dll
|
||||
000210040000 msys-2.0.dll
|
||||
0004C36D0000 msys-pcre-1.dll
|
||||
7FFA46C10000 advapi32.dll
|
||||
7FFA46B50000 msvcrt.dll
|
||||
7FFA44B50000 sechost.dll
|
||||
7FFA46180000 RPCRT4.dll
|
||||
7FFA43520000 CRYPTBASE.DLL
|
||||
7FFA449D0000 bcryptPrimitives.dll
|
||||
@@ -20,6 +20,7 @@ messages.init({ dataDir: DATA_DIR, chain, isProd: IS_PROD });
|
||||
const suiteMeter = require('./suite-meter'); suiteMeter.init({ dataDir: DATA_DIR });
|
||||
const suiteAI = require('./suite-ai'); suiteAI.init({ dataDir: DATA_DIR });
|
||||
const suitePages = require('./suite-pages'); suitePages.init({ dataDir: DATA_DIR });
|
||||
const suiteTools = require('./suite-tools');
|
||||
const tgbot = require('./tgbot');
|
||||
tgbot.init({ dataDir: DATA_DIR, chain, getConfig, messages, baseUrl: 'https://rmcircle.team' });
|
||||
const SESSION_TTL = 8 * 60 * 60 * 1000;
|
||||
@@ -161,7 +162,9 @@ function recruitMsg(evt) {
|
||||
return `🔥 The team just grew!\n\nA new member joined The RM Circle and locked in their position on-chain. The momentum is real.\n\n👉 Claim your spot at ${home} 🚀\n\n${tags}`;
|
||||
}
|
||||
if (evt.type === 'upgraded') {
|
||||
return `⚡ Level up!\n\nMember #${evt.id} just upgraded to ${evt.levelName} on The RM Circle — climbing the ranks and opening up bigger pass-ups.\n\n👉 Start building at ${home} 🚀\n\n${tags}`;
|
||||
const tools = suiteTools.unlockText(evt.level, evt.levelName, getConfig());
|
||||
const toolLine = tools ? `\n\nThat upgrade also unlocked ${tools} — every paid level extends the toolkit that comes with your position.` : '';
|
||||
return `⚡ Level up!\n\nMember #${evt.id} just upgraded to ${evt.levelName} on The RM Circle — climbing the ranks and opening up bigger pass-ups.${toolLine}\n\n👉 Start building at ${home} 🚀\n\n${tags}`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
@@ -953,7 +956,7 @@ async function handleApi(req,res,pathname){
|
||||
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','companionBotToken','miniAppShortName','telegramChatId','telegramTopicId','telegramRecruitTopicId','teamRootId','emailFrom','teamAlertEmail','ownerIds','ownerAlertEmail','orgRootId','ctbOfferPostbackUrl','ctbOfferSecret','recruitCtaUrl','walletNotice','tweetEnabled','tweetCtaUrl','tweetHashtags','blotatoTwitterId','dappFallbackPublic','moonpayPublicKey','moonpaySecretKey','publicRotationMode','publicRotationRootId','rotationExcludeIds','suiteAllowlist'])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 b=await bodyJson(req),cur=getConfig(),next={...cur};for(const k of ['siteName','programName','bridgeHeadline','bridgeSubheadline','premiumEntryPol','dappReferralBaseUrl','telegramUrl','supportLabel','showSponsorName','showQueueProgress','bemobPostbackUrl','telegramBotToken','companionBotToken','miniAppShortName','telegramChatId','telegramTopicId','telegramRecruitTopicId','teamRootId','emailFrom','teamAlertEmail','ownerIds','ownerAlertEmail','orgRootId','ctbOfferPostbackUrl','ctbOfferSecret','recruitCtaUrl','walletNotice','tweetEnabled','tweetCtaUrl','tweetHashtags','blotatoTwitterId','dappFallbackPublic','moonpayPublicKey','moonpaySecretKey','publicRotationMode','publicRotationRootId','rotationExcludeIds','suiteAllowlist','suiteToolsInAlerts'])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.'});
|
||||
@@ -1106,7 +1109,7 @@ chain.startIndexer(evt=>{
|
||||
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 if(evt.type==='upgraded'){const tl=suiteTools.unlockText(evt.level,evt.levelName,getConfig());text=`🚀 TEAM BUILD: #${evt.id} upgraded to ${evt.levelName}.${tl?`\n🧰 Unlocked: ${tl}.`:''}`;}
|
||||
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);
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// One source of truth for "what does this level unlock in the Circle Suite",
|
||||
// shared by the Telegram feeds, the X/Twitter payout posts, and anywhere else
|
||||
// that congratulates an upgrade.
|
||||
//
|
||||
// Two honesty rules baked in:
|
||||
// 1. `live:false` tiers are NEVER announced. We do not congratulate someone
|
||||
// for unlocking software that does not exist yet.
|
||||
// 2. The whole feature is gated on config.suiteToolsInAlerts, so nothing goes
|
||||
// public while the Suite is still in team beta. Flip it on at launch.
|
||||
'use strict';
|
||||
|
||||
const LEVEL_TOOLS = {
|
||||
1: { name: 'Scintilla', live: true, short: 'the Launch Kit — promo center, printable handouts with your QR, the Circle Method course, your dashboard and the AI coach' },
|
||||
2: { name: 'Ascensus', live: true, short: 'the Copy Engine and Page Builder — an AI copywriter for your posts and DMs, plus your own hosted invitation page' },
|
||||
3: { name: 'Fabrica', live: false, short: 'the Email Engine and Video Maker' },
|
||||
4: { name: 'Culmen', live: false, short: 'your own Voice Profile and multi-page funnels' },
|
||||
5: { name: 'Apex', live: false, short: 'the Traffic Desk — syndicated network display advertising' },
|
||||
6: { name: 'Fastigium', live: false, short: 'the Funnel Factory and Replay Funnels' },
|
||||
7: { name: 'Vertex', live: false, short: 'Leader Ops — team radar and coaching digests' },
|
||||
8: { name: 'Corona', live: false, short: 'the Founder Desk — your own AI operator' }
|
||||
};
|
||||
|
||||
// Very short form for character-limited surfaces (X/Twitter).
|
||||
const TERSE = {
|
||||
1: 'the Launch Kit',
|
||||
2: 'the Copy Engine + Page Builder',
|
||||
3: 'the Email Engine + Video Maker',
|
||||
4: 'Voice Profile + funnels',
|
||||
5: 'the Traffic Desk',
|
||||
6: 'the Funnel Factory',
|
||||
7: 'Leader Ops',
|
||||
8: 'the Founder Desk'
|
||||
};
|
||||
|
||||
function levelNumber(level, levelName) {
|
||||
const n = Number(level);
|
||||
if (n >= 1 && n <= 8) return n;
|
||||
if (levelName) {
|
||||
for (const k of Object.keys(LEVEL_TOOLS)) {
|
||||
if (LEVEL_TOOLS[k].name.toLowerCase() === String(levelName).toLowerCase()) return Number(k);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function enabled(cfg) { return !!(cfg && cfg.suiteToolsInAlerts); }
|
||||
|
||||
// Full sentence fragment, or '' when we shouldn't say anything.
|
||||
function unlockText(level, levelName, cfg) {
|
||||
if (!enabled(cfg)) return '';
|
||||
const n = levelNumber(level, levelName);
|
||||
const t = LEVEL_TOOLS[n];
|
||||
if (!t || !t.live) return '';
|
||||
return t.short;
|
||||
}
|
||||
|
||||
// Terse form for tweets, or '' when we shouldn't say anything.
|
||||
function unlockTerse(level, levelName, cfg) {
|
||||
if (!enabled(cfg)) return '';
|
||||
const n = levelNumber(level, levelName);
|
||||
const t = LEVEL_TOOLS[n];
|
||||
if (!t || !t.live) return '';
|
||||
return TERSE[n] || '';
|
||||
}
|
||||
|
||||
module.exports = { LEVEL_TOOLS, unlockText, unlockTerse, levelNumber, enabled };
|
||||
@@ -8,6 +8,7 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const suiteTools = require('./suite-tools');
|
||||
let DATA_DIR = '', chain = null, getConfig = null, messages = null;
|
||||
const FILE = () => path.join(DATA_DIR, 'tg-links.json');
|
||||
|
||||
@@ -101,6 +102,14 @@ async function notifyEvent(evt) {
|
||||
if (!chat) return;
|
||||
const kind = evt.kind === 'upline' ? `${evt.gen ? `Gen ${evt.gen} ` : ''}upgrade pass-up from #${evt.fromId}` : `referral reward from #${evt.fromId}'s entry`;
|
||||
await dm(chat, `💰 You just caught ${evt.pol.toFixed(2)} POL — ${kind}.\nVerify: https://polygonscan.com/tx/${evt.tx}\n\nYour pipeline: https://rmcircle.team/my/${evt.toId}`);
|
||||
} else if (evt.type === 'upgraded') {
|
||||
// Congratulate the member who upgraded, and tell them what it opened up.
|
||||
const chat = memberChat(evt.id);
|
||||
if (!chat) return;
|
||||
const tools = suiteTools.unlockText(evt.level, evt.levelName, getConfig());
|
||||
await dm(chat, `⚡ You're now ${evt.levelName}!\n\nYour reach just extended one generation deeper — payments from that layer can now land on you.` +
|
||||
(tools ? `\n\n🧰 This level also unlocked ${tools}.\nOpen it: https://rmcircle.team/suite` : '') +
|
||||
`\n\nNow teach your two to climb: https://rmcircle.team/training#lesson-8`);
|
||||
} else if (evt.type === 'registered' && evt.referrerId) {
|
||||
const chat = memberChat(evt.referrerId);
|
||||
if (!chat) return;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
// - Never logs the API key.
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const suiteTools = require('./suite-tools');
|
||||
|
||||
const BLOTATO_URL = 'https://backend.blotato.com/v2/posts';
|
||||
const MIN_GAP_MS = 2600; // ~23/min ceiling, under Blotato's 30/min
|
||||
@@ -55,6 +56,21 @@ function render(evt, cfg) {
|
||||
const u = evt.upgrade;
|
||||
const lvl = (u && (u.levelName)) || evt.levelName;
|
||||
const up = (u && u.id && lvl) ? `Member #${u.id} upgraded to ${lvl} and ` : '';
|
||||
// What that upgrade unlocked in the Circle Suite — only when the tools are
|
||||
// live AND config.suiteToolsInAlerts is on (see suite-tools.js).
|
||||
const tool = suiteTools.unlockTerse((u && u.level), lvl, cfg);
|
||||
if (tool) {
|
||||
body = pick([
|
||||
`🚀 Level up! ${up}${pol} POL landed on Member #${evt.toId}'s position — and that upgrade unlocked ${tool}. 💎`,
|
||||
`⚡ ${up}Member #${evt.toId} received ${pol} POL straight from the contract — the upgrade also opened up ${tool}. 🔗`,
|
||||
`🔥 ${up}${pol} POL paid up to Member #${evt.toId} on Polygon — plus ${tool} unlocked with the level. 🚀`,
|
||||
]);
|
||||
const headT = `${body}\n\nSee how it works 👉 ${cta}`;
|
||||
let outT = `${headT}\n\n${tags}`;
|
||||
if (outT.length > 280) outT = headT;
|
||||
if (outT.length > 280) outT = headT.slice(0, 279) + '…';
|
||||
return outT;
|
||||
}
|
||||
body = pick([
|
||||
`🚀 Level up! ${up}${pol} POL just landed on Member #${evt.toId}'s position — instant, on-chain, unstoppable. 💎`,
|
||||
`⚡ Another upgrade paid! ${up}Member #${evt.toId} received ${pol} POL straight from the smart contract — no middleman. 🔗`,
|
||||
|
||||
Reference in New Issue
Block a user