Founder Desk (L8): weekly campaign pack + Suite API

Completes the ladder — no tier is a placeholder now.

Every tier below builds one thing at a time, which is right for someone
still finding their words. At the top the constraint is different: these are
people running organisations who don't have an afternoon to spend clicking.
So one pass produces five social posts across five different angles, three
outreach messages (first approach, follow-up, pyramid-scheme answer), an
email, and three text ads — all carrying their link, all in the member's own
voice profile if they have one.

Partial failures are reported rather than hidden: if the engine can't finish
a section the pack says which, instead of quietly handing over a short week.

API access is real, not a label: a personal key over
GET /me, GET /team, POST /generate. Keys are stored hashed and shown exactly
once, compared in constant time, and every call goes through the same meters
and compliance rules as the web tools.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-08-28 11:09:48 -05:00
parent 85261f2719
commit f05d5a2746
7 changed files with 514 additions and 4 deletions
+77
View File
@@ -0,0 +1,77 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex"><meta name="description" content="Founder Desk — a whole week of promotion built in one pass, plus API access to the Suite."><title>Founder Desk | The Circle Suite</title><link rel="icon" type="image/png" href="/favicon.png"><link rel="stylesheet" href="/styles.css">
<style>
.dw{max-width:900px;margin:0 auto;padding:24px 20px 70px}
.dw h1{font-size:clamp(26px,4.5vw,36px);margin:6px 0 8px}
.dw .lede{color:var(--muted);line-height:1.6;margin-bottom:18px}
#dGate{display:none;border:2px solid var(--gold);border-radius:12px;padding:16px 20px;margin-bottom:18px;line-height:1.6}
.d-card{background:var(--card,#0d2236);border:1px solid var(--line,#24425d);border-radius:14px;padding:18px 20px;margin-bottom:16px}
.d-card h2{font-size:19px;margin:0 0 4px}
.d-card .sub{color:var(--muted);font-size:13.5px;line-height:1.55;margin-bottom:12px}
.d-row{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-top:14px}
#dErr{display:none;color:#f0a05a;margin-top:12px;font-size:14px;line-height:1.55}
.d-sec{margin-top:24px}
.d-sec > h3{font-size:16px;margin:0 0 4px;color:var(--gold);letter-spacing:.3px}
.d-sec > .why{color:var(--muted);font-size:12.5px;margin-bottom:10px;line-height:1.5}
.d-item{border:1px solid var(--line);border-radius:12px;padding:13px 15px;margin-bottom:10px;position:relative}
.d-item .tag{font-size:10.5px;font-weight:900;letter-spacing:1px;text-transform:uppercase;color:var(--teal);margin-bottom:6px}
.d-item .body{white-space:pre-wrap;line-height:1.65;font-size:14.5px}
.d-item .subj{font-weight:800;margin-bottom:5px}
.d-copy{position:absolute;top:11px;right:11px}
.d-ads{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:10px}
.d-ad{border:1px solid var(--line);border-radius:10px;padding:11px 12px;background:var(--bg,#0b1d2e)}
.d-ad .s{font-weight:800;font-size:15px;color:var(--gold);margin-bottom:4px;word-break:break-word}
.d-ad .l{font-size:13px;line-height:1.5;word-break:break-word}
.k-key{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;background:var(--bg,#0b1d2e);
border:1px solid var(--gold);border-radius:8px;padding:10px 12px;word-break:break-all;margin-top:10px}
.k-meta{color:var(--muted);font-size:12.5px;margin-top:8px;line-height:1.55}
pre.k-doc{background:var(--bg,#0b1d2e);border:1px solid var(--line);border-radius:10px;padding:12px 14px;
overflow-x:auto;font-size:12.5px;line-height:1.6;margin-top:10px}
.spin{display:inline-block;width:13px;height:13px;border:2px solid rgba(255,255,255,.25);border-top-color:var(--gold);border-radius:50%;animation:sp .8s linear infinite;vertical-align:-2px;margin-right:7px}
@keyframes sp{to{transform:rotate(360deg)}}
@media (prefers-reduced-motion:reduce){.spin{animation:none}}
.d-note{margin-top:22px;font-size:12.5px;color:var(--muted);line-height:1.6;border-top:1px solid var(--line);padding-top:12px}
</style></head>
<body>
<header class="wrap nav"><a class="brand" href="/suite"><img class="brand-mark" src="/logo.jpg" alt="The RM Circle" width="42" height="42"><span><span id="brandName">RM Circle</span><small>Founder Desk</small></span></a><div class="nav-actions"><a class="btn btn-secondary hide-mobile" href="/suite">← The Suite</a><a class="btn btn-primary" href="/my">My Dashboard</a></div></header>
<main class="dw">
<div class="eyebrow" style="color:var(--gold)">CIRCLE SUITE · CORONA (LEVEL 8)</div>
<h1>Founder <span class="gold">Desk.</span></h1>
<p class="lede">Every tool below this builds one thing at a time, which is right when you are still finding your words. At this level the problem is different: you are running an organisation and you do not have an afternoon to spend clicking. So this builds the whole week at once.</p>
<div id="dGate"></div>
<div id="dBody" style="display:none">
<div class="d-card">
<h2>This week's campaign pack</h2>
<div class="sub">Five social posts across five different angles, three outreach messages, an email for your list, and three text ads ready for the Traffic Desk — all carrying your link, all in one pass.</div>
<div class="d-row">
<button id="dGo" class="btn btn-primary">👑 Build my week</button>
<span class="k-meta" id="dMeter" style="margin:0"></span>
</div>
<div id="dErr"></div>
</div>
<div id="dPack"></div>
<div class="d-card" style="margin-top:24px">
<h2>API access</h2>
<div class="sub">A personal key for pulling your position, your organisation triage, and the writer into whatever you already run.</div>
<div id="dKeyState"></div>
<div class="d-row">
<button id="dIssue" class="btn btn-secondary">Create a key</button>
<button id="dRevoke" class="btn btn-secondary" style="display:none">Revoke it</button>
</div>
<pre class="k-doc">GET /api/suite/v1/me your position, level and link
GET /api/suite/v1/team your organisation, triaged
POST /api/suite/v1/generate {"kind":"post","brief":"..."}
curl -H "Authorization: Bearer YOUR_KEY" \
https://rmcircle.team/api/suite/v1/me</pre>
</div>
</div>
<p class="d-note"><strong style="color:var(--text)">Everything here obeys the same rules as the rest of the Suite</strong> — no income claims, no invented figures, POL quantities only, Premium only. Generating a week does not post anything anywhere; it hands you the words and you decide what goes out. <br><br>Independent team resource · No income is guaranteed · Cryptocurrency involves risk.</p>
</main>
<script src="/suite-founder.js"></script>
<script src="/chat.js" defer></script>
<script src="/translate.js" defer></script><script src="/tg-app.js" defer></script><script src="/nav-dash.js" defer></script></body></html>
+203
View File
@@ -0,0 +1,203 @@
// Founder Desk client — builds the weekly pack and manages the API key.
(function () {
'use strict';
var $ = function (id) { return document.getElementById(id); };
function esc(s) {
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
});
}
function gate(msg) {
$('dGate').style.display = 'block';
$('dGate').innerHTML = msg;
}
function copyBtn(getText) {
var b = document.createElement('button');
b.className = 'btn btn-secondary btn-sm d-copy';
b.textContent = 'Copy';
b.addEventListener('click', function () {
if (navigator.clipboard) navigator.clipboard.writeText(getText());
b.textContent = 'Copied';
setTimeout(function () { b.textContent = 'Copy'; }, 1400);
});
return b;
}
function item(tag, subject, body) {
var d = document.createElement('div');
d.className = 'd-item';
var html = '<div class="tag">' + esc(tag) + '</div>';
if (subject) html += '<div class="subj">' + esc(subject) + '</div>';
html += '<div class="body">' + esc(body) + '</div>';
d.innerHTML = html;
d.appendChild(copyBtn(function () { return (subject ? subject + '\n\n' : '') + body; }));
return d;
}
function section(title, why) {
var s = document.createElement('div');
s.className = 'd-sec';
s.innerHTML = '<h3>' + esc(title) + '</h3><div class="why">' + esc(why) + '</div>';
return s;
}
function renderPack(pack) {
var host = $('dPack');
host.innerHTML = '';
if (!pack) return;
if ((pack.posts || []).length) {
var s1 = section('Social posts — one per angle',
'Post one a day. They deliberately take different angles so the week does not read as the same message five times.');
pack.posts.forEach(function (p) { s1.appendChild(item(p.angle, '', p.text)); });
host.appendChild(s1);
}
if ((pack.messages || []).length) {
var s2 = section('Outreach messages',
'For the conversations that actually build the team — a first approach, a follow-up for someone gone quiet, and the pyramid-scheme answer.');
pack.messages.forEach(function (mm) { s2.appendChild(item(mm.label, '', mm.text)); });
host.appendChild(s2);
}
if ((pack.emails || []).length) {
var s3 = section('Email for your list', 'Drop straight into your autoresponder.');
pack.emails.forEach(function (em) { s3.appendChild(item('Email', em.subject, em.body)); });
host.appendChild(s3);
}
if ((pack.textAds || []).length) {
var s4 = section('Text ads', 'Ready to run from the Traffic Desk.');
var grid = document.createElement('div');
grid.className = 'd-ads';
pack.textAds.forEach(function (a) {
var c = document.createElement('div');
c.className = 'd-ad';
var h = '<div class="s">' + esc(a.subject) + '</div>';
(a.lines || []).forEach(function (l) { if (l) h += '<div class="l">' + esc(l) + '</div>'; });
c.innerHTML = h;
grid.appendChild(c);
});
s4.appendChild(grid);
host.appendChild(s4);
}
// Be straight about anything the engine could not produce, rather than
// quietly handing over a short pack.
if ((pack.problems || []).length) {
var warn = document.createElement('div');
warn.className = 'd-item';
warn.style.borderColor = '#f0a05a';
warn.innerHTML = '<div class="tag" style="color:#f0a05a">Incomplete</div><div class="body">' +
'The engine could not finish: ' + esc(pack.problems.join(', ')) +
'. Everything else above is fine — build again to retry the missing pieces.</div>';
host.appendChild(warn);
}
}
function renderKey(info, freshKey) {
var host = $('dKeyState');
if (freshKey) {
host.innerHTML = '<div class="k-meta"><b style="color:var(--gold)">Copy this now — it is shown once and never again.</b></div>' +
'<div class="k-key" id="dFresh">' + esc(freshKey) + '</div>';
var b = copyBtn(function () { return freshKey; });
b.style.position = 'static';
b.style.marginTop = '8px';
host.appendChild(b);
$('dIssue').textContent = 'Replace this key';
$('dRevoke').style.display = '';
return;
}
if (info && info.exists) {
host.innerHTML = '<div class="k-meta">A key is active (<b>' + esc(info.hint) + '</b>), created ' +
esc(String(info.createdAt || '').slice(0, 10)) +
(info.lastUsedAt ? ', last used ' + esc(String(info.lastUsedAt).slice(0, 10)) : ', never used yet') +
'. The key itself is stored hashed, so it cannot be shown again — create a new one if you have lost it.</div>';
$('dIssue').textContent = 'Replace this key';
$('dRevoke').style.display = '';
} else {
host.innerHTML = '<div class="k-meta">No key yet.</div>';
$('dIssue').textContent = 'Create a key';
$('dRevoke').style.display = 'none';
}
}
async function build(btn) {
btn.disabled = true;
btn.innerHTML = '<span class="spin"></span>Building your week — this takes a minute…';
$('dErr').style.display = 'none';
try {
var r = await fetch('/api/public/suite-founder', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}'
});
var d = await r.json();
if (!r.ok) {
$('dErr').textContent = d.error || 'That did not build — try again.';
$('dErr').style.display = 'block';
} else {
renderPack(d.pack);
if (d.meter) $('dMeter').textContent = d.meter.remaining + ' of ' + d.meter.limit + ' packs left this month';
}
} catch (e) {
$('dErr').textContent = 'Connection hiccup — try again.';
$('dErr').style.display = 'block';
}
btn.disabled = false;
btn.innerHTML = '👑 Build my week';
}
async function issue() {
if (!window.confirm('Create a new API key? Any existing key stops working immediately.')) return;
try {
var r = await fetch('/api/public/suite-founder', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ issueKey: true })
});
var d = await r.json();
if (r.ok) renderKey(d.info, d.key);
} catch (e) {}
}
async function revoke() {
if (!window.confirm('Revoke your API key? Anything using it stops working.')) return;
try {
var r = await fetch('/api/public/suite-founder', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ revokeKey: true })
});
var d = await r.json();
if (r.ok) renderKey(d.info, null);
} catch (e) {}
}
async function boot() {
try {
var r = await fetch('/api/public/suite-founder');
if (r.status === 401) { gate('Youre not signed in yet. <a href="/suite" style="color:var(--gold);font-weight:700">Open the Suite</a> and connect the wallet that holds your position, then come back.'); return; }
if (r.status === 403) {
var g = await r.json();
gate((g.error || 'Not open for this position yet.') + ' <a href="/suite" style="color:var(--gold);font-weight:700">See your Suite</a>.');
return;
}
if (!r.ok) return;
var d = await r.json();
$('dBody').style.display = 'block';
renderPack(d.pack);
renderKey(d.key, null);
if (d.meter) $('dMeter').textContent = d.meter.remaining + ' of ' + d.meter.limit + ' packs left this month';
if (d.writerReady === false) {
$('dGo').disabled = true;
$('dGo').textContent = 'The writer is warming up';
}
} catch (e) {}
}
document.addEventListener('DOMContentLoaded', function () {
boot();
$('dGo').addEventListener('click', function () { build(this); });
$('dIssue').addEventListener('click', issue);
$('dRevoke').addEventListener('click', revoke);
});
})();
+1 -1
View File
@@ -22,7 +22,7 @@
{ lv: 1, ico: '🚦', name: 'Traffic Desk', desc: 'Syndicated network display advertising — run banners or AI-written text ads on the teams own ad network. Monthly impressions scale with your level: 2,500 at Scintilla up to 150,000 at Corona.', href: '/suite/traffic', live: true },
{ lv: 6, ico: '🏭', name: 'Funnel Factory', desc: 'A separate hosted landing page for every audience you talk to — each on its own address, each one something you can point a different ad at.', href: '/suite/funnel', live: true },
{ lv: 7, ico: '🧭', name: 'Leader Ops', desc: 'Everyone below you, read live from the contract and sorted by who needs you most — plus a written coaching plan you can teach forward to your own two.', href: '/suite/leader', live: true },
{ lv: 8, ico: '👑', name: 'Founder Desk', desc: 'Every tool in the Suite at its highest allowance, and the top of the ladder — nothing above this to unlock.', live: false }
{ lv: 8, ico: '👑', name: 'Founder Desk', desc: 'A whole week of promotion built in one pass — posts, outreach messages, an email and text ads — plus an API key to pull your position, your organisation and the writer into anything you already run.', href: '/suite/founder', live: true }
];
var $ = function (id) { return document.getElementById(id); };
+62 -1
View File
@@ -28,6 +28,7 @@ const suiteTextAds = require('./suite-textads');
const suiteVoice = require('./suite-voice'); suiteVoice.init({ dataDir: DATA_DIR });
const suiteSplit = require('./suite-split'); suiteSplit.init({ dataDir: DATA_DIR });
const suiteLeader = require('./suite-leader');
const suiteFounder = require('./suite-founder'); suiteFounder.init({ dataDir: DATA_DIR });
const tgbot = require('./tgbot');
tgbot.init({ dataDir: DATA_DIR, chain, getConfig, messages, baseUrl: 'https://rmcircle.team' });
const SESSION_TTL = 8 * 60 * 60 * 1000;
@@ -959,6 +960,66 @@ async function handleApi(req,res,pathname){
}
}
// -- Founder Desk (level 8) -----------------------------------------------
if(pathname==='/api/public/suite-founder'){
const e=await suiteEntitlement(req).catch(()=>({error:'Chain read hiccup - try again.',code:500}));
if(e.error)return json(res,e.code||500,{error:e.error});
if(!e.inOrg||!e.allowed)return json(res,403,{error:'The Circle Suite is not open for this position yet.'});
if(Number(e.d.level)<suiteFounder.MIN_LEVEL)return json(res,403,{error:'The Founder Desk unlocks at Corona (level 8).',minLevel:suiteFounder.MIN_LEVEL});
if(req.method==='GET'){
return json(res,200,{pack:suiteFounder.loadPack(e.d.id),key:suiteFounder.keyInfo(e.d.id),
level:e.d.level,id:e.d.id,writerReady:suiteAI.configured(),
meter:suiteMeter.check(e.d.id,e.d.level,'founder')});
}
if(req.method==='POST'){
const b=await bodyJson(req)||{};
if(b.issueKey){ return json(res,200,{key:suiteFounder.issueKey(e.d.id),info:suiteFounder.keyInfo(e.d.id)}); }
if(b.revokeKey){ suiteFounder.revokeKey(e.d.id); return json(res,200,{info:suiteFounder.keyInfo(e.d.id)}); }
if(!suiteAI.configured())return json(res,503,{error:'The writer is warming up - try again shortly.'});
const gate=suiteMeter.check(e.d.id,e.d.level,'founder');
if(!gate.allowed)return json(res,429,{error:'You have used all '+gate.limit+' campaign packs this month. It resets on the 1st.',meter:gate});
try{
const pack=await suiteFounder.buildPack({id:e.d.id,voice:suiteVoice.promptBlock(e.d.id)});
suiteMeter.record(e.d.id,'founder',1);
return json(res,200,{pack:pack,meter:suiteMeter.check(e.d.id,e.d.level,'founder')});
}catch(err){ return json(res,502,{error:String(err.message||err)}); }
}
}
// -- Founder API v1: key-authenticated, read-mostly ------------------------
if(pathname.startsWith('/api/suite/v1/')){
const auth=String(req.headers['authorization']||'');
const raw=auth.replace(/^Bearer\s+/i,'').trim();
const memberId=suiteFounder.memberForKey(raw);
if(!memberId)return json(res,401,{error:'Provide your Founder Desk API key as: Authorization: Bearer <key>'});
let md=null;
try{ md=await chain.memberPublic(memberId); }catch(err){}
if(!md||!md.registered)return json(res,404,{error:'Position not found.'});
if(pathname==='/api/suite/v1/me'&&req.method==='GET'){
return json(res,200,{id:memberId,level:md.level,levelName:md.levelName,tier:md.tier,
directCount:md.directCount,qualified:(md.directCount||0)>=2,
totalEarnedPol:md.totalEarnedPol,link:'https://rmcircle.team/join/'+memberId});
}
if(pathname==='/api/suite/v1/team'&&req.method==='GET'){
return json(res,200,{scan:suiteLeader.scan(memberId,25)});
}
if(pathname==='/api/suite/v1/generate'&&req.method==='POST'){
const b=await bodyJson(req)||{};
const kind=String(b.kind||'post');
if(!suiteAI.KINDS[kind])return json(res,400,{error:'Unknown kind. One of: '+Object.keys(suiteAI.KINDS).join(', ')});
const brief=String(b.brief||'').trim().slice(0,1200);
if(brief.length<3)return json(res,400,{error:'Send a brief describing what the piece is about.'});
const gate=suiteMeter.check(memberId,md.level,'copy');
if(!gate.allowed)return json(res,429,{error:'Monthly generation allowance used.',meter:gate});
try{
const text=await suiteAI.generate(kind,brief,{link:'https://rmcircle.team/join/'+memberId,id:memberId,voice:suiteVoice.promptBlock(memberId)});
suiteMeter.record(memberId,'copy',1);
return json(res,200,{text:text,meter:suiteMeter.check(memberId,md.level,'copy')});
}catch(err){ return json(res,502,{error:String(err.message||err)}); }
}
return json(res,404,{error:'Unknown endpoint. Available: GET /api/suite/v1/me, GET /api/suite/v1/team, POST /api/suite/v1/generate'});
}
if(req.method==='POST'&&pathname==='/api/public/suite-traffic-stop'){
const e=await suiteEntitlement(req).catch(()=>({error:'Chain read hiccup — try again.',code:500}));
if(e.error)return json(res,e.code||500,{error:e.error});
@@ -1303,7 +1364,7 @@ const server=http.createServer(async(req,res)=>{
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==='/app'||pathname==='/app/')file=path.join(PUBLIC_DIR,'app.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==='/privacy'||pathname==='/privacy/')file=path.join(PUBLIC_DIR,'privacy.html');else if(pathname==='/refunds'||pathname==='/refunds/')file=path.join(PUBLIC_DIR,'refunds.html');else if(pathname==='/how-pay-works'||pathname==='/how-pay-works/')file=path.join(PUBLIC_DIR,'how-pay-works.html');else if(pathname==='/tools'||pathname==='/tools/')file=path.join(PUBLIC_DIR,'tools.html');else if(pathname==='/fast-start'||pathname==='/fast-start/')file=path.join(PUBLIC_DIR,'fast-start.html');else if(pathname==='/flyers'||pathname==='/flyers/')file=path.join(PUBLIC_DIR,'flyers.html');else if(pathname==='/weekly-rhythm'||pathname==='/weekly-rhythm/')file=path.join(PUBLIC_DIR,'weekly-rhythm.html');else if(pathname==='/generation-pay'||pathname==='/generation-pay/')file=path.join(PUBLIC_DIR,'generation-pay.html');else if(pathname==='/suite'||pathname==='/suite/')file=path.join(PUBLIC_DIR,'suite.html');else if(pathname==='/suite/copy'||pathname==='/suite/copy/')file=path.join(PUBLIC_DIR,'suite-copy.html');else if(pathname==='/suite/page'||pathname==='/suite/page/')file=path.join(PUBLIC_DIR,'suite-page.html');else if(pathname==='/suite/email'||pathname==='/suite/email/')file=path.join(PUBLIC_DIR,'suite-email.html');else if(pathname==='/suite/video'||pathname==='/suite/video/')file=path.join(PUBLIC_DIR,'suite-video.html');else if(pathname==='/suite/traffic'||pathname==='/suite/traffic/')file=path.join(PUBLIC_DIR,'suite-traffic.html');else if(pathname==='/suite/voice'||pathname==='/suite/voice/')file=path.join(PUBLIC_DIR,'suite-voice.html');else if(pathname==='/suite/split'||pathname==='/suite/split/')file=path.join(PUBLIC_DIR,'suite-split.html');else if(pathname==='/suite/funnel'||pathname==='/suite/funnel/')file=path.join(PUBLIC_DIR,'suite-funnel.html');else if(pathname==='/suite/leader'||pathname==='/suite/leader/')file=path.join(PUBLIC_DIR,'suite-leader.html');else if(pathname==='/direct-join'||pathname==='/direct-join/'){if(!getSession(req)){res.writeHead(302,{Location:'/admin'});return res.end();}file=path.join(ROOT,'private','direct-join.html');}else if(pathname==='/join-now'||pathname==='/join-now/'){if(!getConfig().dappFallbackPublic){res.writeHead(302,{Location:'/start'});return res.end();}file=path.join(ROOT,'private','join-now.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:'/join-now'});return res.end();}else{
if(pathname==='/')file=path.join(PUBLIC_DIR,'index.html');else if(pathname==='/app'||pathname==='/app/')file=path.join(PUBLIC_DIR,'app.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==='/privacy'||pathname==='/privacy/')file=path.join(PUBLIC_DIR,'privacy.html');else if(pathname==='/refunds'||pathname==='/refunds/')file=path.join(PUBLIC_DIR,'refunds.html');else if(pathname==='/how-pay-works'||pathname==='/how-pay-works/')file=path.join(PUBLIC_DIR,'how-pay-works.html');else if(pathname==='/tools'||pathname==='/tools/')file=path.join(PUBLIC_DIR,'tools.html');else if(pathname==='/fast-start'||pathname==='/fast-start/')file=path.join(PUBLIC_DIR,'fast-start.html');else if(pathname==='/flyers'||pathname==='/flyers/')file=path.join(PUBLIC_DIR,'flyers.html');else if(pathname==='/weekly-rhythm'||pathname==='/weekly-rhythm/')file=path.join(PUBLIC_DIR,'weekly-rhythm.html');else if(pathname==='/generation-pay'||pathname==='/generation-pay/')file=path.join(PUBLIC_DIR,'generation-pay.html');else if(pathname==='/suite'||pathname==='/suite/')file=path.join(PUBLIC_DIR,'suite.html');else if(pathname==='/suite/copy'||pathname==='/suite/copy/')file=path.join(PUBLIC_DIR,'suite-copy.html');else if(pathname==='/suite/page'||pathname==='/suite/page/')file=path.join(PUBLIC_DIR,'suite-page.html');else if(pathname==='/suite/email'||pathname==='/suite/email/')file=path.join(PUBLIC_DIR,'suite-email.html');else if(pathname==='/suite/video'||pathname==='/suite/video/')file=path.join(PUBLIC_DIR,'suite-video.html');else if(pathname==='/suite/traffic'||pathname==='/suite/traffic/')file=path.join(PUBLIC_DIR,'suite-traffic.html');else if(pathname==='/suite/voice'||pathname==='/suite/voice/')file=path.join(PUBLIC_DIR,'suite-voice.html');else if(pathname==='/suite/split'||pathname==='/suite/split/')file=path.join(PUBLIC_DIR,'suite-split.html');else if(pathname==='/suite/funnel'||pathname==='/suite/funnel/')file=path.join(PUBLIC_DIR,'suite-funnel.html');else if(pathname==='/suite/leader'||pathname==='/suite/leader/')file=path.join(PUBLIC_DIR,'suite-leader.html');else if(pathname==='/suite/founder'||pathname==='/suite/founder/')file=path.join(PUBLIC_DIR,'suite-founder.html');else if(pathname==='/direct-join'||pathname==='/direct-join/'){if(!getSession(req)){res.writeHead(302,{Location:'/admin'});return res.end();}file=path.join(ROOT,'private','direct-join.html');}else if(pathname==='/join-now'||pathname==='/join-now/'){if(!getConfig().dappFallbackPublic){res.writeHead(302,{Location:'/start'});return res.end();}file=path.join(ROOT,'private','join-now.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:'/join-now'});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);
+167
View File
@@ -0,0 +1,167 @@
// Circle Suite — Founder Desk (level 8, Corona).
//
// Every tier below this produces ONE thing at a time: a post, a page, a
// sequence, an ad. That is right for someone still learning what to say. At the
// top of the ladder the constraint is different — these are people running
// organisations who do not have an afternoon to spend clicking one tool at a
// time. So the Founder Desk does the whole week in a single pass, across every
// channel, and hands it over as one pack.
//
// Second half of the tool is API access: a personal key so a founder can pull
// their own position, their organisation triage and generated copy into
// whatever they already run — a spreadsheet, a bot, their own site.
'use strict';
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const suiteAi = require('./suite-ai');
const suiteEmail = require('./suite-email');
const suiteTextAds = require('./suite-textads');
const MIN_LEVEL = 8;
let DATA_DIR = null;
function init(opts) {
DATA_DIR = opts.dataDir;
try { fs.mkdirSync(packDir(), { recursive: true }); } catch (e) {}
}
function packDir() { return path.join(DATA_DIR, 'founder'); }
function packFile(id) { return path.join(packDir(), String(Number(id)) + '.json'); }
function keysFile() { return path.join(DATA_DIR, 'suite-api-keys.json'); }
// ── API keys ────────────────────────────────────────────────────────────────
// Stored hashed. We show the key exactly once, at creation, the way every other
// API does it — if it leaks later we cannot un-leak it, but at least the file
// on disk is not a list of live credentials.
function readKeys() { try { return JSON.parse(fs.readFileSync(keysFile(), 'utf8')); } catch (e) { return {}; } }
function writeKeys(v) { try { fs.writeFileSync(keysFile(), JSON.stringify(v), { mode: 0o600 }); } catch (e) {} }
function hashKey(k) { return crypto.createHash('sha256').update(String(k)).digest('hex'); }
function keyInfo(memberId) {
const all = readKeys();
const rec = all[String(memberId)];
if (!rec) return { exists: false };
return { exists: true, createdAt: rec.createdAt, lastUsedAt: rec.lastUsedAt || null, hint: rec.hint };
}
function issueKey(memberId) {
const raw = 'rmc_' + crypto.randomBytes(24).toString('hex');
const all = readKeys();
all[String(memberId)] = {
hash: hashKey(raw), createdAt: new Date().toISOString(),
hint: raw.slice(0, 8) + '…' + raw.slice(-4)
};
writeKeys(all);
return raw; // the only time this is ever returned
}
function revokeKey(memberId) {
const all = readKeys();
delete all[String(memberId)];
writeKeys(all);
return true;
}
// Constant-time compare against every stored hash. The key set is small (only
// level-8 positions), so a linear scan is fine and keeps the key itself
// unguessable from the file.
function memberForKey(raw) {
if (!raw || typeof raw !== 'string' || raw.length < 16) return null;
const h = Buffer.from(hashKey(raw), 'hex');
const all = readKeys();
let found = null;
Object.keys(all).forEach(function (id) {
const stored = Buffer.from(String(all[id].hash || ''), 'hex');
if (stored.length === h.length && crypto.timingSafeEqual(stored, h)) found = Number(id);
});
if (found != null) {
all[String(found)].lastUsedAt = new Date().toISOString();
writeKeys(all);
}
return found;
}
// ── The weekly campaign pack ────────────────────────────────────────────────
function loadPack(id) { try { return JSON.parse(fs.readFileSync(packFile(id), 'utf8')); } catch (e) { return null; } }
function savePack(id, pack) { try { fs.writeFileSync(packFile(id), JSON.stringify(pack)); } catch (e) {} }
// Angles rotate through the hooks the rest of the site already uses, so the
// week has variety built in rather than seven versions of the same post.
const WEEK_ANGLES = [
{ key: 'two', brief: 'the whole job is two people and helping them get their two' },
{ key: 'graveyard', brief: 'why this one does not disappear like every other program — public contract, holds nobody\'s money' },
{ key: 'pocket', brief: 'the entry is small and one-time, not a subscription that bleeds you every month' },
{ key: 'verify', brief: 'do not trust anyone including me — read the contract yourself before you spend a cent' },
{ key: 'phone', brief: 'it runs entirely from a phone; no laptop, no stock, no shipping' }
];
function splitNumbered(text, want) {
// Tolerant split on "1." / "POST 1" / "---" style separators.
const parts = String(text || '')
.split(/(?:^|\n)\s*(?:[-*#]{3,}|(?:POST|ITEM)?\s*\d+[.)]?\s*)(?=\n|\s)/i)
.map(function (x) { return x.replace(/^[\s*#>-]+/, '').trim(); })
.filter(function (x) { return x.length > 25; });
return parts.slice(0, want);
}
async function buildPack(opts) {
const id = Number(opts.id);
const link = 'https://rmcircle.team/join/' + id;
const member = { link: link, id: id, voice: opts.voice || '' };
const pack = { id: id, at: new Date().toISOString(), posts: [], messages: [], emails: [], textAds: [] };
const problems = [];
// 1) A week of social posts, one per angle.
try {
const postInstruction =
'Write 5 separate social media posts for a member of the RM Circle team build, one for each of these angles:\n' +
WEEK_ANGLES.map(function (a, i) { return (i + 1) + '. ' + a.brief; }).join('\n') + '\n\n' +
'Each post: 40-90 words, punchy opening line, no hashtags, at most one emoji, ending with a soft invitation to look rather than a hard sell, then this exact link on its own final line: ' + link + '\n\n' +
'ABSOLUTE RULES: never promise, guarantee, project or imply income, earnings or returns. Never quote dollar values — POL quantities only. Never mention the Standard tier. Be honest that this is real cryptocurrency with real risk. Do not invent numbers.\n\n' +
(opts.voice ? opts.voice + '\n\n' : '') +
'Separate each post with a line containing only ---. Output nothing else: no headings, no commentary, no numbering.';
const raw = await suiteAi.generateRaw(postInstruction);
pack.posts = splitNumbered(raw, 5).map(function (t, i) {
return { angle: WEEK_ANGLES[i] ? WEEK_ANGLES[i].key : 'general', text: t };
});
if (!pack.posts.length) problems.push('posts');
} catch (e) { problems.push('posts'); }
// 2) Outreach messages — a first approach, a follow-up, an objection reply.
const msgKinds = [
{ kind: 'dm', label: 'First message', brief: 'reaching out to someone you know but have not spoken to in a while' },
{ kind: 'followup', label: 'Follow-up', brief: 'they looked at your link a few days ago and went quiet' },
{ kind: 'objection', label: 'Objection reply', brief: 'they said this sounds like a pyramid scheme' }
];
for (const mk of msgKinds) {
try {
const t = await suiteAi.generate(mk.kind, mk.brief, member);
pack.messages.push({ label: mk.label, kind: mk.kind, text: t });
} catch (e) { problems.push(mk.kind); }
}
// 3) An email the founder can drop into their autoresponder.
try {
pack.emails = await suiteEmail.generate('broadcast', 'a weekly update to your list about the team build', member);
} catch (e) { problems.push('email'); }
// 4) Text ads ready for the Traffic Desk.
try {
const t = await suiteTextAds.generate({ angle: 'general', count: 3 });
pack.textAds = t.variants;
} catch (e) { problems.push('text ads'); }
pack.problems = problems;
const produced = pack.posts.length + pack.messages.length + pack.emails.length + pack.textAds.length;
if (!produced) throw new Error('The engine did not come back with anything usable — try again in a moment.');
savePack(id, pack);
return pack;
}
module.exports = {
init, MIN_LEVEL, buildPack, loadPack,
keyInfo, issueKey, revokeKey, memberForKey, WEEK_ANGLES
};
+3 -1
View File
@@ -25,7 +25,9 @@ const TOOLS = {
video: { minLevel: 3, quota: [0, 0, 4, 8, 12, 20, 20, 20], label: 'Video Maker' },
// Text ads live in the Traffic Desk, which is a level-1 tool, so generation
// has to start at level 1 too. One call returns five ready ads.
textad: { minLevel: 1, quota: [20, 30, 50, 75, 100, 150, 200, 250], label: 'Text Ad Writer' }
textad: { minLevel: 1, quota: [20, 30, 50, 75, 100, 150, 200, 250], label: 'Text Ad Writer' },
// One pack is several engine calls, so the allowance is small by design.
founder: { minLevel: 8, quota: [0, 0, 0, 0, 0, 0, 0, 12], label: 'Founder Desk' }
};
function quotaFor(tool, level) {
+1 -1
View File
@@ -17,7 +17,7 @@ const LEVEL_TOOLS = {
5: { name: 'Apex', live: true, short: 'the Split Tester, and your ad allowance jumps to 50,000 impressions a month' },
6: { name: 'Fastigium', live: true, short: 'the Funnel Factory — a separate hosted landing page for every audience you talk to' },
7: { name: 'Vertex', live: true, short: 'Leader Ops — your whole organisation triaged live from the contract, plus a coaching plan to teach forward' },
8: { name: 'Corona', live: true, short: 'the Founder Desk — every tool at its highest allowance, and the top of the ladder' }
8: { name: 'Corona', live: true, short: 'the Founder Desk — a whole week of promotion built in one pass, plus API access to the Suite' }
};
// Very short form for character-limited surfaces (X/Twitter).