polycratia

Most money bugs I have chased were not float bugs. They were rounding decisions nobody made.

The usual story: an amount shows up as a string from an API, a webhook, a user form. Somebody parses it into whatever numeric type is nearest to hand. Then it goes through a conversion, a fee, a split across investors, a display layer. Every one of those steps rounds, and none of them was asked to. The final number is whatever the last operation happened to leave behind, and three weeks later during reconciliation it is off by a unit nobody can explain.

So I treat inbound amounts the way I treat any hostile input.

Parse from the string, never from a float. The string is what the counterparty actually sent; a float is already a lossy reading of it.

Validate against the asset's own precision. Eighteen decimals is normal on one chain and nonsense on a card rail. If a value carries more digits than the asset can represent, you do not quietly truncate it, you reject it at the boundary. Truncating means you have silently accepted an obligation you cannot settle exactly.

And make rounding a declared policy, checked before the arithmetic runs, not a side effect you find afterwards. Half-up, floor, banker's: the specific choice matters far less than the fact that it was chosen explicitly, is visible in the code, and is the same on the ledger side as on the display side.

That is the thinking behind cryptomoney, a small open-source library I built for exactly this: precision-aware parsing, explicit rounding policy, and loud failure on anything that does not fit the asset.

The deeper point is architectural. Rounding is a business rule. Leave it implicit and it does not disappear, it gets spread across every function that touches an amount, and each one gets a vote. Ledger drift is rarely one dramatic bug. It is a hundred small unowned decisions agreeing to disagree...

When an amount crosses a boundary (chain, rail, service, currency) precision and rounding are part of its type, not a formatting concern for later.

I wrote up more of the design reasoning on the blog: https://polycratia.com/c/36032cb1

If you run multi-asset ledgers, you enforce precision somewhere: at ingestion, at the type, or at the database column. I have seen all three, and the failure modes are very different.

react

$ new-project --brief

or email hey@polycratia.com