// 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 // Priced in dollars because that is what a buyer judges; converted at the InstantAdPay // contract's own rate at purchase (Marty, 2026-09-24). paidBlockCents: 3000, // $30 a block paidListingCents: 1000, // $10, once per mission, charged AT APPROVAL after the review is done paidListingFreeBlocks: 4, // a first order this size or larger gets the listing free }; 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 }; } // How many completions a block buys. The inverse of quote(): the advertiser fixes the spend and // the payout they want hunters to get, and the completion count falls out. // // NOTE, because it is easy to get backwards: with a fixed-price block the house margin is the // same whatever the payout. Raising the payout trades completions for SPEED at the same price, // it does not earn the house more. Margin only moves with the number of blocks sold. So the // upsell is "more blocks", and a higher payout is the reason a buyer needs more of them. function blockFor(hunterPol, totalPol) { const c = cfg(); const hp = r4(Math.max(Number(c.paidBaseHunterPol), Number(hunterPol) || 0)); const gas = Number(c.paidGasPol) || 0; const buf = 1 + (Math.max(0, Number(c.paidReserveBufferPct) || 0) / 100); const pct = Math.min(90, Math.max(0, Number(c.paidMarginPct) || 0)); const reserve = Number(totalPol) * (1 - pct / 100); const n = Math.floor(reserve / ((hp + gas) * buf)); return Object.assign(quote(hp, n), { requestedPol: r4(Number(totalPol)) }); } // What this order costs all in. The listing fee is charged ONCE per mission, at approval, because // it pays for the review that has already happened by then. It is never charged again on that // mission, and a first order of paidListingFreeBlocks or more waives it: a serious first buyer // should not be taxed for the privilege. function orderTotal(blocks, alreadyListed) { const c = cfg(); const n = Math.max(1, Math.round(Number(blocks) || 1)); const blockCents = Number(c.paidBlockCents) * n; const waived = alreadyListed || n >= Number(c.paidListingFreeBlocks); const listingCents = waived ? 0 : Number(c.paidListingCents); return { blocks: n, blockCents, listingCents, totalCents: blockCents + listingCents, listingWaived: waived, listingReason: alreadyListed ? 'already listed' : (waived ? 'waived, ' + n + ' blocks' : 'first order') }; } // ---- 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, blockFor, orderTotal, outstanding, canSell, extraAllowance, isPaid, usedOf, paidMissions };