Banners must stop when they have delivered what was bought
Marty: "if it's just gonna keep running, why would anyone use impressions to place the ad at all?" He is right, and my previous answer — calling the over-delivery a bonus — papered over a real flaw. Banner placements on this network have no cap. Left alone, 2,500 impressions buys an ad that runs until expiry, which makes the allowance decorative and the whole level ladder (2,500 at Scintilla to 150,000 at Corona) worth nothing. Nobody would ever spend the larger allowance for an identical outcome. Two changes so the impressions are genuinely the thing being spent: sweepCompleted() deactivates any campaign that has served its purchased amount. Runs 90s after boot and every 15 minutes, batches its stat reads, and only touches campaigns that are live, un-stopped and provably at or past what was bought. Nothing is refunded — they delivered in full. It re-reads the ledger after the network calls, since deactivating takes real time and another member may have launched in that window. New campaigns get days:30 instead of days:365. The allowance is monthly, so a campaign outliving the month it was paid from is the same bug by another route. Belt and braces — whichever ends it first. Copy corrected too: "still serving as a bonus" became "served in full — closing out", and the footnote now states plainly that a campaign ends when it has served what was bought. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -95,7 +95,7 @@
|
||||
<div class="table-wrap"><table><thead><tr><th>Size</th><th>Sends to</th><th>Bought</th><th>Served</th><th>Left</th><th>Clicks</th><th>Status</th><th></th></tr></thead><tbody id="tfLive"></tbody></table></div>
|
||||
</div>
|
||||
|
||||
<p class="tf-note"><strong style="color:var(--text)">A note on "delivered":</strong> banner placements on this network have no hard stop — once a banner has served the impressions you bought it keeps rotating until it expires, at no further cost to your allowance. That extra is a bonus, not something you paid for. Hit Stop whenever you want it to end. <br><br><strong style="color:var(--text)">What this is, honestly:</strong> display advertising is <strong style="color:var(--text)">awareness volume</strong> — it puts your link in front of people browsing the network. It is not a lead list, and click rates on display inventory are low by nature. The conversations you have with people you know are still what actually builds a team; this runs quietly in the background while you do that. <br><br>All banners use approved team creative, so every ad on the network stays on-brand. Independent team resource · No income is guaranteed · Cryptocurrency involves risk.</p>
|
||||
<p class="tf-note"><strong style="color:var(--text)">When a campaign ends:</strong> it stops once it has served the impressions you bought, and closes itself out within a few minutes of getting there. You can Stop one early at any time and the impressions it has not used go straight back into your balance. <br><br><strong style="color:var(--text)">What this is, honestly:</strong> display advertising is <strong style="color:var(--text)">awareness volume</strong> — it puts your link in front of people browsing the network. It is not a lead list, and click rates on display inventory are low by nature. The conversations you have with people you know are still what actually builds a team; this runs quietly in the background while you do that. <br><br>All banners use approved team creative, so every ad on the network stays on-brand. Independent team resource · No income is guaranteed · Cryptocurrency involves risk.</p>
|
||||
</main>
|
||||
<script src="/suite-traffic.js"></script>
|
||||
<script src="/tool-video.js" data-video="/tv/suite-traffic-desk.mp4" data-secs="· 74 sec" data-title="Watch the Traffic Desk walkthrough" data-key="traffic" defer></script>
|
||||
|
||||
@@ -196,12 +196,14 @@
|
||||
// still going. Say that plainly, and keep Stop available so a member can
|
||||
// actually end it rather than wonder why it never finishes.
|
||||
var delivered = servedShown >= bought && bought > 0;
|
||||
var statusCell = c.stopped
|
||||
var statusCell = c.completed
|
||||
? '<span style="color:var(--muted)">delivered in full</span>'
|
||||
: c.stopped
|
||||
? 'stopped <span style="opacity:.7">(' + Number(c.refunded || 0).toLocaleString() + ' returned)</span>'
|
||||
: !l.live ? 'finished'
|
||||
: delivered
|
||||
? '<span style="color:var(--teal)">delivered</span>' +
|
||||
'<br><span style="font-size:11.5px;color:var(--muted)">still serving as a bonus — Stop to end it</span>'
|
||||
'<br><span style="font-size:11.5px;color:var(--muted)">served in full — closing out</span>'
|
||||
: '<span style="color:var(--teal)">running</span>';
|
||||
var label = c.kind === 'text'
|
||||
? '<b>Text</b><br><span style="font-size:12px">' + esc(c.subject || '') + '</span>'
|
||||
|
||||
@@ -24,6 +24,16 @@ const suiteTools = require('./suite-tools');
|
||||
const suiteEmail = require('./suite-email');
|
||||
const suiteVideo = require('./suite-video'); suiteVideo.init({ dataDir: DATA_DIR, publicDir: PUBLIC_DIR });
|
||||
const suiteTraffic = require('./suite-traffic'); suiteTraffic.init({ dataDir: DATA_DIR });
|
||||
// Close out ad campaigns that have delivered what was bought. Banners on this
|
||||
// network have no cap of their own, so without this an ad runs until expiry
|
||||
// and the impression allowance stops meaning anything. Every 15 minutes, and
|
||||
// once shortly after boot.
|
||||
setTimeout(function(){ suiteTraffic.sweepCompleted().then(function(r){
|
||||
if(r&&r.closed)console.log('traffic sweep: closed '+r.closed+' completed campaign(s)');
|
||||
}).catch(function(){}); }, 90 * 1000);
|
||||
setInterval(function(){ suiteTraffic.sweepCompleted().then(function(r){
|
||||
if(r&&r.closed)console.log('traffic sweep: closed '+r.closed+' completed campaign(s)');
|
||||
}).catch(function(){}); }, 15 * 60 * 1000);
|
||||
const suiteTextAds = require('./suite-textads');
|
||||
const suiteVoice = require('./suite-voice'); suiteVoice.init({ dataDir: DATA_DIR });
|
||||
const suiteSplit = require('./suite-split'); suiteSplit.init({ dataDir: DATA_DIR });
|
||||
|
||||
+76
-2
@@ -188,7 +188,10 @@ async function launch(opts) {
|
||||
const payload = {
|
||||
action: 'create', member_id: Number(opts.id), idem_key: idem,
|
||||
kind: isText ? 'text' : 'banner',
|
||||
impressions: impressions, days: 365, target_url: target,
|
||||
// 30 days, not 365: the allowance is monthly, so a campaign that outlives the
|
||||
// month it was paid from makes the allowance meaningless. Belt and braces
|
||||
// with sweepCompleted() — whichever ends it first.
|
||||
impressions: impressions, days: 30, target_url: target,
|
||||
advertiser_name: (opts.name || 'RM Circle member #' + opts.id).slice(0, 60),
|
||||
advertiser_email: '', catid: 5
|
||||
};
|
||||
@@ -277,4 +280,75 @@ async function stats(adIds) {
|
||||
return r.stats || [];
|
||||
}
|
||||
|
||||
module.exports = { init, configured, status, launch, stop, stats, interpret, allowanceFor, sizes, CREATIVES, ALLOWANCE };
|
||||
// Deactivate anything that has delivered what was bought.
|
||||
//
|
||||
// This exists because banner placements on this network have NO CAP: the
|
||||
// counter runs past the purchase and the ad keeps rotating until its expiry
|
||||
// date. Left alone that quietly destroys the whole point of the allowance —
|
||||
// if 2,500 impressions buys an ad that runs forever, nobody would ever spend
|
||||
// 50,000, and the level ladder from Scintilla to Corona stops meaning
|
||||
// anything. The impressions have to be the thing you are actually spending.
|
||||
//
|
||||
// Runs on a timer, batches its reads, and is deliberately conservative: it
|
||||
// only ever touches campaigns that are live, un-stopped, and provably at or
|
||||
// past their purchased amount. Nothing is refunded — they delivered in full.
|
||||
async function sweepCompleted() {
|
||||
if (!configured()) return { checked: 0, closed: 0 };
|
||||
const all = readLedger();
|
||||
const mk = monthKey();
|
||||
const month = all[mk] || {};
|
||||
const live = [];
|
||||
Object.keys(month).forEach(function (mid) {
|
||||
(month[mid] || []).forEach(function (r) {
|
||||
if (!r.stopped && !r.completed && r.adId) live.push({ mid: mid, row: r });
|
||||
});
|
||||
});
|
||||
if (!live.length) return { checked: 0, closed: 0 };
|
||||
|
||||
const byId = {};
|
||||
for (let i = 0; i < live.length; i += 150) {
|
||||
try {
|
||||
const chunk = await stats(live.slice(i, i + 150).map(function (x) { return x.adId || x.row.adId; }));
|
||||
chunk.forEach(function (st) { byId[st.ad_id] = st; });
|
||||
} catch (e) { return { checked: live.length, closed: 0, error: e.message }; }
|
||||
}
|
||||
|
||||
let closed = 0;
|
||||
for (const item of live) {
|
||||
const r = item.row;
|
||||
const st = byId[r.adId];
|
||||
if (!st || !st.live) continue;
|
||||
const bought = Number(r.bought != null ? r.bought : r.impressions) || 0;
|
||||
if (!bought) continue;
|
||||
const got = interpret(r.kind === 'text' ? 'text' : 'banner', bought, st);
|
||||
if (got.served < bought) continue;
|
||||
try { await callNas({ action: 'deactivate', ad_id: Number(r.adId) }); }
|
||||
catch (e) { continue; }
|
||||
closed++;
|
||||
}
|
||||
|
||||
if (closed) {
|
||||
// Re-read: the deactivate calls above took real time, and another member
|
||||
// may have launched in that window.
|
||||
const fresh = readLedger();
|
||||
const fm = fresh[mk] || {};
|
||||
Object.keys(fm).forEach(function (mid) {
|
||||
(fm[mid] || []).forEach(function (r) {
|
||||
const hit = live.find(function (x) { return x.row.adId === r.adId; });
|
||||
if (!hit) return;
|
||||
const st = byId[r.adId];
|
||||
if (!st || !st.live) return;
|
||||
const bought = Number(r.bought != null ? r.bought : r.impressions) || 0;
|
||||
if (!bought) return;
|
||||
if (interpret(r.kind === 'text' ? 'text' : 'banner', bought, st).served < bought) return;
|
||||
r.completed = true;
|
||||
r.completedAt = new Date().toISOString();
|
||||
r.served = bought;
|
||||
});
|
||||
});
|
||||
writeLedger(fresh);
|
||||
}
|
||||
return { checked: live.length, closed: closed };
|
||||
}
|
||||
|
||||
module.exports = { init, configured, status, launch, stop, stats, interpret, sweepCompleted, allowanceFor, sizes, CREATIVES, ALLOWANCE };
|
||||
|
||||
Reference in New Issue
Block a user