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:
@@ -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
@@ -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 =>
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user