From 37c9fc615c75c90b141723266af6aba7ba4e7817 Mon Sep 17 00:00:00 2001 From: martbost Date: Sun, 30 Aug 2026 04:56:51 -0500 Subject: [PATCH] Traffic Desk: the network counts banners and text ads in OPPOSITE directions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Marty reported banner impressions never moving while text ads did. They were serving the whole time — we were reading the counter backwards. Measured against the live network, because none of this is documented: TEXT `remaining` counts DOWN from the purchase to zero. ad 2689: 1,111 left mid-flight, 0 once complete. BANNER `remaining` counts UP as impressions deliver, straight past the amount bought. ad 2707: 3,521 one day, 6,129 the next, on a 2,500 buy. Every live banner sits above its purchase and rising. Reading both as count-down made every banner report "0 served" while quietly delivering thousands — and made stop() treat delivered impressions as unserved, so stopping a finished banner refunded the lot. That was giving back inventory that had already been spent on the network. interpret(kind, bought, stat) now resolves both directions in one place, used by stop() and by the table. served + left always reconciles to what was bought and neither can exceed it. This also supersedes the earlier "counter drift" note: the drift WAS the count-up, seen through a count-down lens. Co-Authored-By: Claude Fable 5 --- public/suite-traffic.js | 11 ++++++++--- suite-traffic.js | 44 +++++++++++++++++++++++++++++++++++------ 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/public/suite-traffic.js b/public/suite-traffic.js index d1fa0f0..9071647 100644 --- a/public/suite-traffic.js +++ b/public/suite-traffic.js @@ -181,10 +181,15 @@ // can stop it rendering as nonsense. Clamp to what the member actually // bought so the row always reconciles: served + left = bought. Any genuine // over-delivery is a bonus to them and needs no explanation on this table. + // The network counts `remaining` DOWN for text ads and UP for banners — + // measured, not documented. Reading both the same way showed every banner + // as 0 served while it was quietly delivering thousands. var bought = Number(c.bought != null ? c.bought : c.impressions) || 0; - var rawLeft = l.remaining != null ? Number(l.remaining) : null; - var left = rawLeft == null ? null : Math.max(0, Math.min(rawLeft, bought)); - var servedShown = left == null ? Number(served) : Math.max(0, bought - left); + var rawRem = l.remaining != null ? Number(l.remaining) : null; + var servedShown, left; + if (rawRem == null) { servedShown = Number(served) || 0; left = null; } + else if (c.kind === 'text') { left = Math.max(0, Math.min(rawRem, bought)); servedShown = bought - left; } + else { servedShown = Math.max(0, Math.min(rawRem, bought)); left = bought - servedShown; } var statusCell = c.stopped ? 'stopped (' + Number(c.refunded || 0).toLocaleString() + ' returned)' : (l.live ? 'running' : 'finished'); diff --git a/suite-traffic.js b/suite-traffic.js index 18ee804..7a6c4e7 100644 --- a/suite-traffic.js +++ b/suite-traffic.js @@ -79,6 +79,37 @@ function record(memberId, entry) { writeLedger(trimmed); } +// The ad network counts the SAME `remaining` column in opposite directions for +// the two placement types, which is not documented anywhere and had to be +// measured: +// +// TEXT ads — `remaining` counts DOWN from the purchase to zero as it serves. +// (ad 2689: 1,111 left mid-flight, 0 once complete.) +// BANNER ads — `remaining` counts UP as impressions are delivered, straight +// past the amount purchased. (ad 2707: 3,521 one day, 6,129 the +// next, on a 2,500 buy.) +// +// Reading both the same way made every banner report "0 served" while quietly +// delivering thousands — and made stop() treat delivered impressions as +// unserved and refund them. +// +// Returns what the member should see: served + left always reconciles to what +// they bought, and neither can exceed it. +function interpret(kind, bought, stat) { + const b = Math.max(0, Number(bought) || 0); + const rem = Math.max(0, Number(stat && stat.remaining) || 0); + let served; + if (kind === 'text') { + // count-down: what is gone is what was bought minus what is left + served = b - Math.min(rem, b); + } else { + // count-up: `remaining` IS the delivered count + served = Math.min(rem, b); + } + served = Math.max(0, Math.min(served, b)); + return { served: served, left: Math.max(0, b - served) }; +} + // ── signed call to the NAS bridge ─────────────────────────────────────────── function callNas(payload) { return new Promise(function (resolve, reject) { @@ -194,12 +225,15 @@ async function stop(memberId, adId) { if (pre.stopped) throw new Error('That banner is already stopped.'); let served = 0, unserved = 0; + const boughtGuess = Number(pre.bought != null ? pre.bought : pre.impressions) || 0; try { const s = await callNas({ action: 'stats', ad_ids: [Number(adId)] }); const st = (s.stats || [])[0]; if (st) { - served = Math.max(0, Number(st.served) || 0); - unserved = Math.max(0, Number(st.remaining) || 0); + // Interpret by placement type — banners count up, text counts down. + const r = interpret(pre.kind === 'text' ? 'text' : 'banner', boughtGuess, st); + served = r.served; + unserved = r.left; } } catch (e) { /* if stats are unavailable, refund nothing rather than guess */ } @@ -225,9 +259,7 @@ async function stop(memberId, adId) { // shows what they launched, not just what it ended up costing them. if (row.bought == null) row.bought = row.impressions; - // A handful of rows on the network carry a `remaining` larger than their - // `assigned` (a pre-existing counter quirk). Clamp, or a member sees - // "1,002 returned" on an ad they bought 1,000 impressions for. + // interpret() already reconciled these against what was bought. unserved = Math.min(unserved, row.bought); served = row.bought - unserved; row.stopped = true; @@ -245,4 +277,4 @@ async function stats(adIds) { return r.stats || []; } -module.exports = { init, configured, status, launch, stop, stats, allowanceFor, sizes, CREATIVES, ALLOWANCE }; +module.exports = { init, configured, status, launch, stop, stats, interpret, allowanceFor, sizes, CREATIVES, ALLOWANCE };