Paid missions: margin on IAP's basis, and the reserve covers gas

Margin is now 20% of what the advertiser pays, the same basis as InstantAdPay's
platform share, rather than a mark-up on the reserve. Members already understand
that number, so it needs no second explanation.

The reserve was covering the drips and nothing else. Every drip is its own
transaction and the faucet pays the gas: measured at 0.0060 POL across recent
payouts, 0.60 per hundred completions. Small, but it was coming quietly out of
house money and it is exactly the shape of thing that becomes a shortfall on a
busy chain. Gas is now bought by the advertiser at a 0.01 allowance, and drips
plus gas carry a 10% buffer on top (Marty: happy to hold more back than the
arithmetic demands). The house float doubles to 50.

outstanding() reserves on the same basis it sold on, so the solvency check
cannot drift away from what was actually promised.

  0.30/completion -> 30 drips + 1 gas + 10% = 34.1 reserved, 8.525 margin,
  advertiser pays 42.625 POL, about $4.34 for 100 verified 45-second visits.

20 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-24 09:57:12 -05:00
parent 0b4f8c627f
commit c81c720602
+117 -96
View File
@@ -1,96 +1,117 @@
// Advertiser-funded missions (Marty, 2026-09-24). // 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 // 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 // dripped out of what they paid. Two rules follow from that, and everything here exists to hold
// them: // them:
// //
// 1. A paid completion is NOT house spend. It does not count against dailyCapPol, and it does // 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 // 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 // allowance by one, because the company is not funding it. ("Each additional mission paid by
// an advertiser should allow +1 above the cap.") // an advertiser should allow +1 above the cap.")
// //
// 2. THE SOLVENCY RULE. The faucet is one wallet holding house float AND every advertiser's // 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 // 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: // for, and a hunter completes a mission only to watch the drip fail. So:
// //
// faucet balance >= house float + sum(outstanding reserves) // faucet balance >= house float + sum(outstanding reserves)
// //
// checked before a sale is accepted, never after. // 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 // 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 // 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 // more to get completed sooner), so the reserve is exact rather than estimated. No draw, no
// variance, no guessing. // variance, no guessing.
'use strict'; 'use strict';
const store = require('./store'); const store = require('./store');
const DEFAULTS = { const DEFAULTS = {
paidEnabled: 1, paidEnabled: 1,
paidBaseHunterPol: 0.30, // the floor an advertiser may set as the hunter's payout 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 // The company's share, on the SAME BASIS as InstantAdPay's platform 20%: a percentage of what
paidMinBlock: 100, // completions per block // the advertiser pays, not a mark-up on the reserve. So at 20 the hunters get 80% of the
paidMaxConcurrent: 0, // 0 = no limit; supply is elastic now that paid missions add capacity // purchase and the house keeps 20, which is the number members already understand.
paidHouseFloatPol: 25, // POL kept back for house missions, never counted as sellable paidMarginPct: 20,
}; paidMinBlock: 100, // completions per block
function cfg() { return Object.assign({}, DEFAULTS, store.read('settings', {})); } paidMaxConcurrent: 0, // 0 = no limit; supply is elastic now that paid missions add capacity
const r4 = n => Math.round(Number(n) * 10000) / 10000; // 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
// ---- what a block costs ----------------------------------------------------------------- // turn a sold mission into a shortfall. Without this the gas came quietly out of house money.
// hunterPol is what each completing hunter receives; the advertiser pays that plus the margin. paidGasPol: 0.01,
function quote(hunterPol, completions) { // Marty, 2026-09-24: happy to hold more back than the arithmetic demands. Applied to drips and
const c = cfg(); // gas together, so a failed-and-retried send or a price spike is already covered.
const hp = r4(Math.max(Number(c.paidBaseHunterPol), Number(hunterPol) || 0)); paidReserveBufferPct: 10,
const n = Math.max(Number(c.paidMinBlock), Math.round(Number(completions) || 0)); paidHouseFloatPol: 50, // POL kept back for house missions, never counted as sellable
const reserve = r4(hp * n); // exact: no draw, so no buffer needed };
const margin = r4(reserve * (Number(c.paidMarginPct) / 100)); function cfg() { return Object.assign({}, DEFAULTS, store.read('settings', {})); }
return { hunterPol: hp, completions: n, reserve, margin, total: r4(reserve + margin) }; const r4 = n => Math.round(Number(n) * 10000) / 10000;
}
// ---- what a block costs -----------------------------------------------------------------
// ---- reserves --------------------------------------------------------------------------- // hunterPol is what each completing hunter receives; the advertiser pays that plus the margin.
const paidMissions = () => store.read('missions', []).filter(m => m.paid); function quote(hunterPol, completions) {
const c = cfg();
// completions already granted against a mission (failed ones do not count, they never paid) const hp = r4(Math.max(Number(c.paidBaseHunterPol), Number(hunterPol) || 0));
function usedOf(missionId) { const n = Math.max(Number(c.paidMinBlock), Math.round(Number(completions) || 0));
return store.read('payouts', []).filter(p => p.missionId === missionId && p.status !== 'failed' && !p.prize && !p.ref).length; // 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);
// what every live paid mission still owes its hunters const buf = Math.max(0, Number(c.paidReserveBufferPct) || 0);
function outstanding() { const reserve = r4((drips + gas) * (1 + buf / 100));
let pol = 0, left = 0; // Margin is a share of what the advertiser pays, the same basis as InstantAdPay's platform 20%.
for (const m of paidMissions()) { const pct = Math.min(90, Math.max(0, Number(c.paidMarginPct) || 0));
const remaining = Math.max(0, Number(m.budget || 0) - usedOf(m.id)); const total = r4(reserve / (1 - pct / 100));
left += remaining; return { hunterPol: hp, completions: n, drips, gas, bufferPct: buf, reserve,
pol += remaining * Number(m.hunterPol || 0); margin: r4(total - reserve), total, marginPct: pct };
} }
return { reservePol: r4(pol), completionsLeft: left, missions: paidMissions().length };
} // ---- reserves ---------------------------------------------------------------------------
const paidMissions = () => store.read('missions', []).filter(m => m.paid);
// THE guard. Given the faucet's real balance, can we take this sale?
function canSell(balancePol, addReservePol) { // completions already granted against a mission (failed ones do not count, they never paid)
const c = cfg(); function usedOf(missionId) {
const o = outstanding(); return store.read('payouts', []).filter(p => p.missionId === missionId && p.status !== 'failed' && !p.prize && !p.ref).length;
const float = Number(c.paidHouseFloatPol) || 0; }
const committed = r4(o.reservePol + float + Number(addReservePol || 0));
const bal = r4(Number(balancePol) || 0); // what every live paid mission still owes its hunters
return { function outstanding() {
ok: bal >= committed, let pol = 0, left = 0;
balance: bal, committed, houseFloat: float, const c = cfg();
alreadyReserved: o.reservePol, adding: r4(Number(addReservePol) || 0), const gas = Number(c.paidGasPol) || 0;
shortBy: bal >= committed ? 0 : r4(committed - bal), 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;
// ---- the hunter's allowance --------------------------------------------------------------- // reserved on the same basis it was sold on: drip + gas + buffer, per completion still owed
// Base allowance is the company-funded missionsPerDay. Every live paid mission the hunter has pol += remaining * (Number(m.hunterPol || 0) + gas) * buf;
// not yet done adds one, because that completion costs the company nothing. }
function extraAllowance(doneIds) { return { reservePol: r4(pol), completionsLeft: left, missions: paidMissions().length };
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;
} // THE guard. Given the faucet's real balance, can we take this sale?
function canSell(balancePol, addReservePol) {
function isPaid(missionId) { const c = cfg();
const m = store.read('missions', []).find(x => x.id === missionId); const o = outstanding();
return !!(m && m.paid); const float = Number(c.paidHouseFloatPol) || 0;
} const committed = r4(o.reservePol + float + Number(addReservePol || 0));
const bal = r4(Number(balancePol) || 0);
module.exports = { cfg, quote, outstanding, canSell, extraAllowance, isPaid, usedOf, paidMissions }; 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 };