From 7374dc8f1b2749ea99c5ee082677490178882869 Mon Sep 17 00:00:00 2001 From: martbost Date: Thu, 10 Sep 2026 07:50:59 -0500 Subject: [PATCH] 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 --- ads.js | Bin 67705 -> 68448 bytes burner.js | 10 +++++++--- coach.js | 7 ++++++- public/assets/my.js | 2 +- public/my.html | 2 +- 5 files changed, 15 insertions(+), 6 deletions(-) diff --git a/ads.js b/ads.js index eae9fb79081cfa9cb778fea476567dbff22e2c0e..2633ed9b1c1b24843c4e5f16a9d941f5e2ee5840 100644 GIT binary patch delta 546 zcmcJM%}N6?6onDG(v=$pg>tQ6Cpgn~)rBHb>QIEXimkYCp?8v41Cz`+$yCKYggBsY zAwqHC+lU*VLEM;jx)ApUvN`#==X~euzVQB7cnRwmNhyrs%#mCMJlSUi?x0$QROy&2 z13}6L+(2{zp%`+YC}R`@rMDpAnkPffTu_mL$!&%~$iZ9-%ICqZ&_2}J=dPC(>#GIW z*no%c;vubQgC^C4fQHQ-=%Bf*NW5`sE;zI@8#o$4jes{%awES4q%=u6gQ-Nu-TH9x zDt{vBc0KaApQvd(} diff --git a/burner.js b/burner.js index 94bb514..206e2e1 100644 --- a/burner.js +++ b/burner.js @@ -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; } diff --git a/coach.js b/coach.js index 0e80ce8..90b7604 100644 --- a/coach.js +++ b/coach.js @@ -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 diff --git a/public/assets/my.js b/public/assets/my.js index 5d82998..127e8de 100644 --- a/public/assets/my.js +++ b/public/assets/my.js @@ -990,7 +990,7 @@ const d = r.directs || []; $('coachSummary').innerHTML = d.length ? '' + d.length + ' direct' + (d.length === 1 ? '' : 's') + ' · ' + r.stalled + ' quiet for 3+ days' + (r.stalled ? ' · start at the top' : '') : ''; if (!d.length) { el.innerHTML = '

No directs yet. When someone joins through your link they show up here with their next step.

'; return; } - el.innerHTML = d.map(x => '
' + esc(x.name) + (x.stalled ? ' quiet ' + x.quietDays + 'd' : '') + '' + el.innerHTML = d.map(x => '
' + esc(x.name) + (x.stalled ? ' quiet ' + x.quietDays + 'd' : '') + (x.bound === false ? ' not bound to you on-chain' : '') + '' + '' + esc(x.label) + ' → ' + esc(x.next) + '' + 'rung ' + x.rung + '/6' + '' + (x.buyerCount ? x.buyerCount + ' buyer' + (x.buyerCount === 1 ? '' : 's') : '') + '' diff --git a/public/my.html b/public/my.html index c78f4ed..8d57092 100644 --- a/public/my.html +++ b/public/my.html @@ -858,7 +858,7 @@ - +