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 @@
SizeSends toBoughtServedLeftClicksStatus
-

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 };