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