← back to Ken
kalshi-dash/scripts/reconcile-canary.mjs
88 lines
#!/usr/bin/env node
// Ken paper-sim RECONCILIATION CANARY (born 2026-08-03 after the bigint-string over-credit bug).
// Read-only. Verifies the ken_portfolios rollup columns still equal the authoritative per-trade
// ledger, and that balance = seed - invested + returned. Any divergence beyond a small rounding
// tolerance means a credit/settle bug has crept back in (the exact class of the 441x over-credit
// where node-postgres bigint-as-string made `cost_cents + pnl` concatenate).
//
// Exit 0 = reconciled (silent-ish). Exit 1 = DIVERGENCE (prints ALERT). Never writes anything.
// Run: KEN_DATABASE_URL=... node scripts/reconcile-canary.mjs
import pg from 'pg';
const TOL_CENTS = 200; // $2 tolerance absorbs benign rounding; the bug produced 100x+ divergence
const url = process.env.KEN_DATABASE_URL;
if (!url) { console.error('reconcile-canary: KEN_DATABASE_URL not set'); process.exit(2); }
const pool = new pg.Pool({ connectionString: url });
// TK-11999: local Postgres restarts (brew/launchd) made this canary exit 2 when a tick landed
// mid-restart ("database system is starting up", socket ENOENT, ECONNREFUSED with an empty
// message). Retry ONLY those transient connection states with backoff; a persistent failure
// still exits 2 so a genuinely dead DB stays visible.
const TRANSIENT = new Set(['57P03', '57P01', '57P02', 'ECONNREFUSED', 'ENOENT', 'ECONNRESET']);
const isTransient = (e) => TRANSIENT.has(e?.code) ||
(Array.isArray(e?.errors) && e.errors.some(x => TRANSIENT.has(x?.code))) ||
/starting up|shutting down/i.test(e?.message || '');
async function queryWithRetry(sql, attempts = 6) {
for (let i = 1; ; i++) {
try { return await pool.query(sql); }
catch (e) {
if (i >= attempts || !isTransient(e)) throw e;
const waitMs = Math.min(5000 * i, 20000);
console.error(`[reconcile-canary] transient db error (${e.code || 'no-code'}: ${e.message || 'empty'}) — retry ${i}/${attempts - 1} in ${waitMs / 1000}s`);
await new Promise(r => setTimeout(r, waitMs));
}
}
}
try {
const { rows: [r] } = await queryWithRetry(`
SELECT
(SELECT COALESCE(SUM(total_returned_cents),0) FROM ken_portfolios) AS rollup_returned,
(SELECT COALESCE(SUM(CASE WHEN pnl_cents>0 THEN cost_cents+pnl_cents
WHEN pnl_cents=0 AND resolved_at IS NOT NULL THEN cost_cents
ELSE 0 END),0) FROM ken_portfolio_trades) AS ledger_returned,
(SELECT COALESCE(SUM(total_invested_cents),0) FROM ken_portfolios) AS rollup_invested,
(SELECT COALESCE(SUM(cost_cents),0) FROM ken_portfolio_trades) AS ledger_invested,
(SELECT COALESCE(SUM(balance_cents),0) FROM ken_portfolios) AS balance,
(SELECT COALESCE(SUM(starting_balance_cents),0) FROM ken_portfolios) AS seed
`);
// pg returns bigint as string — coerce (the very bug this canary guards against).
const n = (v) => Number(v);
const rollupRet = n(r.rollup_returned), ledgerRet = n(r.ledger_returned);
const rollupInv = n(r.rollup_invested), ledgerInv = n(r.ledger_invested);
const balance = n(r.balance), seed = n(r.seed);
const expectedBalance = seed - rollupInv + rollupRet;
const checks = [
{ name: 'returned (rollup vs ledger)', a: rollupRet, b: ledgerRet },
{ name: 'invested (rollup vs ledger)', a: rollupInv, b: ledgerInv },
{ name: 'balance (vs seed-inv+ret)', a: balance, b: expectedBalance },
];
const fails = checks.filter(c => Math.abs(c.a - c.b) > TOL_CENTS);
const usd = (c) => '$' + (c / 100).toFixed(2);
if (fails.length === 0) {
console.log(`[reconcile-canary] OK — returned=${usd(rollupRet)} invested=${usd(rollupInv)} balance=${usd(balance)} (all reconcile within $${TOL_CENTS/100})`);
process.exit(0);
}
console.error('🚨 [reconcile-canary] DIVERGENCE — a credit/settle bug is back:');
const lines = fails.map(c => `${c.name}: ${usd(c.a)} vs ${usd(c.b)} (off ${usd(Math.abs(c.a - c.b))}, ${(c.b !== 0 ? (c.a / c.b).toFixed(1) : '∞')}x)`);
for (const l of lines) console.error(' ' + l);
// Best-effort alert to CNCP parking-lot (local, no auth). Never let alerting failure mask the exit code.
try {
await fetch('http://127.0.0.1:3333/api/parking-lot', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ project: 'ken', title: 'Ken reconcile-canary DIVERGENCE (paper accounting bug back)', note: lines.join(' | ') }),
signal: AbortSignal.timeout(5000),
});
} catch { /* CNCP down — the log + exit 1 still carry the alert */ }
process.exit(1);
} catch (e) {
console.error('[reconcile-canary] error:', e.code || 'no-code', e.message || String(e));
process.exit(2);
} finally {
await pool.end();
}