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>
This commit is contained in:
martbost
2026-08-28 10:19:36 -05:00
parent 931eb4336b
commit d5c9e33e0e
7 changed files with 420 additions and 29 deletions
+38 -11
View File
@@ -111,10 +111,26 @@ function callNas(payload) {
async function launch(opts) {
const level = Number(opts.level) || 1;
const size = String(opts.size || '');
if (!CREATIVES[size]) throw new Error('Pick one of the available banner sizes.');
const file = String(opts.creative || '');
if (CREATIVES[size].indexOf(file) === -1) throw new Error('Pick one of the team banner designs.');
const isText = opts.kind === 'text';
let size = '', file = '', subject = '', lines = [];
if (isText) {
// Limits measured from live network inventory — see suite-textads.js.
subject = String(opts.subject || '').trim();
lines = (Array.isArray(opts.lines) ? opts.lines : []).slice(0, 3)
.map(function (l) { return String(l || '').replace(/[<>]/g, '').trim(); });
while (lines.length < 3) lines.push('');
if (!subject || !lines[0]) throw new Error('That text ad is missing its headline or first line.');
if (Array.from(subject).length > 20) throw new Error('The headline is longer than the network allows.');
if (lines.some(function (l) { return Array.from(l).length > 24; })) {
throw new Error('One of those lines is longer than the network allows.');
}
} else {
size = String(opts.size || '');
if (!CREATIVES[size]) throw new Error('Pick one of the available banner sizes.');
file = String(opts.creative || '');
if (CREATIVES[size].indexOf(file) === -1) throw new Error('Pick one of the team banner designs.');
}
const impressions = Math.max(100, Math.min(allowanceFor(level), Number(opts.impressions) || 0));
const st = status(opts.id, level);
@@ -132,17 +148,28 @@ async function launch(opts) {
}
const idem = 'rmc-' + opts.id + '-' + monthKey() + '-' + crypto.randomBytes(6).toString('hex');
const res = await callNas({
action: 'create', member_id: Number(opts.id), idem_key: idem, kind: 'banner',
size: size, impressions: impressions, days: 365,
target_url: target,
banner_url: 'https://rmcircle.team/banners/' + file,
const payload = {
action: 'create', member_id: Number(opts.id), idem_key: idem,
kind: isText ? 'text' : 'banner',
impressions: impressions, days: 365, target_url: target,
advertiser_name: (opts.name || 'RM Circle member #' + opts.id).slice(0, 60),
advertiser_email: '', catid: 5
});
};
if (isText) {
payload.subject = subject;
payload.lines = lines;
} else {
payload.size = size;
payload.banner_url = 'https://rmcircle.team/banners/' + file;
}
const res = await callNas(payload);
const entry = {
adId: res.ad_id, impressions: impressions, size: size, creative: file,
adId: res.ad_id, impressions: impressions,
size: isText ? 'text' : size, creative: isText ? '' : file,
kind: isText ? 'text' : 'banner',
subject: isText ? subject : undefined,
lines: isText ? lines : undefined,
target: target, at: new Date().toISOString()
};
record(opts.id, entry);