Featured rotation format: duration-priced link rotation with disclosed dilution

- New 'featured' type: headline + link + 1/2/7 day run, flat up-front price (40cr/day)
- Composer discloses dilution (N links share the rotation, yours makes N+1) before buying
- Overview featured strip shows the live rotation; expires automatically

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-06 08:58:22 -05:00
parent b25b4c67fe
commit 37a2b97bfd
12 changed files with 90 additions and 31 deletions
BIN
View File
Binary file not shown.
+1
View File
@@ -114,6 +114,7 @@ async function bootstrap() {
await alterSafe('ALTER TABLE campaigns ADD COLUMN height INT NULL'); // banner size (IAB) → also NAS height await alterSafe('ALTER TABLE campaigns ADD COLUMN height INT NULL'); // banner size (IAB) → also NAS height
await alterSafe('ALTER TABLE campaigns ADD COLUMN nas_ad_id INT NULL'); // syndicated NAS sponsorads.ID await alterSafe('ALTER TABLE campaigns ADD COLUMN nas_ad_id INT NULL'); // syndicated NAS sponsorads.ID
await alterSafe('ALTER TABLE campaigns ADD COLUMN nas_served INT NOT NULL DEFAULT 0'); // NAS impressions already reconciled into spend await alterSafe('ALTER TABLE campaigns ADD COLUMN nas_served INT NOT NULL DEFAULT 0'); // NAS impressions already reconciled into spend
await alterSafe('ALTER TABLE campaigns ADD COLUMN expires BIGINT NULL'); // featured rotation end time
await q(`CREATE TABLE IF NOT EXISTS sponsor_messages ( await q(`CREATE TABLE IF NOT EXISTS sponsor_messages (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
from_member INT NOT NULL, from_member INT NOT NULL,
+37 -5
View File
@@ -26,6 +26,20 @@
if (d.buyerCount < 5) return 'Level 2 is open. ' + (5 - d.buyerCount) + ' more qualifying buyer(s) unlock level 3 and the full three-level flow.'; if (d.buyerCount < 5) return 'Level 2 is open. ' + (5 - d.buyerCount) + ' more qualifying buyer(s) unlock level 3 and the full three-level flow.';
return 'Fully qualified. Every level pays you, and you catch the pass-ups that under-qualified positions below you let slip. Keep sharing and keep your campaigns running.'; return 'Fully qualified. Every level pays you, and you catch the pass-ups that under-qualified positions below you let slip. Keep sharing and keep your campaigns running.';
} }
// featured rotation strip on the overview
async function loadFeatured() {
try {
const r = await (await fetch('/api/featured')).json();
const card = $('featuredCard'); if (!card) return;
if (!r.items || !r.items.length) { card.hidden = true; return; }
card.hidden = false;
$('featSub').textContent = r.items.length + ' link' + (r.items.length === 1 ? '' : 's') + ' in rotation';
$('featStrip').innerHTML = r.items.map(i =>
'<a href="' + i.url + '" target="_blank" rel="noopener nofollow"><span>' + esc(i.title) + '</span>'
+ (i.by ? '<span class="by">' + esc(i.by) + '</span>' : '') + '</a>').join('');
} catch (e) {}
}
// achievement badges: the milestone ladder as unlockable medals + share-to-image // achievement badges: the milestone ladder as unlockable medals + share-to-image
const BADGES = [ const BADGES = [
{ key: 'payouts', label: 'Payouts On', sub: 'wallet linked', icon: '<circle cx="12" cy="12" r="8"/><path d="M9 12l2 2 4-4"/>' }, { key: 'payouts', label: 'Payouts On', sub: 'wallet linked', icon: '<circle cx="12" cy="12" r="8"/><path d="M9 12l2 2 4-4"/>' },
@@ -212,6 +226,7 @@
setInboxBadge(d.inboxUnread || 0); setInboxBadge(d.inboxUnread || 0);
if (d.sponsorMsg) showSponsorModal(d.sponsorMsg); if (d.sponsorMsg) showSponsorModal(d.sponsorMsg);
renderBadges(d); renderBadges(d);
loadFeatured();
loadCharts(d); loadCharts(d);
$('nextMove').textContent = nextMove(d); $('nextMove').textContent = nextMove(d);
renderSteps(d); renderSteps(d);
@@ -493,10 +508,25 @@
$('cSoloRow').hidden = t !== 'solo'; $('cSoloRow').hidden = t !== 'solo';
$('cSoloHint').hidden = t !== 'solo'; $('cSoloHint').hidden = t !== 'solo';
$('cVideoRow').hidden = t !== 'video'; $('cVideoRow').hidden = t !== 'video';
$('cFeaturedRow').hidden = t !== 'featured';
$('cTitle').placeholder = t === 'solo' ? 'Subject line (max 80)' : 'Headline (max 60)'; $('cTitle').placeholder = t === 'solo' ? 'Subject line (max 80)' : 'Headline (max 60)';
soloHint(); soloHint();
if (t === 'video') videoHint(); if (t === 'video') videoHint();
if (t === 'featured') featHint();
}); });
function featHint() {
if (!lastRates || !lastRates.featuredDurations) return;
if ($('cFeatDays') && !$('cFeatDays').options.length)
$('cFeatDays').innerHTML = lastRates.featuredDurations.map(dys =>
'<option value="' + dys + '">' + dys + ' day' + (dys > 1 ? 's' : '') + ' — ' + (dys * lastRates.featuredPerDay) + ' credits</option>').join('');
fetch('/api/featured/stats').then(r => r.json()).then(s => {
const dys = Number($('cFeatDays').value) || (lastRates.featuredDurations[0]);
$('cFeatHint').textContent = (s.activeCount || 0) + ' link' + (s.activeCount === 1 ? '' : 's')
+ ' currently share the rotation — yours would make ' + ((s.activeCount || 0) + 1)
+ '. Runs ' + dys + ' day' + (dys > 1 ? 's' : '') + ' for ' + (dys * lastRates.featuredPerDay) + ' credits.';
}).catch(() => {});
}
document.addEventListener('change', e => { if (e.target && e.target.id === 'cFeatDays') featHint(); });
// video composer: tier dropdown + upload + price hint // video composer: tier dropdown + upload + price hint
function videoHint() { function videoHint() {
if (!lastRates || !lastRates.videoTiers) return; if (!lastRates || !lastRates.videoTiers) return;
@@ -529,14 +559,16 @@
// in raw mode the source of truth is the textarea; sync it back first // in raw mode the source of truth is the textarea; sync it back first
let soloBody = $('cSoloEd').innerHTML; let soloBody = $('cSoloEd').innerHTML;
if ($('cSoloRaw') && !$('cSoloRaw').hidden) soloBody = $('cSoloRaw').value; if ($('cSoloRaw') && !$('cSoloRaw').hidden) soloBody = $('cSoloRaw').value;
const isVideo = $('cType').value === 'video'; const t = $('cType').value;
await api('/api/my/campaigns', { type: $('cType').value, name: $('cName').value, const isVideo = t === 'video', isFeat = t === 'featured';
await api('/api/my/campaigns', { type: t, name: $('cName').value,
targetUrl: $('cTarget').value, imageUrl: $('cImage').value, size: $('cSize').value, targetUrl: $('cTarget').value, imageUrl: $('cImage').value, size: $('cSize').value,
title: isVideo ? $('cVideoTitle').value : $('cTitle').value, title: isVideo ? $('cVideoTitle').value : isFeat ? $('cFeatTitle').value : $('cTitle').value,
body: $('cType').value === 'solo' ? soloBody : $('cBody').value, body: t === 'solo' ? soloBody : $('cBody').value,
videoUrl: $('cVideoUrl').value, watchSecs: Number($('cWatchSecs').value), videoUrl: $('cVideoUrl').value, watchSecs: Number($('cWatchSecs').value),
days: Number($('cFeatDays').value),
ctaLabel: isVideo ? $('cVideoCta').value : $('cCtaLabel').value, ctaLabel: isVideo ? $('cVideoCta').value : $('cCtaLabel').value,
budget: Number($('cBudget').value) }); budget: isFeat ? (Number($('cFeatDays').value) * (lastRates.featuredPerDay || 40)) : Number($('cBudget').value) });
IAP.status('Campaign is live. It starts serving right away.', 'ok'); IAP.status('Campaign is live. It starts serving right away.', 'ok');
$('cName').value = ''; $('cBudget').value = ''; $('cName').value = ''; $('cBudget').value = '';
$('cSoloEd').innerHTML = ''; $('cSoloRaw').value = ''; $('cCtaLabel').value = ''; $('cSoloEd').innerHTML = ''; $('cSoloRaw').value = ''; $('cCtaLabel').value = '';
+6
View File
@@ -259,6 +259,12 @@ textarea{resize:vertical;font:inherit}
#boBurger{display:block} #boBurger{display:block}
.bo-content{padding:18px} .bo-content{padding:18px}
} }
/* ── featured rotation strip ── */
.feat-strip{display:flex;flex-direction:column;gap:8px;margin-top:10px}
.feat-strip a{display:flex;justify-content:space-between;gap:10px;padding:11px 14px;border-radius:10px;
background:var(--panel);border:1px solid var(--line-strong);color:var(--ink);text-decoration:none;font-weight:600}
.feat-strip a:hover{border-color:var(--mint)}
.feat-strip .by{font-size:12px;color:var(--muted);font-weight:500;white-space:nowrap}
/* ── achievement badges ── */ /* ── achievement badges ── */
.badges{display:flex;gap:16px;flex-wrap:wrap;margin-top:12px} .badges{display:flex;gap:16px;flex-wrap:wrap;margin-top:12px}
.badge-a{display:flex;flex-direction:column;align-items:center;gap:8px;width:120px;text-align:center} .badge-a{display:flex;flex-direction:column;align-items:center;gap:8px;width:120px;text-align:center}
+4 -4
View File
@@ -5,7 +5,7 @@
<title>The contract | InstantAdPay</title> <title>The contract | InstantAdPay</title>
<meta name="description" content="Plain-language review of the InstantAdPay settlement contract: what it does, what nobody can change, what the operator can and cannot touch, and how to verify all of it yourself."> <meta name="description" content="Plain-language review of the InstantAdPay settlement contract: what it does, what nobody can change, what the operator can and cannot touch, and how to verify all of it yourself.">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap"> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
<link rel="stylesheet" href="/assets/site.css?v=20260906c"> <link rel="stylesheet" href="/assets/site.css?v=20260906d">
</head> </head>
<body> <body>
<div class="wrap"> <div class="wrap">
@@ -129,8 +129,8 @@
<div class="small">Advertising services with a performance referral program. Not an investment product; no income guarantees. Crypto transactions are irreversible. Never spend what you cannot afford.</div> <div class="small">Advertising services with a performance referral program. Not an investment product; no income guarantees. Crypto transactions are irreversible. Never spend what you cannot afford.</div>
</footer> </footer>
</div> </div>
<script src="/assets/common.js?v=20260906c"></script> <script src="/assets/common.js?v=20260906d"></script>
<script src="/assets/contract.js?v=20260906c"></script> <script src="/assets/contract.js?v=20260906d"></script>
<script src="/assets/chat.js?v=20260906c"></script> <script src="/assets/chat.js?v=20260906d"></script>
</body> </body>
</html> </html>
+5 -5
View File
@@ -5,7 +5,7 @@
<title>InstantAdPay: advertise and earn, locked in code</title> <title>InstantAdPay: advertise and earn, locked in code</title>
<meta name="description" content="Real ad packages with instant on-chain settlement. Every purchase pays the sponsor line in the same transaction, verifiable by anyone on the live ledger."> <meta name="description" content="Real ad packages with instant on-chain settlement. Every purchase pays the sponsor line in the same transaction, verifiable by anyone on the live ledger.">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap"> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
<link rel="stylesheet" href="/assets/site.css?v=20260906c"> <link rel="stylesheet" href="/assets/site.css?v=20260906d">
</head> </head>
<body> <body>
@@ -410,9 +410,9 @@
</div> </div>
</section> </section>
<script src="/assets/common.js?v=20260906c"></script> <script src="/assets/common.js?v=20260906d"></script>
<script src="/assets/wallet.js?v=20260906c"></script> <script src="/assets/wallet.js?v=20260906d"></script>
<script src="/assets/home.js?v=20260906c"></script> <script src="/assets/home.js?v=20260906d"></script>
<script src="/assets/chat.js?v=20260906c"></script> <script src="/assets/chat.js?v=20260906d"></script>
</body> </body>
</html> </html>
+4 -4
View File
@@ -5,7 +5,7 @@
<title>Live ledger | InstantAdPay</title> <title>Live ledger | InstantAdPay</title>
<meta name="description" content="Every purchase, payout, and pass-up on InstantAdPay, streamed straight from the blockchain with a verify link on every line."> <meta name="description" content="Every purchase, payout, and pass-up on InstantAdPay, streamed straight from the blockchain with a verify link on every line.">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap"> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
<link rel="stylesheet" href="/assets/site.css?v=20260906c"> <link rel="stylesheet" href="/assets/site.css?v=20260906d">
</head> </head>
<body> <body>
<div class="wrap"> <div class="wrap">
@@ -25,8 +25,8 @@
<div>InstantAdPay · <a href="/">how it works</a> · <a id="contractLink" href="#" target="_blank" rel="noopener">contract source ↗</a></div> <div>InstantAdPay · <a href="/">how it works</a> · <a id="contractLink" href="#" target="_blank" rel="noopener">contract source ↗</a></div>
</footer> </footer>
</div> </div>
<script src="/assets/common.js?v=20260906c"></script> <script src="/assets/common.js?v=20260906d"></script>
<script src="/assets/ledger.js?v=20260906c"></script> <script src="/assets/ledger.js?v=20260906d"></script>
<script src="/assets/chat.js?v=20260906c"></script> <script src="/assets/chat.js?v=20260906d"></script>
</body> </body>
</html> </html>
+15 -5
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"> <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Member area | InstantAdPay</title> <title>Member area | InstantAdPay</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap"> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
<link rel="stylesheet" href="/assets/site.css?v=20260906c"> <link rel="stylesheet" href="/assets/site.css?v=20260906d">
</head> </head>
<body class="bo-body"> <body class="bo-body">
@@ -186,6 +186,10 @@
</div> </div>
<div class="nc-steps" id="ncSteps"></div> <div class="nc-steps" id="ncSteps"></div>
</div> </div>
<div class="card" id="featuredCard" hidden>
<div class="card-head"><h3>Featured links</h3><span class="sub" id="featSub">member links in rotation</span></div>
<div id="featStrip" class="feat-strip"></div>
</div>
<div class="card" id="badgeCard" hidden> <div class="card" id="badgeCard" hidden>
<div class="card-head"><h3>Your achievements</h3><span class="sub">unlock as you build</span></div> <div class="card-head"><h3>Your achievements</h3><span class="sub">unlock as you build</span></div>
<div class="badges" id="badgeStrip"></div> <div class="badges" id="badgeStrip"></div>
@@ -295,6 +299,7 @@
<option value="login">Login ad (per day)</option> <option value="login">Login ad (per day)</option>
<option value="solo">Solo ad (inbox delivery)</option> <option value="solo">Solo ad (inbox delivery)</option>
<option value="video">Video ad (watch to earn)</option> <option value="video">Video ad (watch to earn)</option>
<option value="featured">Featured link (rotation)</option>
</select></p> </select></p>
<p><input id="cName" placeholder="Campaign name" style="width:100%"></p> <p><input id="cName" placeholder="Campaign name" style="width:100%"></p>
<p><input id="cBudget" type="number" placeholder="Budget (credits)" min="10" style="width:100%"></p> <p><input id="cBudget" type="number" placeholder="Budget (credits)" min="10" style="width:100%"></p>
@@ -337,6 +342,11 @@
placeholder="Call-to-action button label (default: Learn more) — the button opens your target URL"></p> placeholder="Call-to-action button label (default: Learn more) — the button opens your target URL"></p>
</div> </div>
<p class="small muted" id="cSoloHint" hidden></p> <p class="small muted" id="cSoloHint" hidden></p>
<div id="cFeaturedRow" hidden>
<p><input id="cFeatTitle" maxlength="70" placeholder="Headline for your featured link" style="width:100%"></p>
<p><select id="cFeatDays" style="width:100%"></select></p>
<p class="small muted" id="cFeatHint"></p>
</div>
<div id="cVideoRow" hidden> <div id="cVideoRow" hidden>
<p><input id="cVideoUrl" placeholder="Video URL (direct https link ending .mp4 or .webm)" style="width:100%"></p> <p><input id="cVideoUrl" placeholder="Video URL (direct https link ending .mp4 or .webm)" style="width:100%"></p>
<p><input type="file" id="cVideoFile" accept="video/mp4,video/webm" hidden> <p><input type="file" id="cVideoFile" accept="video/mp4,video/webm" hidden>
@@ -545,9 +555,9 @@
</div> </div>
</div> </div>
<script src="/assets/common.js?v=20260906c"></script> <script src="/assets/common.js?v=20260906d"></script>
<script src="/assets/wallet.js?v=20260906c"></script> <script src="/assets/wallet.js?v=20260906d"></script>
<script src="/assets/my.js?v=20260906c"></script> <script src="/assets/my.js?v=20260906d"></script>
<script src="/assets/chat.js?v=20260906c"></script> <script src="/assets/chat.js?v=20260906d"></script>
</body> </body>
</html> </html>
+3 -3
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"> <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Transaction | InstantAdPay</title> <title>Transaction | InstantAdPay</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap"> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
<link rel="stylesheet" href="/assets/site.css?v=20260906c"> <link rel="stylesheet" href="/assets/site.css?v=20260906d">
</head> </head>
<body> <body>
<div id="nav"></div> <div id="nav"></div>
@@ -33,7 +33,7 @@
<p><a href="/ledger">← Back to the live ledger</a> · <a href="/contract">Read the contract review</a></p> <p><a href="/ledger">← Back to the live ledger</a> · <a href="/contract">Read the contract review</a></p>
</div> </div>
</section> </section>
<script src="/assets/common.js?v=20260906c"></script> <script src="/assets/common.js?v=20260906d"></script>
<script src="/assets/tx.js?v=20260906c"></script> <script src="/assets/tx.js?v=20260906d"></script>
</body> </body>
</html> </html>
+2 -2
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
<title>Viewing ad — InstantAdPay</title> <title>Viewing ad — InstantAdPay</title>
<link rel="stylesheet" href="/assets/site.css?v=20260906c"> <link rel="stylesheet" href="/assets/site.css?v=20260906d">
<style> <style>
html,body{height:100%;margin:0;overflow:hidden} html,body{height:100%;margin:0;overflow:hidden}
.vw{display:flex;flex-direction:column;height:100vh;height:100dvh;background:var(--bg,#04110c);color:var(--ink,#e8fff7)} .vw{display:flex;flex-direction:column;height:100vh;height:100dvh;background:var(--bg,#04110c);color:var(--ink,#e8fff7)}
@@ -43,6 +43,6 @@
</div> </div>
<iframe class="vframe" id="vFrame" sandbox="allow-scripts allow-same-origin allow-forms allow-popups" referrerpolicy="no-referrer" title="Advertiser site"></iframe> <iframe class="vframe" id="vFrame" sandbox="allow-scripts allow-same-origin allow-forms allow-popups" referrerpolicy="no-referrer" title="Advertiser site"></iframe>
</div> </div>
<script src="/assets/view.js?v=20260906c"></script> <script src="/assets/view.js?v=20260906d"></script>
</body> </body>
</html> </html>
+3 -3
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"> <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Banner wall | InstantAdPay</title> <title>Banner wall | InstantAdPay</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap"> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
<link rel="stylesheet" href="/assets/site.css?v=20260906c"> <link rel="stylesheet" href="/assets/site.css?v=20260906d">
</head> </head>
<body> <body>
<div id="nav"></div> <div id="nav"></div>
@@ -34,7 +34,7 @@
</div> </div>
</div> </div>
</section> </section>
<script src="/assets/common.js?v=20260906c"></script> <script src="/assets/common.js?v=20260906d"></script>
<script src="/assets/wall.js?v=20260906c"></script> <script src="/assets/wall.js?v=20260906d"></script>
</body> </body>
</html> </html>
+10
View File
@@ -652,6 +652,16 @@ const server = http.createServer(async (req, res) => {
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' }); if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
return json(res, 200, await messages.markRead(s.email, m[1])); return json(res, 200, await messages.markRead(s.email, m[1]));
} }
// -- featured rotation: the live featured links + dilution stats
if (p === '/api/featured' && req.method === 'GET') {
const items = await ads.serveFeatured();
const names = await accounts.namesForMembers([...new Set(items.map(i => i.memberId).filter(Boolean))]);
for (const i of items) i.by = (i.memberId && names[i.memberId]) ? '@' + names[i.memberId] : (i.memberId ? 'member #' + i.memberId : null);
return json(res, 200, { items });
}
if (p === '/api/featured/stats' && req.method === 'GET') {
return json(res, 200, await ads.featuredStats());
}
// -- QR code (SVG) for any InstantAdPay URL — used on bio/wall pages // -- QR code (SVG) for any InstantAdPay URL — used on bio/wall pages
if (p === '/api/qr' && req.method === 'GET') { if (p === '/api/qr' && req.method === 'GET') {
const data = String(u.searchParams.get('d') || '').slice(0, 300); const data = String(u.searchParams.get('d') || '').slice(0, 300);