6a7c5161ff
Marty's read was right — the upper tiers felt flat, and the reason is that they scaled QUANTITY, not KIND. L1->L2 was a category change (you couldn't write, now you can). Everything above L4 was a multiplier on a capability already owned at L2: sound more like you, measure it, more pages, five at once. Meanwhile the price doubles each rung — Corona costs ~17x Culmen. No batch button is worth 17x a voice profile. The fix is a change of axis. Every tool from L1 to L8 was a "me" tool, but past the first levels this business isn't about you: get two, help your two, teach them to teach. So the top now operates on the ORGANISATION. L7 Team Grants — hand Suite capacity down to anyone in your own org. Deliberately from a separate pool that the level grants, NOT out of the leader's own allowance: making someone choose between equipping their team and using their own tools means nobody ever grants anything. Recipients are verified in-org against the contract. Grants can lift a tool the recipient's level hasn't unlocked — that's the point, not a loophole. Being helped is made visible to the recipient, since half the value is knowing an upline backed you. L8 Network Intelligence — every team ad pooled and anonymised: which angles, headlines, creative and formats actually earn their impressions. No member alone has enough traffic to learn anything from display; together they do. It can't be bought, and it compounds monthly with no further work. Holds two lines: nothing identifies anyone, and nothing under 2,000 impressions gets a reported rate — under-evidenced rows are counted and disclosed rather than silently dropped. The written readout is hand-authored, not generated: these are conclusions about money, and a model paraphrasing a table is exactly where an invented number slips in. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
100 lines
4.3 KiB
JavaScript
100 lines
4.3 KiB
JavaScript
// Circle Suite usage metering. Per-position, per-tool, per-calendar-month
|
|
// counters with level-based quotas. Storage matches the house pattern: one
|
|
// JSON file in the data volume, read/written on demand (low volume, human
|
|
// scale — a member generating copy is not a hot path).
|
|
//
|
|
// Quotas rise with level because cost should scale with commitment, and the
|
|
// meter is ALWAYS visible to the member: no silent throttling, no surprise
|
|
// cutoffs. Hitting a cap shows what unlocks more, never a dead end.
|
|
'use strict';
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const suiteGrants = require('./suite-grants');
|
|
|
|
let DATA_DIR = null;
|
|
function init(opts) { DATA_DIR = opts.dataDir; }
|
|
function file() { return path.join(DATA_DIR, 'suite-usage.json'); }
|
|
function readAll() { try { return JSON.parse(fs.readFileSync(file(), 'utf8')); } catch (e) { return {}; } }
|
|
function writeAll(v) { try { fs.writeFileSync(file(), JSON.stringify(v)); } catch (e) {} }
|
|
function monthKey() { const d = new Date(); return d.getUTCFullYear() + '-' + String(d.getUTCMonth() + 1).padStart(2, '0'); }
|
|
|
|
// tool -> minimum level, and per-level monthly allowance (index = level-1).
|
|
const TOOLS = {
|
|
copy: { minLevel: 2, quota: [0, 150, 250, 400, 600, 800, 1000, 1500], label: 'Copy Engine' },
|
|
page: { minLevel: 2, quota: [0, 3, 5, 15, 25, 50, 50, 50], label: 'Page Builder' },
|
|
email: { minLevel: 3, quota: [0, 0, 100, 200, 300, 500, 500, 500], label: 'Email Engine' },
|
|
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' },
|
|
// 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) {
|
|
const t = TOOLS[tool];
|
|
if (!t) return 0;
|
|
const lv = Math.max(1, Math.min(8, Number(level) || 1));
|
|
if (lv < t.minLevel) return 0;
|
|
return t.quota[lv - 1] || 0;
|
|
}
|
|
|
|
// Capacity handed down by an upline this month. It is added on TOP of the
|
|
// level quota and, deliberately, can lift a tool the member's own level has
|
|
// not unlocked yet — a leader giving page builds to someone on Scintilla is
|
|
// exactly the point of the feature, not a loophole in it.
|
|
function grantedFor(memberId, tool) {
|
|
try { return suiteGrants.receivedBy(memberId, tool) || 0; } catch (e) { return 0; }
|
|
}
|
|
|
|
function used(memberId, tool) {
|
|
const all = readAll();
|
|
const m = all[monthKey()] || {};
|
|
const rec = m[String(memberId)] || {};
|
|
return Number(rec[tool] || 0);
|
|
}
|
|
|
|
// Can this position use this tool right now? Returns the full meter state so
|
|
// the UI can render it without a second call.
|
|
function check(memberId, level, tool) {
|
|
const t = TOOLS[tool];
|
|
if (!t) return { allowed: false, reason: 'unknown-tool', used: 0, limit: 0, remaining: 0 };
|
|
const base = quotaFor(tool, level);
|
|
const granted = grantedFor(memberId, tool);
|
|
const limit = base + granted;
|
|
const u = used(memberId, tool);
|
|
if (limit <= 0) {
|
|
return { allowed: false, reason: 'locked', minLevel: t.minLevel, used: u, limit: 0, remaining: 0, granted: 0, label: t.label };
|
|
}
|
|
const remaining = Math.max(0, limit - u);
|
|
return {
|
|
allowed: remaining > 0,
|
|
reason: remaining > 0 ? 'ok' : 'quota',
|
|
used: u, limit, base, granted, remaining, label: t.label, resets: monthKey()
|
|
};
|
|
}
|
|
|
|
function record(memberId, tool, n) {
|
|
const all = readAll();
|
|
const mk = monthKey();
|
|
if (!all[mk]) all[mk] = {};
|
|
const id = String(memberId);
|
|
if (!all[mk][id]) all[mk][id] = {};
|
|
all[mk][id][tool] = Number(all[mk][id][tool] || 0) + (Number(n) || 1);
|
|
// keep only the last 3 months of counters — the file stays tiny forever
|
|
const keep = Object.keys(all).sort().slice(-3);
|
|
const trimmed = {};
|
|
keep.forEach(function (k) { trimmed[k] = all[k]; });
|
|
writeAll(trimmed);
|
|
return all[mk][id][tool];
|
|
}
|
|
|
|
// Every meter for one position — powers the Suite's usage strip.
|
|
function meters(memberId, level) {
|
|
const out = {};
|
|
Object.keys(TOOLS).forEach(function (tool) { out[tool] = check(memberId, level, tool); });
|
|
return out;
|
|
}
|
|
|
|
module.exports = { init, check, record, meters, quotaFor, TOOLS };
|