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>
180 lines
7.0 KiB
JavaScript
180 lines
7.0 KiB
JavaScript
// Circle Suite — Network Intelligence (Corona, L8).
|
|
//
|
|
// Every member running ads is generating an answer to the same question: which
|
|
// wording actually makes a stranger click. Until now each of them only saw
|
|
// their own slice, which on display inventory is far too small to learn from —
|
|
// a member with 5,000 impressions and 4 clicks knows nothing.
|
|
//
|
|
// Pooled across the whole team it becomes a real asset, and one that cannot be
|
|
// bought or copied: it is the team's own accumulated result. It also gets
|
|
// better every month without anyone building anything, which is exactly what
|
|
// the top of a ladder should feel like.
|
|
//
|
|
// Two rules it holds to:
|
|
// 1. NOTHING identifies a member. No ids, no names, no per-person numbers.
|
|
// This is a picture of what works, not a leaderboard of who is working.
|
|
// 2. It refuses to report anything that has not earned the right to be
|
|
// reported. Display click rates are tiny, so a row with 300 impressions
|
|
// behind it is noise. Under-evidenced rows are counted and disclosed, not
|
|
// quietly dropped, so the reader knows what was left out.
|
|
'use strict';
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const suiteTraffic = require('./suite-traffic');
|
|
|
|
let DATA_DIR = null;
|
|
function init(opts) { DATA_DIR = opts.dataDir; }
|
|
|
|
const MIN_LEVEL = 8;
|
|
const MIN_SERVED = 2000; // per row, before we will report a rate
|
|
const MIN_ROWS = 2; // before we will compare rows at all
|
|
|
|
function readJson(f) { try { return JSON.parse(fs.readFileSync(path.join(DATA_DIR, f), 'utf8')); } catch (e) { return {}; } }
|
|
|
|
// Pull every campaign the team has run, from every month we still hold.
|
|
function allCampaigns() {
|
|
const led = readJson('traffic-grants.json');
|
|
const out = [];
|
|
Object.keys(led).forEach(function (mk) {
|
|
const byMember = led[mk] || {};
|
|
Object.keys(byMember).forEach(function (mid) {
|
|
(byMember[mid] || []).forEach(function (c) {
|
|
out.push({
|
|
adId: Number(c.adId), kind: c.kind || 'banner', size: c.size || '',
|
|
creative: c.creative || '', subject: c.subject || '',
|
|
target: c.target || '', month: mk
|
|
});
|
|
});
|
|
});
|
|
});
|
|
return out;
|
|
}
|
|
|
|
// The angle is encoded in the destination (?v=pocket etc). Personal pages carry
|
|
// no angle, so they group separately rather than being lumped into "general".
|
|
function angleOf(target) {
|
|
const m = String(target || '').match(/[?&]v=([a-z]+)/i);
|
|
if (m) return m[1].toLowerCase();
|
|
if (String(target).indexOf('/p/') !== -1) return 'personal page';
|
|
return 'general';
|
|
}
|
|
|
|
function blankRow(label) {
|
|
return { label: label, served: 0, clicks: 0, ads: 0 };
|
|
}
|
|
|
|
function finish(map) {
|
|
const rows = Object.keys(map).map(function (k) {
|
|
const r = map[k];
|
|
r.rate = r.served > 0 ? r.clicks / r.served : 0;
|
|
r.enough = r.served >= MIN_SERVED;
|
|
return r;
|
|
});
|
|
const solid = rows.filter(function (r) { return r.enough; }).sort(function (a, b) { return b.rate - a.rate; });
|
|
const thin = rows.filter(function (r) { return !r.enough; });
|
|
return {
|
|
rows: solid,
|
|
thin: thin.length,
|
|
thinServed: thin.reduce(function (s, r) { return s + r.served; }, 0),
|
|
comparable: solid.length >= MIN_ROWS
|
|
};
|
|
}
|
|
|
|
async function report() {
|
|
const camps = allCampaigns();
|
|
if (!camps.length) return { ready: false, reason: 'No campaigns have run yet — this fills in as the team advertises.' };
|
|
|
|
// NAS caps a stats call, so batch.
|
|
const ids = camps.map(function (c) { return c.adId; }).filter(Boolean);
|
|
const stats = {};
|
|
for (let i = 0; i < ids.length; i += 150) {
|
|
try {
|
|
const chunk = await suiteTraffic.stats(ids.slice(i, i + 150));
|
|
chunk.forEach(function (s) { stats[s.ad_id] = s; });
|
|
} catch (e) { /* partial data still beats none */ }
|
|
}
|
|
if (!Object.keys(stats).length) {
|
|
return { ready: false, reason: 'The ad network did not return counters just now — try again shortly.' };
|
|
}
|
|
|
|
const byFormat = {}, bySize = {}, byAngle = {}, byCreative = {}, byHeadline = {};
|
|
let totalServed = 0, totalClicks = 0, counted = 0;
|
|
|
|
camps.forEach(function (c) {
|
|
const s = stats[c.adId];
|
|
if (!s) return;
|
|
const served = Math.max(0, Number(s.served) || 0);
|
|
const clicks = Math.max(0, Number(s.hits) || 0);
|
|
if (served <= 0) return;
|
|
counted++;
|
|
totalServed += served; totalClicks += clicks;
|
|
|
|
const add = function (map, key, label) {
|
|
if (!key) return;
|
|
if (!map[key]) map[key] = blankRow(label || key);
|
|
map[key].served += served; map[key].clicks += clicks; map[key].ads++;
|
|
};
|
|
|
|
add(byFormat, c.kind === 'text' ? 'text' : 'banner', c.kind === 'text' ? 'Text ads' : 'Banner ads');
|
|
add(byAngle, angleOf(c.target));
|
|
if (c.kind === 'text') {
|
|
if (c.subject) add(byHeadline, c.subject.toLowerCase(), c.subject);
|
|
} else {
|
|
if (c.size) add(bySize, c.size);
|
|
if (c.creative) add(byCreative, c.creative);
|
|
}
|
|
});
|
|
|
|
return {
|
|
ready: true,
|
|
totals: {
|
|
ads: counted, served: totalServed, clicks: totalClicks,
|
|
rate: totalServed > 0 ? totalClicks / totalServed : 0,
|
|
months: Object.keys(readJson('traffic-grants.json')).length
|
|
},
|
|
format: finish(byFormat),
|
|
angle: finish(byAngle),
|
|
size: finish(bySize),
|
|
creative: finish(byCreative),
|
|
headline: finish(byHeadline),
|
|
minServed: MIN_SERVED
|
|
};
|
|
}
|
|
|
|
// One plain-English read of the numbers. Deliberately written here rather than
|
|
// generated: these are conclusions about money, and a model paraphrasing a
|
|
// table is exactly where an invented number would slip in.
|
|
function readout(rep) {
|
|
if (!rep || !rep.ready) return [];
|
|
const out = [];
|
|
const pct = function (r) { return (r * 100).toFixed(3) + '%'; };
|
|
|
|
out.push('Across ' + rep.totals.ads.toLocaleString() + ' team ads: ' +
|
|
rep.totals.served.toLocaleString() + ' impressions served, ' +
|
|
rep.totals.clicks.toLocaleString() + ' clicks — ' + pct(rep.totals.rate) + ' overall.');
|
|
|
|
if (rep.format.comparable) {
|
|
const top = rep.format.rows[0], next = rep.format.rows[1];
|
|
const edge = next.rate > 0 ? ((top.rate - next.rate) / next.rate) * 100 : 0;
|
|
out.push(top.label + ' are pulling ' + pct(top.rate) + ' against ' + pct(next.rate) + ' for ' +
|
|
next.label.toLowerCase() + (edge >= 20 ? ' — a ' + Math.round(edge) + '% edge worth acting on.' : ' — close enough to call it a tie.'));
|
|
}
|
|
if (rep.angle.comparable) {
|
|
const a = rep.angle.rows[0];
|
|
out.push('The "' + a.label + '" angle is the strongest performer at ' + pct(a.rate) +
|
|
' across ' + a.served.toLocaleString() + ' impressions.');
|
|
}
|
|
if (rep.creative.comparable) {
|
|
const c = rep.creative.rows[0];
|
|
out.push('Best banner: ' + c.label + ' at ' + pct(c.rate) + '.');
|
|
}
|
|
const thinTotal = rep.format.thin + rep.angle.thin + rep.size.thin + rep.creative.thin + rep.headline.thin;
|
|
if (thinTotal) {
|
|
out.push(thinTotal + ' grouping(s) are held back for now — under ' + rep.minServed.toLocaleString() +
|
|
' impressions each, which is not enough to read anything into.');
|
|
}
|
|
return out;
|
|
}
|
|
|
|
module.exports = { init, report, readout, MIN_LEVEL, MIN_SERVED };
|