> ## Documentation Index
> Fetch the complete documentation index at: https://arc-doc.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# ADR 0001: Money as Integer Minor Units, Enforced by Lint

> Every monetary value is a bigint count of minor units. No floats anywhere in monetary APIs, enforced by ESLint rather than left to convention.

<span className="arc-eyebrow">ADR 0001 · Accepted · Phase 0</span>

<Info>
  **Status:** Accepted · **Phase:** 0 · **Supersedes:** none · **Superseded by:** none
</Info>

***

## Context

Arc's central invariant is that every journal balances exactly: debits equal credits, with **no tolerance**. That requirement alone constrains the numeric representation, because a tolerance is not a policy anyone can defend: "how far off is acceptable?" has no answer that survives contact with a growing transaction volume.

The system also handles assets with wildly different scales:

| Asset class          | Decimals | Typical magnitude in base units |
| -------------------- | -------- | ------------------------------- |
| EUR, USD, KES, NGN   | 2        | 1e2 – 1e10                      |
| USDC, USDT           | 6        | 1e6 – 1e15                      |
| ETH and most ERC-20s | 18       | 1e18 and up                     |

An IEEE-754 double is exact only to 2^53 ≈ 9.007e15. That covers cents comfortably and covers an 18-decimal token not at all: a single ordinary balance exceeds it.

So the question was not "are floats accurate enough?" but "what representation makes the exactness requirement structural rather than aspirational?"

***

## Decision

Every monetary value is an **integer count of the currency's minor units, held as a `bigint`, paired with a currency code**.

No `number` appears in any monetary API. Rounding happens only through `divRound`, which requires an explicit mode, and the residual is always returned so it can be posted to a rounding account.

This is enforced by ESLint rather than by convention. In source, the following are errors:

```text theme={"dark"}
error  Fractional number literal. Monetary values are bigint minor units       no-restricted-syntax
error  'Math.round' is restricted — use divRound() from @arc/money             no-restricted-properties
error  Unexpected use of 'parseFloat'. Floats cannot represent money exactly   no-restricted-globals
error  toFixed() formats a float. Use Money.toDecimalString()                  no-restricted-syntax
```

<div className="arc-claim">
  A convention that depends on every future developer remembering it is not a convention: it is a countdown. Making it a build failure is what turns the decision into a property of the system.
</div>

Two files carry a scoped exception with a written justification: the seeded PRNG in `packages/chain/src/random.ts` (32-bit bit-mixing, not money) and the Jaro–Winkler implementation in `services/risk/src/sanctions.ts` (similarity thresholds, not amounts). Each disable is file-scoped with the reasoning above it, rather than a repository-wide relaxation.

***

## Consequences

### Good

* **Arithmetic is exact and associative.** `(a + b) + c === a + (b + c)` holds, which it does not for floats. Every reconciliation and batching path depends on this.
* **The balance invariant needs no epsilon.** The ledger check is `debits === credits`, full stop.
* **18-decimal assets work without special-casing.** No separate code path for chain assets versus fiat.
* **Rounding is a stated policy at each boundary** rather than an emergent property of whichever operation happened to run last.
* **Residuals stay auditable.** `divResidual` returns the exact leftover so it can be posted to a rounding account and queried later.

### Costs

<div className="arc-gap">
  * **Every amount must be converted at the system edge.** JSON carries minor units as **strings**, never numbers, because most clients parse a JSON number as a float, which would silently reintroduce the exact problem this decision exists to remove. Every API, event envelope and database boundary needs an explicit codec.
  * **Callers must choose a rounding mode explicitly.** More verbose. That verbosity *is* the point: a rounding mode chosen by default is a rounding mode nobody reviewed.
  * **Ergonomic cost.** `Money.multiply(rate)` is more typing than `amount * rate`, and every contributor has to learn the type before they can write a line of monetary code.
  * **Third-party interop.** Anything handing back a float must be converted at the edge, carefully, once.
</div>

***

## Alternatives

<AccordionGroup>
  <Accordion title="Floats: rejected" icon="xmark">
    Binary fractions cannot represent most decimal money values. `0.1 + 0.2 === 0.30000000000000004`, and each error is around 1e-17.

    Individually negligible; **structurally fatal**, because the errors accumulate exactly in the FX, fee, and batching paths: the paths that run most often and feed reconciliation. The result is a system where the trial balance is close to zero and nobody can say why it is not zero.

    The 2^53 ceiling settles it independently: an 18-decimal token balance cannot be held in a double at all.
  </Accordion>

  <Accordion title="A decimal library, or Postgres NUMERIC: viable, rejected" icon="scale-balanced">
    This is the mainstream answer and it is a good one. Arbitrary-precision decimals handle the accuracy problem correctly and are widely used in financial systems.

    Rejected for three reasons:

    1. **Integers are faster**, and the ledger's hot path is summing entries.
    2. **Integers map directly** onto both ISO-4217 minor units and on-chain base units. A decimal type requires a conversion at every chain boundary, which is a conversion that can be wrong.
    3. **It removes any question of where precision is configured.** A decimal library has a precision setting somewhere, and "somewhere" eventually means "in two places that disagree".

    The honest counterpoint: a decimal type is more ergonomic and less surprising to new contributors. That cost was accepted.
  </Accordion>

  <Accordion title="Integer cents as number rather than bigint: rejected" icon="hashtag">
    Sidesteps the fractional-representation problem while keeping familiar arithmetic, and works fine for fiat.

    Rejected on the 2^53 ceiling alone. For a chain-agnostic system settling in 6- and 18-decimal assets, this is not a close call.
  </Accordion>
</AccordionGroup>

***

## Related

<CardGroup cols={2}>
  <Card title="Why money is never a float" icon="calculator" href="/architecture/money">
    The same decision explained rather than recorded, with the rounding-residual mechanism worked through.
  </Card>

  <Card title="The cent that vanished" icon="book" href="/stories/the-cent-that-vanished">
    What this decision prevents, told as the failure it prevents.
  </Card>
</CardGroup>
