# 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](/guides/write-a-holon-yaml-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](/blog/what-a-receipt-must-say) print decimal
strings that are exact renderings of integers, and [pricing an
agent](/guides/price-an-ai-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.

Published 2026-09-23 by Holon.
