Traffic Desk: NAS remaining counts down for banners too — trust the bridge's served field

The count-up theory made every fresh banner read as fully delivered, so the
sweeper deactivated brand-new campaigns minutes after launch and stamped them
complete (caught by Marty 2026-09-01 on ad 2713).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-02 04:59:47 -05:00
parent 747977b368
commit bf5e8bdee9
+15 -18
View File
@@ -79,32 +79,29 @@ function record(memberId, entry) {
writeLedger(trimmed); writeLedger(trimmed);
} }
// The ad network counts the SAME `remaining` column in opposite directions for // `remaining` counts DOWN for BOTH placement types — measured across 1,604
// the two placement types, which is not documented anywhere and had to be // network ads, and it is exactly what the bridge's stats action already
// measured: // computes (`served = assigned - remaining`, falling back to hits when
// assigned was never set). Trust the bridge's `served` field first.
// //
// TEXT ads — `remaining` counts DOWN from the purchase to zero as it serves. // The earlier "banners count UP" theory (and its ad-2707 evidence) was the
// (ad 2689: 1,111 left mid-flight, 0 once complete.) // network's own post-insert counter top-ups being misread; acting on it made
// BANNER ads — `remaining` counts UP as impressions are delivered, straight // every fresh banner look fully delivered the moment it launched, so the
// past the amount purchased. (ad 2707: 3,521 one day, 6,129 the // sweeper deactivated brand-new campaigns and stamped them complete (bug
// next, on a 2,500 buy.) // Marty caught 2026-09-01: a banner "delivered all its impressions"
// // instantly). Do not resurrect the count-up branch.
// 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 // Returns what the member should see: served + left always reconciles to what
// they bought, and neither can exceed it. // they bought, and neither can exceed it.
function interpret(kind, bought, stat) { function interpret(kind, bought, stat) {
const b = Math.max(0, Number(bought) || 0); const b = Math.max(0, Number(bought) || 0);
const rem = Math.max(0, Number(stat && stat.remaining) || 0);
let served; let served;
if (kind === 'text') { if (stat && stat.served != null) {
// count-down: what is gone is what was bought minus what is left served = Number(stat.served) || 0;
served = b - Math.min(rem, b);
} else { } else {
// count-up: `remaining` IS the delivered count // count-down: what is gone is what was bought minus what is left
served = Math.min(rem, b); const rem = Math.max(0, Number(stat && stat.remaining) || 0);
served = b - Math.min(rem, b);
} }
served = Math.max(0, Math.min(served, b)); served = Math.max(0, Math.min(served, b));
return { served: served, left: Math.max(0, b - served) }; return { served: served, left: Math.max(0, b - served) };