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:
+84
-3
@@ -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 =>
|
||||
|
||||
Reference in New Issue
Block a user