From 0a50d0765807fc0fcea06636ccd722dc494c796a Mon Sep 17 00:00:00 2001 From: martbost Date: Fri, 18 Sep 2026 16:18:27 -0500 Subject: [PATCH] Login ads become a flat 30-day slot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The daily meter charged 100 credits for every day a campaign was shown, with no end date. It produced 74-day queues, and worse, each new campaign added 100/day of charges while adding no inventory: on 18 September, 15 campaigns were charged 1,500 credits against a platform that had produced 367 login impressions in its entire life. That had stopped being a pricing inefficiency and become members paying for delivery that did not exist. Now: 3,000 credits buys 30 days of unlimited impressions, charged once at purchase, with no delivery promise — the model Marty wanted, matching how ClickBaitPays sells theirs. The price is deliberately identical to 30 days at the old daily rate, so nobody pays more than before; they just get a known end instead of an open-ended drain. It reuses the `featured` shape, which was already a flat up-front buy that runs to `expires`. Both daily meters — MySQL and JSON — now skip any campaign with an expiry, so a slot can never be metered. The existing sweep closes it on day 30 and returns anything unspent. qa/login-slot.mjs, 9 checks, including that the slot price still equals 30 days at the old rate and that neither meter can touch a slot campaign. Member walk clean. Co-Authored-By: Claude Opus 5 (1M context) --- ads.js | Bin 107164 -> 108737 bytes public/my.html | 2 +- qa/login-slot.mjs | 42 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 qa/login-slot.mjs diff --git a/ads.js b/ads.js index 48c95c018d52259423dd17bea093b3be7690956e..a132a92c63d0f307063a4720ed4c05d56e1263cb 100644 GIT binary patch delta 1153 zcmZuxL2J}N7-hj0R8Vib7KG1ByKQ&V*w&)09>iXXZ9x_hJawAP?hYn1VP?|pO6%fD z#2$ozdl7$wSi!Rge}Ly+Jo*ppdIrkS*y2h?IIC#x(=h#9h7PAVD1bSn%O(3SSlz5z%}Jz=}x@m#8&BraoO zG9EXU3KT@rr&u$`x`Vs1B;+2KRsqmgZ(3TsvY z9EOdv#dYX(g+_5QapSg=l<*FV{5*i38)&zRqC^?!0BMY&>4z4vG-IM8&GGMvqq*Yb z@x|g@SbgN%M;Mc(Na1#$bZ~|Wn9#vN$;FTP>=8$(tgjW##rn0G2jo9*ve=td$i><+ zwUwqBnlHbf$0KALQtk5{3ufqWtvFh&4V-P~UJ&l{fGEIU4o@lNmSc2Y5R?bID(-`8 z%W5Gzbq6@R_P5CQGl77hzRoNUU}=4J%x{kFtSn>nVR(1VP1r@k^f=GTu^JNASx=hV zTua1L_H7p!5Mwgf4UofQ8y?L*^>9R@P4y@kLC#{%(h8Y#-~3;o;SO>ushuYY1utX>%i|^;43Dl?rd)=H zJJrh4jQ>@P6GwHIwCz0I<+{EXL}d_Mgol@>D$~X3>+0~dTG<+I)hhP=+X`K9hJF22 It9*I$2Qe3ZJpcdz delta 48 zcmV-00MGxy(FUBw2C(1-lLQAC0S&WJ2aGDStuuQmlS;b|vyELYAd}&NF|vMvp0^Vp G0j`PHiW7JM diff --git a/public/my.html b/public/my.html index 81d49f6..8274de8 100644 --- a/public/my.html +++ b/public/my.html @@ -1037,7 +1037,7 @@ - + diff --git a/qa/login-slot.mjs b/qa/login-slot.mjs new file mode 100644 index 0000000..49ddd84 --- /dev/null +++ b/qa/login-slot.mjs @@ -0,0 +1,42 @@ +// Login ads are a flat 30-day slot, not a daily meter. +// +// The old model charged 100 credits for every day a campaign was shown, with no end date. +// That produced 74-day queues, and every new campaign added 100/day of charges while adding +// no inventory — on 18 Sep, 15 campaigns were charged 1,500 credits against a platform that +// had produced 367 login impressions in its whole life. +import { createRequire } from 'node:module'; +const require = createRequire(import.meta.url); +const ads = require('../ads.js'); + +const ok = [], bad = []; +const t = (n, c, extra) => { (c ? ok : bad).push(n + (c || !extra ? '' : ' -> ' + extra)); }; + +const r = ads.rates(); +t('a slot price is configured', Number(r.loginSlotCredits) > 0, String(r.loginSlotCredits)); +t('a slot length is configured', Number(r.loginSlotDays) > 0, String(r.loginSlotDays)); +t('the slot is 3,000 credits for 30 days', r.loginSlotCredits === 3000 && r.loginSlotDays === 30, + r.loginSlotCredits + ' / ' + r.loginSlotDays); + +// the price matches what 30 days used to cost, so nobody pays more than before +t('same total as 30 days at the old daily rate', + r.loginSlotCredits === 30 * r.loginCreditsPerDay, + r.loginSlotCredits + ' vs ' + (30 * r.loginCreditsPerDay)); + +// the source guarantees that matter, checked directly — these are the behaviours that stop +// a slot draining against days when the gate produced nothing +const src = require('fs').readFileSync(new URL('../ads.js', import.meta.url), 'utf8'); + +t('buying a slot charges the whole price up front', + /Login-ad slot \(/.test(src) && /chargeCredits\(campaign\.id, v\.c\.budget, 'Login-ad slot/.test(src)); +t('a slot gets an end date at purchase', + /out\.expires = Date\.now\(\) \+ days \* 86400000/.test(src)); +t('the MySQL daily meter skips slot campaigns', + /expires IS NULL AND \(last_day_charged/.test(src)); +t('the JSON daily meter skips slot campaigns', + /if \(c\.expires\) continue; \/\/ slot buy/.test(src)); +t('a short budget is refused with the slot price named', + /A login-ad slot is ' \+ cost \+ ' credits for ' \+ days \+ ' days/.test(src)); + +console.log('PASS ' + ok.length); +for (const b of bad) console.log('FAIL ' + b); +process.exit(bad.length ? 1 : 0);