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>
This commit is contained in:
+38
-1
@@ -41,6 +41,11 @@ const DEFAULTS = {
|
|||||||
// gas together, so a failed-and-retried send or a price spike is already covered.
|
// gas together, so a failed-and-retried send or a price spike is already covered.
|
||||||
paidReserveBufferPct: 10,
|
paidReserveBufferPct: 10,
|
||||||
paidHouseFloatPol: 50, // POL kept back for house missions, never counted as sellable
|
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', {})); }
|
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;
|
||||||
@@ -63,6 +68,38 @@ function quote(hunterPol, completions) {
|
|||||||
margin: r4(total - reserve), total, marginPct: pct };
|
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 ---------------------------------------------------------------------------
|
// ---- reserves ---------------------------------------------------------------------------
|
||||||
const paidMissions = () => store.read('missions', []).filter(m => m.paid);
|
const paidMissions = () => store.read('missions', []).filter(m => m.paid);
|
||||||
|
|
||||||
@@ -114,4 +151,4 @@ function isPaid(missionId) {
|
|||||||
return !!(m && m.paid);
|
return !!(m && m.paid);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { cfg, quote, outstanding, canSell, extraAllowance, isPaid, usedOf, paidMissions };
|
module.exports = { cfg, quote, blockFor, orderTotal, outstanding, canSell, extraAllowance, isPaid, usedOf, paidMissions };
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
// What a dollar is worth in POL right now.
|
||||||
|
//
|
||||||
|
// Mission blocks are priced in dollars ($30 entry, $10 listing) because that is how Marty thinks
|
||||||
|
// about them and how a buyer judges them. The faucet pays in POL. So something has to convert,
|
||||||
|
// and it has to be a rate nobody can argue with.
|
||||||
|
//
|
||||||
|
// It is read from the InstantAdPay contract's own quote: the contract stores each package's price
|
||||||
|
// in cents and quotes the POL to pay right now, so priceCents / quoteWei IS the rate members are
|
||||||
|
// already paying at today. No third-party price feed, no oracle to trust, no disagreement between
|
||||||
|
// what a mission costs here and what a package costs there.
|
||||||
|
//
|
||||||
|
// Cached for a few minutes and fail-soft: a purchase is refused rather than priced off a stale or
|
||||||
|
// missing rate, because getting this wrong silently means selling delivery below cost.
|
||||||
|
'use strict';
|
||||||
|
const https = require('https');
|
||||||
|
|
||||||
|
const IAP = '0xbe1eca72aff47d13d8e907e523f55eb9e2d365e0';
|
||||||
|
const RPCS = ['https://polygon-bor-rpc.publicnode.com', 'https://1rpc.io/matic'];
|
||||||
|
const SEL_COUNT = '0xe0f6ef87'; // productCount()
|
||||||
|
const SEL_PRODUCT = '0xbf712fd6'; // products(uint32) -> priceCents is word 0, active word 3
|
||||||
|
const SEL_QUOTE = '0x7de85694'; // quoteWei(uint32)
|
||||||
|
const TTL = 5 * 60000;
|
||||||
|
|
||||||
|
let cache = { usdPerPol: 0, at: 0 };
|
||||||
|
|
||||||
|
function rpc(method, params) {
|
||||||
|
const body = JSON.stringify({ jsonrpc: '2.0', id: 1, method, params });
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let i = 0;
|
||||||
|
const attempt = () => {
|
||||||
|
const u = new URL(RPCS[i]);
|
||||||
|
const rq = https.request({ hostname: u.hostname, path: u.pathname, method: 'POST', timeout: 12000,
|
||||||
|
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } }, res => {
|
||||||
|
let d = ''; res.on('data', c => d += c);
|
||||||
|
res.on('end', () => {
|
||||||
|
try { const j = JSON.parse(d); if (j.error) throw new Error(j.error.message); resolve(j.result); }
|
||||||
|
catch (e) { if (++i < RPCS.length) attempt(); else reject(e); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
rq.on('error', () => { if (++i < RPCS.length) attempt(); else reject(new Error('rpc unreachable')); });
|
||||||
|
rq.on('timeout', () => { rq.destroy(); });
|
||||||
|
rq.end(body);
|
||||||
|
};
|
||||||
|
attempt();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const call = data => rpc('eth_call', [{ to: IAP, data }, 'latest']);
|
||||||
|
const enc = n => BigInt(n).toString(16).padStart(64, '0');
|
||||||
|
const word = (r, i) => r.slice(2 + i * 64, 2 + (i + 1) * 64);
|
||||||
|
|
||||||
|
// Walk the catalogue until a product quotes a non-zero POL cost, then take cents / POL from it.
|
||||||
|
async function fetchRate() {
|
||||||
|
const n = parseInt(word(await call(SEL_COUNT), 0), 16);
|
||||||
|
for (let id = 1; id <= Math.min(n, 12); id++) {
|
||||||
|
let wei = 0n, cents = 0;
|
||||||
|
try { wei = BigInt('0x' + word(await call(SEL_QUOTE + enc(id)), 0)); } catch (e) { continue; }
|
||||||
|
if (wei <= 0n) continue;
|
||||||
|
try {
|
||||||
|
const p = await call(SEL_PRODUCT + enc(id));
|
||||||
|
cents = parseInt(word(p, 0), 16); // priceCents
|
||||||
|
const active = parseInt(word(p, 3), 16) === 1;
|
||||||
|
const exists = parseInt(word(p, 4), 16) === 1;
|
||||||
|
if (!active || !exists) cents = 0;
|
||||||
|
} catch (e) { /* fall through */ }
|
||||||
|
if (!cents) continue;
|
||||||
|
const pol = Number(wei / 10n ** 12n) / 1e6;
|
||||||
|
if (pol > 0) return (cents / 100) / pol; // dollars per POL
|
||||||
|
}
|
||||||
|
throw new Error('no product quoted a usable price');
|
||||||
|
}
|
||||||
|
|
||||||
|
// dollars per POL, e.g. 0.1019
|
||||||
|
async function usdPerPol(force) {
|
||||||
|
if (!force && cache.usdPerPol > 0 && Date.now() - cache.at < TTL) return cache.usdPerPol;
|
||||||
|
const r = await fetchRate();
|
||||||
|
if (!(r > 0) || !isFinite(r)) throw new Error('rate came back unusable');
|
||||||
|
cache = { usdPerPol: r, at: Date.now() };
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// POL needed for a price in cents. Throws rather than guessing: a purchase priced off a rate we
|
||||||
|
// could not read is a purchase that might sell delivery below cost.
|
||||||
|
async function polForCents(cents) {
|
||||||
|
const rate = await usdPerPol();
|
||||||
|
const pol = (Number(cents) / 100) / rate;
|
||||||
|
return Math.round(pol * 10000) / 10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { usdPerPol, polForCents, _cache: () => cache };
|
||||||
Reference in New Issue
Block a user