Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Amount Convention — NanoMoney & Pico-Units

This is the most important convention in the billing platform. Getting this wrong would corrupt financial data.

The Rule

All internal monetary amounts are stored as i128 integers with scale=12 (pico-units). Field names always end in _nanos. Never use f64 for money. Ever.

Scale=12 Explained

1 USD = 1_000_000_000_000 (one trillion raw units)
       10^12 raw units per major currency unit

Examples:

Display amountInternal representation
$0.000
$0.0000011_000_000
$0.0110_000_000_000
$1.001_000_000_000_000
$9.999_990_000_000_000
$100.00100_000_000_000_000
$1,000,000.001_000_000_000_000_000_000

Why Not Floats?

IEEE 754 double-precision has ~15-16 significant decimal digits. Sounds sufficient — but isn’t:

>>> 0.1 + 0.2
0.30000000000000004  # NOT 0.3

In a billing system summing millions of small charges, these errors compound. $0.000001 * 1_000_000 in float arithmetic can yield $0.9999999999999999 instead of $1.00. That’s money lost.

With i128:

#![allow(unused)]
fn main() {
let per_token: i128 = 1_000_000; // $0.000001
let tokens: i128 = 1_000_000;
let total: i128 = per_token * tokens; // exactly 1_000_000_000_000 = $1.00
}

Why Not Cents (scale=2)?

Cents (i64 with scale=2) break down for AI/API pricing:

  • $0.000001 per token = 0.000001 * 100 = 0.0001 cents → rounds to zero
  • Scale=12 preserves sub-nanodollar precision needed for usage-based pricing

The NanoMoney Type

#![allow(unused)]
fn main() {
pub struct NanoMoney {
    raw: i128,    // pico-units
    currency: Currency,
}

impl NanoMoney {
    pub fn new(nanos: i128, currency: &Currency) -> Self { ... }
    pub fn to_money_round(&self) -> Money { ... }  // rounds ONCE at output
    pub fn add(&self, other: &NanoMoney) -> NanoMoney { ... }
    pub fn mul_decimal(&self, factor: &Decimal) -> NanoMoney { ... }
}
}

NanoAccumulator for Hot Paths

For real-time spend tracking (spend alerts, rate limiting), the platform uses a NanoAccumulator:

#![allow(unused)]
fn main() {
// Atomic i128 operations for concurrent accumulation
pub struct NanoAccumulator {
    inner: DashMap<CustomerId, AtomicI128>,
}
}

Redis implementation uses a Lua script for atomic compare-and-add.

Where Rounding Happens

Rounding happens exactly once: at invoice output, when converting from pico-units to display currency.

#![allow(unused)]
fn main() {
// In InvoicingService::finalize():
let display_total = NanoMoney::new(invoice.total_nanos, &invoice.currency)
    .to_money_round(); // rounds to 2 decimal places here
}

Never round intermediate calculations. The NanoMoney type does not round on arithmetic operations.

API Serialization

In API responses, _nanos fields are serialized as strings (not integers) to avoid JavaScript’s 53-bit integer limit (Number.MAX_SAFE_INTEGER = 9_007_199_254_740_991):

{
  "total_nanos": "9990000000000"
}

JavaScript clients should use BigInt to parse these values:

const totalNanos = BigInt(response.total_nanos);
const dollars = Number(totalNanos / 1_000_000_000_000n);
const cents = Number((totalNanos % 1_000_000_000_000n) / 10_000_000_000n);
const display = `$${dollars}.${String(cents).padStart(2, '0')}`;

Summary

Storage:    i128 pico-units (scale=12)
Field name: _nanos suffix
Type:       NanoMoney for arithmetic
Output:     Round ONCE via .to_money_round()
API:        Serialize as string to avoid JS BigInt overflow
NEVER:      Use f64 for money calculations