Credits: reserve live campaign budgets so purchased credits can't be over-committed; burner dry-runs and skips reverting burns; coaching flags directs not bound to you on-chain

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-10 07:50:59 -05:00
parent 9ebb9a86b2
commit 7374dc8f1b
5 changed files with 15 additions and 6 deletions
BIN
View File
Binary file not shown.
+7 -3
View File
@@ -7,7 +7,7 @@ const fs = require('fs');
let chain = null, ads = null;
const ABI = ['function consume(uint32 memberId_, uint8 creditType, uint256 amount, bytes32 campaignRef)', 'function engineSigner() view returns (address)'];
const state = { enabled: false, address: null, signer: null, balanceWei: '0', lastRun: 0, lastError: null, burned: 0, lastTx: null, mismatch: false };
const state = { enabled: false, address: null, signer: null, balanceWei: '0', lastRun: 0, lastError: null, burned: 0, lastTx: null, mismatch: false, skipped: {} };
let running = false;
function keyHex() {
@@ -53,6 +53,10 @@ async function tick() {
try {
// Polygon nodes reject low priority fees and some public RPCs answer fee
// queries with 500s: set the fees ourselves from the server's estimate
// dry-run first: a revert here (usually "Insufficient credits", the member's on-chain
// balance is below what the engine metered) costs no gas and is left for the admin
try { await ctr.consume.staticCall(Number(b.memberId), 0, BigInt(b.amount), refToBytes32(b.ref)); }
catch (e) { state.skipped[b.id] = String(e.reason || e.shortMessage || e.message).slice(0, 120); continue; }
const g = await chain.suggestedFees();
const overrides = { gasLimit: 120000n, maxPriorityFeePerGas: BigInt(g.maxPriorityFeePerGas), maxFeePerGas: BigInt(g.maxFeePerGas) };
const tx = await ctr.consume(Number(b.memberId), 0, BigInt(b.amount), refToBytes32(b.ref), overrides);
@@ -64,8 +68,8 @@ async function tick() {
// a node error (500, timeout, rate limit): move to the next RPC for the next tick.
// an "Insufficient credits" revert means the member's on-chain balance is
// already lower than the engine thinks; leave it pending for the admin to review
if (/server response|timeout|rate|429|503|502|500/i.test(String(e.message))) rotateRpc();
break;
if (/server response|timeout|rate|429|503|502|500/i.test(String(e.message))) { rotateRpc(); break; }
state.skipped[b.id] = String(e.reason || e.shortMessage || e.message).slice(0, 120);
}
}
} finally { running = false; }
+6 -1
View File
@@ -61,16 +61,21 @@ async function describe(acct, now = Date.now()) {
const stalled = rung < 6 && quietDays >= STALL_DAYS;
const R = RUNGS[rung];
return { rung, label: R.label, next: R.next, say: R.say, quietDays, stalled, buyerCount: mm ? mm.buyerCount : 0,
counted: !!(mm && mm.countedAsBuyer), joined: acct.created, lastSeen: acct.lastSeen || 0 };
counted: !!(mm && mm.countedAsBuyer), joined: acct.created, lastSeen: acct.lastSeen || 0, onchainSponsorId: mm ? mm.sponsorId : null };
}
// coaching view for a sponsor: every direct, ladder rung, stalled flag, what to say
async function coachView(email) {
const levels = await accounts.downline(email, 1);
const directs = levels.length ? levels[0].members : [];
const me = await accounts.byEmail(email);
const myId = me ? (me.memberId || 0) : 0;
const now = Date.now();
const out = [];
for (const d of directs) {
const c = await describe(d, now);
// bound: the contract pays whoever the member registered under. A direct who activated with
// no sponsor (or a different one) is in this line on the site but pays this member nothing.
c.bound = c.onchainSponsorId == null ? null : (myId > 0 && c.onchainSponsorId === myId);
out.push(Object.assign({ email: d.email, name: d.username ? '@' + d.username : (d.memberId ? 'member #' + d.memberId : d.email.replace(/^(.).*(@.*)$/, '$1***$2')), memberId: d.memberId || 0 }, c));
}
// most actionable first: stalled lowest rung, then quiet days
+1 -1
View File
@@ -990,7 +990,7 @@
const d = r.directs || [];
$('coachSummary').innerHTML = d.length ? '<b>' + d.length + '</b> direct' + (d.length === 1 ? '' : 's') + ' · <b>' + r.stalled + '</b> quiet for 3+ days' + (r.stalled ? ' · start at the top' : '') : '';
if (!d.length) { el.innerHTML = '<p class="muted small">No directs yet. When someone joins through your link they show up here with their next step.</p>'; return; }
el.innerHTML = d.map(x => '<div class="lin-row' + (x.stalled ? ' own' : '') + '"><span class="nm">' + esc(x.name) + (x.stalled ? ' <span class="badge amber">quiet ' + x.quietDays + 'd</span>' : '') + '</span>'
el.innerHTML = d.map(x => '<div class="lin-row' + (x.stalled ? ' own' : '') + '"><span class="nm">' + esc(x.name) + (x.stalled ? ' <span class="badge amber">quiet ' + x.quietDays + 'd</span>' : '') + (x.bound === false ? ' <span class="badge amber" title="On-chain this member registered under ' + (x.onchainSponsorId ? 'member #' + x.onchainSponsorId : 'no sponsor') + ', so the contract pays their purchases there, not to you. Sponsor binding is permanent at first activation.">not bound to you on-chain</span>' : '') + '</span>'
+ '<span class="em">' + esc(x.label) + ' → ' + esc(x.next) + '</span>'
+ '<span class="id">rung ' + x.rung + '/6</span>'
+ '<span class="dt">' + (x.buyerCount ? x.buyerCount + ' buyer' + (x.buyerCount === 1 ? '' : 's') : '') + '</span>'
+1 -1
View File
@@ -858,7 +858,7 @@
<script src="/assets/common.js?v=20260910b"></script>
<script src="/assets/wallet.js?v=20260910a"></script>
<script src="/assets/promo.js?v=20260909b"></script>
<script src="/assets/my.js?v=20260910g"></script>
<script src="/assets/my.js?v=20260910h"></script>
<script src="/assets/chat.js?v=20260907l"></script>
</body>
</html>