diff --git a/public/admin.html b/public/admin.html index 6d41a47..65e8a8e 100644 --- a/public/admin.html +++ b/public/admin.html @@ -434,6 +434,11 @@

Where the money went

per period
+
+

PolHunter payouts

all time, its own faucet
+
+

Marketing spend, paid in POL from the PolHunter wallet. None of it passes through the ad contract, so it is not in the lines above.

+

Wallets

live balances
@@ -488,6 +493,6 @@
- + diff --git a/public/assets/admin.js b/public/assets/admin.js index c4955be..bc97f4a 100644 --- a/public/assets/admin.js +++ b/public/assets/admin.js @@ -715,6 +715,25 @@ ['Net after fixed costs', '$' + Math.round(platUsd - fixed).toLocaleString(), 'fixed ' + Math.round(fixed).toLocaleString() + ' over ' + months.toFixed(1) + ' month(s)'], ['Pass-ups', r.passedUp.count, r.passedUp.unqualified + ' unqualified ยท ' + r.passedUp.sendFailed + ' send-failed'] ].map(t => '
' + esc(String(t[1])) + '
' + esc(t[0]) + '
' + esc(t[2]) + '
').join(''); + // PolHunter: a marketing cost paid from its own faucet, so it is read across rather than + // derived from our chain events (Marty, 2026-09-23) + const ph = r.polhunter, phEl = $('pnlHunt'); + if (phEl) { + if (!ph || ph.error || !ph.totals) phEl.innerHTML = 'PolHunter figures unavailable' + (ph && ph.error ? ' (' + esc(ph.error) + ')' : '') + '.'; + else { + const t = ph.totals, rf = ph.referrals || {}, usd = n => '$' + (Number(n) * px).toFixed(2); + const findPol = Math.max(0, Number(t.pol || 0) - Number(rf.paidPol || 0)); + phEl.innerHTML = 'LinePOLUSD now' + + [['Mission finds (' + Number(t.paid || 0).toLocaleString() + ' paid)', findPol], + ['Referral bounties (' + Number(rf.bounties || 0) + ')', null], + ['Referral matches (' + Number(rf.matches || 0) + ')', null], + ['Referral payouts total', Number(rf.pol || 0)], + ['All PolHunter payouts', Number(t.pol || 0)]] + .filter(x => x[1] !== null) + .map(x => '' + x[0] + '' + Number(x[1]).toFixed(4) + '' + usd(x[1]) + '').join('') + + 'Hunters paid' + Number(t.hunters || 0) + 'today ' + Number(t.today || 0).toFixed(3) + ' POL'; + } + } $('pnlSplit').innerHTML = 'LinePOLUSD now' + [['Level 1 (50%)', r.byTier[1]], ['Level 2 (20%)', r.byTier[2]], ['Level 3 (10%)', r.byTier[3]], ['Platform (20% + pass-ups)', r.platformWei]].map(x => '' + x[0] + '' + pol(x[1]) + '' + usdOf(x[1], px) + '').join(''); const W = r.wallets || {}, B = r.balances || {}; diff --git a/server.js b/server.js index ff69dc2..022d216 100644 --- a/server.js +++ b/server.js @@ -371,6 +371,23 @@ async function imageCheck(url) { // A video campaign's source has to actually exist. The create form only matched the URL // shape, so a placeholder (yourdomain.com/....mp4) went live on 2026-09-11 and sat there for // eleven days handing every Tier 1 viewer a black player they could never finish. +// PolHunter pays real POL out of its own faucet and none of it passes through this contract, so +// the P&L cannot derive it from chain events and has to read it across (Marty, 2026-09-23). Cached +// and fail-soft: the P&L page must never hang or break because the other site is down. +let huntCache = { at: 0, data: null }; +async function huntLedger() { + if (Date.now() - huntCache.at < 120000) return huntCache.data; + const base = String(process.env.HUNT_URL || 'https://polhunter.com').replace(/\/+$/, ''); + try { + const r = await fetch(base + '/api/ledger', { signal: AbortSignal.timeout(6000) }); + if (!r.ok) throw new Error('HTTP ' + r.status); + const j = await r.json(); + huntCache = { at: Date.now(), data: { totals: j.totals || null, referrals: j.referrals || null, pools: j.pools || null, at: Date.now() } }; + } catch (e) { + huntCache = { at: Date.now(), data: huntCache.data || { error: e.message } }; + } + return huntCache.data; +} async function videoCheck(url) { const u = String(url || '').trim(); if (/^\/uploads\//.test(u)) return { ok: true }; // uploaded here, already on our disk @@ -2223,7 +2240,11 @@ const server = http.createServer(async (req, res) => { const memberId = (acct && acct.memberId) || s.memberId || 0; if (!memberId) { res.writeHead(302, { Location: '/my#wallet' }); return res.end(); } const b64u = b => Buffer.from(b).toString('base64').replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_'); - const payload = JSON.stringify({ iat: Date.now(), exp: Date.now() + 5 * 60000, nonce: crypto.randomBytes(12).toString('hex'), memberId: Number(memberId), email: s.email, wallet: (acct && acct.address) || s.address || null, username: (acct && acct.username) || null }); + // the sponsor rides across too (Marty, 2026-09-23): PolHunter pays a referral bounty, and the + // person who gets it has to be the one this member actually joined under here, not a cookie + // PolHunter guessed at. sponsorRef is the upline's username or code, matching /join/. + let sponsorRef = (acct && acct.sponsorRef) || ''; + const payload = JSON.stringify({ iat: Date.now(), exp: Date.now() + 5 * 60000, nonce: crypto.randomBytes(12).toString('hex'), memberId: Number(memberId), email: s.email, wallet: (acct && acct.address) || s.address || null, username: (acct && acct.username) || null, sponsorRef: sponsorRef || null, joinedAt: (acct && acct.created) || null }); const tok = b64u(payload) + '.' + b64u(crypto.createHmac('sha256', secret).update(payload).digest()); res.writeHead(302, { Location: huntUrl + '/auth?t=' + tok, 'Cache-Control': 'no-store', 'Set-Cookie': 'iap.return=; Path=/; SameSite=Lax; Max-Age=0' + (IS_PROD ? '; Secure' : '') }); return res.end(); // the loop is closed } @@ -3136,6 +3157,7 @@ const server = http.createServer(async (req, res) => { // wallets' live balances and an admin-entered fixed monthly cost if (p === '/api/admin/pnl' && req.method === 'GET') { if (!isAdmin(req)) return json(res, 401, { error: 'auth' }); + const hunt = await huntLedger(); // marketing spend that never touches this contract const days = Math.max(0, Number(u.searchParams.get('days') || 30)); let latest = 0; try { latest = parseInt(await chain.rpc('eth_blockNumber', []), 16); } catch (e) {} const fromBlock = days ? latest - Math.round(days * 43200) : 0; @@ -3157,7 +3179,7 @@ const server = http.createServer(async (req, res) => { purchases: { count: purchases.length, volumeWei: sum(purchases, e => e.paidWei).toString(), usdCents: purchases.reduce((n, e) => n + (e.priceCents || 0), 0), byPackage: byPkg }, platformWei: sum(admin, e => e.amountWei).toString(), memberPayoutsWei: sum(tier, e => e.amountWei).toString(), byTier, passedUp: { count: passed.length, unqualified: passed.filter(e => e.reason === 'unqualified').length, sendFailed: passed.filter(e => e.reason === 'send-failed').length }, - wallets, balances, fixedMonthlyUsd: Number(siteConfig().pnlFixedMonthlyUsd) || 0, burner: burner.status() }); + wallets, balances, fixedMonthlyUsd: Number(siteConfig().pnlFixedMonthlyUsd) || 0, burner: burner.status(), polhunter: hunt }); } if (p === '/api/admin/burner' && req.method === 'GET') { if (!isAdmin(req)) return json(res, 401, { error: 'auth' });