Files
polhunter/lib/paid.js
T
martbost 8a006a90c1 Paid missions: $30 blocks, a $10 listing, priced off the live POL rate
Marty's pricing, settled. A block is $30 and buys completions rather than a
fixed count: the advertiser picks what hunters earn and the count falls out, so
a higher payout trades visits for speed at the same price.

  0.30/completion -> 666 visits      0.75 -> 272      1.00 -> 204

The listing fee is $10, charged ONCE per mission and only at approval, because
that is when the review it pays for has actually happened. Never charged again
on that mission, and waived entirely on a first order of 4 blocks or more.

  1 block  $40    2 blocks  $70    4 blocks  $120 (listing waived)
  repeat   $30

No volume discount, deliberately. At a 20% margin the reserve is a hard cost
owed to hunters, so a discount comes entirely out of the house share with five
times leverage: 10% off the price is half the margin gone. The levers that do
not cost anything are the listing waiver and priority placement.

Dollars convert at the InstantAdPay contract's own quote (priceCents/quoteWei),
so a mission is priced at exactly the rate members already pay for packages. No
third-party feed. Reads $0.1055/POL right now. It throws rather than guessing:
a purchase priced off a rate we could not read is a purchase that might sell
delivery below cost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 10:49:08 -05:00

155 lines
8.4 KiB
JavaScript

// 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 };