From cb2542fca5f4e10e0ee2e29876e03f3ccdbee754 Mon Sep 17 00:00:00 2001 From: martbost Date: Sun, 6 Sep 2026 09:21:59 -0500 Subject: [PATCH] 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 --- ads.js | Bin 53487 -> 58954 bytes db.js | 8 ++++ public/assets/my.js | 87 +++++++++++++++++++++++++++++++++++++++++-- public/contract.html | 8 ++-- public/index.html | 10 ++--- public/ledger.html | 8 ++-- public/my.html | 35 ++++++++++++++--- public/shorts.html | 4 +- public/tx.html | 6 +-- public/view.html | 4 +- public/wall.html | 6 +-- server.js | 43 +++++++++++++++++++++ 12 files changed, 188 insertions(+), 31 deletions(-) diff --git a/ads.js b/ads.js index 5cc0df6aac9b38e90b3fa1c898f0a50181f4708b..a610cf9e6f376d2e25e5c2d8ec01de4612269e18 100644 GIT binary patch delta 3224 zcmb7G-ES0C6i)*PEQSxI1*xeATDmhZ9ZCfQyKD=#1q3UUN=<`=+|JzXj-8zuXYO=q zpeYbPLPQB%q7h%jL|!yTQy+}+0pf!n@d2NG^A8aJ024ju-r3TYqOon#&E9j**Y9_J z=ia}y9ruJY6s=WI>qus9uDI8*(t^AOYsSkR+O(8l zlXsd&Xd&rt?wPq$bz|IRMoQt62W2tOh6t%U-Yht2l-a^25-*Q& zp%1gXW6$Lp+tJX)R)l_94OEu(>afI8X1r}S=T=`CM?WS0ypa<+Ue|aESU@(D5p4}9!64i^bsw4iB~durzrQo zvQ#!-StB=}Y>_uFtj#H#GYRK`T>b7w`SZ&x#>nAQou}=f2vZ0l(QNb3@DbY` zHDPF2-g|3(!>}A2Z`%Xy?cmuiW>+eeK!o_oFiqR%!I-ENb5l8uJ>Es=&w)EyfFSjK zOJ03@i;mu*qMtbt%9D=21g46TUE8n5)_Ijgi=-XEA>fBQIT4j-ehQ0G{ zI+!xuw##1N8fPh>an|;#QKAPM_CYrEXKgh|YTT*Mo2g(MsfG6P@8=rj_z%ScdJqfX z*!t7S*oP_5T_%lK1imA-A2QcaSs|E%3wLa1mQk_hVQOYWw zSf~Sa5gI1(*Il8e=n;9rm4ke@yff9jk9HSQQ!kMPjUpzI2t1M(*2rn398+cgYQ3KO_J6cID$@GUBCdJE}n%zp-lNY>Kkwv#s*4 zrWNwomSrspWqP^r|zJFA7n?+QR^A z#Vm2sk$%|=Z=6r&WOQh&e6P~{7>36%^5Xl?x0AGVmwHH*bXJ+X%xMRvB)czdTBos} z!*O}~($?J%eM|lMFJ+NEU+tO$;rvq9e52LvE}I|`M= B5gPyi diff --git a/db.js b/db.js index ca5bb05..22fc33b 100644 --- a/db.js +++ b/db.js @@ -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 expires BIGINT NULL'); // featured rotation end time 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 ( id INT AUTO_INCREMENT PRIMARY KEY, from_member INT NOT NULL, diff --git a/public/assets/my.js b/public/assets/my.js index 201275e..6f51266 100644 --- a/public/assets/my.js +++ b/public/assets/my.js @@ -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 = 'That\'s today\'s visits. Come back tomorrow.'; + } 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 = '' + (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.') + ''; + return; + } + visState.token = r.token; visState.id = r.ad.id; visState.dwell = r.status.dwell || 8; + $('vsBox').innerHTML = '' + esc(r.ad.title || 'Member site') + '
Open the site and stay ' + visState.dwell + 's.'; + 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 => diff --git a/public/contract.html b/public/contract.html index 1e9d182..c48e481 100644 --- a/public/contract.html +++ b/public/contract.html @@ -5,7 +5,7 @@ The contract | InstantAdPay - +
@@ -129,8 +129,8 @@
Advertising services with a performance referral program. Not an investment product; no income guarantees. Crypto transactions are irreversible. Never spend what you cannot afford.
- - - + + + diff --git a/public/index.html b/public/index.html index 8a3e6fd..5df55bd 100644 --- a/public/index.html +++ b/public/index.html @@ -5,7 +5,7 @@ InstantAdPay: advertise and earn, locked in code - + @@ -410,9 +410,9 @@ - - - - + + + + diff --git a/public/ledger.html b/public/ledger.html index fea6ca9..7f63f2a 100644 --- a/public/ledger.html +++ b/public/ledger.html @@ -5,7 +5,7 @@ Live ledger | InstantAdPay - +
@@ -25,8 +25,8 @@
- - - + + + diff --git a/public/my.html b/public/my.html index d32d90e..b994f96 100644 --- a/public/my.html +++ b/public/my.html @@ -4,7 +4,7 @@ Member area | InstantAdPay - + @@ -300,6 +300,7 @@ +

@@ -342,6 +343,11 @@ placeholder="Call-to-action button label (default: Learn more) — the button opens your target URL">

+ + + - - - - + + + + diff --git a/public/shorts.html b/public/shorts.html index b702bed..c39b9d9 100644 --- a/public/shorts.html +++ b/public/shorts.html @@ -4,7 +4,7 @@ Shorts | InstantAdPay - +