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
+27 -6
View File
@@ -27,10 +27,20 @@ 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
// 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 paidMinBlock: 100, // completions per block
paidMaxConcurrent: 0, // 0 = no limit; supply is elastic now that paid missions add capacity 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 // 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', {})); } function cfg() { return Object.assign({}, DEFAULTS, store.read('settings', {})); }
const r4 = n => Math.round(Number(n) * 10000) / 10000; const r4 = n => Math.round(Number(n) * 10000) / 10000;
@@ -41,9 +51,16 @@ function quote(hunterPol, completions) {
const c = cfg(); const c = cfg();
const hp = r4(Math.max(Number(c.paidBaseHunterPol), Number(hunterPol) || 0)); const hp = r4(Math.max(Number(c.paidBaseHunterPol), Number(hunterPol) || 0));
const n = Math.max(Number(c.paidMinBlock), Math.round(Number(completions) || 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 // What must actually be held: the drips, the gas to send them, and the safety buffer.
const margin = r4(reserve * (Number(c.paidMarginPct) / 100)); const drips = r4(hp * n);
return { hunterPol: hp, completions: n, reserve, margin, total: r4(reserve + margin) }; 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 --------------------------------------------------------------------------- // ---- reserves ---------------------------------------------------------------------------
@@ -57,10 +74,14 @@ function usedOf(missionId) {
// what every live paid mission still owes its hunters // what every live paid mission still owes its hunters
function outstanding() { function outstanding() {
let pol = 0, left = 0; 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()) { for (const m of paidMissions()) {
const remaining = Math.max(0, Number(m.budget || 0) - usedOf(m.id)); const remaining = Math.max(0, Number(m.budget || 0) - usedOf(m.id));
left += remaining; left += remaining;
pol += remaining * Number(m.hunterPol || 0); // 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 }; return { reservePol: r4(pol), completionsLeft: left, missions: paidMissions().length };
} }