Files
rm-circle-team-router/suite-ai.js
T
martbost 3f8d23d66c Circle Suite: complete the ladder — every paid level now unlocks live software
Four new tools, so no tier is a placeholder any more:

L4 Voice Profile (/suite/voice) — five short answers that ride along with
every Copy Engine and Email Engine generation. It sits BEFORE the compliance
block in the prompt on purpose: a personal voice must never be able to talk
the model out of the honesty rules. suite-email builds its own prompt, so the
profile is threaded in there separately or email would keep sounding generic
while the Copy Engine sounded like the member.

L5 Split Tester (/suite/split) — 2-3 variants launched as one test with the
impressions split evenly, then click counters compared. Deliberately
conservative: it will not declare a winner until both arms have real volume
AND the leader is clearly ahead, because a 3-click gap on 400 impressions is
noise. A genuine tie is reported as a tie, which is useful information too.
A failed arm rolls back the whole test rather than leaving half of it running.
Apex is the right home for it — that is where impressions jump to 50,000.

L6 Funnel Factory (/suite/funnel) — extra named pages at /p/<id>/<slug>, so a
leader can run one page per audience and point different ads at each. Missing
slugs fall back to the member's main page, same no-dead-ends rule as /p/<id>.

L7 Leader Ops (/suite/leader) — the coaching radar already existed in chain.js
and only admin could see it, which was backwards: the leaders running those
legs need it more than anyone. Now scoped to the member's own organisation,
with a written plan built on contract-read facts only. Follows the
teach-forward rule — the digest gives the leader words to hand down, not just
numbers to act on.

Tile copy across the wall and the payout alerts now describes what actually
exists rather than what was sketched. suite-tools.js was stale in both
directions (it still had the Traffic Desk at L5 and L3 not live).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-28 10:49:36 -05:00

138 lines
8.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Circle Suite AI layer — talks to the Suite's own Hermes engine (provisioned
// like a BV tenant, own $10-capped OpenRouter key). Creds live in the data
// volume as suite-engine.json, never in env or the repo.
//
// Design rule: the RM knowledge and the compliance rules are OUR system prompt,
// sent with every request. They live in version control, not in the engine's
// knowledge base, so a rule change is a deploy — not a retraining.
'use strict';
const fs = require('fs');
const path = require('path');
let DATA_DIR = null;
function init(opts) { DATA_DIR = opts.dataDir; }
function creds() {
try { return JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'suite-engine.json'), 'utf8')); }
catch (e) { return null; }
}
function configured() { const c = creds(); return !!(c && c.url && c.key); }
// ── The non-negotiables. Every generation carries these. ────────────────────
const COMPLIANCE = [
'NEVER promise, guarantee, project or imply income, earnings, returns or profit.',
'NEVER use hype like "guaranteed", "risk-free", "passive income", "get rich", "financial freedom is certain".',
'Do NOT invent numbers. The ONLY money figures you may state: Premium entry is 362 POL; the first upgrade (Ascensus) is 621.40 POL; upgrade prices double at each level. If you are unsure of a figure, leave it out.',
'NEVER quote dollar values — POL\'s price moves. Speak in POL quantities or plain language only.',
'We promote the PREMIUM tier only. Never mention the Standard tier or its prices.',
'Be honest that this involves real cryptocurrency and real risk, and that results depend on effort — especially in anything longer than a couple of sentences.',
'Never claim the team or the contract does something it does not. No fake urgency, no fake scarcity, no invented testimonials or member results.'
].join(' ');
const FACTS = [
'The RM Circle is a crypto team build running on a public, verified smart contract on Polygon (0x33BdAEEfd6d17D80aE53816c916dFb26c4fB2DAF). The code cannot be changed and holds no member funds — every payment is person-to-person in the same transaction, and anyone can verify it on-chain.',
'The whole job: get 2 personal referrals to qualify, help those 2 get their 2, and teach them to do the same. Depth over width.',
'Two income streams: your directs\' entry payments come to you, and upgrade payments travel up the matrix to the first qualified upline whose level is at or above the level the buyer is leaving ("your level is your reach").',
'Upgrades carry no fees — 100% goes to a member. The 5% admin charge exists on entries only.',
'Levels: Scintilla, Ascensus, Fabrica, Culmen, Apex, Fastigium, Vertex, Corona.',
'Every paid position also gets a license to the Circle Suite — the team\'s marketing toolkit (promo center, printable handouts with the member\'s QR, training course, live dashboard, AI coach, and more as it ships).'
].join(' ');
const VOICE = 'Write like a straight-talking internet marketer who has been around long enough to hate hype: plain words, short sentences, honest about risk, warm and confident, never salesy or breathless. Contractions are good. No corporate filler. No emoji unless the format asks for it.';
// kind -> {label, shape}
const KINDS = {
post: { label: 'Social post', shape: 'Write ONE social media post of 40-90 words. Punchy opening line. No hashtags. At most one emoji. End with a soft invitation to look, not a hard sell.' },
dm: { label: 'Direct message', shape: 'Write ONE short direct message to send a friend or contact, 30-60 words. Conversational, personal, zero pressure. It should sound like a text from a real person, not a pitch. Ask a question or invite them to take a look.' },
followup: { label: 'Follow-up message', shape: 'Write ONE short follow-up message (30-70 words) to someone who looked but has not decided. No guilt, no pressure, no fake urgency. Be useful and easy to say no to.' },
objection: { label: 'Objection reply', shape: 'Write ONE reply (60-120 words) that answers the objection honestly. Concede whatever is true in it first, then give the real answer, then invite them to verify for themselves. Never argue.' },
email: { label: 'Email', shape: 'Write ONE short email: a subject line on the first line prefixed "Subject: ", then a blank line, then a 90-160 word body and a sign-off line. Plain text, no HTML.' },
story: { label: 'Short story/testimonial-style post', shape: 'Write ONE first-person post of 80-140 words telling a small, believable, PERSONAL story about the member\'s own experience or thinking. Do NOT invent earnings, results, or events — keep it about motivation, doubts, or what they are learning.' }
};
function buildMessages(kind, brief, member) {
const k = KINDS[kind] || KINDS.post;
const link = member && member.link ? member.link : '';
const sys =
'You write promotional copy for a member of the RM Circle team.\n\n' +
'WHAT IT IS: ' + FACTS + '\n\n' +
'VOICE: ' + VOICE + '\n\n' +
// The member's own voice profile, when they have one (level 4+). It sits
// BEFORE compliance deliberately: compliance must be the last word in the
// prompt, so a personal voice can never talk the model out of it.
(member && member.voice ? member.voice + '\n\n' : '') +
'COMPLIANCE (absolute, overrides everything else): ' + COMPLIANCE + '\n\n' +
'FORMAT: ' + k.shape + '\n\n' +
(link
? 'CALL TO ACTION: end with this exact link on its own final line, nothing after it: ' + link + '\n\n'
: 'Do not invent a link.\n\n') +
'Output ONLY the finished copy. No preamble, no explanation, no options, no quotation marks around it, no markdown headers.';
const user = 'Write ' + k.label.toLowerCase() + '.\n\nWhat this piece is about / who it is for:\n' + brief;
return [{ role: 'system', content: sys }, { role: 'user', content: user }];
}
// Engine call. Long timeout: the agent narrates internally before answering.
function generate(kind, brief, member) {
return call(buildMessages(kind, brief, member)).then(function (t) { return cleanup(t, member); });
}
function call(messages) {
return new Promise(function (resolve, reject) {
const c = creds();
if (!c) return reject(new Error('The engine is not configured yet.'));
const body = JSON.stringify({ model: c.model || 'hermes-agent', messages: messages });
const u = new URL(c.url.replace(/\/$/, '') + '/chat/completions');
const lib = u.protocol === 'https:' ? require('https') : require('http');
const req = lib.request({
hostname: u.hostname, port: u.port, path: u.pathname, method: 'POST',
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body), 'Authorization': 'Bearer ' + c.key }
}, function (res) {
let data = '';
res.on('data', function (d) { data += d; });
res.on('end', function () {
if (res.statusCode !== 200) return reject(new Error('Engine returned ' + res.statusCode));
try {
const j = JSON.parse(data);
const text = (j.choices && j.choices[0] && j.choices[0].message && j.choices[0].message.content || '').trim();
if (!text) return reject(new Error('The engine came back empty — try again.'));
resolve(text);
} catch (e) { reject(new Error('Could not read the engine response.')); }
});
});
req.on('error', function (e) { reject(new Error('Could not reach the engine: ' + e.message)); });
req.setTimeout(240000, function () { req.destroy(new Error('The engine took too long — try again.')); });
req.write(body); req.end();
});
}
// Deterministic post-processing: strip scaffolding the model sometimes adds,
// and make sure the member's real link is the one that ships.
function cleanup(text, member) {
let t = String(text || '');
t = t.replace(/^```[a-z]*\s*/i, '').replace(/```\s*$/i, '');
t = t.replace(/^(here('|’)s|here is)[^\n:]{0,60}:\s*/i, '');
t = t.replace(/^["“](.+)["”]$/s, '$1');
const link = member && member.link;
if (link) {
// any rmcircle link the model produced becomes the member's own
t = t.replace(/https?:\/\/(?:www\.)?rmcircle\.team\/\S*/gi, link);
t = t.replace(/\[LINK\]|\{link\}/gi, link);
if (t.indexOf(link) === -1) t = t.replace(/\s*$/, '\n\n' + link);
}
return t.trim();
}
// Raw generation for tools that supply their own instruction (Page Builder).
// Still carries the facts + compliance + voice; only FORMAT is the caller's.
function generateRaw(instruction) {
return call([
{ role: 'system', content:
'You write for a member of the RM Circle team.\n\nWHAT IT IS: ' + FACTS +
'\n\nVOICE: ' + VOICE +
'\n\nCOMPLIANCE (absolute): ' + COMPLIANCE +
'\n\nFollow the requested output format EXACTLY. No preamble, no explanation, no markdown fences.' },
{ role: 'user', content: instruction }
]);
}
module.exports = { init, configured, generate, generateRaw, KINDS };