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);
}
// 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:
// `remaining` counts DOWN for BOTH placement types — measured across 1,604
// network ads, and it is exactly what the bridge's stats action already
// 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.
// (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.
// The earlier "banners count UP" theory (and its ad-2707 evidence) was the
// network's own post-insert counter top-ups being misread; acting on it made
// every fresh banner look fully delivered the moment it launched, so the
// sweeper deactivated brand-new campaigns and stamped them complete (bug
// Marty caught 2026-09-01: a banner "delivered all its impressions"
// instantly). Do not resurrect the count-up branch.
//
// 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);
if (stat && stat.served != null) {
served = Number(stat.served) || 0;
} else {
// count-up: `remaining` IS the delivered count
served = Math.min(rem, b);
// count-down: what is gone is what was bought minus what is left
const rem = Math.max(0, Number(stat && stat.remaining) || 0);
served = b - Math.min(rem, b);
}
served = Math.max(0, Math.min(served, b));
return { served: served, left: Math.max(0, b - served) };