Files
rm-circle-team-router/suite-meter.js
T
martbost d5c9e33e0e Traffic Desk: AI-written text ads alongside banners
Text placements out-perform banners on this network — 357 text ads average
~171 clicks each against ~150 for banners, on far less inventory — so
members can now run them too.

The format is measured, not guessed. Across 60 live text ads: Subject caps
at 20 characters and Body is ALWAYS exactly three <br>-joined lines of at
most 24 characters. Every ad in the sample obeyed it.

Emoji: the NAS columns are latin1, but so is the connection, so UTF-8 bytes
pass through and render — which is how the existing emoji ads got there. The
bridge's clean() would have deleted every emoji via iconv //TRANSLIT//IGNORE,
so text copy now uses clean_ad_text(), which preserves them and strips angle
brackets instead. The bridge builds the <br> separators itself from a lines[]
array, so a member can never inject markup.

The engine writes plain text to a tight budget and we apply emoji ourselves:
models cannot count characters, least of all with emoji, so decoration is
arithmetic we control. It also lets the emoji rotate independently of the
copy. The palette carries no money emoji — the network is full of them, but
implying earnings outranks matching the neighbours.

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

87 lines
3.5 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');
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' }
};
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;
}
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 limit = quotaFor(tool, level);
const u = used(memberId, tool);
if (limit <= 0) {
return { allowed: false, reason: 'locked', minLevel: t.minLevel, used: u, limit: 0, remaining: 0, label: t.label };
}
const remaining = Math.max(0, limit - u);
return {
allowed: remaining > 0,
reason: remaining > 0 ? 'ok' : 'quota',
used: u, limit, 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 };