P&L: account for money the chain cannot explain

The P&L took revenue from AdminPaid events and holdings from eth_getBalance,
so anything else moving through a receiver was invisible. Receiver A has earned
10,871.89 POL and holds 4,561.19; nothing explained the difference, and the gap
would have widened every time Marty drew from it. "Why would I ever keep a
receiver wallet that I never take anything out of? That's stupid, so I need to
be able to account for it."

Three kinds, because three different things move through that wallet and only
one is profit. Revenue is outside income, a ClickBaitPays withdrawal paid in
POL, and counts toward profit. A draw is house profit spent on something, such
as funding RM Circle #139's Culmen to Apex upgrade; it leaves the wallet but is
not a cost of running InstantAdPay. A transfer is Marty's own capital parked in
the Tangem receiver for safekeeping, and moves the balance only: counting it as
revenue would report his savings as earnings.

The pane now states earned, plus revenue, less draws, against what is actually
held, and names any unexplained drift rather than hiding it in a balance.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-23 18:10:56 -05:00
parent 8656f7ce4d
commit 6ff21f76ee
4 changed files with 4061 additions and 3860 deletions
+516 -498
View File
File diff suppressed because it is too large Load Diff
+53
View File
@@ -739,10 +739,63 @@
const W = r.wallets || {}, B = r.balances || {};
$('pnlWallets').innerHTML = '<tr><th>Wallet</th><th>Address</th><th>Balance</th></tr>'
+ [['Owner / fee A (Tangem)', W.feeA, B.feeA], ['Fee B', W.feeB, B.feeB], ['Engine (gas)', W.engine, B.engine]].filter(x => x[1]).map(x => '<tr><td>' + x[0] + '</td><td class="mono small">' + esc(x[1]) + '</td><td class="mono">' + (x[2] == null ? '?' : pol(x[2]) + ' POL') + '</td></tr>').join('');
renderLedger(r);
$('pnlFixed').value = r.fixedMonthlyUsd || 0;
const b = r.burner || {};
$('burnerLine').textContent = !b.hasEthers ? 'ethers is not installed in this build.' : !b.keyPresent ? 'No engine key configured (ENGINE_KEY). Burns stay pending until it is set.' : b.mismatch ? 'ENGINE_KEY does not match the contract engine signer. Disabled.' : 'Engine wallet ' + b.address + ' holds ' + pol(b.balanceWei) + ' POL. That is its gas fund, not a cost: one burn uses about 0.003 POL (roughly 48,000 gas), paid by this wallet, never by the member. ' + b.burned + ' burn' + (b.burned === 1 ? '' : 's') + ' since boot' + (b.lastRun ? ' · last check ' + when(b.lastRun) : '') + (b.lastError ? ' · last error: ' + b.lastError : '') + (b.skipped && Object.keys(b.skipped).length ? ' · skipped (needs review): ' + Object.entries(b.skipped).map(([k, v]) => k + ' (' + v + ')').join(', ') : '');
}
// The ledger: hand-entered movements the chain index cannot see, plus the reconciliation that
// makes "earned" and "on hand" agree. A drift that will not go to zero is a transfer nobody
// wrote down (Marty, 2026-09-23).
const LD_KIND = { revenue: 'Revenue', draw: 'Draw', transfer: 'Transfer' };
const LD_WALLET = { feeA: 'Receiver A', feeB: 'Receiver B', engine: 'Engine', hunt: 'PolHunter' };
const pol4 = n => Number(n || 0).toLocaleString(undefined, { maximumFractionDigits: 4 });
function renderLedger(r) {
const L = (r && r.ledger) || {}, rows = L.entries || [], t = L.totals || {}, rec = L.reconcile || {};
if ($('pnlLedger')) {
$('pnlLedger').innerHTML = '<tr><th>Date</th><th>Wallet</th><th>Kind</th><th>POL</th><th>What for</th><th></th></tr>'
+ (rows.length ? rows.map(e => '<tr><td>' + e.date + '</td><td>' + (LD_WALLET[e.wallet] || e.wallet) + '</td>'
+ '<td>' + (LD_KIND[e.kind] || e.kind) + '</td>'
+ '<td' + (e.dir === 'out' ? ' style="color:var(--bad)"' : '') + '>' + (e.dir === 'out' ? '-' : '+') + pol4(e.amount)
+ (e.usd ? ' <span class="muted">($' + Number(e.usd).toFixed(2) + ')</span>' : '') + '</td>'
+ '<td>' + esc(e.note || '') + '</td>'
+ '<td><button class="btn small sec" type="button" data-ldkill="' + e.id + '">Remove</button></td></tr>').join('')
: '<tr><td colspan="6" class="muted">Nothing recorded yet.</td></tr>');
$('pnlLedger').querySelectorAll('[data-ldkill]').forEach(b => b.addEventListener('click', async () => {
if (!await IAP.confirmBox('Remove this ledger line?')) return;
try { await api('/api/admin/ledger/delete', { id: b.dataset.ldkill }); loadPnl(); }
catch (e) { IAP.status('Could not remove it: ' + e.message, 'bad'); }
}));
}
if ($('pnlRecon')) {
const drift = Number(rec.drift || 0);
const ok = Math.abs(drift) < 0.01;
$('pnlRecon').innerHTML = 'Receiver A earned <b>' + pol4(rec.earnedPol) + ' POL</b> on chain'
+ (t.revenuePol ? ' plus <b>' + pol4(t.revenuePol) + '</b> outside revenue' : '')
+ (t.drawPol ? ', less <b>' + pol4(t.drawPol) + '</b> drawn' : '')
+ (t.transferPol ? ', ' + (t.transferPol > 0 ? 'plus ' : 'less ') + '<b>' + pol4(Math.abs(t.transferPol)) + '</b> transferred in for storage' : '')
+ '. Expected <b>' + pol4(rec.expected) + '</b>, actually holding <b>' + pol4(rec.actual) + '</b>. '
+ (ok ? '<span style="color:var(--mint)">Reconciled.</span>'
: '<span style="color:var(--amber)">Unexplained: ' + pol4(drift) + ' POL</span> '
+ '<span class="muted">(' + (drift > 0 ? 'more on hand than accounted for, so income or a transfer in is missing a line'
: 'less on hand than accounted for, so a draw is missing a line') + ')</span>');
}
}
if ($('ldAdd')) $('ldAdd').addEventListener('click', async () => {
const body = { dir: $('ldDir').value, kind: $('ldKind').value, wallet: $('ldWallet').value,
amount: Number($('ldAmount').value) || 0, usd: Number($('ldUsd').value) || 0,
date: $('ldDate').value, note: $('ldNote').value };
try {
await api('/api/admin/ledger', body);
$('ldAmount').value = ''; $('ldUsd').value = ''; $('ldNote').value = '';
IAP.status('Line added.', 'good'); loadPnl();
} catch (e) { IAP.status(e.message, 'bad'); }
});
// a draw is almost never revenue and an outside deposit almost never a draw: move the kind with
// the direction, but leave it editable
if ($('ldDir')) $('ldDir').addEventListener('change', () => {
$('ldKind').value = $('ldDir').value === 'in' ? 'revenue' : 'draw';
});
document.querySelectorAll('#pnlPeriods [data-days]').forEach(b => b.addEventListener('click', () => { pnlDays = Number(b.dataset.days); document.querySelectorAll('#pnlPeriods [data-days]').forEach(x => x.classList.toggle('on', x === b)); loadPnl().catch(e => IAP.status(e.message, 'bad')); }));
if ($('pnlFixedSave')) $('pnlFixedSave').addEventListener('click', async () => { try { await api('/api/admin/site', { pnlFixedMonthlyUsd: Number($('pnlFixed').value) || 0 }, 'PATCH'); IAP.status('Saved.', 'ok'); loadPnl(); } catch (e) { IAP.status(e.message, 'bad'); } });
if ($('burnerRun')) $('burnerRun').addEventListener('click', async () => { try { const r = await api('/api/admin/burner/run', {}); IAP.status('Burner ran: ' + (r.burned || 0) + ' burned.', 'ok'); loadPnl(); } catch (e) { IAP.status(e.message, 'bad'); } });