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 };