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:
@@ -0,0 +1,75 @@
|
||||
// Rewards: the weighted draw, the daily cap, the queue, and the ledger.
|
||||
//
|
||||
// settings (admin, on the volume): { minPol, maxPol, dailyCapPol, lowBalancePol }
|
||||
// defaults 0.05 / 1 / 20 / 40 (Marty, 2026-09-19). The cap is a setting, not a constant,
|
||||
// because POL's dollar price moves.
|
||||
//
|
||||
// The draw is weighted low: uniform on a log scale between min and max, so most drips sit near
|
||||
// the floor and a 1 POL hit is rare enough to be talked about. Same range every day, whatever
|
||||
// the cap.
|
||||
//
|
||||
// A completion is recorded the moment the proof checks out. If today's paid total plus this
|
||||
// drip would cross the cap, the drip is queued (status 'queued') and paid on a later day in
|
||||
// order; the hunter did the work and is never told no. The faucet pays 'due' entries.
|
||||
'use strict';
|
||||
const store = require('./store');
|
||||
const { ctDay } = require('./missions');
|
||||
|
||||
const DEFAULTS = { minPol: 0.05, maxPol: 1, dailyCapPol: 20, lowBalancePol: 40 };
|
||||
function settings() { return Object.assign({}, DEFAULTS, store.read('settings', {})); }
|
||||
function setSettings(patch) { return store.update('settings', {}, s => Object.assign(s, patch)); }
|
||||
|
||||
function draw(min, max) {
|
||||
const lo = Math.log(min), hi = Math.log(max);
|
||||
const v = Math.exp(lo + Math.random() * (hi - lo));
|
||||
return Math.round(Math.max(min, Math.min(max, v)) * 10000) / 10000;
|
||||
}
|
||||
|
||||
function paidToday(day) {
|
||||
return store.read('payouts', []).filter(p => p.day === day && p.status !== 'failed').reduce((n, p) => n + p.pol, 0);
|
||||
}
|
||||
|
||||
// has this member completed this mission already?
|
||||
function completed(memberId, missionId) {
|
||||
return store.read('payouts', []).some(p => p.memberId === Number(memberId) && p.missionId === missionId && p.status !== 'failed');
|
||||
}
|
||||
|
||||
// record a completion; decide paid-today vs queued
|
||||
function grant(member, mission) {
|
||||
const s = settings();
|
||||
const pol = draw(Number(s.minPol), Number(s.maxPol));
|
||||
const day = ctDay();
|
||||
const budgetLeft = mission.budget ? mission.budget - store.read('payouts', []).filter(p => p.missionId === mission.id && p.status !== 'failed').length : Infinity;
|
||||
if (budgetLeft <= 0) return { error: 'This mission has paid out all it was funded for.' };
|
||||
const overCap = paidToday(day) + pol > Number(s.dailyCapPol);
|
||||
const rec = {
|
||||
id: Date.now().toString(36) + Math.random().toString(36).slice(2, 8),
|
||||
memberId: Number(member.memberId), email: member.email, wallet: member.wallet, username: member.username || null,
|
||||
missionId: mission.id, site: mission.site, pol, day, at: Date.now(),
|
||||
status: overCap ? 'queued' : 'due', tx: null, paidAt: null, error: null,
|
||||
};
|
||||
store.update('payouts', [], all => { all.push(rec); return all; });
|
||||
return { ok: true, rec, queued: overCap };
|
||||
}
|
||||
|
||||
// what the faucet should pay now: due entries, then queued ones as long as today's cap allows
|
||||
function payable() {
|
||||
const s = settings(); const day = ctDay();
|
||||
let room = Number(s.dailyCapPol) - paidToday(day);
|
||||
const all = store.read('payouts', []);
|
||||
const out = [];
|
||||
for (const p of all.filter(p => p.status === 'due')) { out.push(p); }
|
||||
for (const p of all.filter(p => p.status === 'queued').sort((a, b) => a.at - b.at)) { if (p.pol <= room) { room -= p.pol; out.push(p); } else break; }
|
||||
return out;
|
||||
}
|
||||
function mark(id, patch) { return store.update('payouts', [], all => { const p = all.find(x => x.id === id); if (p) Object.assign(p, patch); return all; }); }
|
||||
|
||||
function ledger(n) { return store.read('payouts', []).filter(p => p.status === 'paid').sort((a, b) => b.paidAt - a.paidAt).slice(0, n || 50); }
|
||||
function mine(memberId) { return store.read('payouts', []).filter(p => p.memberId === Number(memberId)).sort((a, b) => b.at - a.at); }
|
||||
function totals() {
|
||||
const all = store.read('payouts', []);
|
||||
const paid = all.filter(p => p.status === 'paid');
|
||||
return { paid: paid.length, pol: Math.round(paid.reduce((n, p) => n + p.pol, 0) * 10000) / 10000, queued: all.filter(p => p.status === 'queued').length, today: paidToday(ctDay()), hunters: new Set(paid.map(p => p.memberId)).size };
|
||||
}
|
||||
|
||||
module.exports = { settings, setSettings, draw, grant, completed, payable, mark, ledger, mine, totals, paidToday };
|
||||
Reference in New Issue
Block a user