Login ads become a flat 30-day slot

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) <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-18 16:18:27 -05:00
parent ccc372152d
commit 0a50d07658
3 changed files with 43 additions and 1 deletions
+42
View File
@@ -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);