Give the top of the ladder a real prize: Team Grants + Network Intelligence

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>
This commit is contained in:
martbost
2026-08-28 12:26:39 -05:00
parent 1d52597c1c
commit 6a7c5161ff
11 changed files with 760 additions and 11 deletions
+143
View File
@@ -0,0 +1,143 @@
// Circle Suite — Team Grants (Vertex L7, Corona L8).
//
// The problem this fixes: every tool from L1 to L8 was a "me" tool — help ME
// write, help ME post, help ME measure. But past the first couple of levels the
// business is not about you. It is get two, help your two, teach them to teach.
// So the top of the ladder was scaling the wrong axis: more of your own
// capability, at double the price each rung.
//
// A grant lets a leader hand Suite capacity DOWN into their own organisation —
// impressions, generations, page builds, ad batches. Three things make it work:
//
// 1. It comes from a POOL that is a perk of the level, not a tax on the
// leader's own allowance. Making leaders choose between equipping their
// team and using their own tools would just mean nobody grants anything.
// 2. It can only go to someone inside the grantor's own organisation, checked
// against the contract. This is not a way to hand capacity to strangers.
// 3. It expires with the month, like every other allowance here. Grants are
// for unblocking someone now, not for building up a balance.
'use strict';
const fs = require('fs');
const path = require('path');
let DATA_DIR = null;
function init(opts) { DATA_DIR = opts.dataDir; }
function file() { return path.join(DATA_DIR, 'team-grants.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'); }
const MIN_LEVEL = 7;
// What a leader can give away each month, by level (index = level - 1).
// Zero below Vertex — this is the thing the top of the ladder buys.
const POOLS = {
traffic: { label: 'Ad impressions', unit: 'impressions', step: 1000, max: 50000,
pool: [0, 0, 0, 0, 0, 0, 60000, 200000] },
copy: { label: 'Copy Engine generations', unit: 'generations', step: 25, max: 300,
pool: [0, 0, 0, 0, 0, 0, 300, 1000] },
page: { label: 'Page builds', unit: 'pages', step: 1, max: 20,
pool: [0, 0, 0, 0, 0, 0, 15, 50] },
textad: { label: 'Text ad batches', unit: 'batches', step: 5, max: 60,
pool: [0, 0, 0, 0, 0, 0, 60, 200] }
};
function tools() {
return Object.keys(POOLS).map(function (k) {
return { key: k, label: POOLS[k].label, unit: POOLS[k].unit, step: POOLS[k].step, max: POOLS[k].max };
});
}
function poolFor(tool, level) {
const t = POOLS[tool];
if (!t) return 0;
const lv = Math.max(1, Math.min(8, Number(level) || 1));
return t.pool[lv - 1] || 0;
}
function rows(mk) {
const all = readAll();
return all[mk || monthKey()] || [];
}
// How much of `tool` this position has RECEIVED this month.
function receivedBy(memberId, tool) {
return rows().reduce(function (s, r) {
return s + ((Number(r.to) === Number(memberId) && r.tool === tool) ? Number(r.n) || 0 : 0);
}, 0);
}
// How much of `tool` this position has GIVEN AWAY this month.
function givenBy(memberId, tool) {
return rows().reduce(function (s, r) {
return s + ((Number(r.by) === Number(memberId) && r.tool === tool) ? Number(r.n) || 0 : 0);
}, 0);
}
// Who gave it to them — so a recipient can see it came from their upline and
// not from nowhere. That is half the point: it should feel like being helped.
function receivedDetail(memberId) {
const out = {};
rows().forEach(function (r) {
if (Number(r.to) !== Number(memberId)) return;
if (!out[r.tool]) out[r.tool] = { total: 0, from: [] };
out[r.tool].total += Number(r.n) || 0;
out[r.tool].from.push({ by: Number(r.by), n: Number(r.n), at: r.at });
});
return out;
}
function status(memberId, level) {
const t = {};
Object.keys(POOLS).forEach(function (k) {
const pool = poolFor(k, level);
const given = givenBy(memberId, k);
t[k] = {
label: POOLS[k].label, unit: POOLS[k].unit, step: POOLS[k].step, max: POOLS[k].max,
pool: pool, given: given, remaining: Math.max(0, pool - given)
};
});
return {
level: Number(level) || 0, canGrant: Number(level) >= MIN_LEVEL,
tools: t, resets: monthKey(),
history: rows().filter(function (r) { return Number(r.by) === Number(memberId); }).slice(-40).reverse(),
received: receivedDetail(memberId)
};
}
// Hand capacity to someone. The caller is responsible for having verified that
// `to` is inside `by`'s organisation — that check needs the chain and lives in
// the route, not here.
function grant(opts) {
const by = Number(opts.by), to = Number(opts.to);
const tool = String(opts.tool || '');
const n = Math.floor(Number(opts.n) || 0);
const level = Number(opts.level) || 0;
if (!POOLS[tool]) throw new Error('That is not something you can grant.');
if (level < MIN_LEVEL) throw new Error('Team Grants unlock at Vertex (level 7).');
if (!to || to === by) throw new Error('Pick someone in your team other than yourself.');
if (n <= 0) throw new Error('Choose how much to give.');
if (n > POOLS[tool].max) throw new Error('The most you can give one person at a time is ' + POOLS[tool].max.toLocaleString() + '.');
const pool = poolFor(tool, level);
const given = givenBy(by, tool);
if (given + n > pool) {
throw new Error('That is more than you have left to give this month — ' +
Math.max(0, pool - given).toLocaleString() + ' ' + POOLS[tool].unit + ' remaining.');
}
const all = readAll();
const mk = monthKey();
if (!all[mk]) all[mk] = [];
const rec = { by: by, to: to, tool: tool, n: n, at: new Date().toISOString() };
all[mk].push(rec);
// Keep three months, same as every other ledger here.
const keep = Object.keys(all).sort().slice(-3);
const trimmed = {};
keep.forEach(function (k) { trimmed[k] = all[k]; });
writeAll(trimmed);
return rec;
}
module.exports = { init, MIN_LEVEL, POOLS, tools, poolFor, status, grant, receivedBy, givenBy, receivedDetail };