PolHunter v0.2: the hunt engine

Hand-off sign-in from InstantAdPay (lib/sso.js: HMAC token, five minutes, single use; no sign-up,
no mailer), missions with per-member per-visit proof codes (lib/missions.js: the embed is
answered only from the mission's own origin, only after the dwell; hiding place rotates by day
and member), weighted-low rewards with a daily cap that queues rather than refuses
(lib/rewards.js), the faucet sender on ethers with a low-balance alert (lib/faucet.js), the
one-line embed for the sites (public/embed.js), the hunter board, the public ledger, an
admin page, and the site's own look. Telegram is behind the OUTBOUND gate like everything else.
13-check end-to-end test in test/run.js.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-19 14:17:44 -05:00
parent 1a6166abc3
commit 523d57624e
15 changed files with 1001 additions and 75 deletions
+43
View File
@@ -0,0 +1,43 @@
/* PolHunter embed. One line on a mission site:
<script src="https://polhunter.com/embed.js" data-slots="3" async></script>
plus, where a code may appear, elements with data-ph-slot="0" ... data-ph-slot="N-1".
When a hunter arrives with ?ph=<token> (their own, from their PolHunter board) the token is kept
in sessionStorage for this tab, and after the mission's dwell the code is fetched from
polhunter.com and rendered in ONE of the slots (which one is picked per hunter per day). The
request carries this page's Origin; polhunter.com answers only for the mission's own host and
only after the dwell. Anyone else loading this page sees nothing at all. */
(function () {
var API = 'https://polhunter.com';
try {
var q = new URLSearchParams(location.search), t = q.get('ph');
if (t) { sessionStorage.setItem('ph.token', t); q.delete('ph'); history.replaceState(null, '', location.pathname + (q.toString() ? '?' + q : '') + location.hash); }
t = sessionStorage.getItem('ph.token'); if (!t) return;
} catch (e) { return; }
// slots: explicit [data-ph-slot="i"] elements, or the i-th match of data-selector (e.g. ".video-card"
// on a training page, ".card" on a programs page), else a fixed badge in the corner
var me = document.currentScript, slots = Number((me && me.getAttribute('data-slots')) || 1) || 1, sel = me && me.getAttribute('data-selector');
function slotEl(i) {
var el = document.querySelector('[data-ph-slot="' + i + '"]'); if (el) return el;
if (sel) { var all = document.querySelectorAll(sel); if (all.length) { var host = all[i % all.length]; var d0 = document.createElement('div'); d0.style.cssText = 'margin:10px 0'; host.appendChild(d0); return d0; } }
var d = document.createElement('div'); d.setAttribute('data-ph-slot', String(i)); d.style.cssText = 'position:fixed;right:16px;bottom:16px;z-index:99999'; document.body.appendChild(d); return d;
}
function render(code, slot) {
var el = slotEl(slot); el.innerHTML = '';
var box = document.createElement('div');
box.setAttribute('role', 'note');
box.style.cssText = 'display:inline-flex;align-items:center;gap:10px;padding:10px 14px;border-radius:14px;background:linear-gradient(135deg,#1a0f3d,#2b1a63);color:#f1eefb;font:600 14px/1.2 system-ui,-apple-system,"Segoe UI",sans-serif;box-shadow:0 8px 30px rgba(130,71,229,.45),0 0 0 1px rgba(255,255,255,.08);letter-spacing:.02em';
box.innerHTML = '<span style="width:22px;height:22px;border-radius:50%;background:radial-gradient(circle at 35% 35%,#c9a4ff,#8247e5 60%,#4b2a8c);box-shadow:0 0 14px rgba(130,71,229,.9);flex:none"></span><span>PolHunter code <b style="font-family:ui-monospace,Menlo,monospace;font-size:16px;letter-spacing:.14em;color:#f3be43">' + code + '</b></span>';
el.appendChild(box);
}
var tries = 0;
function ask() {
tries++; if (tries > 40) return;
fetch(API + '/api/embed/code?t=' + encodeURIComponent(t), { mode: 'cors', credentials: 'omit' }).then(function (r) { return r.json(); }).then(function (r) {
if (r && r.code) { render(r.code, Number(r.slot) % slots); try { sessionStorage.removeItem('ph.token'); } catch (e) {} return; }
if (r && r.wait) { setTimeout(ask, Math.min(r.wait, 10) * 1000); return; }
// expired / gone / wrong origin: say nothing
}).catch(function () { setTimeout(ask, 8000); });
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', ask); else ask();
})();