Whole sequences, not one-offs. Pick the job, say who the list is, and it writes the series in the team's voice with your link — ready to paste into any autoresponder.
+
+
+
+
+
+
+
+
+
Mention anything you want included: your story, their situation, objections you keep hearing.
+
+
+
+
+
+
+
+
+
+
Teach it forward: a member with a follow-up sequence running never loses a warm prospect to silence — show your two how to set theirs up. Read every email before you send it; your name is on it. Generated copy follows the team's rules — no income promises, no invented numbers, honest about risk. Independent team resource · No income is guaranteed · Cryptocurrency involves risk.
+
+
+
+
diff --git a/public/suite-email.js b/public/suite-email.js
new file mode 100644
index 0000000..844704a
--- /dev/null
+++ b/public/suite-email.js
@@ -0,0 +1,124 @@
+// Email Engine client — sequence picker, brief, then a card per email with
+// its own copy button (people paste them one at a time into an autoresponder).
+(function () {
+ 'use strict';
+ var KINDS = [
+ { k: 'welcome', label: 'Welcome series (4)' },
+ { k: 'followup', label: 'Follow-up series (4)' },
+ { k: 'reengage', label: 'Re-engagement (3)' },
+ { k: 'broadcast', label: 'Single broadcast' }
+ ];
+ var PLACEHOLDERS = {
+ welcome: "e.g. New members who joined from my warm market — most are brand new to crypto and a little nervous",
+ followup: "e.g. People who watched my video and said they'd think about it — no pressure, keep it useful",
+ reengage: "e.g. Members who joined a month ago, got their wallet set up, then went quiet",
+ broadcast: "e.g. My marketing list — they know me from my content, not from crypto. Lead with the toolkit angle."
+ };
+ var $ = function (id) { return document.getElementById(id); };
+ var kind = 'welcome', busy = false;
+
+ function renderKinds() {
+ var host = $('emKinds');
+ host.innerHTML = '';
+ KINDS.forEach(function (x) {
+ var b = document.createElement('button');
+ b.type = 'button';
+ b.className = 'em-kind' + (x.k === kind ? ' on' : '');
+ b.textContent = x.label;
+ b.addEventListener('click', function () {
+ kind = x.k;
+ $('emBrief').placeholder = PLACEHOLDERS[kind] || '';
+ renderKinds();
+ });
+ host.appendChild(b);
+ });
+ }
+
+ function showMeter(m) {
+ if (!m || !m.limit) { $('emMeter').textContent = ''; return; }
+ $('emMeter').innerHTML = '' + m.remaining + ' of ' + m.limit + ' emails left this month';
+ }
+
+ function gate(msg) {
+ var g = $('emGate');
+ g.style.display = 'block';
+ g.innerHTML = msg;
+ $('emCard').style.opacity = '.55';
+ $('emGo').disabled = true;
+ $('emBrief').disabled = true;
+ }
+
+ function renderEmails(list) {
+ var out = $('emOut');
+ out.innerHTML = '';
+ list.forEach(function (e, i) {
+ var d = document.createElement('div');
+ d.className = 'em-mail';
+ d.innerHTML = '
Email ' + (i + 1) + '
' +
+ '
Copied.
';
+ d.querySelector('.s').textContent = e.subject;
+ d.querySelector('.b').textContent = e.body;
+ d.querySelector('button').addEventListener('click', function () {
+ navigator.clipboard.writeText('Subject: ' + e.subject + '\n\n' + e.body).then(function () {
+ var ok = d.querySelector('.ok');
+ ok.style.display = '';
+ setTimeout(function () { ok.style.display = 'none'; }, 2200);
+ });
+ });
+ out.appendChild(d);
+ });
+ out.style.display = 'block';
+ }
+
+ async function boot() {
+ renderKinds();
+ try {
+ var r = await fetch('/api/public/suite-meters');
+ if (r.status === 401) { gate('You’re not signed in yet. Open the Suite and connect the wallet that holds your position, then come back.'); return; }
+ if (r.status === 403) { gate('The Email Engine isn’t open for this position yet. See your Suite.'); return; }
+ if (!r.ok) return;
+ var d = await r.json();
+ var m = d.meters && d.meters.email;
+ if (m && m.reason === 'locked') {
+ gate('The Email Engine unlocks at Fabrica (level 3). You’re at level ' + d.level + ' — keep climbing and it opens, along with the Video Maker. See what upgrades cost →');
+ return;
+ }
+ showMeter(m);
+ } catch (e) {}
+ }
+
+ async function run() {
+ if (busy) return;
+ busy = true;
+ $('emErr').style.display = 'none';
+ $('emGo').disabled = true;
+ $('emGo').innerHTML = 'Writing the sequence… (up to two minutes)';
+ try {
+ var r = await fetch('/api/public/suite-email', {
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ kind: kind, brief: $('emBrief').value.trim() })
+ });
+ var d = await r.json();
+ if (!r.ok) {
+ $('emErr').textContent = d.error || 'That didn’t go through — try again.';
+ $('emErr').style.display = 'block';
+ if (d.meter) showMeter(d.meter);
+ } else {
+ renderEmails(d.emails || []);
+ showMeter(d.meter);
+ $('emOut').scrollIntoView({ behavior: 'smooth', block: 'nearest' });
+ }
+ } catch (e) {
+ $('emErr').textContent = 'Connection hiccup — try again.';
+ $('emErr').style.display = 'block';
+ }
+ busy = false;
+ $('emGo').disabled = false;
+ $('emGo').textContent = '✉️ Write the sequence';
+ }
+
+ document.addEventListener('DOMContentLoaded', function () {
+ boot();
+ $('emGo').addEventListener('click', run);
+ });
+})();
diff --git a/public/suite.js b/public/suite.js
index 2c27ff3..5ec18d6 100644
--- a/public/suite.js
+++ b/public/suite.js
@@ -15,7 +15,7 @@
{ lv: 1, ico: '🤖', name: 'AI Coach', desc: '24/7 answers about the system, in 21+ languages — the chat bubble on every page of this site.', href: '#', live: true, coach: true },
{ lv: 2, ico: '✍️', name: 'Copy Engine', desc: 'An AI copywriter tuned to this business: posts, DMs, follow-ups and objection replies with your link filled in.', href: '/suite/copy', live: true },
{ lv: 2, ico: '🧱', name: 'Page Builder', desc: 'Answer four questions, get your own hosted invitation page — your story, your angle video, your QR.', href: '/suite/page', live: true },
- { lv: 3, ico: '📧', name: 'Email Engine', desc: 'Follow-up sequences and broadcasts in the team voice, exportable to any autoresponder.', live: false },
+ { lv: 3, ico: '📧', name: 'Email Engine', desc: 'Welcome series, follow-up sequences and broadcasts in the team voice — exportable to any autoresponder.', href: '/suite/email', live: true },
{ lv: 3, ico: '🎥', name: 'Video Maker', desc: 'The team’s master promo videos rendered with your personal end-card — your name, your QR, your link.', live: false },
{ lv: 4, ico: '🗣️', name: 'Voice Profile + Funnels', desc: 'Output that sounds like YOU, plus multi-page funnels on your own team subdomain.', live: false },
{ lv: 5, ico: '🚦', name: 'Traffic Desk', desc: 'Syndicated network display advertising: monthly ad credits on the team’s own network, banner and text placements, rotator priority, and AI campaign packs.', live: false },
diff --git a/server.js b/server.js
index 3ec2e8a..c55e4d9 100644
--- a/server.js
+++ b/server.js
@@ -21,6 +21,7 @@ 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 suiteEmail = require('./suite-email');
const tgbot = require('./tgbot');
tgbot.init({ dataDir: DATA_DIR, chain, getConfig, messages, baseUrl: 'https://rmcircle.team' });
const SESSION_TTL = 8 * 60 * 60 * 1000;
@@ -704,6 +705,29 @@ async function handleApi(req,res,pathname){
}catch(err){ return json(res,502,{error:String(err.message||err)}); }
}
+ if(req.method==='POST'&&pathname==='/api/public/suite-email'){
+ 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.'});
+ const b=await bodyJson(req)||{};
+ const kind=String(b.kind||'welcome');
+ const brief=String(b.brief||'').trim().slice(0,1200);
+ if(!suiteEmail.KINDS[kind])return json(res,400,{error:'Unknown sequence type.'});
+ if(!suiteAI.configured())return json(res,503,{error:'The Email Engine is warming up — try again shortly.'});
+ const gate=suiteMeter.check(e.d.id,e.d.level,'email');
+ if(!gate.allowed){
+ return json(res,429,{error:gate.reason==='locked'
+ ? 'The Email Engine unlocks at Fabrica (level 3). Your next upgrades open it.'
+ : 'You have used all '+gate.limit+' email generations this month. It resets on the 1st — or a level upgrade raises your allowance.',meter:gate});
+ }
+ try{
+ const link='https://rmcircle.team/join/'+e.d.id;
+ const emails=await suiteEmail.generate(kind,brief,{link:link,id:e.d.id});
+ suiteMeter.record(e.d.id,'email',emails.length||1);
+ return json(res,200,{emails:emails,meter:suiteMeter.check(e.d.id,e.d.level,'email')});
+ }catch(err){ return json(res,502,{error:String(err.message||err)}); }
+ }
+
if(req.method==='GET'&&pathname==='/api/public/suite-meters'){
const e=await suiteEntitlement(req).catch(()=>({error:'Chain read hiccup.',code:500}));
if(e.error)return json(res,e.code||500,{error:e.error});
@@ -1037,7 +1061,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==='/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==='/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);
diff --git a/suite-email.js b/suite-email.js
new file mode 100644
index 0000000..ab60790
--- /dev/null
+++ b/suite-email.js
@@ -0,0 +1,92 @@
+// Circle Suite — Email Engine (L3). Same engine, same compliance layer as the
+// Copy Engine; the difference is shape: it writes SEQUENCES, not single pieces.
+//
+// Output contract is the same trick that works elsewhere: labelled plain text
+// parsed deterministically, with a forgiving parser (models drop colons and
+// add markdown emphasis).
+'use strict';
+const suiteAI = require('./suite-ai');
+
+const KINDS = {
+ welcome: {
+ label: 'Welcome sequence',
+ count: 4,
+ brief: 'a 4-email welcome sequence for someone who just joined the team under this member. Email 1 welcomes them and points at the first 48 hours. Email 2 covers getting their first two. Email 3 handles the doubt that shows up in week one. Email 4 is about teaching their two to do the same.'
+ },
+ followup: {
+ label: 'Follow-up sequence',
+ count: 4,
+ brief: 'a 4-email follow-up sequence for someone who looked at the opportunity but has not joined. Space the emails a few days apart in tone. No pressure, no fake deadlines, no guilt. Each email should be useful on its own and easy to ignore.'
+ },
+ reengage: {
+ label: 'Re-engagement sequence',
+ count: 3,
+ brief: 'a 3-email re-engagement sequence for a member who joined but went quiet. Warm, no shaming, assume life got busy. Make the next step small and specific.'
+ },
+ broadcast: {
+ label: 'Single broadcast',
+ count: 1,
+ brief: 'ONE standalone broadcast email to a list about the team build.'
+ }
+};
+
+function buildPrompt(kind, brief, member) {
+ const k = KINDS[kind] || KINDS.broadcast;
+ const link = (member && member.link) || '';
+ return (
+ 'Write ' + k.brief + '\n\n' +
+ 'CONTEXT FROM THE MEMBER (who the list is, what to emphasise):\n' +
+ (brief || '(nothing specific — write something honest and general)') + '\n\n' +
+ 'Return EXACTLY this shape, repeated ' + k.count + ' time(s), and nothing else:\n' +
+ 'EMAIL 1\n' +
+ 'SUBJECT: one line, under 60 characters, no clickbait, no ALL CAPS\n' +
+ 'BODY: 90-170 words, plain text, short paragraphs separated by blank lines, ending with a sign-off line\n' +
+ (k.count > 1 ? 'EMAIL 2\nSUBJECT: ...\nBODY: ...\n(and so on)\n' : '') + '\n' +
+ (link ? 'Where a link belongs, use exactly: ' + link + '\n' : 'Do not invent a link.\n') +
+ 'No HTML, no markdown headers, no emoji in subject lines, no merge tags.'
+ );
+}
+
+// Tolerant parse: EMAIL n / SUBJECT / BODY, colon and emphasis optional.
+function parseEmails(raw) {
+ const text = String(raw || '').replace(/\r/g, '');
+ const marks = [];
+ const re = /(?:^|\n)[ \t]*[*#>\s]*EMAIL[ \t]*#?\s*(\d+)[ \t]*[*#]*[ \t]*:?[ \t]*/gi;
+ let m;
+ while ((m = re.exec(text)) !== null) marks.push({ n: Number(m[1]), at: m.index, from: m.index + m[0].length });
+ const chunks = [];
+ if (!marks.length) {
+ chunks.push(text);
+ } else {
+ marks.forEach(function (mk, i) {
+ chunks.push(text.slice(mk.from, i + 1 < marks.length ? marks[i + 1].at : text.length));
+ });
+ }
+ const out = [];
+ chunks.forEach(function (c) {
+ const sm = c.match(/(?:^|\n)[ \t]*[*#>\s]*SUBJECT[ \t]*[*#]*[ \t]*:?[ \t]*[*#]*[ \t]*(.*)/i);
+ const bm = c.match(/(?:^|\n)[ \t]*[*#>\s]*BODY[ \t]*[*#]*[ \t]*:?[ \t]*[*#]*[ \t]*([\s\S]*)/i);
+ const subject = sm ? sm[1].replace(/[*#]+/g, '').trim().slice(0, 120) : '';
+ let body = bm ? bm[1] : (sm ? c.slice(c.indexOf(sm[0]) + sm[0].length) : c);
+ body = String(body).replace(/^[*#\s]+/, '').trim();
+ if (subject || body) out.push({ subject: subject || '(no subject)', body: body });
+ });
+ return out.filter(function (e) { return e.body && e.body.length > 20; });
+}
+
+async function generate(kind, brief, member) {
+ const raw = await suiteAI.generateRaw(buildPrompt(kind, brief, member));
+ let emails = parseEmails(raw);
+ if (!emails.length) throw new Error('The writer came back incomplete — try again.');
+ const link = member && member.link;
+ if (link) {
+ emails = emails.map(function (e) {
+ let b = e.body.replace(/https?:\/\/(?:www\.)?rmcircle\.team\/\S*/gi, link);
+ b = b.replace(/\[LINK\]|\{link\}/gi, link);
+ return { subject: e.subject, body: b };
+ });
+ }
+ return emails;
+}
+
+module.exports = { KINDS, generate, parseEmails };