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
+11 -2
View File
@@ -162,17 +162,25 @@ function state(member) {
const me = hunter(id) || { memberId: id }; const me = hunter(id) || { memberId: id };
const rows = refRows().filter(p => Number(p.memberId) === id); const rows = refRows().filter(p => Number(p.memberId) === id);
const today = ctDay(); const today = ctDay();
// the same floor onFind pays by, so the card can never promise an earning it will not make
const floor = Date.parse(String(c.refNewAfter || ''));
const gateOk = Number.isFinite(floor) && floor > 0;
const mine = broughtBy(id).map(x => { const mine = broughtBy(id).map(x => {
const theirFinds = store.read('payouts', []).filter(p => Number(p.memberId) === Number(x.memberId) && !p.ref && !p.prize && p.status !== 'failed'); const theirFinds = store.read('payouts', []).filter(p => Number(p.memberId) === Number(x.memberId) && !p.ref && !p.prize && p.status !== 'failed');
const paid = bountyPaidFor(x.memberId); const paid = bountyPaidFor(x.memberId);
const isNew = !!(gateOk && x.joinedAt && Number(x.joinedAt) >= floor);
return { return {
name: x.username || ('#' + x.memberId), name: x.username || ('#' + x.memberId),
// when they actually joined InstantAdPay, not when this directory first noticed them: // when they actually joined InstantAdPay, not when this directory first noticed them:
// the backfill stamped firstSeen at seeding time and made everyone look brought-in today // the backfill stamped firstSeen at seeding time and made everyone look brought-in today
joinedAt: x.joinedAt || x.firstSeen || null, joinedAt: x.joinedAt || x.firstSeen || null,
finds: theirFinds.length, finds: theirFinds.length,
state: paid ? 'earning' : theirFinds.length ? 'hunting' : 'signed up, no find yet', // an account that predates the mission was already a member: say so plainly rather than
stillNew: x.joinedAt ? (Date.now() - Number(x.joinedAt)) / 86400000 <= Number(c.refMatchDays) : false, // show them as 'hunting' next to a bounty that will never arrive
isNew,
state: !isNew ? 'already a member before the mission, earns nothing'
: paid ? 'earning' : theirFinds.length ? 'hunting' : 'signed up, no find yet',
stillNew: isNew && (Date.now() - Number(x.joinedAt)) / 86400000 <= Number(c.refMatchDays),
}; };
}); });
return { return {
@@ -182,6 +190,7 @@ function state(member) {
matchPct: Number(c.refMatchPct), matchPct: Number(c.refMatchPct),
matchDays: Number(c.refMatchDays), matchDays: Number(c.refMatchDays),
brought: mine.length, brought: mine.length,
broughtNew: mine.filter(x => x.isNew).length, // the only ones that can ever pay
broughtToday: mine.filter(x => x.joinedAt && ctDay(x.joinedAt) === today).length, broughtToday: mine.filter(x => x.joinedAt && ctDay(x.joinedAt) === today).length,
earnedPol: round4(rows.reduce((n, p) => n + p.pol, 0)), earnedPol: round4(rows.reduce((n, p) => n + p.pol, 0)),
earnedTodayPol: round4(rows.filter(p => p.day === today).reduce((n, p) => n + p.pol, 0)), earnedTodayPol: round4(rows.filter(p => p.day === today).reduce((n, p) => n + p.pol, 0)),
+52 -52
View File
@@ -1,52 +1,52 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<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>Your board · PolHunter</title> <title>Your board · PolHunter</title>
<meta name="robots" content="noindex"> <meta name="robots" content="noindex">
<link rel="stylesheet" href="/style.css?v=16"> <link rel="stylesheet" href="/style.css?v=17">
</head> </head>
<body> <body>
<div class="wrap"> <div class="wrap">
<header class="top"> <header class="top">
<a class="mark" href="/"><span class="coin"></span>PolHunter</a> <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> <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> </header>
<div class="standing" id="standing"></div> <div class="standing" id="standing"></div>
<section class="section" style="padding-top:10px"> <section class="section" style="padding-top:10px">
<h2>Your missions</h2> <h2>Your missions</h2>
<div class="sub" id="rangeLine">Each find pays a random drip. Your codes are yours alone.</div> <div class="sub" id="rangeLine">Each find pays a random drip. Your codes are yours alone.</div>
<div id="refSlot"></div> <div id="refSlot"></div>
<div class="grid" id="missions"><div class="card"><p>Loading your board…</p></div></div> <div class="grid" id="missions"><div class="card"><p>Loading your board…</p></div></div>
</section> </section>
<section class="section" id="badgesSec"> <section class="section" id="badgesSec">
<h2>Your badges</h2> <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="sub">Earned from your finds. Share one and the card carries your name and your link.</div>
<div class="badge-strip" id="badgeStrip"></div> <div class="badge-strip" id="badgeStrip"></div>
</section> </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> <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"> <section class="section">
<h2>Your drips</h2> <h2>Your drips</h2>
<div class="ledger" id="mine"></div> <div class="ledger" id="mine"></div>
</section> </section>
<section class="section" id="share"> <section class="section" id="share">
<h2>Share your hunt</h2> <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="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> <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> <textarea id="sharetext" rows="4" style="margin-top:10px"></textarea>
<div class="sharebtns" id="sharebtns"></div> <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> <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> </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> <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>
<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> <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> <script src="/app.js?v=17"></script>
</body> </body>
</html> </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 // 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. // code box when they are back. One open mission at a time.
(() => { (() => {
const $ = id => document.getElementById(id); const $ = id => document.getElementById(id);
const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c])); 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; }; 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 } let board = null, open = null; // open: { missionId, token, url, dwell, started }
// toasts: small, stacked, gone in a few seconds // toasts: small, stacked, gone in a few seconds
function toast(msg, kind, link) { function toast(msg, kind, link) {
let host = $('toasts'); if (!host) { host = document.createElement('div'); host.id = 'toasts'; document.body.appendChild(host); } 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>' : ''); 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')); host.appendChild(t); requestAnimationFrame(() => t.classList.add('in'));
setTimeout(() => { t.classList.remove('in'); setTimeout(() => t.remove(), 400); }, kind === 'big' ? 9000 : 6000); setTimeout(() => { t.classList.remove('in'); setTimeout(() => t.remove(), 400); }, kind === 'big' ? 9000 : 6000);
} }
const seenPaid = new Set(); let seenLedger = null, paidPrimed = false; 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 // 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). // records what is already paid: history is on the ledger, not in the toasts (Marty, 2026-09-21).
function watchPaid() { function watchPaid() {
for (const d of (board.drips || [])) if (d.status === 'paid' && d.tx) { 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' }); 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); seenPaid.add(d.id);
} }
paidPrimed = true; paidPrimed = true;
} }
// other hunters' finds, as they happen // other hunters' finds, as they happen
async function watchLedger() { async function watchLedger() {
try { const r = await (await fetch('/api/ledger')).json(); const rows = r.recent || []; 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) + '.', ''); } 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)); seenLedger = new Set(rows.map(x => x.tx));
} catch (e) {} } catch (e) {}
} }
setInterval(watchLedger, 45000); setInterval(async () => { if (board && (board.drips || []).some(d => d.status === 'due' || d.status === 'sent')) { await load(); } }, 30000); setInterval(watchLedger, 45000); setInterval(async () => { if (board && (board.drips || []).some(d => d.status === 'due' || d.status === 'sent')) { await load(); } }, 30000);
function card(m) { function card(m) {
const isOpen = open && open.missionId === m.id; const isOpen = open && open.missionId === m.id;
const r = m.reward || {}; const r = m.reward || {};
return '<div class="card' + (isOpen ? ' mission-open' : '') + '" data-id="' + esc(m.id) + '">' 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>' + '<span class="tag' + (m.done ? ' done' : '') + '">' + (m.done ? 'Done today' : esc(m.site)) + '</span>'
+ '<h3>' + esc(m.name) + '</h3><p>' + esc(m.brief) + '</p>' + '<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>' + '<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 + (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="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="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>' + '<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>' + '<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 class="cardact"><button class="btn" data-start="' + esc(m.id) + '">Start mission</button></div>')
+ '</div>'; + '</div>';
} }
// The best-paid thing on the board, and the reason it sits first: sharing used to live on a page // 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). // nobody opened, so nobody shared (Marty, 2026-09-23).
function referralCard() { function referralCard() {
const rf = board.referral; if (!rf || !rf.on) return ''; const rf = board.referral; if (!rf || !rf.on) return '';
const link = (board.share && board.share.link) || ''; const link = (board.share && board.share.link) || '';
const today = rf.broughtToday, total = rf.brought; const today = rf.broughtToday, total = rf.brought;
const people = (rf.people || []).slice(0, 5).map(p => 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(''); '<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">' return '<div class="card ref-card">'
+ '<span class="tag gold">Best paid</span>' + '<span class="tag gold">Best paid</span>'
+ '<h3>🤝 Bring a hunter</h3>' + '<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>. ' + '<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. ' + '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>' + '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>' : '') + '<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>'
+ '<div class="codebox"><input id="refLink" readonly value="' + esc(link) + '"><button class="btn pol" id="refCopy">Copy</button></div>' + (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>' : '')
+ '<p style="margin-top:10px"><span class="range">' + today + ' brought today</span> ' + '<div class="codebox"><input id="refLink" readonly value="' + esc(link) + '"><button class="btn pol" id="refCopy">Copy</button></div>'
+ '<span style="color:var(--dim);font-size:12px">· ' + total + ' all time · ' + rf.earnedPol + ' POL earned</span></p>' + '<p style="margin-top:10px"><span class="range">' + today + ' brought today</span> '
+ (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>') + '<span style="color:var(--dim);font-size:12px">· ' + total + ' all time, ' + (rf.broughtNew || 0) + ' of them new · ' + rf.earnedPol + ' POL earned</span></p>'
+ '<div class="ref-watch"><button class="btn ghost sm" type="button" id="refPlay">▶ Watch: how it works (46s)</button></div>' + (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>'; + '<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() { // the explainer plays in place, the way the teaser does on the home page
const b = $('refPlay'); if (!b) return; function wireRefVideo() {
b.addEventListener('click', () => { const b = $('refPlay'); if (!b) return;
const host = b.parentElement; b.addEventListener('click', () => {
const v = document.createElement('video'); const host = b.parentElement;
v.controls = true; v.autoplay = true; v.playsInline = true; v.className = 'ref-video'; const v = document.createElement('video');
v.poster = 'https://coolify-saasytop.nyc3.digitaloceanspaces.com/training/polhunter-bring-a-hunter.jpg?v=20260923a'; v.controls = true; v.autoplay = true; v.playsInline = true; v.className = 'ref-video';
v.src = 'https://coolify-saasytop.nyc3.digitaloceanspaces.com/training/polhunter-bring-a-hunter.mp4?v=20260923a'; v.poster = 'https://coolify-saasytop.nyc3.digitaloceanspaces.com/training/polhunter-bring-a-hunter.jpg?v=20260923a';
host.innerHTML = ''; host.appendChild(v); v.src = 'https://coolify-saasytop.nyc3.digitaloceanspaces.com/training/polhunter-bring-a-hunter.mp4?v=20260923a';
v.play().catch(() => {}); 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'; } const IAP = 'https://instantadpay.com';
// no wallet on the InstantAdPay account yet: the three steps, in place of the missions 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'; }
function onboarding() { // no wallet on the InstantAdPay account yet: the three steps, in place of the missions
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>' function onboarding() {
+ '<div class="steps" style="margin-top:14px">' 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="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="steps" style="margin-top:14px">'
+ '<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>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>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 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>'; + '<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() { // badge cards: art, lock state, share
const el = $('badgeStrip'); if (!el || !board.badgeCards) return; function badgeStrip() {
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(''); const el = $('badgeStrip'); if (!el || !board.badgeCards) return;
el.querySelectorAll('[data-share]').forEach(x => x.addEventListener('click', () => shareBadge(x.dataset.share, false))); 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; function shareBadge(id, unlocked) {
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 b = (board.badgeCards || []).find(x => x.id === id); if (!b || !b.earned) return;
const T = encodeURIComponent(text), U = encodeURIComponent(b.page); 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;
$('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>' const T = encodeURIComponent(text), U = encodeURIComponent(b.page);
+ '<img class="share-img" src="' + b.image + '?t=' + Date.now() + '" alt="' + esc(b.name) + '">' $('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>'
+ '<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>' + '<img class="share-img" src="' + b.image + '?t=' + Date.now() + '" alt="' + esc(b.name) + '">'
+ '<p class="small" style="margin-top:10px;text-align:center">The card carries your name; the link carries your referral.</p>'; + '<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>'
$('shareModal').hidden = false; + '<p class="small" style="margin-top:10px;text-align:center">The card carries your name; the link carries your referral.</p>';
$('shareCopy').onclick = async () => { try { await navigator.clipboard.writeText(b.page); } catch (e) {} $('shareCopy').textContent = 'Copied'; setTimeout(() => { $('shareCopy').textContent = 'Copy link'; }, 1200); }; $('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() { document.addEventListener('click', e => { if (e.target.id === 'shareClose' || e.target.id === 'shareModal') $('shareModal').hidden = true; });
badgeStrip(); function render() {
$('who').innerHTML = '<i></i> ' + esc(board.me.username ? '@' + board.me.username : '#' + board.me.memberId) + (board.me.wallet ? '' : ' · no wallet linked'); badgeStrip();
const r = board.missions[0] && board.missions[0].reward; $('who').innerHTML = '<i></i> ' + esc(board.me.username ? '@' + board.me.username : '#' + board.me.memberId) + (board.me.wallet ? '' : ' · no wallet linked');
if (r) $('rangeLine').textContent = 'Each find pays a random drip between ' + r.minPol + ' and ' + r.maxPol + ' POL. Your codes are yours alone.'; const r = board.missions[0] && board.missions[0].reward;
const pool = board.pool || {}; if (r) $('rangeLine').textContent = 'Each find pays a random drip between ' + r.minPol + ' and ' + r.maxPol + ' POL. Your codes are yours alone.';
if (!board.me.wallet) $('missions').innerHTML = onboarding(); const pool = board.pool || {};
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>' : '') if (!board.me.wallet) $('missions').innerHTML = onboarding();
+ (board.missions.length ? board.missions.map(card).join('') : '<div class="card"><p>No missions are open right now. Check back soon.</p></div>'); 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>' : '')
if ($('refSlot')) $('refSlot').innerHTML = board.me.wallet ? referralCard() : ''; + (board.missions.length ? board.missions.map(card).join('') : '<div class="card"><p>No missions are open right now. Check back soon.</p></div>');
wireRefVideo(); if ($('refSlot')) $('refSlot').innerHTML = board.me.wallet ? referralCard() : '';
const rc = $('refCopy'); wireRefVideo();
if (rc) rc.addEventListener('click', async () => { const rc = $('refCopy');
const v = $('refLink').value; if (rc) rc.addEventListener('click', async () => {
try { await navigator.clipboard.writeText(v); } catch (e) { $('refLink').select(); document.execCommand('copy'); } const v = $('refLink').value;
rc.textContent = 'Copied'; setTimeout(() => { rc.textContent = 'Copy'; }, 1600); 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 = []; // standing: badges and rank
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); const rk = board.rank || {}; const line = [];
$('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>') 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);
+ (line.length ? '<span class="rankline">' + line.join(' \u00b7 ') + ' \u00b7 <a href="/leaders">leaderboard</a></span>' : '<span class="rankline"><a href="/leaders">leaderboard</a></span>'); $('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>')
// share panel + (line.length ? '<span class="rankline">' + line.join(' \u00b7 ') + ' \u00b7 <a href="/leaders">leaderboard</a></span>' : '<span class="rankline"><a href="/leaders">leaderboard</a></span>');
if (board.share) { // share panel
const link = board.share.link; $('mylink').value = link; if (board.share) {
const best = (board.drips || []).filter(d => d.status === 'paid').sort((a, b) => b.pol - a.pol)[0]; // brag about the biggest find const link = board.share.link; $('mylink').value = link;
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 const best = (board.drips || []).filter(d => d.status === 'paid').sort((a, b) => b.pol - a.pol)[0]; // brag about the biggest find
: '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; 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
if (!$('sharetext').dataset.touched) $('sharetext').value = text; : '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;
const L = encodeURIComponent(link), T = () => encodeURIComponent($('sharetext').value); if (!$('sharetext').dataset.touched) $('sharetext').value = text;
$('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 L = encodeURIComponent(link), T = () => encodeURIComponent($('sharetext').value);
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(); }; $('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>';
wire(); $('sharetext').oninput = () => { $('sharetext').dataset.touched = '1'; wire(); }; 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(); };
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); }; wire(); $('sharetext').oninput = () => { $('sharetext').dataset.touched = '1'; wire(); };
$('copylink').onclick = () => copy($('mylink'), $('copylink')); $('copytext').onclick = () => copy($('sharetext'), $('copytext')); 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.'); watchPaid(); if (seenLedger === null) watchLedger();
$('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('') 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.');
: '<div class="row"><span class="site">Nothing yet. Your first find goes here.</span></div>'; $('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('')
document.querySelectorAll('[data-start]').forEach(b => b.addEventListener('click', () => start(b.dataset.start))); : '<div class="row"><span class="site">Nothing yet. Your first find goes here.</span></div>';
const s = $('submit'); if (s) { s.addEventListener('click', submit); $('code').addEventListener('keydown', e => { if (e.key === 'Enter') submit(); }); tick(); } 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(); } let explorer = 'https://polygonscan.com';
async function start(id) { async function load() { board = await api('/api/my/board'); render(); }
const r = await api('/api/my/start', { missionId: id }); async function start(id) {
if (r.error) { toast(esc(r.error), 'bad'); if (r.spent || r.dailyDone) await load(); return; } const r = await api('/api/my/start', { missionId: id });
open = { missionId: id, token: r.token, url: r.url, dwell: r.dwell, started: Date.now(), expires: r.expires }; if (r.error) { toast(esc(r.error), 'bad'); if (r.spent || r.dailyDone) await load(); return; }
window.open(r.url, '_blank', 'noopener'); open = { missionId: id, token: r.token, url: r.url, dwell: r.dwell, started: Date.now(), expires: r.expires };
render(); window.open(r.url, '_blank', 'noopener');
} render();
function tick() { }
const el = $('timer'); if (!el || !open) return; function tick() {
const left = Math.max(0, open.dwell - Math.floor((Date.now() - open.started) / 1000)); const el = $('timer'); if (!el || !open) return;
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.'; const left = Math.max(0, open.dwell - Math.floor((Date.now() - open.started) / 1000));
if (open.expires && Date.now() > open.expires) { el.innerHTML = 'This attempt timed out. Start the mission again.'; return; } 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.';
setTimeout(tick, 1000); 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; async function submit() {
$('submit').disabled = true; const code = $('code').value.trim(); if (!code) return;
const r = await api('/api/my/submit', { token: open.token, code }); $('submit').disabled = true;
const m = $('msg'); m.className = 'msg ' + (r.error ? 'bad' : 'ok'); m.textContent = r.error || r.message; const r = await api('/api/my/submit', { token: open.token, code });
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'); const m = $('msg'); m.className = 'msg ' + (r.error ? 'bad' : 'ok'); m.textContent = r.error || r.message;
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; } 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');
// an achievement unlocked by this find: celebrate it once the board has reloaded 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; }
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); } // an achievement unlocked by this find: celebrate it once the board has reloaded
$('submit').disabled = false; 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); }
if (!r.error || r.spent) { open = null; setTimeout(load, 900); } $('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(); 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-watch{margin-top:14px}
.ref-video{width:100%;max-width:560px;border-radius:14px;display:block;background:#000} .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)} .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)}