// 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 // The company's share, on the SAME BASIS as InstantAdPay's platform 20%: a percentage of what // the advertiser pays, not a mark-up on the reserve. So at 20 the hunters get 80% of the // purchase and the house keeps 20, which is the number members already understand. paidMarginPct: 20, paidMinBlock: 100, // completions per block paidMaxConcurrent: 0, // 0 = no limit; supply is elastic now that paid missions add capacity // Every drip is its own transaction and the faucet pays the gas. Measured at 0.0060 POL across // recent payouts (21,000 gas, the plain-transfer floor); held at 0.01 so a busy chain cannot // turn a sold mission into a shortfall. Without this the gas came quietly out of house money. paidGasPol: 0.01, // Marty, 2026-09-24: happy to hold more back than the arithmetic demands. Applied to drips and // gas together, so a failed-and-retried send or a price spike is already covered. paidReserveBufferPct: 10, paidHouseFloatPol: 50, // 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)); // What must actually be held: the drips, the gas to send them, and the safety buffer. const drips = r4(hp * n); const gas = r4(Number(c.paidGasPol) * n); const buf = Math.max(0, Number(c.paidReserveBufferPct) || 0); const reserve = r4((drips + gas) * (1 + buf / 100)); // Margin is a share of what the advertiser pays, the same basis as InstantAdPay's platform 20%. const pct = Math.min(90, Math.max(0, Number(c.paidMarginPct) || 0)); const total = r4(reserve / (1 - pct / 100)); return { hunterPol: hp, completions: n, drips, gas, bufferPct: buf, reserve, margin: r4(total - reserve), total, marginPct: pct }; } // ---- 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; const c = cfg(); const gas = Number(c.paidGasPol) || 0; const buf = 1 + (Math.max(0, Number(c.paidReserveBufferPct) || 0) / 100); for (const m of paidMissions()) { const remaining = Math.max(0, Number(m.budget || 0) - usedOf(m.id)); left += remaining; // reserved on the same basis it was sold on: drip + gas + buffer, per completion still owed pol += remaining * (Number(m.hunterPol || 0) + gas) * buf; } 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 };