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
+84 -3
View File
@@ -509,6 +509,8 @@
$('cSoloHint').hidden = t !== 'solo';
$('cVideoRow').hidden = t !== 'video';
$('cFeaturedRow').hidden = t !== 'featured';
$('cVisitsRow').hidden = t !== 'visits';
if (t === 'visits') visitHint();
$('cTitle').placeholder = t === 'solo' ? 'Subject line (max 80)' : 'Headline (max 60)';
soloHint();
if (t === 'video') videoHint();
@@ -539,6 +541,15 @@
+ ' 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(); });
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
function videoHint() {
if (!lastRates || !lastRates.videoTiers) return;
@@ -575,10 +586,11 @@
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,
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,
videoUrl: $('cVideoUrl').value, watchSecs: Number($('cWatchSecs').value),
days: Number($('cFeatDays').value), startDay: featStartDay,
count: Number($('cVisitCount').value),
ctaLabel: isVideo ? $('cVideoCta').value : $('cCtaLabel').value,
budget: isFeat ? (Number($('cFeatDays').value) * (lastRates.featuredPerDay || 40)) : Number($('cBudget').value) });
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)
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 ──
const vidState = { token: null, secs: 0, maxSeen: 0, done: false, credited: false };
async function loadVideoStatus() {
@@ -727,15 +806,17 @@
// Earn credits sub-tabs: Watch ads | Inbox
let earnSub = 'watch';
function setEarnSub(which) {
earnSub = ['inbox', 'videos'].includes(which) ? which : 'watch';
const w = $('earn-watch'), i = $('earn-inbox'), v = $('earn-videos');
earnSub = ['inbox', 'videos', 'visits'].includes(which) ? which : 'watch';
const w = $('earn-watch'), i = $('earn-inbox'), v = $('earn-videos'), vs = $('earn-visits');
if (w) w.hidden = earnSub !== 'watch';
if (i) i.hidden = earnSub !== 'inbox';
if (v) v.hidden = earnSub !== 'videos';
if (vs) vs.hidden = earnSub !== 'visits';
document.querySelectorAll('.subtabs [data-earn]').forEach(b =>
b.classList.toggle('on', b.dataset.earn === earnSub));
if (earnSub === 'inbox') loadInbox();
else if (earnSub === 'videos') loadVideoStatus();
else if (earnSub === 'visits') loadVisitStatus();
else earnRefresh();
}
document.querySelectorAll('.subtabs [data-earn]').forEach(b =>