Verified visits (7th ad format): guaranteed unique human-visit packs

- Buy N verified visits flat up-front; each is a distinct member, dwell + captcha verified
- Verified visits earn sub-tab: open site (new tab), dwell, human-check, earn
- Per-viewer dedup (visit_seen unique), daily cap, completes at N; single-use tokens

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-06 09:21:59 -05:00
parent f30a462ca8
commit cb2542fca5
12 changed files with 188 additions and 31 deletions
BIN
View File
Binary file not shown.
+8
View File
@@ -116,6 +116,14 @@ async function bootstrap() {
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 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 starts BIGINT NULL'); // featured run start (booked day)
await q(`CREATE TABLE IF NOT EXISTS visit_seen (
campaign_id INT NOT NULL,
email VARCHAR(190) NOT NULL,
day CHAR(10) NOT NULL,
ts BIGINT NOT NULL,
UNIQUE KEY uq_visit (campaign_id, email),
INDEX (email, day)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
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,
+84 -3
View File
@@ -509,6 +509,8 @@
$('cSoloHint').hidden = t !== 'solo'; $('cSoloHint').hidden = t !== 'solo';
$('cVideoRow').hidden = t !== 'video'; $('cVideoRow').hidden = t !== 'video';
$('cFeaturedRow').hidden = t !== 'featured'; $('cFeaturedRow').hidden = t !== 'featured';
$('cVisitsRow').hidden = t !== 'visits';
if (t === 'visits') visitHint();
$('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();
@@ -539,6 +541,15 @@
+ ' for ' + (dys * lastRates.featuredPerDay) + ' credits. Max ' + (s ? s.slotsPerDay : 10) + ' links share any day.'; + ' for ' + (dys * lastRates.featuredPerDay) + ' credits. Max ' + (s ? s.slotsPerDay : 10) + ' links share any day.';
} }
document.addEventListener('change', e => { if (e.target && e.target.id === 'cFeatDays') featHint(); }); document.addEventListener('change', e => { if (e.target && e.target.id === 'cFeatDays') featHint(); });
function visitHint() {
if (!lastRates) return;
const n = Number($('cVisitCount').value) || 0;
const cost = n * (lastRates.visitCostPerVisit || 3);
$('cVisitHint').textContent = (lastRates.visitCostPerVisit || 3) + ' credits per verified visit'
+ (n >= (lastRates.visitMinPack || 20) ? ' — ' + n + ' visits = ' + cost + ' credits' : ' (min ' + (lastRates.visitMinPack || 20) + ')')
+ '. Each is a unique member, dwell + human-check verified.';
}
$('cVisitCount').addEventListener('input', visitHint);
// 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;
@@ -575,10 +586,11 @@
const isVideo = t === 'video', isFeat = t === 'featured'; const isVideo = t === 'video', isFeat = t === 'featured';
await api('/api/my/campaigns', { type: t, name: $('cName').value, 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 : isFeat ? $('cFeatTitle').value : $('cTitle').value, title: isVideo ? $('cVideoTitle').value : isFeat ? $('cFeatTitle').value : t === 'visits' ? $('cVisitTitle').value : $('cTitle').value,
body: t === '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), startDay: featStartDay, days: Number($('cFeatDays').value), startDay: featStartDay,
count: Number($('cVisitCount').value),
ctaLabel: isVideo ? $('cVideoCta').value : $('cCtaLabel').value, ctaLabel: isVideo ? $('cVideoCta').value : $('cCtaLabel').value,
budget: isFeat ? (Number($('cFeatDays').value) * (lastRates.featuredPerDay || 40)) : 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');
@@ -592,6 +604,73 @@
// defers the busy() lookup to click time (busy is declared below) // defers the busy() lookup to click time (busy is declared below)
function busy2(btn, fn) { return (...a) => busy(btn, fn)(...a); } function busy2(btn, fn) { return (...a) => busy(btn, fn)(...a); }
// ── verified visits: open a member's site (new tab), dwell, human-check, earn ──
const visState = { token: null, id: null, dwell: 8 };
async function loadVisitStatus() {
try {
const st = await (await fetch('/api/my/visits')).json();
if (st.error) return;
$('vsProgress').textContent = 'today: ' + (st.status.count || 0) + ' / ' + st.status.cap + ' verified visits';
$('vsStartBtn').hidden = st.status.count >= st.status.cap;
if (st.status.count >= st.status.cap) $('vsBox').innerHTML = '<span class="muted small">That\'s today\'s visits. Come back tomorrow.</span>';
} catch (e) {}
}
async function loadVisit() {
$('vsCheck').hidden = true; $('vsVisit').hidden = true; $('vsHint').textContent = '';
let r = null;
try { r = await (await fetch('/api/my/visits')).json(); } catch (e) {}
if (!r || !r.ad) {
$('vsBox').innerHTML = '<span class="muted small">' + (r && r.status && r.status.count >= r.status.cap
? 'That\'s today\'s visits. Come back tomorrow.' : 'No verified-visit packs are running right now. Check back soon.') + '</span>';
return;
}
visState.token = r.token; visState.id = r.ad.id; visState.dwell = r.status.dwell || 8;
$('vsBox').innerHTML = '<b>' + esc(r.ad.title || 'Member site') + '</b><br><span class="muted small">Open the site and stay ' + visState.dwell + 's.</span>';
const visit = $('vsVisit');
visit.href = r.ad.url; visit.hidden = false;
$('vsStartBtn').textContent = 'Loading…'; $('vsStartBtn').disabled = true;
// opening the site starts the dwell; then we ask the human-check
visit.onclick = () => {
let left = visState.dwell;
$('vsHint').textContent = 'Counting your visit: ' + left + 's';
const t = setInterval(async () => {
left--;
if (left > 0) { $('vsHint').textContent = 'Counting your visit: ' + left + 's'; return; }
clearInterval(t);
$('vsHint').textContent = 'One quick check to count the visit:';
let c = await (await fetch('/api/my/visitchallenge?token=' + visState.token)).json();
if (c.early) { await new Promise(r2 => setTimeout(r2, (c.wait || 1) * 1000 + 300)); c = await (await fetch('/api/my/visitchallenge?token=' + visState.token)).json(); }
if (c.error) { $('vsHint').textContent = c.error; return; }
renderVisitCheck(c);
}, 1000);
};
$('vsStartBtn').textContent = 'Next visit'; $('vsStartBtn').disabled = false;
}
function renderVisitCheck(c) {
$('vsPrompt').textContent = 'Click the ' + c.prompt + ':';
const w = $('vsOpts'); w.innerHTML = '';
c.options.forEach((em, i) => {
const b = document.createElement('button');
b.className = 'btn small sec'; b.style.marginRight = '6px'; b.textContent = em;
b.addEventListener('click', () => answerVisit(i));
w.appendChild(b);
});
$('vsCheck').hidden = false;
}
async function answerVisit(i) {
const r = await (await fetch('/api/my/visitdone', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: visState.token, answer: i }) })).json();
if (r.error) {
if (r.retry) { const c = await (await fetch('/api/my/visitchallenge?token=' + visState.token)).json(); if (!c.error) return renderVisitCheck(c); }
$('vsHint').textContent = r.error; $('vsCheck').hidden = true; return;
}
$('vsCheck').hidden = true;
$('vsHint').textContent = '+' + r.credited + ' credits — visit counted. Load the next one.';
$('vsProgress').textContent = 'today: ' + (r.status.count || 0) + ' / ' + (r.status.cap || 0) + ' verified visits';
IAP.status('+' + r.credited + ' credits for a verified visit.', 'ok');
loadDashboard();
}
$('vsStartBtn').addEventListener('click', () => loadVisit());
// ── watch-to-earn videos: escape-proof player, server-clock reward ── // ── watch-to-earn videos: escape-proof player, server-clock reward ──
const vidState = { token: null, secs: 0, maxSeen: 0, done: false, credited: false }; const vidState = { token: null, secs: 0, maxSeen: 0, done: false, credited: false };
async function loadVideoStatus() { async function loadVideoStatus() {
@@ -727,15 +806,17 @@
// Earn credits sub-tabs: Watch ads | Inbox // Earn credits sub-tabs: Watch ads | Inbox
let earnSub = 'watch'; let earnSub = 'watch';
function setEarnSub(which) { function setEarnSub(which) {
earnSub = ['inbox', 'videos'].includes(which) ? which : 'watch'; earnSub = ['inbox', 'videos', 'visits'].includes(which) ? which : 'watch';
const w = $('earn-watch'), i = $('earn-inbox'), v = $('earn-videos'); const w = $('earn-watch'), i = $('earn-inbox'), v = $('earn-videos'), vs = $('earn-visits');
if (w) w.hidden = earnSub !== 'watch'; if (w) w.hidden = earnSub !== 'watch';
if (i) i.hidden = earnSub !== 'inbox'; if (i) i.hidden = earnSub !== 'inbox';
if (v) v.hidden = earnSub !== 'videos'; if (v) v.hidden = earnSub !== 'videos';
if (vs) vs.hidden = earnSub !== 'visits';
document.querySelectorAll('.subtabs [data-earn]').forEach(b => document.querySelectorAll('.subtabs [data-earn]').forEach(b =>
b.classList.toggle('on', b.dataset.earn === earnSub)); b.classList.toggle('on', b.dataset.earn === earnSub));
if (earnSub === 'inbox') loadInbox(); if (earnSub === 'inbox') loadInbox();
else if (earnSub === 'videos') loadVideoStatus(); else if (earnSub === 'videos') loadVideoStatus();
else if (earnSub === 'visits') loadVisitStatus();
else earnRefresh(); else earnRefresh();
} }
document.querySelectorAll('.subtabs [data-earn]').forEach(b => document.querySelectorAll('.subtabs [data-earn]').forEach(b =>
+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=20260906f"> <link rel="stylesheet" href="/assets/site.css?v=20260906g">
</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=20260906f"></script> <script src="/assets/common.js?v=20260906g"></script>
<script src="/assets/contract.js?v=20260906f"></script> <script src="/assets/contract.js?v=20260906g"></script>
<script src="/assets/chat.js?v=20260906f"></script> <script src="/assets/chat.js?v=20260906g"></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=20260906f"> <link rel="stylesheet" href="/assets/site.css?v=20260906g">
</head> </head>
<body> <body>
@@ -410,9 +410,9 @@
</div> </div>
</section> </section>
<script src="/assets/common.js?v=20260906f"></script> <script src="/assets/common.js?v=20260906g"></script>
<script src="/assets/wallet.js?v=20260906f"></script> <script src="/assets/wallet.js?v=20260906g"></script>
<script src="/assets/home.js?v=20260906f"></script> <script src="/assets/home.js?v=20260906g"></script>
<script src="/assets/chat.js?v=20260906f"></script> <script src="/assets/chat.js?v=20260906g"></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=20260906f"> <link rel="stylesheet" href="/assets/site.css?v=20260906g">
</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=20260906f"></script> <script src="/assets/common.js?v=20260906g"></script>
<script src="/assets/ledger.js?v=20260906f"></script> <script src="/assets/ledger.js?v=20260906g"></script>
<script src="/assets/chat.js?v=20260906f"></script> <script src="/assets/chat.js?v=20260906g"></script>
</body> </body>
</html> </html>
+30 -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=20260906f"> <link rel="stylesheet" href="/assets/site.css?v=20260906g">
</head> </head>
<body class="bo-body"> <body class="bo-body">
@@ -300,6 +300,7 @@
<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> <option value="featured">Featured link (rotation)</option>
<option value="visits">Verified visits (guaranteed unique)</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>
@@ -342,6 +343,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="cVisitsRow" hidden>
<p><input id="cVisitTitle" maxlength="80" placeholder="Headline (what members see before visiting)" style="width:100%"></p>
<p><input id="cVisitCount" type="number" min="20" placeholder="How many verified visits?" style="width:100%"></p>
<p class="small muted" id="cVisitHint"></p>
</div>
<div id="cFeaturedRow" hidden> <div id="cFeaturedRow" hidden>
<p><input id="cFeatTitle" maxlength="70" placeholder="Headline for your featured link" style="width:100%"></p> <p><input id="cFeatTitle" maxlength="70" placeholder="Headline for your featured link" style="width:100%"></p>
<p class="small muted" style="margin-bottom:4px">Pick a start day (occupancy shown per day):</p> <p class="small muted" style="margin-bottom:4px">Pick a start day (occupancy shown per day):</p>
@@ -368,6 +374,7 @@
<div class="subtabs" role="tablist"> <div class="subtabs" role="tablist">
<button class="subtab on" data-earn="watch" type="button">Watch ads</button> <button class="subtab on" data-earn="watch" type="button">Watch ads</button>
<button class="subtab" data-earn="videos" type="button">Watch videos</button> <button class="subtab" data-earn="videos" type="button">Watch videos</button>
<button class="subtab" data-earn="visits" type="button">Verified visits</button>
<button class="subtab" data-earn="inbox" type="button">Inbox Ads<span class="pill" id="inboxBadge2" hidden></span></button> <button class="subtab" data-earn="inbox" type="button">Inbox Ads<span class="pill" id="inboxBadge2" hidden></span></button>
</div> </div>
@@ -415,6 +422,24 @@
</div> </div>
</div> </div>
<div class="earn-sub" id="earn-visits" hidden>
<div class="card">
<h3>Verified visits, earn credits</h3>
<p class="muted small">Visit a member's site in a new tab, stay the required few seconds,
pass a quick check, and earn. Each site counts once for you — real, unique human visits.</p>
<p><span class="badge" id="vsProgress">…</span></p>
<div class="card" style="background:rgba(4,8,7,.5)">
<div id="vsBox" class="small muted" style="min-height:70px;display:grid;place-items:center">Press the button to load a site to visit.</div>
<div id="vsCheck" hidden style="margin-top:10px"><span class="prompt" id="vsPrompt"></span> <span id="vsOpts"></span></div>
<p style="margin-top:12px">
<button class="btn" id="vsStartBtn" type="button">Load a visit</button>
<a class="btn sec" id="vsVisit" target="_blank" rel="noopener nofollow" hidden>Open the site</a>
</p>
<p class="small muted" id="vsHint"></p>
</div>
</div>
</div>
<div class="earn-sub" id="earn-inbox" hidden> <div class="earn-sub" id="earn-inbox" hidden>
<div class="card" id="inboxListCard"> <div class="card" id="inboxListCard">
<h3>Solo ads inbox</h3> <h3>Solo ads inbox</h3>
@@ -558,9 +583,9 @@
</div> </div>
</div> </div>
<script src="/assets/common.js?v=20260906f"></script> <script src="/assets/common.js?v=20260906g"></script>
<script src="/assets/wallet.js?v=20260906f"></script> <script src="/assets/wallet.js?v=20260906g"></script>
<script src="/assets/my.js?v=20260906f"></script> <script src="/assets/my.js?v=20260906g"></script>
<script src="/assets/chat.js?v=20260906f"></script> <script src="/assets/chat.js?v=20260906g"></script>
</body> </body>
</html> </html>
+2 -2
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>Shorts | InstantAdPay</title> <title>Shorts | 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=20260906f"> <link rel="stylesheet" href="/assets/site.css?v=20260906g">
<style> <style>
html,body{height:100%;margin:0;overflow:hidden;background:#000} html,body{height:100%;margin:0;overflow:hidden;background:#000}
.sh{position:fixed;inset:0;display:flex;flex-direction:column;background:#000;color:var(--ink,#e8fff7)} .sh{position:fixed;inset:0;display:flex;flex-direction:column;background:#000;color:var(--ink,#e8fff7)}
@@ -43,6 +43,6 @@
</div> </div>
</div> </div>
</div> </div>
<script src="/assets/shorts.js?v=20260906f"></script> <script src="/assets/shorts.js?v=20260906g"></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=20260906f"> <link rel="stylesheet" href="/assets/site.css?v=20260906g">
</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=20260906f"></script> <script src="/assets/common.js?v=20260906g"></script>
<script src="/assets/tx.js?v=20260906f"></script> <script src="/assets/tx.js?v=20260906g"></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=20260906f"> <link rel="stylesheet" href="/assets/site.css?v=20260906g">
<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=20260906f"></script> <script src="/assets/view.js?v=20260906g"></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=20260906f"> <link rel="stylesheet" href="/assets/site.css?v=20260906g">
</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=20260906f"></script> <script src="/assets/common.js?v=20260906g"></script>
<script src="/assets/wall.js?v=20260906f"></script> <script src="/assets/wall.js?v=20260906g"></script>
</body> </body>
</html> </html>
+43
View File
@@ -35,6 +35,7 @@ fs.mkdirSync(UPLOADS_DIR, { recursive: true });
const uploadCounts = new Map(); // email:day -> uploads today const uploadCounts = new Map(); // email:day -> uploads today
const gauntletTokens = new Map(); // email -> welcome-tour token (server-clock dwell floor) const gauntletTokens = new Map(); // email -> welcome-tour token (server-clock dwell floor)
const videoTokens = new Map(); // email -> watch-to-earn video token (server-clock watch floor) const videoTokens = new Map(); // email -> watch-to-earn video token (server-clock watch floor)
const visitTokens = new Map(); // email -> verified-visit token (dwell + captcha floor)
// walk the referral chain upward via sponsorRef (code/username/member id) // walk the referral chain upward via sponsorRef (code/username/member id)
async function uplineSlides(email, depth = 3) { async function uplineSlides(email, depth = 3) {
const out = []; const out = [];
@@ -767,6 +768,48 @@ const server = http.createServer(async (req, res) => {
const status = await ads.recordVideoWatch(s.email); const status = await ads.recordVideoWatch(s.email);
return json(res, 200, { ok: true, credited: tier.reward, status }); return json(res, 200, { ok: true, credited: tier.reward, status });
} }
// -- verified visits: view a member's site (new tab) for the dwell, pass a
// human check, and it counts as one guaranteed unique visit for the pack
if (p === '/api/my/visits' && req.method === 'GET') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const status = await ads.visitStatus(s.email);
if (status.count >= status.cap) return json(res, 200, { ad: null, status });
const ad = await ads.serveVisit(s.email);
if (!ad) return json(res, 200, { ad: null, status });
const token = crypto.randomBytes(16).toString('hex');
visitTokens.set(s.email, { token, ts: Date.now(), id: ad.id });
return json(res, 200, { ad, token, status });
}
if (p === '/api/my/visitchallenge' && req.method === 'GET') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const t = visitTokens.get(s.email);
if (!t || t.token !== String(u.searchParams.get('token') || '')) return json(res, 400, { error: 'That visit is no longer open.' });
const dwellMs = (ads.rates().visitDwellSeconds || 8) * 1000;
const age = Date.now() - t.ts;
if (age < dwellMs - 400) return json(res, 200, { early: true, wait: Math.ceil((dwellMs - age) / 1000) });
if (age > 5 * 60 * 1000) { visitTokens.delete(s.email); return json(res, 400, { error: 'That visit went stale. Load a fresh one.' }); }
const pick = CAPTCHA.slice().sort(() => Math.random() - 0.5).slice(0, 5);
const answer = Math.floor(Math.random() * pick.length);
t.challenge = { answer };
return json(res, 200, { prompt: pick[answer][1], options: pick.map(x => x[0]) });
}
if (p === '/api/my/visitdone' && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const b = await readBody(req);
const t = visitTokens.get(s.email);
if (!t || t.token !== String(b.token || '')) return json(res, 400, { error: 'That visit did not check out. Load the next one.' });
const dwellMs = (ads.rates().visitDwellSeconds || 8) * 1000;
if (Date.now() - t.ts < dwellMs - 400) return json(res, 400, { error: 'Give the site the full visit first.' });
if (!t.challenge) return json(res, 400, { error: 'Finish the quick check first.', retry: true });
if (Number(b.answer) !== t.challenge.answer) { t.challenge = null; return json(res, 400, { error: 'Wrong pick.', retry: true }); }
visitTokens.delete(s.email);
const r = await ads.completeVisit(s.email, t.id);
if (r.error) return json(res, 400, r);
return json(res, 200, Object.assign(r, { status: await ads.visitStatus(s.email) }));
}
// -- onsite solo ads: member inbox with read rewards // -- onsite solo ads: member inbox with read rewards
if (p === '/api/my/inbox' && req.method === 'GET') { if (p === '/api/my/inbox' && req.method === 'GET') {
const s = await auth.fromRequest(req); const s = await auth.fromRequest(req);