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
+111
View File
@@ -0,0 +1,111 @@
// Money moving in and out of the receiver wallets, which the chain index cannot tell us about
// (Marty, 2026-09-23). The P&L derives revenue from AdminPaid events and holdings from
// eth_getBalance, so two real things were invisible:
//
// OUT a draw. Marty funded RM Circle #139's Culmen->Apex upgrade from Receiver A. That is a
// legitimate use of house profit, but nothing recorded it, so "earned" and "on hand" drift
// apart for ever. ("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.")
// IN outside income paid into the same wallet, e.g. a ClickBaitPays withdrawal. Real profit,
// but it is not an AdminPaid event, so the revenue lines never see it.
//
// Both are hand-entered because only Marty knows what a transfer was for. Storage:
// DATA_DIR/ledger.json { entries: [{id, date, dir, asset, amount, usd, wallet, note, tx, ts}] }
const fs = require('fs');
const path = require('path');
let FILE = null;
const DIRS = ['in', 'out'];
const WALLETS = ['feeA', 'feeB', 'engine', 'hunt'];
// assets we can actually value; anything else is recorded but valued only by the usd field
const ASSETS = ['POL', 'USDC', 'USDT', 'DAI', 'other'];
// WHY a kind and not just a direction (Marty, 2026-09-23): three different things move through
// Receiver A and only one of them is profit.
// revenue outside income earned by the business, e.g. a ClickBaitPays withdrawal in POL.
// Counts toward profit AND toward the balance.
// draw house profit spent on something, e.g. funding RM Circle #139's upgrade. Leaves the
// wallet, but it is not a cost of running InstantAdPay.
// transfer Marty's own capital moved in or out for safekeeping, e.g. consolidating balances
// from other owned positions into the Tangem receiver. Touches the balance ONLY and
// must never show up as revenue, or the P&L reports his savings as earnings.
const KINDS = ['revenue', 'draw', 'transfer'];
function init(opts) { FILE = path.join(opts.dataDir, 'ledger.json'); }
function load() { try { return JSON.parse(fs.readFileSync(FILE, 'utf8')); } catch (e) { return { entries: [] }; } }
function save(d) {
const tmp = FILE + '.tmp';
fs.writeFileSync(tmp, JSON.stringify(d, null, 1));
fs.renameSync(tmp, FILE); // never leave a half-written ledger behind
}
const newId = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
const num = v => { const n = Number(v); return Number.isFinite(n) && n >= 0 ? Math.round(n * 1e6) / 1e6 : 0; };
function entries() {
return load().entries.slice().sort((a, b) =>
(b.date || '').localeCompare(a.date || '') || (b.ts || 0) - (a.ts || 0));
}
function put(input) {
const d = load();
const dir = DIRS.includes(input.dir) ? input.dir : null;
if (!dir) return { error: 'Say whether the money came in or went out.' };
const amount = num(input.amount);
const usd = num(input.usd);
if (!amount && !usd) return { error: 'Give an amount, in POL or in dollars.' };
const note = String(input.note || '').trim().slice(0, 200);
if (!note) return { error: 'Say what this was for. An unexplained line is the thing we are fixing.' };
const date = /^\d{4}-\d{2}-\d{2}$/.test(String(input.date || '')) ? input.date
: new Date().toLocaleDateString('en-CA', { timeZone: 'America/Chicago' }); // Central, like every other day key here
const e = {
id: input.id && d.entries.find(x => x.id === input.id) ? input.id : newId(),
date, dir, amount, usd, note,
asset: ASSETS.includes(input.asset) ? input.asset : 'POL',
kind: KINDS.includes(input.kind) ? input.kind : (dir === 'in' ? 'revenue' : 'draw'),
wallet: WALLETS.includes(input.wallet) ? input.wallet : 'feeA',
tx: /^0x[0-9a-fA-F]{64}$/.test(String(input.tx || '')) ? String(input.tx).toLowerCase() : '',
ts: Date.now(),
};
const i = d.entries.findIndex(x => x.id === e.id);
if (i >= 0) d.entries[i] = e; else d.entries.push(e);
save(d);
return { ok: true, entry: e };
}
function remove(id) {
const d = load();
const i = d.entries.findIndex(x => x.id === id);
if (i < 0) return { error: 'No such entry.' };
const [gone] = d.entries.splice(i, 1);
save(d);
return { ok: true, entry: gone };
}
// Totals per wallet, split by direction. POL is summed separately from dollar-denominated assets
// because only POL can be reconciled against an on-chain balance.
function totals(wallet) {
const rows = load().entries.filter(e => !wallet || e.wallet === wallet);
const t = { inPol: 0, outPol: 0, inUsd: 0, outUsd: 0, count: rows.length,
// revenue and draws are the P&L numbers; transfers move the balance and nothing else
revenuePol: 0, revenueUsd: 0, drawPol: 0, drawUsd: 0, transferPol: 0 };
for (const e of rows) {
const pol = e.asset === 'POL' ? e.amount : 0;
if (e.dir === 'in') { t.inPol += pol; t.inUsd += e.usd; } else { t.outPol += pol; t.outUsd += e.usd; }
if (e.kind === 'revenue') { t.revenuePol += pol; t.revenueUsd += e.usd; }
else if (e.kind === 'draw') { t.drawPol += pol; t.drawUsd += e.usd; }
else t.transferPol += (e.dir === 'in' ? pol : -pol);
}
for (const k of Object.keys(t)) if (k !== 'count') t[k] = Math.round(t[k] * 1e6) / 1e6;
return t;
}
// earned (AdminPaid) + POL in - POL out = what should be sitting there. Any drift is a transfer
// nobody has written down yet, and naming the number is the whole point of this file.
function reconcile(earnedPol, actualPol, wallet) {
const t = totals(wallet || 'feeA');
const expected = Math.round((Number(earnedPol || 0) + t.inPol - t.outPol) * 1e6) / 1e6;
const actual = Math.round(Number(actualPol || 0) * 1e6) / 1e6;
return { earnedPol: Number(earnedPol || 0), expected, actual,
drift: Math.round((actual - expected) * 1e6) / 1e6, totals: t };
}
module.exports = { init, entries, put, remove, totals, reconcile, DIRS, ASSETS, KINDS, WALLETS };
+19 -1
View File
@@ -445,6 +445,24 @@
<p class="small muted" style="margin:10px 0 0">Fixed monthly cost (USD) for the net line: <input type="number" id="pnlFixed" min="0" step="1" style="width:110px"> <button class="btn small sec" type="button" id="pnlFixedSave">Save</button></p>
</div>
</div>
<div class="card">
<div class="card-head"><h3>Wallet ledger</h3><span class="sub">money in and out that the chain cannot explain</span></div>
<p class="small muted" style="margin:0 0 10px">The lines above come from contract events and live balances. Anything else that moves through a receiver goes here: outside income, money drawn out and spent, and your own funds parked for safekeeping. <b>Revenue</b> counts toward profit. <b>Draw</b> is house profit spent on something. <b>Transfer</b> moves the balance only and is never counted as earnings.</p>
<div id="pnlRecon" class="small" style="margin-bottom:10px"></div>
<div class="tablewrap"><table class="adm-table" id="pnlLedger"></table></div>
<div class="grid" style="grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:8px;margin-top:12px">
<select id="ldDir"><option value="in">Money in</option><option value="out">Money out</option></select>
<select id="ldKind"><option value="revenue">Revenue</option><option value="draw">Draw</option><option value="transfer">Transfer</option></select>
<select id="ldWallet"><option value="feeA">Receiver A</option><option value="feeB">Receiver B</option><option value="engine">Engine</option><option value="hunt">PolHunter</option></select>
<input type="number" id="ldAmount" min="0" step="0.0001" placeholder="Amount in POL">
<input type="number" id="ldUsd" min="0" step="0.01" placeholder="USD (optional)">
<input type="date" id="ldDate">
</div>
<div class="grid" style="grid-template-columns:1fr auto;gap:8px;margin-top:8px">
<input type="text" id="ldNote" maxlength="200" placeholder="What was this for? e.g. RM Circle #139 Culmen to Apex">
<button class="btn small" type="button" id="ldAdd">Add line</button>
</div>
</div>
<div class="card">
<div class="card-head"><h3>Automatic credit burner</h3><span class="sub">settles pending campaign spend on-chain from the engine wallet</span></div>
<p class="small" id="burnerLine"></p>
@@ -493,6 +511,6 @@
</div>
<script src="/assets/common.js?v=20260923b"></script>
<script src="/assets/admin.js?v=20260923a"></script>
<script src="/assets/admin.js?v=20260923b"></script>
</body>
</html>
+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'); } });
+20 -1
View File
@@ -34,6 +34,7 @@ const blog = require('./blog');
const adminMember = require('./adminmember');
const syndicate = require('./syndicate');
const releases = require('./releases');
const ledger = require('./ledger');
const updates = require('./updates');
const audit = require('./audit'); // counter audit: views vs delivery logs, charges vs shows (Marty, 2026-09-15) // member update emails from Admin > Releases (Marty, 2026-09-14)
const leaderboard = require('./leaderboard');
@@ -475,6 +476,7 @@ async function boot() {
loadOpenTokens();
syndicate.init({ dataDir: DATA_DIR, publicDir: PUBLIC_DIR, uploadsDir: UPLOADS_DIR });
releases.init({ dataDir: DATA_DIR });
ledger.init({ dataDir: DATA_DIR });
toolkit.init({ dataDir: DATA_DIR, ads, accounts, siteConfig, coach, messages, promos, videomaker, chain });
updates.init({ dataDir: DATA_DIR, accounts, releases, mailer, drip, sendy, adminEmail: ADMIN_EMAIL });
audit.init({ dataDir: DATA_DIR, notify: text => { const sc = siteConfig(); if (sc.telegramBotToken && sc.telegramAdminChatId) telegramSend(sc.telegramAdminChatId, text).catch(() => {}); else if (ADMIN_EMAIL && mailer.hasKey()) mailer.send(ADMIN_EMAIL, 'InstantAdPay: counter audit', text).catch(() => {}); } });
@@ -3176,6 +3178,7 @@ const server = http.createServer(async (req, res) => {
const fromBlock = days ? latest - Math.round(days * 43200) : 0;
const evs = chain.recentEvents(1e9).filter(e => !days || e.block >= fromBlock);
const sum = (list, f) => list.reduce((n, e) => n + BigInt(f(e) || '0'), 0n);
const pol = wei => Number(BigInt(wei) / 10n ** 12n) / 1e6; // 6-decimal POL, no float drift
const purchases = evs.filter(e => e.type === 'Purchase');
const tier = evs.filter(e => e.type === 'TierPaid');
const admin = evs.filter(e => e.type === 'AdminPaid');
@@ -3192,7 +3195,23 @@ const server = http.createServer(async (req, res) => {
purchases: { count: purchases.length, volumeWei: sum(purchases, e => e.paidWei).toString(), usdCents: purchases.reduce((n, e) => n + (e.priceCents || 0), 0), byPackage: byPkg },
platformWei: sum(admin, e => e.amountWei).toString(), memberPayoutsWei: sum(tier, e => e.amountWei).toString(), byTier,
passedUp: { count: passed.length, unqualified: passed.filter(e => e.reason === 'unqualified').length, sendFailed: passed.filter(e => e.reason === 'send-failed').length },
wallets, balances, fixedMonthlyUsd: Number(siteConfig().pnlFixedMonthlyUsd) || 0, burner: burner.status(), polhunter: hunt });
wallets, balances, fixedMonthlyUsd: Number(siteConfig().pnlFixedMonthlyUsd) || 0, burner: burner.status(), polhunter: hunt,
// hand-entered movements in and out of the receivers: draws the chain cannot explain, and
// outside income (a ClickBaitPays withdrawal, say) that is not an AdminPaid event
ledger: { entries: ledger.entries(), totals: ledger.totals('feeA'),
reconcile: ledger.reconcile(pol(sum(admin, e => e.amountWei)), pol(BigInt(balances.feeA || '0')), 'feeA') } });
}
if (p === '/api/admin/ledger' && req.method === 'POST') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
const b = await readBody(req);
const r = ledger.put(b);
return json(res, r.error ? 400 : 200, r);
}
if (p === '/api/admin/ledger/delete' && req.method === 'POST') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
const b = await readBody(req);
const r = ledger.remove(String(b.id || ''));
return json(res, r.error ? 400 : 200, r);
}
if (p === '/api/admin/burner' && req.method === 'GET') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });