diff --git a/lib/paid.js b/lib/paid.js index c09df43..ef4fbc2 100644 --- a/lib/paid.js +++ b/lib/paid.js @@ -1,96 +1,117 @@ -// 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 }; +// 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 };