diff --git a/public/suite-traffic.html b/public/suite-traffic.html
index c0450d7..8b11317 100644
--- a/public/suite-traffic.html
+++ b/public/suite-traffic.html
@@ -95,7 +95,7 @@
| Size | Sends to | Bought | Served | Left | Clicks | Status | |
|---|
- A note on "delivered": 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.
What this is, honestly: display advertising is awareness volume — 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.
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.
+ When a campaign ends: 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.
What this is, honestly: display advertising is awareness volume — 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.
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.
diff --git a/public/suite-traffic.js b/public/suite-traffic.js
index ecef0f2..f4d2ecb 100644
--- a/public/suite-traffic.js
+++ b/public/suite-traffic.js
@@ -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
+ ? 'delivered in full'
+ : c.stopped
? 'stopped (' + Number(c.refunded || 0).toLocaleString() + ' returned)'
: !l.live ? 'finished'
: delivered
? 'delivered' +
- '
still serving as a bonus — Stop to end it'
+ '
served in full — closing out'
: 'running';
var label = c.kind === 'text'
? 'Text
' + esc(c.subject || '') + ''
diff --git a/server.js b/server.js
index 7a0f0fe..f2d972a 100644
--- a/server.js
+++ b/server.js
@@ -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 });
diff --git a/suite-traffic.js b/suite-traffic.js
index 7a6c4e7..bc23426 100644
--- a/suite-traffic.js
+++ b/suite-traffic.js
@@ -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 };