// Advertiser-funded missions (Marty, 2026-09-24). // // A member buys a mission: they pay POL into the faucet wallet, and hunters who complete it are // dripped out of what they paid. Two rules follow from that, and everything here exists to hold // them: // // 1. A paid completion is NOT house spend. It does not count against dailyCapPol, and it does // not use up one of the hunter's missionsPerDay. Each live paid mission raises that hunter's // allowance by one, because the company is not funding it. ("Each additional mission paid by // an advertiser should allow +1 above the cap.") // // 2. THE SOLVENCY RULE. The faucet is one wallet holding house float AND every advertiser's // unspent reserve. If reserves ever exceed the balance we have sold delivery we cannot pay // for, and a hunter completes a mission only to watch the drip fail. So: // // faucet balance >= house float + sum(outstanding reserves) // // checked before a sale is accepted, never after. // // Money in is split at purchase: the hunter payout is reserved, the rest is company margin the // P&L can recognise. The advertiser sets the hunter payout themselves (base 0.30, they may pay // more to get completed sooner), so the reserve is exact rather than estimated. No draw, no // variance, no guessing. 'use strict'; const store = require('./store'); const DEFAULTS = { paidEnabled: 1, paidBaseHunterPol: 0.30, // the floor an advertiser may set as the hunter's payout paidMarginPct: 100, // company margin on top of the hunter payout, as a percentage paidMinBlock: 100, // completions per block paidMaxConcurrent: 0, // 0 = no limit; supply is elastic now that paid missions add capacity paidHouseFloatPol: 25, // POL kept back for house missions, never counted as sellable }; function cfg() { return Object.assign({}, DEFAULTS, store.read('settings', {})); } const r4 = n => Math.round(Number(n) * 10000) / 10000; // ---- what a block costs ----------------------------------------------------------------- // hunterPol is what each completing hunter receives; the advertiser pays that plus the margin. function quote(hunterPol, completions) { const c = cfg(); const hp = r4(Math.max(Number(c.paidBaseHunterPol), Number(hunterPol) || 0)); const n = Math.max(Number(c.paidMinBlock), Math.round(Number(completions) || 0)); const reserve = r4(hp * n); // exact: no draw, so no buffer needed const margin = r4(reserve * (Number(c.paidMarginPct) / 100)); return { hunterPol: hp, completions: n, reserve, margin, total: r4(reserve + margin) }; } // ---- reserves --------------------------------------------------------------------------- const paidMissions = () => store.read('missions', []).filter(m => m.paid); // completions already granted against a mission (failed ones do not count, they never paid) function usedOf(missionId) { return store.read('payouts', []).filter(p => p.missionId === missionId && p.status !== 'failed' && !p.prize && !p.ref).length; } // what every live paid mission still owes its hunters function outstanding() { let pol = 0, left = 0; for (const m of paidMissions()) { const remaining = Math.max(0, Number(m.budget || 0) - usedOf(m.id)); left += remaining; pol += remaining * Number(m.hunterPol || 0); } return { reservePol: r4(pol), completionsLeft: left, missions: paidMissions().length }; } // THE guard. Given the faucet's real balance, can we take this sale? function canSell(balancePol, addReservePol) { const c = cfg(); const o = outstanding(); const float = Number(c.paidHouseFloatPol) || 0; const committed = r4(o.reservePol + float + Number(addReservePol || 0)); const bal = r4(Number(balancePol) || 0); return { ok: bal >= committed, balance: bal, committed, houseFloat: float, alreadyReserved: o.reservePol, adding: r4(Number(addReservePol) || 0), shortBy: bal >= committed ? 0 : r4(committed - bal), }; } // ---- the hunter's allowance --------------------------------------------------------------- // Base allowance is the company-funded missionsPerDay. Every live paid mission the hunter has // not yet done adds one, because that completion costs the company nothing. function extraAllowance(doneIds) { const done = doneIds instanceof Set ? doneIds : new Set(doneIds || []); return paidMissions().filter(m => m.active && !done.has(m.id) && Math.max(0, Number(m.budget || 0) - usedOf(m.id)) > 0).length; } function isPaid(missionId) { const m = store.read('missions', []).find(x => x.id === missionId); return !!(m && m.paid); } module.exports = { cfg, quote, outstanding, canSell, extraAllowance, isPaid, usedOf, paidMissions };