The board says the one condition out loud

The card read "when someone new opens PolHunter" and listed everyone a hunter
had brought as hunting or earning, including members who joined InstantAdPay
before the mission existed and can never pay a bounty. So the board promised an
earning the payout code would refuse.

It now says what actually qualifies: someone who has never had an InstantAdPay
account. People who were already members are listed as exactly that, the match
window is not claimed for them, and the counts show how many of the brought are
genuinely new.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-23 11:41:54 -05:00
parent d04474fd7b
commit 2503b1e1a2
4 changed files with 254 additions and 240 deletions
+52 -52
View File
@@ -1,52 +1,52 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Your board · PolHunter</title>
<meta name="robots" content="noindex">
<link rel="stylesheet" href="/style.css?v=16">
</head>
<body>
<div class="wrap">
<header class="top">
<a class="mark" href="/"><span class="coin"></span>PolHunter</a>
<nav class="nav"><span class="pill" id="who"><i></i> …</span><a class="btn ghost sm" href="/leaders">Leaderboard</a><a class="btn ghost sm" href="/promo">Promo tools</a><a class="btn ghost sm" href="/logout">Sign out</a></nav>
</header>
<div class="standing" id="standing"></div>
<section class="section" style="padding-top:10px">
<h2>Your missions</h2>
<div class="sub" id="rangeLine">Each find pays a random drip. Your codes are yours alone.</div>
<div id="refSlot"></div>
<div class="grid" id="missions"><div class="card"><p>Loading your board…</p></div></div>
</section>
<section class="section" id="badgesSec">
<h2>Your badges</h2>
<div class="sub">Earned from your finds. Share one and the card carries your name and your link.</div>
<div class="badge-strip" id="badgeStrip"></div>
</section>
<div class="adslot" data-ad="board"><!-- nas: 468x60 on phones, 728x90 on wider screens --><script>(function(){var m=window.innerWidth<600;document.write('<scr'+'ipt src="https://www.networkadspace.com/showadss.php?'+(m?'w=468&h=60&n=1&bw=468&bh=60':'w=728&h=90&n=1&bw=728&bh=90')+'&c=999"></scr'+'ipt>');})();</script></div>
<section class="section">
<h2>Your drips</h2>
<div class="ledger" id="mine"></div>
</section>
<section class="section" id="share">
<h2>Share your hunt</h2>
<div class="sub">Your link puts sign-ups on your InstantAdPay line. Post a find, or just the link.</div>
<div class="copybox"><input id="mylink" readonly value=""><button class="btn pol sm" id="copylink">Copy link</button></div>
<textarea id="sharetext" rows="4" style="margin-top:10px"></textarea>
<div class="sharebtns" id="sharebtns"></div>
<p class="small" style="margin-top:10px">Banners, swipes and the teaser video are on the <a href="/promo">promo tools</a> page, already carrying your link.</p>
</section>
<footer class="foot">Rewards are for completed missions, not income. The daily pool is limited; when it is spent, claims close until midnight Central. Cryptocurrency involves risk of loss.</footer>
</div>
<div class="modal" id="shareModal" hidden><div class="modal-card"><button class="modal-x" id="shareClose" type="button" aria-label="Close">×</button><div id="shareBody"></div></div></div>
<script src="/app.js?v=16"></script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Your board · PolHunter</title>
<meta name="robots" content="noindex">
<link rel="stylesheet" href="/style.css?v=17">
</head>
<body>
<div class="wrap">
<header class="top">
<a class="mark" href="/"><span class="coin"></span>PolHunter</a>
<nav class="nav"><span class="pill" id="who"><i></i> …</span><a class="btn ghost sm" href="/leaders">Leaderboard</a><a class="btn ghost sm" href="/promo">Promo tools</a><a class="btn ghost sm" href="/logout">Sign out</a></nav>
</header>
<div class="standing" id="standing"></div>
<section class="section" style="padding-top:10px">
<h2>Your missions</h2>
<div class="sub" id="rangeLine">Each find pays a random drip. Your codes are yours alone.</div>
<div id="refSlot"></div>
<div class="grid" id="missions"><div class="card"><p>Loading your board…</p></div></div>
</section>
<section class="section" id="badgesSec">
<h2>Your badges</h2>
<div class="sub">Earned from your finds. Share one and the card carries your name and your link.</div>
<div class="badge-strip" id="badgeStrip"></div>
</section>
<div class="adslot" data-ad="board"><!-- nas: 468x60 on phones, 728x90 on wider screens --><script>(function(){var m=window.innerWidth<600;document.write('<scr'+'ipt src="https://www.networkadspace.com/showadss.php?'+(m?'w=468&h=60&n=1&bw=468&bh=60':'w=728&h=90&n=1&bw=728&bh=90')+'&c=999"></scr'+'ipt>');})();</script></div>
<section class="section">
<h2>Your drips</h2>
<div class="ledger" id="mine"></div>
</section>
<section class="section" id="share">
<h2>Share your hunt</h2>
<div class="sub">Your link puts sign-ups on your InstantAdPay line. Post a find, or just the link.</div>
<div class="copybox"><input id="mylink" readonly value=""><button class="btn pol sm" id="copylink">Copy link</button></div>
<textarea id="sharetext" rows="4" style="margin-top:10px"></textarea>
<div class="sharebtns" id="sharebtns"></div>
<p class="small" style="margin-top:10px">Banners, swipes and the teaser video are on the <a href="/promo">promo tools</a> page, already carrying your link.</p>
</section>
<footer class="foot">Rewards are for completed missions, not income. The daily pool is limited; when it is spent, claims close until midnight Central. Cryptocurrency involves risk of loss.</footer>
</div>
<div class="modal" id="shareModal" hidden><div class="modal-card"><button class="modal-x" id="shareClose" type="button" aria-label="Close">×</button><div id="shareBody"></div></div></div>
<script src="/app.js?v=17"></script>
</body>
</html>
+187 -186
View File
@@ -1,186 +1,187 @@
// The hunter's board: missions in their own order, a live timer while they are on the site, a
// code box when they are back. One open mission at a time.
(() => {
const $ = id => document.getElementById(id);
const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
const api = async (path, body) => { const r = await fetch(path, body ? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) } : {}); const j = await r.json().catch(() => ({})); if (r.status === 401) location.href = '/?signin=1'; return j; };
let board = null, open = null; // open: { missionId, token, url, dwell, started }
// toasts: small, stacked, gone in a few seconds
function toast(msg, kind, link) {
let host = $('toasts'); if (!host) { host = document.createElement('div'); host.id = 'toasts'; document.body.appendChild(host); }
const t = document.createElement('div'); t.className = 'toast ' + (kind || ''); t.innerHTML = msg + (link ? ' <a href="' + esc(link.href) + '" target="_blank" rel="noopener">' + esc(link.text) + '</a>' : '');
host.appendChild(t); requestAnimationFrame(() => t.classList.add('in'));
setTimeout(() => { t.classList.remove('in'); setTimeout(() => t.remove(), 400); }, kind === 'big' ? 9000 : 6000);
}
const seenPaid = new Set(); let seenLedger = null, paidPrimed = false;
// a drip that flips to paid while the board is open gets a toast with its proof. The first pass only
// records what is already paid: history is on the ledger, not in the toasts (Marty, 2026-09-21).
function watchPaid() {
for (const d of (board.drips || [])) if (d.status === 'paid' && d.tx) {
if (paidPrimed && !seenPaid.has(d.id)) toast('\u{1F4B0} <b>' + d.pol + ' POL</b> landed in your wallet.', 'ok', { href: explorer + '/tx/' + d.tx, text: 'verify \u2197' });
seenPaid.add(d.id);
}
paidPrimed = true;
}
// other hunters' finds, as they happen
async function watchLedger() {
try { const r = await (await fetch('/api/ledger')).json(); const rows = r.recent || [];
if (seenLedger) { const fresh = rows.filter(x => !seenLedger.has(x.tx)).slice(0, 2); for (const x of fresh) if (!board || !board.me || x.who !== (board.me.username ? '@' + board.me.username : '#' + board.me.memberId)) toast('\u{1F3AF} ' + esc(x.who) + ' just found <b>' + x.pol + ' POL</b> on ' + esc(x.site) + '.', ''); }
seenLedger = new Set(rows.map(x => x.tx));
} catch (e) {}
}
setInterval(watchLedger, 45000); setInterval(async () => { if (board && (board.drips || []).some(d => d.status === 'due' || d.status === 'sent')) { await load(); } }, 30000);
function card(m) {
const isOpen = open && open.missionId === m.id;
const r = m.reward || {};
return '<div class="card' + (isOpen ? ' mission-open' : '') + '" data-id="' + esc(m.id) + '">'
+ '<span class="tag' + (m.done ? ' done' : '') + '">' + (m.done ? 'Done today' : esc(m.site)) + '</span>'
+ '<h3>' + esc(m.name) + '</h3><p>' + esc(m.brief) + '</p>'
+ '<p style="margin-top:10px"><span class="range">' + r.minPol + ' to ' + r.maxPol + ' POL</span> <span style="color:var(--dim);font-size:12px">· ' + m.dwell + 's on the site</span></p>'
+ (m.done ? '' : (board.pool && board.pool.spent && !isOpen) ? '<div class="cardact"><span class="tag gold">Closed until midnight Central</span></div>' : (board.daily && !board.daily.left && !isOpen) ? '<div class="cardact"><span class="tag">Back tomorrow</span></div>' : isOpen
? '<div class="timer" id="timer">Your code appears on the site after <b>' + m.dwell + 's</b>. Keep that tab open.</div>'
+ '<div class="codebox"><input id="code" maxlength="6" placeholder="6 CHARACTERS" autocomplete="off" spellcheck="false" inputmode="text" autocapitalize="characters"><button class="btn pol" id="submit">Claim</button></div><div class="site" style="font-size:12px;margin-top:6px">Six characters. On the site, tap the code to copy it, then paste it here.</div>'
+ '<div class="msg" id="msg"></div>'
+ '<p style="margin-top:8px"><a href="' + esc(open.url) + '" target="_blank" rel="noopener">Open the site again ↗</a></p>'
: '<div class="cardact"><button class="btn" data-start="' + esc(m.id) + '">Start mission</button></div>')
+ '</div>';
}
// The best-paid thing on the board, and the reason it sits first: sharing used to live on a page
// nobody opened, so nobody shared (Marty, 2026-09-23).
function referralCard() {
const rf = board.referral; if (!rf || !rf.on) return '';
const link = (board.share && board.share.link) || '';
const today = rf.broughtToday, total = rf.brought;
const people = (rf.people || []).slice(0, 5).map(p =>
'<div class="ref-row"><span>' + esc(p.name) + '</span><span class="ref-state">' + esc(p.state) + '</span></div>').join('');
return '<div class="card ref-card">'
+ '<span class="tag gold">Best paid</span>'
+ '<h3>🤝 Bring a hunter</h3>'
+ '<p>Send your link. When someone new opens PolHunter and finds their first code, you get <b>' + rf.bountyPol + ' POL</b>. '
+ 'After that you earn <b>' + rf.matchPct + '%</b> on top of everything they find for their first ' + rf.matchDays + ' days. '
+ 'It comes out of our pool, never theirs.</p>'
+ (rf.firstBonusPol ? '<p class="ref-bonus">\u{1F947} <b>' + rf.firstBonusPol + ' POL bonus, still unclaimed.</b> It goes to the first hunter who brings someone all the way through. Nobody has yet.</p>' : '')
+ '<div class="codebox"><input id="refLink" readonly value="' + esc(link) + '"><button class="btn pol" id="refCopy">Copy</button></div>'
+ '<p style="margin-top:10px"><span class="range">' + today + ' brought today</span> '
+ '<span style="color:var(--dim);font-size:12px">· ' + total + ' all time · ' + rf.earnedPol + ' POL earned</span></p>'
+ (people ? '<div class="ref-list">' + people + '</div>' : '<p style="color:var(--dim);font-size:12px;margin-top:10px">Nobody yet. One message is all it takes.</p>')
+ '<div class="ref-watch"><button class="btn ghost sm" type="button" id="refPlay">▶ Watch: how it works (46s)</button></div>'
+ '</div>';
}
// the explainer plays in place, the way the teaser does on the home page
function wireRefVideo() {
const b = $('refPlay'); if (!b) return;
b.addEventListener('click', () => {
const host = b.parentElement;
const v = document.createElement('video');
v.controls = true; v.autoplay = true; v.playsInline = true; v.className = 'ref-video';
v.poster = 'https://coolify-saasytop.nyc3.digitaloceanspaces.com/training/polhunter-bring-a-hunter.jpg?v=20260923a';
v.src = 'https://coolify-saasytop.nyc3.digitaloceanspaces.com/training/polhunter-bring-a-hunter.mp4?v=20260923a';
host.innerHTML = ''; host.appendChild(v);
v.play().catch(() => {});
});
}
const IAP = 'https://instantadpay.com';
function until(ms) { const s = Math.max(0, Math.round((ms - Date.now()) / 60000)); const h = Math.floor(s / 60), m = s % 60; return h ? h + 'h ' + m + 'm' : m + 'm'; }
// no wallet on the InstantAdPay account yet: the three steps, in place of the missions
function onboarding() {
return '<div class="notice"><b>One thing before your first hunt.</b> Drips are paid to the wallet on your InstantAdPay account, and yours has none yet.</div>'
+ '<div class="steps" style="margin-top:14px">'
+ '<div class="step"><b>Create your free InstantAdPay account.</b> Already done: you are signed in as ' + esc(board.me.username ? '@' + board.me.username : '#' + board.me.memberId) + '.</div>'
+ '<div class="step"><b>Link your wallet.</b> In your InstantAdPay dashboard open <a href="' + IAP + '/my#wallet" target="_blank" rel="noopener">Wallet</a>, press connect wallet and approve. Any Polygon wallet works (MetaMask, Trust, Phantom with Polygon on).</div>'
+ '<div class="step"><b>Come back through the door.</b> Return to your dashboard and press <a href="' + IAP + '/api/my/polhunter">Open my board</a> again. Your missions unlock the moment a wallet is on the account.</div>'
+ '</div>';
}
// badge cards: art, lock state, share
function badgeStrip() {
const el = $('badgeStrip'); if (!el || !board.badgeCards) return;
el.innerHTML = board.badgeCards.map(b => '<div class="badge-a' + (b.earned ? '' : ' locked') + '"><img src="' + b.art + '" alt="' + esc(b.name) + '" loading="lazy"><div class="bl">' + b.icon + ' ' + esc(b.name) + '</div><div class="bs">' + esc(b.why) + '</div>' + (b.earned ? '<button class="btn pol sm" data-share="' + b.id + '">Share</button>' : '<div class="bs">\u{1F512} locked</div>') + '</div>').join('');
el.querySelectorAll('[data-share]').forEach(x => x.addEventListener('click', () => shareBadge(x.dataset.share, false)));
}
function shareBadge(id, unlocked) {
const b = (board.badgeCards || []).find(x => x.id === id); if (!b || !b.earned) return;
const text = (unlocked ? 'Achievement unlocked on PolHunter: ' : 'I earned ') + b.name + ' (' + b.why + ') on PolHunter. Visit a site, find your code, get a drip of POL to your wallet. ' + b.page;
const T = encodeURIComponent(text), U = encodeURIComponent(b.page);
$('shareBody').innerHTML = '<div class="share-hd">' + (unlocked ? '<span class="tag gold">Achievement unlocked</span>' : '') + '<h3>' + b.icon + ' ' + esc(b.name) + '</h3><p class="site">' + esc(b.why) + '</p></div>'
+ '<img class="share-img" src="' + b.image + '?t=' + Date.now() + '" alt="' + esc(b.name) + '">'
+ '<div class="sharebtns" style="justify-content:center"><a class="btn ghost sm" target="_blank" rel="noopener" href="https://twitter.com/intent/tweet?text=' + T + '">X</a><a class="btn ghost sm" target="_blank" rel="noopener" href="https://t.me/share/url?url=' + U + '&text=' + encodeURIComponent(text.replace(b.page, '').trim()) + '">Telegram</a><a class="btn ghost sm" target="_blank" rel="noopener" href="https://www.facebook.com/sharer/sharer.php?u=' + U + '">Facebook</a><a class="btn ghost sm" target="_blank" rel="noopener" href="https://wa.me/?text=' + T + '">WhatsApp</a><a class="btn ghost sm" href="' + b.image + '" download="polhunter-' + b.id + '.jpg">Save image</a><button class="btn pol sm" id="shareCopy">Copy link</button></div>'
+ '<p class="small" style="margin-top:10px;text-align:center">The card carries your name; the link carries your referral.</p>';
$('shareModal').hidden = false;
$('shareCopy').onclick = async () => { try { await navigator.clipboard.writeText(b.page); } catch (e) {} $('shareCopy').textContent = 'Copied'; setTimeout(() => { $('shareCopy').textContent = 'Copy link'; }, 1200); };
}
document.addEventListener('click', e => { if (e.target.id === 'shareClose' || e.target.id === 'shareModal') $('shareModal').hidden = true; });
function render() {
badgeStrip();
$('who').innerHTML = '<i></i> ' + esc(board.me.username ? '@' + board.me.username : '#' + board.me.memberId) + (board.me.wallet ? '' : ' · no wallet linked');
const r = board.missions[0] && board.missions[0].reward;
if (r) $('rangeLine').textContent = 'Each find pays a random drip between ' + r.minPol + ' and ' + r.maxPol + ' POL. Your codes are yours alone.';
const pool = board.pool || {};
if (!board.me.wallet) $('missions').innerHTML = onboarding();
else $('missions').innerHTML = (pool.spent ? '<div class="notice" style="grid-column:1/-1"><b>Today\u2019s POL pool is spent.</b> No more claims today. Claims reopen at midnight Central' + (pool.resetsAt ? ', in ' + until(pool.resetsAt) : '') + '.</div>' : '')
+ (board.missions.length ? board.missions.map(card).join('') : '<div class="card"><p>No missions are open right now. Check back soon.</p></div>');
if ($('refSlot')) $('refSlot').innerHTML = board.me.wallet ? referralCard() : '';
wireRefVideo();
const rc = $('refCopy');
if (rc) rc.addEventListener('click', async () => {
const v = $('refLink').value;
try { await navigator.clipboard.writeText(v); } catch (e) { $('refLink').select(); document.execCommand('copy'); }
rc.textContent = 'Copied'; setTimeout(() => { rc.textContent = 'Copy'; }, 1600);
});
// standing: badges and rank
const rk = board.rank || {}; const line = [];
if (rk.week) line.push('This week <b>#' + rk.week.rank + '</b> of ' + rk.week.of); if (rk.all) line.push('All time <b>#' + rk.all.rank + '</b> of ' + rk.all.of);
$('standing').innerHTML = (board.badges && board.badges.length ? '<span class="badges" title="Your badges">' + board.badges.map(b => '<span class="badge" title="' + esc(b.name) + ': ' + esc(b.why) + '">' + b.icon + ' ' + esc(b.name) + '</span>').join('') + '</span>' : '<span class="site">No badges yet. Your first find earns one.</span>')
+ (line.length ? '<span class="rankline">' + line.join(' \u00b7 ') + ' \u00b7 <a href="/leaders">leaderboard</a></span>' : '<span class="rankline"><a href="/leaders">leaderboard</a></span>');
// share panel
if (board.share) {
const link = board.share.link; $('mylink').value = link;
const best = (board.drips || []).filter(d => d.status === 'paid').sort((a, b) => b.pol - a.pol)[0]; // brag about the biggest find
const text = best ? 'I just found ' + best.pol + ' POL on ' + best.site + ' with PolHunter. Visit a site, find your code, get a drip of POL to your wallet, on chain. ' + link
: 'PolHunter: visit one of our sites, find your code on the page, and a random drip of POL lands in your wallet. On chain, with a link to prove it. ' + link;
if (!$('sharetext').dataset.touched) $('sharetext').value = text;
const L = encodeURIComponent(link), T = () => encodeURIComponent($('sharetext').value);
$('sharebtns').innerHTML = '<a class="btn ghost sm" target="_blank" rel="noopener" data-x>X</a><a class="btn ghost sm" target="_blank" rel="noopener" data-tg>Telegram</a><a class="btn ghost sm" target="_blank" rel="noopener" href="https://www.facebook.com/sharer/sharer.php?u=' + L + '">Facebook</a><a class="btn ghost sm" target="_blank" rel="noopener" data-wa>WhatsApp</a><button class="btn pol sm" id="copytext">Copy text</button>';
const wire = () => { $('sharebtns').querySelector('[data-x]').href = 'https://twitter.com/intent/tweet?text=' + T(); $('sharebtns').querySelector('[data-tg]').href = 'https://t.me/share/url?url=' + L + '&text=' + encodeURIComponent($('sharetext').value.replace(link, '').trim()); $('sharebtns').querySelector('[data-wa]').href = 'https://wa.me/?text=' + T(); };
wire(); $('sharetext').oninput = () => { $('sharetext').dataset.touched = '1'; wire(); };
const copy = async (el, btn) => { try { await navigator.clipboard.writeText(el.value); } catch (e) { el.select(); document.execCommand('copy'); } const t = btn.textContent; btn.textContent = 'Copied'; setTimeout(() => { btn.textContent = t; }, 1200); };
$('copylink').onclick = () => copy($('mylink'), $('copylink')); $('copytext').onclick = () => copy($('sharetext'), $('copytext'));
}
watchPaid(); if (seenLedger === null) watchLedger();
const dl = board.daily; if (dl && $('rangeLine')) $('rangeLine').innerHTML = ($('rangeLine').textContent.replace(/\s*·\s*You have.*$/, '')) + ' \u00b7 ' + (dl.left ? 'You have <b>' + dl.left + ' of ' + dl.limit + '</b> missions left today.' : '<b>That is your ' + dl.limit + ' for today.</b> Fresh missions at midnight Central.');
$('mine').innerHTML = board.drips.length ? board.drips.map(d => '<div class="row"><span>' + esc(d.site) + '</span><span class="site">' + ({ paid: 'paid', sent: 'sending', due: 'paying next', queued: 'queued for the next pool', failed: 'held, being looked at' }[d.status] || d.status) + '</span><span class="pol">+' + d.pol + ' POL</span>' + (d.tx ? '<a class="when" target="_blank" rel="noopener" href="' + explorer + '/tx/' + d.tx + '">verify ↗</a>' : '<span class="when"></span>') + '</div>').join('')
: '<div class="row"><span class="site">Nothing yet. Your first find goes here.</span></div>';
document.querySelectorAll('[data-start]').forEach(b => b.addEventListener('click', () => start(b.dataset.start)));
const s = $('submit'); if (s) { s.addEventListener('click', submit); $('code').addEventListener('keydown', e => { if (e.key === 'Enter') submit(); }); tick(); }
}
let explorer = 'https://polygonscan.com';
async function load() { board = await api('/api/my/board'); render(); }
async function start(id) {
const r = await api('/api/my/start', { missionId: id });
if (r.error) { toast(esc(r.error), 'bad'); if (r.spent || r.dailyDone) await load(); return; }
open = { missionId: id, token: r.token, url: r.url, dwell: r.dwell, started: Date.now(), expires: r.expires };
window.open(r.url, '_blank', 'noopener');
render();
}
function tick() {
const el = $('timer'); if (!el || !open) return;
const left = Math.max(0, open.dwell - Math.floor((Date.now() - open.started) / 1000));
el.innerHTML = left > 0 ? 'Your code appears on the site in about <b>' + left + 's</b>. <b>Stay on the site tab</b> until it shows (switching away pauses it), then come back and paste it here.' : 'Your code should be showing on the site now. If it is not, switch to the site tab and give it a few seconds. Then paste it here.';
if (open.expires && Date.now() > open.expires) { el.innerHTML = 'This attempt timed out. Start the mission again.'; return; }
setTimeout(tick, 1000);
}
async function submit() {
const code = $('code').value.trim(); if (!code) return;
$('submit').disabled = true;
const r = await api('/api/my/submit', { token: open.token, code });
const m = $('msg'); m.className = 'msg ' + (r.error ? 'bad' : 'ok'); m.textContent = r.error || r.message;
if (r.error) toast(esc(r.error), 'bad'); else toast('\u{1F389} <b>Found it!</b> ' + r.pol + ' POL is on its way to your wallet.', 'big');
if (!r.error) { const a = document.createElement('a'); a.href = '#share'; a.textContent = ' Share this find \u2197'; a.style.marginLeft = '6px'; m.appendChild(a); delete $('sharetext').dataset.touched; }
// an achievement unlocked by this find: celebrate it once the board has reloaded
if (!r.error && r.unlocked && r.unlocked.length) { const id = r.unlocked[0]; toast('\u{1F3C6} <b>Achievement unlocked!</b>', 'big'); setTimeout(async () => { await load(); shareBadge(id, true); }, 1200); }
$('submit').disabled = false;
if (!r.error || r.spent) { open = null; setTimeout(load, 900); }
}
fetch('/api/config').then(r => r.json()).then(c => { explorer = c.explorer || explorer; }).catch(() => {});
load();
})();
// The hunter's board: missions in their own order, a live timer while they are on the site, a
// code box when they are back. One open mission at a time.
(() => {
const $ = id => document.getElementById(id);
const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
const api = async (path, body) => { const r = await fetch(path, body ? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) } : {}); const j = await r.json().catch(() => ({})); if (r.status === 401) location.href = '/?signin=1'; return j; };
let board = null, open = null; // open: { missionId, token, url, dwell, started }
// toasts: small, stacked, gone in a few seconds
function toast(msg, kind, link) {
let host = $('toasts'); if (!host) { host = document.createElement('div'); host.id = 'toasts'; document.body.appendChild(host); }
const t = document.createElement('div'); t.className = 'toast ' + (kind || ''); t.innerHTML = msg + (link ? ' <a href="' + esc(link.href) + '" target="_blank" rel="noopener">' + esc(link.text) + '</a>' : '');
host.appendChild(t); requestAnimationFrame(() => t.classList.add('in'));
setTimeout(() => { t.classList.remove('in'); setTimeout(() => t.remove(), 400); }, kind === 'big' ? 9000 : 6000);
}
const seenPaid = new Set(); let seenLedger = null, paidPrimed = false;
// a drip that flips to paid while the board is open gets a toast with its proof. The first pass only
// records what is already paid: history is on the ledger, not in the toasts (Marty, 2026-09-21).
function watchPaid() {
for (const d of (board.drips || [])) if (d.status === 'paid' && d.tx) {
if (paidPrimed && !seenPaid.has(d.id)) toast('\u{1F4B0} <b>' + d.pol + ' POL</b> landed in your wallet.', 'ok', { href: explorer + '/tx/' + d.tx, text: 'verify \u2197' });
seenPaid.add(d.id);
}
paidPrimed = true;
}
// other hunters' finds, as they happen
async function watchLedger() {
try { const r = await (await fetch('/api/ledger')).json(); const rows = r.recent || [];
if (seenLedger) { const fresh = rows.filter(x => !seenLedger.has(x.tx)).slice(0, 2); for (const x of fresh) if (!board || !board.me || x.who !== (board.me.username ? '@' + board.me.username : '#' + board.me.memberId)) toast('\u{1F3AF} ' + esc(x.who) + ' just found <b>' + x.pol + ' POL</b> on ' + esc(x.site) + '.', ''); }
seenLedger = new Set(rows.map(x => x.tx));
} catch (e) {}
}
setInterval(watchLedger, 45000); setInterval(async () => { if (board && (board.drips || []).some(d => d.status === 'due' || d.status === 'sent')) { await load(); } }, 30000);
function card(m) {
const isOpen = open && open.missionId === m.id;
const r = m.reward || {};
return '<div class="card' + (isOpen ? ' mission-open' : '') + '" data-id="' + esc(m.id) + '">'
+ '<span class="tag' + (m.done ? ' done' : '') + '">' + (m.done ? 'Done today' : esc(m.site)) + '</span>'
+ '<h3>' + esc(m.name) + '</h3><p>' + esc(m.brief) + '</p>'
+ '<p style="margin-top:10px"><span class="range">' + r.minPol + ' to ' + r.maxPol + ' POL</span> <span style="color:var(--dim);font-size:12px">· ' + m.dwell + 's on the site</span></p>'
+ (m.done ? '' : (board.pool && board.pool.spent && !isOpen) ? '<div class="cardact"><span class="tag gold">Closed until midnight Central</span></div>' : (board.daily && !board.daily.left && !isOpen) ? '<div class="cardact"><span class="tag">Back tomorrow</span></div>' : isOpen
? '<div class="timer" id="timer">Your code appears on the site after <b>' + m.dwell + 's</b>. Keep that tab open.</div>'
+ '<div class="codebox"><input id="code" maxlength="6" placeholder="6 CHARACTERS" autocomplete="off" spellcheck="false" inputmode="text" autocapitalize="characters"><button class="btn pol" id="submit">Claim</button></div><div class="site" style="font-size:12px;margin-top:6px">Six characters. On the site, tap the code to copy it, then paste it here.</div>'
+ '<div class="msg" id="msg"></div>'
+ '<p style="margin-top:8px"><a href="' + esc(open.url) + '" target="_blank" rel="noopener">Open the site again ↗</a></p>'
: '<div class="cardact"><button class="btn" data-start="' + esc(m.id) + '">Start mission</button></div>')
+ '</div>';
}
// The best-paid thing on the board, and the reason it sits first: sharing used to live on a page
// nobody opened, so nobody shared (Marty, 2026-09-23).
function referralCard() {
const rf = board.referral; if (!rf || !rf.on) return '';
const link = (board.share && board.share.link) || '';
const today = rf.broughtToday, total = rf.brought;
const people = (rf.people || []).slice(0, 5).map(p =>
'<div class="ref-row"><span>' + esc(p.name) + '</span><span class="ref-state">' + esc(p.state) + '</span></div>').join('');
return '<div class="card ref-card">'
+ '<span class="tag gold">Best paid</span>'
+ '<h3>🤝 Bring a hunter</h3>'
+ '<p>Send your link. When someone who has <b>never had an InstantAdPay account</b> joins on it, opens PolHunter and finds their first code, you get <b>' + rf.bountyPol + ' POL</b>. '
+ 'After that you earn <b>' + rf.matchPct + '%</b> on top of everything they find for their first ' + rf.matchDays + ' days. '
+ 'It comes out of our pool, never theirs.</p>'
+ '<p class="ref-note">This pays for new people only. Someone who was already a member before this mission started earns nothing, however much they hunt.</p>'
+ (rf.firstBonusPol ? '<p class="ref-bonus">\u{1F947} <b>' + rf.firstBonusPol + ' POL bonus, still unclaimed.</b> It goes to the first hunter who brings someone all the way through. Nobody has yet.</p>' : '')
+ '<div class="codebox"><input id="refLink" readonly value="' + esc(link) + '"><button class="btn pol" id="refCopy">Copy</button></div>'
+ '<p style="margin-top:10px"><span class="range">' + today + ' brought today</span> '
+ '<span style="color:var(--dim);font-size:12px">· ' + total + ' all time, ' + (rf.broughtNew || 0) + ' of them new · ' + rf.earnedPol + ' POL earned</span></p>'
+ (people ? '<div class="ref-list">' + people + '</div>' : '<p style="color:var(--dim);font-size:12px;margin-top:10px">Nobody yet. One message is all it takes.</p>')
+ '<div class="ref-watch"><button class="btn ghost sm" type="button" id="refPlay">▶ Watch: how it works (46s)</button></div>'
+ '</div>';
}
// the explainer plays in place, the way the teaser does on the home page
function wireRefVideo() {
const b = $('refPlay'); if (!b) return;
b.addEventListener('click', () => {
const host = b.parentElement;
const v = document.createElement('video');
v.controls = true; v.autoplay = true; v.playsInline = true; v.className = 'ref-video';
v.poster = 'https://coolify-saasytop.nyc3.digitaloceanspaces.com/training/polhunter-bring-a-hunter.jpg?v=20260923a';
v.src = 'https://coolify-saasytop.nyc3.digitaloceanspaces.com/training/polhunter-bring-a-hunter.mp4?v=20260923a';
host.innerHTML = ''; host.appendChild(v);
v.play().catch(() => {});
});
}
const IAP = 'https://instantadpay.com';
function until(ms) { const s = Math.max(0, Math.round((ms - Date.now()) / 60000)); const h = Math.floor(s / 60), m = s % 60; return h ? h + 'h ' + m + 'm' : m + 'm'; }
// no wallet on the InstantAdPay account yet: the three steps, in place of the missions
function onboarding() {
return '<div class="notice"><b>One thing before your first hunt.</b> Drips are paid to the wallet on your InstantAdPay account, and yours has none yet.</div>'
+ '<div class="steps" style="margin-top:14px">'
+ '<div class="step"><b>Create your free InstantAdPay account.</b> Already done: you are signed in as ' + esc(board.me.username ? '@' + board.me.username : '#' + board.me.memberId) + '.</div>'
+ '<div class="step"><b>Link your wallet.</b> In your InstantAdPay dashboard open <a href="' + IAP + '/my#wallet" target="_blank" rel="noopener">Wallet</a>, press connect wallet and approve. Any Polygon wallet works (MetaMask, Trust, Phantom with Polygon on).</div>'
+ '<div class="step"><b>Come back through the door.</b> Return to your dashboard and press <a href="' + IAP + '/api/my/polhunter">Open my board</a> again. Your missions unlock the moment a wallet is on the account.</div>'
+ '</div>';
}
// badge cards: art, lock state, share
function badgeStrip() {
const el = $('badgeStrip'); if (!el || !board.badgeCards) return;
el.innerHTML = board.badgeCards.map(b => '<div class="badge-a' + (b.earned ? '' : ' locked') + '"><img src="' + b.art + '" alt="' + esc(b.name) + '" loading="lazy"><div class="bl">' + b.icon + ' ' + esc(b.name) + '</div><div class="bs">' + esc(b.why) + '</div>' + (b.earned ? '<button class="btn pol sm" data-share="' + b.id + '">Share</button>' : '<div class="bs">\u{1F512} locked</div>') + '</div>').join('');
el.querySelectorAll('[data-share]').forEach(x => x.addEventListener('click', () => shareBadge(x.dataset.share, false)));
}
function shareBadge(id, unlocked) {
const b = (board.badgeCards || []).find(x => x.id === id); if (!b || !b.earned) return;
const text = (unlocked ? 'Achievement unlocked on PolHunter: ' : 'I earned ') + b.name + ' (' + b.why + ') on PolHunter. Visit a site, find your code, get a drip of POL to your wallet. ' + b.page;
const T = encodeURIComponent(text), U = encodeURIComponent(b.page);
$('shareBody').innerHTML = '<div class="share-hd">' + (unlocked ? '<span class="tag gold">Achievement unlocked</span>' : '') + '<h3>' + b.icon + ' ' + esc(b.name) + '</h3><p class="site">' + esc(b.why) + '</p></div>'
+ '<img class="share-img" src="' + b.image + '?t=' + Date.now() + '" alt="' + esc(b.name) + '">'
+ '<div class="sharebtns" style="justify-content:center"><a class="btn ghost sm" target="_blank" rel="noopener" href="https://twitter.com/intent/tweet?text=' + T + '">X</a><a class="btn ghost sm" target="_blank" rel="noopener" href="https://t.me/share/url?url=' + U + '&text=' + encodeURIComponent(text.replace(b.page, '').trim()) + '">Telegram</a><a class="btn ghost sm" target="_blank" rel="noopener" href="https://www.facebook.com/sharer/sharer.php?u=' + U + '">Facebook</a><a class="btn ghost sm" target="_blank" rel="noopener" href="https://wa.me/?text=' + T + '">WhatsApp</a><a class="btn ghost sm" href="' + b.image + '" download="polhunter-' + b.id + '.jpg">Save image</a><button class="btn pol sm" id="shareCopy">Copy link</button></div>'
+ '<p class="small" style="margin-top:10px;text-align:center">The card carries your name; the link carries your referral.</p>';
$('shareModal').hidden = false;
$('shareCopy').onclick = async () => { try { await navigator.clipboard.writeText(b.page); } catch (e) {} $('shareCopy').textContent = 'Copied'; setTimeout(() => { $('shareCopy').textContent = 'Copy link'; }, 1200); };
}
document.addEventListener('click', e => { if (e.target.id === 'shareClose' || e.target.id === 'shareModal') $('shareModal').hidden = true; });
function render() {
badgeStrip();
$('who').innerHTML = '<i></i> ' + esc(board.me.username ? '@' + board.me.username : '#' + board.me.memberId) + (board.me.wallet ? '' : ' · no wallet linked');
const r = board.missions[0] && board.missions[0].reward;
if (r) $('rangeLine').textContent = 'Each find pays a random drip between ' + r.minPol + ' and ' + r.maxPol + ' POL. Your codes are yours alone.';
const pool = board.pool || {};
if (!board.me.wallet) $('missions').innerHTML = onboarding();
else $('missions').innerHTML = (pool.spent ? '<div class="notice" style="grid-column:1/-1"><b>Today\u2019s POL pool is spent.</b> No more claims today. Claims reopen at midnight Central' + (pool.resetsAt ? ', in ' + until(pool.resetsAt) : '') + '.</div>' : '')
+ (board.missions.length ? board.missions.map(card).join('') : '<div class="card"><p>No missions are open right now. Check back soon.</p></div>');
if ($('refSlot')) $('refSlot').innerHTML = board.me.wallet ? referralCard() : '';
wireRefVideo();
const rc = $('refCopy');
if (rc) rc.addEventListener('click', async () => {
const v = $('refLink').value;
try { await navigator.clipboard.writeText(v); } catch (e) { $('refLink').select(); document.execCommand('copy'); }
rc.textContent = 'Copied'; setTimeout(() => { rc.textContent = 'Copy'; }, 1600);
});
// standing: badges and rank
const rk = board.rank || {}; const line = [];
if (rk.week) line.push('This week <b>#' + rk.week.rank + '</b> of ' + rk.week.of); if (rk.all) line.push('All time <b>#' + rk.all.rank + '</b> of ' + rk.all.of);
$('standing').innerHTML = (board.badges && board.badges.length ? '<span class="badges" title="Your badges">' + board.badges.map(b => '<span class="badge" title="' + esc(b.name) + ': ' + esc(b.why) + '">' + b.icon + ' ' + esc(b.name) + '</span>').join('') + '</span>' : '<span class="site">No badges yet. Your first find earns one.</span>')
+ (line.length ? '<span class="rankline">' + line.join(' \u00b7 ') + ' \u00b7 <a href="/leaders">leaderboard</a></span>' : '<span class="rankline"><a href="/leaders">leaderboard</a></span>');
// share panel
if (board.share) {
const link = board.share.link; $('mylink').value = link;
const best = (board.drips || []).filter(d => d.status === 'paid').sort((a, b) => b.pol - a.pol)[0]; // brag about the biggest find
const text = best ? 'I just found ' + best.pol + ' POL on ' + best.site + ' with PolHunter. Visit a site, find your code, get a drip of POL to your wallet, on chain. ' + link
: 'PolHunter: visit one of our sites, find your code on the page, and a random drip of POL lands in your wallet. On chain, with a link to prove it. ' + link;
if (!$('sharetext').dataset.touched) $('sharetext').value = text;
const L = encodeURIComponent(link), T = () => encodeURIComponent($('sharetext').value);
$('sharebtns').innerHTML = '<a class="btn ghost sm" target="_blank" rel="noopener" data-x>X</a><a class="btn ghost sm" target="_blank" rel="noopener" data-tg>Telegram</a><a class="btn ghost sm" target="_blank" rel="noopener" href="https://www.facebook.com/sharer/sharer.php?u=' + L + '">Facebook</a><a class="btn ghost sm" target="_blank" rel="noopener" data-wa>WhatsApp</a><button class="btn pol sm" id="copytext">Copy text</button>';
const wire = () => { $('sharebtns').querySelector('[data-x]').href = 'https://twitter.com/intent/tweet?text=' + T(); $('sharebtns').querySelector('[data-tg]').href = 'https://t.me/share/url?url=' + L + '&text=' + encodeURIComponent($('sharetext').value.replace(link, '').trim()); $('sharebtns').querySelector('[data-wa]').href = 'https://wa.me/?text=' + T(); };
wire(); $('sharetext').oninput = () => { $('sharetext').dataset.touched = '1'; wire(); };
const copy = async (el, btn) => { try { await navigator.clipboard.writeText(el.value); } catch (e) { el.select(); document.execCommand('copy'); } const t = btn.textContent; btn.textContent = 'Copied'; setTimeout(() => { btn.textContent = t; }, 1200); };
$('copylink').onclick = () => copy($('mylink'), $('copylink')); $('copytext').onclick = () => copy($('sharetext'), $('copytext'));
}
watchPaid(); if (seenLedger === null) watchLedger();
const dl = board.daily; if (dl && $('rangeLine')) $('rangeLine').innerHTML = ($('rangeLine').textContent.replace(/\s*·\s*You have.*$/, '')) + ' \u00b7 ' + (dl.left ? 'You have <b>' + dl.left + ' of ' + dl.limit + '</b> missions left today.' : '<b>That is your ' + dl.limit + ' for today.</b> Fresh missions at midnight Central.');
$('mine').innerHTML = board.drips.length ? board.drips.map(d => '<div class="row"><span>' + esc(d.site) + '</span><span class="site">' + ({ paid: 'paid', sent: 'sending', due: 'paying next', queued: 'queued for the next pool', failed: 'held, being looked at' }[d.status] || d.status) + '</span><span class="pol">+' + d.pol + ' POL</span>' + (d.tx ? '<a class="when" target="_blank" rel="noopener" href="' + explorer + '/tx/' + d.tx + '">verify ↗</a>' : '<span class="when"></span>') + '</div>').join('')
: '<div class="row"><span class="site">Nothing yet. Your first find goes here.</span></div>';
document.querySelectorAll('[data-start]').forEach(b => b.addEventListener('click', () => start(b.dataset.start)));
const s = $('submit'); if (s) { s.addEventListener('click', submit); $('code').addEventListener('keydown', e => { if (e.key === 'Enter') submit(); }); tick(); }
}
let explorer = 'https://polygonscan.com';
async function load() { board = await api('/api/my/board'); render(); }
async function start(id) {
const r = await api('/api/my/start', { missionId: id });
if (r.error) { toast(esc(r.error), 'bad'); if (r.spent || r.dailyDone) await load(); return; }
open = { missionId: id, token: r.token, url: r.url, dwell: r.dwell, started: Date.now(), expires: r.expires };
window.open(r.url, '_blank', 'noopener');
render();
}
function tick() {
const el = $('timer'); if (!el || !open) return;
const left = Math.max(0, open.dwell - Math.floor((Date.now() - open.started) / 1000));
el.innerHTML = left > 0 ? 'Your code appears on the site in about <b>' + left + 's</b>. <b>Stay on the site tab</b> until it shows (switching away pauses it), then come back and paste it here.' : 'Your code should be showing on the site now. If it is not, switch to the site tab and give it a few seconds. Then paste it here.';
if (open.expires && Date.now() > open.expires) { el.innerHTML = 'This attempt timed out. Start the mission again.'; return; }
setTimeout(tick, 1000);
}
async function submit() {
const code = $('code').value.trim(); if (!code) return;
$('submit').disabled = true;
const r = await api('/api/my/submit', { token: open.token, code });
const m = $('msg'); m.className = 'msg ' + (r.error ? 'bad' : 'ok'); m.textContent = r.error || r.message;
if (r.error) toast(esc(r.error), 'bad'); else toast('\u{1F389} <b>Found it!</b> ' + r.pol + ' POL is on its way to your wallet.', 'big');
if (!r.error) { const a = document.createElement('a'); a.href = '#share'; a.textContent = ' Share this find \u2197'; a.style.marginLeft = '6px'; m.appendChild(a); delete $('sharetext').dataset.touched; }
// an achievement unlocked by this find: celebrate it once the board has reloaded
if (!r.error && r.unlocked && r.unlocked.length) { const id = r.unlocked[0]; toast('\u{1F3C6} <b>Achievement unlocked!</b>', 'big'); setTimeout(async () => { await load(); shareBadge(id, true); }, 1200); }
$('submit').disabled = false;
if (!r.error || r.spent) { open = null; setTimeout(load, 900); }
}
fetch('/api/config').then(r => r.json()).then(c => { explorer = c.explorer || explorer; }).catch(() => {});
load();
})();
+4
View File
@@ -137,3 +137,7 @@ textarea{width:100%;padding:12px 14px;border-radius:12px;border:1px solid var(--
.ref-watch{margin-top:14px}
.ref-video{width:100%;max-width:560px;border-radius:14px;display:block;background:#000}
.ref-bonus{margin-top:10px;padding:9px 12px;border:1px solid rgba(243,190,67,.38);border-radius:12px;background:rgba(243,190,67,.07);font-size:13.5px;color:var(--ink)}
/* the one condition that decides whether a referral ever pays, said plainly on the card itself
rather than discovered when the bounty does not arrive (Marty, 2026-09-23) */
.ref-note{margin-top:8px;font-size:12.5px;line-height:1.45;color:var(--dim)}