Building & Shipping

The Ledger That Refuses to Lie

Brett Ridenour Brett Ridenour · Published August 2026

I opened my inbox in Raleigh yesterday morning and the Freebo weekly Sentry digest was sitting at the top. Fifteen errors, same signature, all week:

INSUFFICIENT_FUND: account "loc_xxx:clearing:stripe"
needs USD 12300, has USD 0

Not a customer bounced payment. Not a Stripe decline. The account that ran out of money is inside my own ledger.

That is the strangest error I have shipped so far, and it took me a minute to parse why my own database was telling me it did not have enough funds to move money to another one of my own accounts. So this is a short walk through what that means, why I actually like getting it, and what it says about picking a real double-entry ledger for anything financial.

Two accounts, one imaginary dollar

Freebo runs its money through Formance, an open-source ledger that owns the source of truth for every cent. Every reservation, payout, refund, and Stripe fee is written first as a Formance transaction and only then reflected into Postgres for reads. Formance is not a table of balances. It is a chain of postings — every posting has a source account and a destination account and an amount, and the postings must sum to zero.

The account named clearing:stripe in that error is a pass-through account. When a customer pays for a charter, the ledger flow looks like this:

world              --> customers:receivable   (invoice created)
world              --> clearing:stripe        (payment lands)
clearing:stripe    --> assets:bank            (base amount to operator)
clearing:stripe    --> liabilities:sales_tax  (tax withheld)
clearing:stripe    --> liabilities:platform_fee (Freebo's cut)
customers:receivable --> world                (AR cleared)

clearing:stripe never holds money for long. Every posting into it should be immediately drained by postings out of it in the same transaction. On paper the balance stays close to zero forever.

Which is exactly why “insufficient funds” is a revealing error, not a boring one. Something tried to source more money from that account than had ever been posted into it. In a normal database that would silently produce a negative number and everyone would move on with their day. Formance will not write the transaction at all.

What the fifteen errors actually mean

Every one of those errors is a real financial event that did not post. Not “posted wrong and needs cleanup.” Did not post. Formance rejected the transaction, the retry queue re-tried it a few times, and eventually Sentry recorded a permanent failure.

That is the trade-off. In exchange for a ledger that cannot lie, you get errors that block progress. If a webhook shows up and the ledger says the source account is empty, the write fails, and my Postgres reflection of the world stays behind the truth until I fix the mismatch.

For a payments platform this is the correct default. The alternatives — silently allowing a negative balance, letting the write partially apply, coercing an “impossible” transaction into the closest legal one — are all worse than a loud stop. A negative balance in a clearing account is not a small accounting curiosity. It means the model I use to describe how money moves and the actual movement of money have quietly gone out of sync, and every downstream number based on that model is now wrong.

The ledger is telling me that story loud, in a Sentry channel, before it becomes a reconciliation email six months from now.

The account name schema. Every location has its own subtree so one operator's error can't spill into another's balances

The shape of the underlying bug

I do not have the fix in this post — that is a job for when I am back at my desk with Railway open. But the shape of it is worth naming, because the ledger is telling me something useful.

Two events in the system source from clearing:stripe: the payment webhook (which drains it fully into bank and fee accounts in the same transaction), and the Stripe payout event that fires days later (which expects to move that same money from clearing:stripe into clearing:payout and out to the operator’s bank).

Those two events cannot both be right. If the payment webhook drains the clearing account down to zero the second the customer pays, the payout event has nothing to source from and every payout blows up with INSUFFICIENT_FUND. If the payment webhook is supposed to leave the money parked in the clearing account until the payout arrives, then the “drain to bank” postings are firing at the wrong stage of the money’s actual journey.

Either way, the ledger caught a mismatch between my mental model of Stripe’s timing and Stripe’s actual timing. In Postgres this would have shown up as an aging report or a slow-drifting balance number that only made sense after I stared at three CSVs. In Formance it shows up as a hard stop.

  1. Aug 8
    Payment webhook fires
    clearing:stripe credited, drained the same transaction, balance back to zero.
  2. Aug 8–11
    Stripe holds the money
    Real cash still on the Stripe balance side. Ledger doesn't know that.
  3. Aug 11
    Payout webhook fires
    Tries to source funds from clearing:stripe. Ledger says no.
  4. Aug 11–15
    Retry, retry, Sentry
    Fifteen identical failures, one per payout event that hit an empty clearing account.

The reason to like getting this error

A silent number that is wrong is worse than a loud transaction that will not write. The second one you can fix. The first one becomes a lawsuit.

— the whole point of the boring foundation

If I had built Freebo on Postgres balances — a balance_cents column on a payout row, updated on webhook receipt — this bug would still exist. The payments would still be posting into a balance the payout code assumed was zero. The difference is that Postgres would have happily written a -12300 balance and then rendered a payout summary with a negative operator earnings figure. It would look wrong to a careful human reading a report. It would look fine to code.

Formance has no UPDATE balance = balance - 12300 path. There is no update at all. There is only a posting from one account to another, and the ledger has to be able to defend the posting against the entire history of that account. If it cannot, no posting.

You could describe that as fragile. I would describe it as honest. The system will not participate in a transaction that its own arithmetic says is impossible. That is a strange thing to celebrate about a piece of infrastructure until the day it saves you.

The fifteen errors last week are the shape of that honesty. When I am back at the desk on Tuesday the fix is a few Numscript lines and a webhook re-order. Until then, no operator is being paid the wrong amount and no report is quietly telling me a lie. The clearing account is sitting on zero, refusing to move.

Which, for a ledger, is exactly the right thing to do.