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:
+143
@@ -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 };
|
||||
Reference in New Issue
Block a user