From 6e187c4f416f1ad90387bf12a3364ae9aea00d16 Mon Sep 17 00:00:00 2001 From: martbost Date: Thu, 10 Sep 2026 10:03:57 -0500 Subject: [PATCH] Campaigns: optional daily cap on banner/text paces spend on-site and hands Network Ad Space one day's allowance at a time Co-Authored-By: Claude Fable 5.1 --- ads.js | Bin 68448 -> 70532 bytes db.js | 3 +++ nas.js | 3 ++- public/assets/my.js | 6 ++++-- public/my.html | 4 +++- 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/ads.js b/ads.js index 2633ed9b1c1b24843c4e5f16a9d941f5e2ee5840..8d14ecb4fd30fac8398649a5972bd1ecc0e138c3 100644 GIT binary patch delta 1898 zcmZ`)&ube;6h=5SMWGZ@N5s^|Pfmz-Yeg2lgi2niCUzY=w(3}fK-=JqcE{EtX?NKj zRbCn+3O$wHw&dI%O0F&Y2lUiJpuZp#Qplmbh0;TAJ(c#o8SP53T^+>k%=`Jh?|u9G zgEOBzJo9yJuBVC6<6syD6k5!2xL<(#_hEb}_@OB1aODcvj@K6@i`m`qI`ee+2$q(> z^@Qy@tP379ZRel@rq3-~1mJNWEEZ+~f2QjM0!-#b_w?6i#}P15S+7bHdml71`1$cn z4*f6(*@MHm6@YM|#iKtT&z=Vu8A1(Oed~aW=$rZJl{TO7F|iF-06#R*83f)b%TfW3 zR5Ev>4=`k5`OCTZ#@s*C!y=ZGREXWA{SqopzuPTRS51#QqJkCUb$FA95A6&YZ;^kojSRY;H^D>(PpT_Zjx#><7 zHkrGIVbWgW^*8^1`4uwEBgmLUGh?G#&zx`dd)_JwH#M^et&-{M)#%x+&*FcV^V!=I zsQ4<<9!=;JSm-+33kW0Gxr7W+s(`xbAYdc6cz*rIsccaKPT``zHkYEAo4@6X6Xhnx zclGG`_FDYb)~{210ahBTprwwm(S*kBtu1(Wmj<*)IZhre*9r@;NCsGroD{w-6cQ&{ z@^Ttek6zumm|2Lmoky9-yYte85j?91uWcm=#m{avgCVX0GX5$?28v=K=lhP}et^tv z+xSjAOqWHbAl_y7`;JK)gd811KiVN$F-3(AvS6as!^R^KpT=EmY!b0*=?1js9p};xr2htFiG8PD>8P zs7pqu^F@5A>*@f9X4K)%jkVo1uq~rrD8l=lG~2`j9ya>D7Wc6l9cjV7nu&M5yO^0} z17?c^%VbWYYGd|&b}Dxo&ed%6_HI7@^wIiR8YH91(CV42=P~=h0o0iQT!&O9ECj&1 zU3b776DPWlTPp^x-vPVl`8)`4mS`X_h_HLSf?oJYn~kodEkLT-X2^)fN>j4JL4{32 zZgbFDNF58)x55eaf-c_|I*M>0II}1*F>26uyNSMybyThAbzGwQ_OvY&()oN}YMkAn%m_)fZf&FP~gEi+m+$ zqmRFsj(+;~T=c=i-0(EE8RcX2LZd_qNOElJ?&yOAz`Fsb4IJ;(gz?Gk+AIa^L$2wx PJ5)>NDE{G>&ny1{t}<~+ delta 111 zcmV-#0FeKLrv%`Z1h76Gv!EUa6#^iAv#B)q2(zv|J^_<@RfDsNKOqXU0ZPmRlh;iz zlL$`N0$nMS@lZvxI#1OD0Vk6{rWgSyvyD^90ke8lN^`Se$}IttRe2VZZa)dL(97fk Rld;Pwv-QqB2eb9qk2;n}F1Y{z diff --git a/db.js b/db.js index 153ba92..db73d39 100644 --- a/db.js +++ b/db.js @@ -120,6 +120,9 @@ async function bootstrap() { await alterSafe('ALTER TABLE campaigns ADD COLUMN expires BIGINT NULL'); // featured rotation end time await alterSafe('ALTER TABLE campaigns ADD COLUMN starts BIGINT NULL'); // featured run start (booked day) await alterSafe('ALTER TABLE campaigns ADD COLUMN house TINYINT NOT NULL DEFAULT 0'); // admin house ad: free, never charged + await alterSafe('ALTER TABLE campaigns ADD COLUMN daily_cap INT NULL'); // optional credits/day pacing (banner + text) + await alterSafe('ALTER TABLE campaigns ADD COLUMN day_spent INT NOT NULL DEFAULT 0'); + await alterSafe("ALTER TABLE campaigns ADD COLUMN day_key CHAR(10) NULL"); // linked positions: extra wallets owned by one email account (Qualified Start). // Each is its own on-chain member sponsored by the account's main member. await q(`CREATE TABLE IF NOT EXISTS positions ( diff --git a/nas.js b/nas.js index b012f0f..8d8188b 100644 --- a/nas.js +++ b/nas.js @@ -58,7 +58,8 @@ async function pushCampaign(c, opts = {}) { const kind = nasKind(c.type); if (!kind) return { skipped: 'type' }; const budgetLeft = c.budget - (c.spent || 0); - const assigned = Math.max(1, Math.floor(budgetLeft * impressionsPerCredit(c.type))); + const window = c.dailyCap ? Math.min(c.dailyCap, budgetLeft) : budgetLeft; // capped campaigns start with one day's allowance + const assigned = Math.max(1, Math.floor(window * impressionsPerCredit(c.type))); const token = 'iap_' + crypto.randomBytes(8).toString('hex'); // manage token = Username const now = new Date(); const days = Number(opts.days || 30); diff --git a/public/assets/my.js b/public/assets/my.js index 60db133..993e071 100644 --- a/public/assets/my.js +++ b/public/assets/my.js @@ -608,7 +608,7 @@ tbl.innerHTML = '' + '' + r.campaigns.map(c => '' - + '' + + '' + '' + '' + '' @@ -708,6 +708,7 @@ // fixed-cost types derive their spend (featured = day slots, visits = flat pack), // so hide the free-form Budget field for them to avoid confusion $('cBudgetRow').hidden = (t === 'featured' || t === 'visits'); + if ($('cCapRow')) $('cCapRow').hidden = !(t === 'banner' || t === 'text'); // solo ads have a real floor (5cr x 10 deliveries): default to 50 so 10 isn't rejected if (t === 'solo' && (!$('cBudget').value || Number($('cBudget').value) < 50)) $('cBudget').value = (lastRates && lastRates.soloCostPerRecipient ? lastRates.soloCostPerRecipient : 5) * 10; if (t === 'visits') visitHint(); @@ -801,12 +802,13 @@ videoW: cVidDims ? cVidDims.w : null, videoH: cVidDims ? cVidDims.h : null, days: Number($('cFeatDays').value), startDay: featStartDay, count: Number($('cVisitCount').value), + dailyCap: ($('cDailyCap') && (t === 'banner' || t === 'text')) ? Number($('cDailyCap').value) || 0 : 0, ctaLabel: isVideo ? $('cVideoCta').value : $('cCtaLabel').value, budget: isFeat ? (Number($('cFeatDays').value) * (lastRates.featuredPerDay || 40)) : t === 'visits' ? (Number($('cVisitCount').value) * (lastRates.visitCostPerVisit || 3)) : Number($('cBudget').value) }); IAP.status('Campaign is live. It starts serving right away.', 'ok'); // clear EVERY field so no target/creative carries into the next campaign ['cName', 'cBudget', 'cTarget', 'cImage', 'cTitle', 'cBody', 'cCtaLabel', - 'cVideoUrl', 'cVideoTitle', 'cVideoCta', 'cVisitTitle', 'cVisitCount', 'cFeatTitle'] + 'cVideoUrl', 'cVideoTitle', 'cVideoCta', 'cVisitTitle', 'cVisitCount', 'cFeatTitle', 'cDailyCap'] .forEach(id => { if ($(id)) $(id).value = ''; }); $('cSoloEd').innerHTML = ''; if ($('cSoloRaw')) $('cSoloRaw').value = ''; $('cVideoInfo').textContent = ''; $('cVideoPrev').hidden = true; $('cVideoPrev').innerHTML = ''; diff --git a/public/my.html b/public/my.html index edd044f..1df5c3f 100644 --- a/public/my.html +++ b/public/my.html @@ -431,6 +431,8 @@

+

+ Network Ad Space can deliver a lot of impressions fast. A daily cap spreads a banner or text budget over days instead of hours.

Banner and text targets are shown full screen in the @@ -872,7 +874,7 @@ - +

NameTypeViews hereNetwork viewsClicksSpentBudgetStatus
' + c.name + '' + c.type + (c.type === 'banner' && c.width ? ' ' + c.width + '×' + c.height + '' : '') + '' + c.type + (c.type === 'banner' && c.width ? ' ' + c.width + '×' + c.height + '' : '') + (c.dailyCap ? ' cap ' + c.dailyCap + '/day' : '') + '' + c.imps.toLocaleString() + '' + (['banner', 'text'].includes(c.type) ? (c.impsNas || 0).toLocaleString() : 'n/a') + '' + c.clicks + '