2026-09-23 · Holon

Money in integers, never floats

Holon stores every amount as a decimal string in files and converts it to an integer number of micro-units, one millionth of a currency unit, in code. No float ever touches a balance. Every movement of money is a journal entry whose lines sum to exactly zero, and the tests check that the sum over all accounts is still zero.

One millionth, and no smaller

A price in a manifest is a string: "0.002", "0.035", "0.000500". The code parses it into a BigInt of micro-units, a millionth of a currency unit, with a regular expression that accepts at most six decimals and refuses anything else. 0.002 EUR becomes 2000n. Balances, holds, budgets, splits and royalties are all BigInt from that point on. The value only becomes a decimal string again when it is written back to a receipt or an API response.

Six decimals is not an arbitrary number. Agent prices are small: our own agents sit between 0.001 and 0.004 EUR per call, and a per-unit price can be a fraction of a cent per unit. At two decimals, a 10% platform fee on a 0.002 EUR call is unrepresentable. At six, the fee is 200n micro-units and the author's share is 1800n, exactly.

What a float would cost

The classic objection is that a double has fifteen significant digits, which is plenty for fractions of a cent. The problem is not magnitude, it is that decimal fractions are not representable in binary. 0.1 + 0.2 is famously not 0.3. The error is tiny per operation and it does not stay per operation.

Take a split. An agent priced at 0.035 EUR is a fork that owes 15% to the agent it came from. The gateway takes 10% for the platform, then the royalty off the creator's share. In micro-units the three lines are exact: 3500 to the platform, 26775 to the fork author, 4725 upstream, and they add up to 35000. The rounding rule is written down once (the fee rounds down, currency conversion rounds up) and applied to integers, so the result is the same on every machine and in every run.

Do the same in floating point and each split leaves a residue of a fraction of a micro-unit. Now run it across many calls, each with a fee, a creator share, one or two royalty hops, a hold placed at the worst case and released at the real cost, and the residues accumulate somewhere. In practice they accumulate in the one account nobody watches, and you find out during an audit that the platform is short by an amount nobody can explain. The worst part is not the money. It is that you lose the ability to say, with a straight face, that the numbers add up.

There is a second reason, less about arithmetic and more about trust. A float is a lossy representation, so two implementations of the same specification can disagree about a bill. An integer count of micro-units is a fact. When an author asks why a call paid what it paid, the answer is a subtraction anyone can redo.

How we check it

Checking money is not a matter of reading the code carefully. It is a matter of making the wrong state impossible to write down, then testing that it never appears.

In the hosted gateway, every movement is a journal entry: two or more lines, one per account, in integer micro-units, that sum to exactly zero. Balances are sums of lines, not a field someone updates. Postgres enforces three things: an entry whose lines do not sum to zero is refused at commit, journal rows can never be updated or deleted, and every entry carries a unique idempotency key such as settle:<receipt>, so the same movement cannot be recorded twice. A correction is a new entry, never an edit.

Money entering the platform comes from an external: account, today only external:demo-credit. That is the trick that makes the invariant total: because the source of funds is itself an account, the sum over every account in the system is always zero. Signing up for 5.00 EUR of demo credit is external:demo-credit at minus five and the new account at plus five.

On top of that, hosted.audit() recomputes every balance from the journal and compares it to the gateway's in-memory ledger. The tests call it after traffic. The dev gateway has its own version: a helper that sums the accounts and asserts the total is unchanged, run after failed calls, timeouts, per-unit overruns and orchestrated runs. The backup drill does it too. A gateway with traffic is copied, restored into an empty database, and must come back with the same accounts and receipts and a journal that still sums to zero.

Holds are the one thing that is not journaled. A call reserves its worst case on the payer's account before it runs, and the reservation exists only while the call is in flight. Nothing is owed to anyone yet, so there is nothing to record. If the process restarts, in-flight calls are closed as failed with gateway_restarted, never billed, and their holds are released. Any gateway-side error after funds are held releases them as well: money is never stuck.

What follows from it

Prices in a manifest are strings, so an author cannot ship 0.1 + 0.2. Mandate budgets are strings. Currency conversion happens at the mandate's rate, on integers, rounding up, so a caller is never surprised by a converted bill. Receipts print decimal strings that are exact renderings of integers, and pricing an agent is the one place an author has to think in these units, down to 0.000500 per call if that is what the work is worth.

None of this is visible when things work, which is the point. It becomes visible the day someone asks where a tenth of a cent went, and the answer is a row.

Go further

Published 2026-09-23. Figures are measured as explained in how we measure.