Skip to content

Search documentation

Search documentation pages, sections, and topics.

On this page

Money

Monetary amounts in Ledfra are strings like USD:42.01, never JSON numbers. Precision is exact end to end, and nothing is ever rounded through a floating-point value on the way in or out.

Why storing money in a database is its own problem

Every team building a product that handles money makes this decision early, usually in about four seconds, and usually wrong. The three common answers all fail differently:

  • A float or double. 0.1 + 0.2 is not 0.3 in binary floating point, and it never will be. Errors are tiny individually and accumulate silently across millions of rows, which is the worst possible failure mode: nothing breaks, the numbers are simply wrong by an amount that grows.
  • A string or varchar with no structure. Precision survives, but nothing else does: no validation, no currency, and every read is a parse that some caller will get wrong.
  • An integer of minor units. The right instinct, and the standard answer. But it only works if the scale is carried alongside it and never mixed up, and it quietly breaks for currencies that don't have two decimal places or that need more precision than an int4 can hold.

Ledfra makes the decision once, at the boundary, so it cannot be made differently by two services later. On the wire an amount is a string that carries its own currency. Internally it is stored as an exact integer of minor units, wide enough that large values in high-precision currencies cannot overflow it.

The amount string format

The format is <currency_code>:<amount>:

  • Currency code - three or more uppercase letters (USD, JPY, BTC).
  • Amount - a positive decimal, written the way the currency is written.
ExampleMeaning
USD:42.0142 dollars and 1 cent
JPY:1010 yen, no minor units at all
BTC:0.00000001one satoshi, eight decimal places

Because the currency travels with the amount, a value is never ambiguous in transit and a client can never accidentally apply the wrong scale to it. There is no separate currency field to forget, and no unit convention to agree on out of band.

Direction is never a sign

Amounts are always positive. Which way money moved is expressed by the entry's operation, DEBIT or CREDIT, not by a minus sign.

json
{
  "description": "Booking 1043 paid",
  "entries": [
    { "account": "assets:stripe",              "operation": "DEBIT",  "amount": "USD:100.00" },
    { "account": "income:commission",          "operation": "CREDIT", "amount": "USD:10.00"  },
    { "account": "liabilities:payables-hosts", "operation": "CREDIT", "amount": "USD:90.00"  }
  ]
}

This is deliberate, and it removes a whole class of bug. With signed amounts there are two ways to express the same movement and no way for the system to tell a deliberate negative from a sign error. With operations there is exactly one representation, and the balancing rule (debits must equal credits) can actually check it. See Transactions for why a debit increases an asset but decreases a liability.

Rules that keep amounts correct

  • The currency must belong to the ledger. Each amount is validated against the ledger's configured currency. An unknown or mismatched one is rejected with a 400 rather than coerced.
  • Respect the currency's precision. Use only as many decimal places as the currency supports: USD:42.01 is valid, USD:42.014 is not. Ledfra rejects the extra digit instead of silently rounding it away, because silent rounding is how money goes missing.
  • Amounts are positive. A negative amount is a validation error - use the opposite operation instead.
  • Never round-trip a display string. Format for humans at the edge, and send the exact value back. A number that has been through a currency formatter is no longer the truth.
One currency per ledger, today
The amount format handles fiat, crypto, and points alike, but a ledger is denominated in a single currency and each account holds one currency. Ledfra does not convert between currencies and multi-currency ledgers are not supported yet. If you need two currencies, you need two ledgers, and no report spans them.

Where to go next

  1. Transactions - how entries pair up and why every transaction must balance.
  2. Accounts & templates - what these amounts post against.
  3. Quickstart - post a real amount through the API.
  4. What is Ledfra? - the model these amounts live in.