# Money & Amounts

> How monetary amounts are represented: integer minor units, an explicit ISO 4217 currency on every amount, and rounding rules that never lose a cent.

Any system that handles money, whether it charges cards, pays invoices or just keeps a balance, rests on how it writes down an amount, so that's where Money starts. Money is never a float: every amount is an integer count of the currency's smallest unit, and every amount travels with its currency. It's the one decision you can't cheaply change later, because by the time it's wrong it's wrong in every row, every API response, every report and every client that ever parsed one.

## Integer Minor Units

Amounts are integers in the currency's **minor unit**: cents for USD, pence for GBP, whole yen for JPY.

```json
{
  "amount": 1999,
  "currency": "USD"
}
```

That's $19.99. Not `19.99`, not `"19.99"`, not `1999.0`.

### Why Not Floats

Binary floating point can't represent most decimal fractions. `0.1 + 0.2` is `0.30000000000000004` in every language that uses IEEE 754 doubles, which is nearly all of them. One line item off by a billionth of a cent is invisible. A million of them summed into a settlement report is a reconciliation break nobody can explain, because every individual number "looks right". Nothing crashes; totals are just occasionally a cent off, and the cause is spread across the whole codebase.

### Why Not Decimal Strings

`"19.99"` as a string is exact, and some well-known APIs use it. It's a reasonable choice, and I still don't make it:

- **It invites the bug it avoids.** The first client that writes `parseFloat(amount)` is back to floats, and JSON makes that the path of least resistance. [RFC 8259](https://datatracker.ietf.org/doc/html/rfc8259#section-6) leaves number precision to each parser, so a bare JSON number with a decimal point has the same problem.
- **It hides the exponent.** `"1000"` could mean ¥1,000 or $1,000.00 depending on who wrote it. An integer in minor units forces everyone to look up the currency's exponent, once, in one place.
- **Integers compare exactly.** Reconciliation becomes equality, sums are exact in any language with 64-bit integers, and there's no question about trailing zeros.

## Currency Is Always Explicit

Every amount has a `currency` next to it, as an uppercase [ISO 4217](https://www.six-group.com/en/products-services/financial-information/data-standards.html) alphabetic code. There's no default currency, and no amount is ever sent alone.

```json
{
  "amount": 5000,
  "currency": "JPY",
  "fee": { "amount": 150, "currency": "JPY" }
}
```

A default currency works right up until the first non-default customer, and then every amount stored without one is ambiguous. Pairing the two also stops the other classic bug, adding amounts in different currencies: a function that receives two amounts with different currencies fails instead of returning a number that means nothing. [Fowler's Money pattern](https://martinfowler.com/eaaCatalog/money.html) is the same idea in code: amount and currency are one value, never two loose fields.

## Exponents Come From a Table

Not every currency has two decimal places, and `1000` means very different things:

| Currency | ISO 4217 exponent | `1000` means |
| -------- | ----------------- | ------------ |
| `USD` | 2 | $10.00 |
| `EUR` | 2 | €10.00 |
| `JPY` | 0 | ¥1,000 |
| `KRW` | 0 | ₩1,000 |
| `KWD` | 3 | 1.000 KWD |
| `BHD` | 3 | 1.000 BHD |

Keep the exponent table in one place in code, and convert to a display value only at the edges: UI, receipts, exports.

The table has to be per integration, not just ISO, because processors sometimes disagree with the standard. [Stripe](https://docs.stripe.com/currencies) represents Icelandic króna and Ugandan shilling as two-decimal amounts for backward compatibility, though ISO 4217 gives both an exponent of 0. Code that assumes "ISO says 0, so send whole units" charges a hundredth of the intended amount. Map internal amounts to each provider's representation at the boundary, and test the conversion for every currency you enable.

## Rounding

Rounding happens once, at a defined point, with a defined rule. Rounding at every step is how 3 × 33.33 becomes 99.99 in one place and 100.00 in another.

- **Calculate in higher precision, round once.** Fees, tax and FX are computed with an exact decimal type (never a float) and rounded to minor units only at the end.
- **Use banker's rounding (half-even)** unless a regulation or network rule says otherwise. Half-up rounds every `.5` the same way, so over millions of transactions the error accumulates in one direction; half-even sends half of them each way and the bias cancels out.
- **Allocate remainders explicitly.** Splitting $10.00 three ways is `334 + 333 + 333`, never three times `333` with a cent quietly lost. The remainder goes to a deterministic recipient, so the same split always produces the same result.

```text
split(1000, 3) = [334, 333, 333]   # sums to 1000
```

The test for any money calculation: do the parts sum exactly to the whole? If not, a cent went somewhere, and an auditor will eventually ask where.

## Storage

| Data | Column type |
| ---- | ----------- |
| Amount | `BIGINT` (minor units) |
| Currency | `CHAR(3)` |
| Intermediate rates (FX, percentages) | `NUMERIC` with explicit precision |

`BIGINT` because `INTEGER` tops out around 21 million dollars in cents, which a single large invoice or a platform's daily volume will exceed. Name columns in pairs for what they hold: `amount` and `currency`, or `fee_amount` and `fee_currency` (see [Database Naming Conventions](https://www.syntaqx.com/docs/database/naming-conventions)).

## Negative Amounts

Amounts in requests are always positive; direction comes from the operation: a refund, a payout, a fee. A `POST /refunds` with `"amount": -500` is ambiguous (refund 500, or reverse a refund?), and a sign is easy to drop or double. Signed amounts appear in exactly one place, ledger entries, where every debit and credit has to balance to zero. That ledger is where amounts go once they start moving, in [Payment States & Ledgers](https://www.syntaqx.com/docs/money/payment-states); before that, the next problem is making sure an amount moves only once, which is [Idempotency](https://www.syntaqx.com/docs/requests/idempotency).

## References

**Specifications**

- [SIX: ISO 4217 currency codes](https://www.six-group.com/en/products-services/financial-information/data-standards.html), the maintenance agency's current code and exponent lists
- [RFC 8259: The JSON Data Interchange Format](https://datatracker.ietf.org/doc/html/rfc8259), section 6 on number precision

**Reference**

- [PostgreSQL: Numeric types](https://www.postgresql.org/docs/current/datatype-numeric.html), `BIGINT` range and exact `NUMERIC`
- [The Floating-Point Guide](https://floating-point-gui.de/), why `0.1 + 0.2` isn't `0.3`

**In practice**

- [Stripe: Supported currencies](https://docs.stripe.com/currencies), minor units, zero-decimal currencies, and provider exceptions like ISK and UGX
- [Martin Fowler: Money](https://martinfowler.com/eaaCatalog/money.html), amount and currency as one value, and allocation

---

Canonical URL: https://www.syntaqx.com/docs/money/amounts
