Money & currency
NGN only, integer kobo, no floats, ever. How amounts ride the wire, and the one rule that keeps you from being off by a kobo at scale.
Money in Acute follows one rule, no exceptions: every amount is an integer number of kobo. Not naira. Not a decimal. Not a float. An integer count of the smallest unit. Get this right once and you never reconcile a phantom kobo again.
One naira is 100 kobo. Acute represents every amount as a whole number of kobo:
| Naira | Kobo (what you send) |
|---|---|
| ₦1.00 | 100 |
| ₦5,000.00 | 500000 |
| ₦300,000.00 | 30000000 |
| ₦0.50 | 50 |
amount_in_kobo = naira × 100So baseAmount: 500000 is ₦5,000.00, and the tier1 balance cap of 30000000 is
₦300,000.00. When you display money, divide by 100 and format to two decimals at the
edge. Never carry naira-with-decimals through your logic.
Amounts cross the wire as ordinary JSON integers: a JavaScript number. No
string-encoding, no BigInt serialization games:
{
"balance": 500000,
"baseAmount": 500000,
"fee": 7500
}Kobo amounts sit comfortably inside JavaScript's safe-integer range
(Number.MAX_SAFE_INTEGER ≈ 9 × 10¹⁵ kobo, far past any naira balance you'll hold),
so a number is exact here. Treat them as integers in your own code: int /
bigint / decimal columns, never float.
Floats are how you lose money
0.1 + 0.2 !== 0.3 in every language with IEEE-754 floats. Do that a million times
across a million transactions and you're off by real money. Acute never uses floats for
amounts, and neither should you. Integer kobo end to end: the only safe way to count
money.
NGN is the only currency today. Wallets are created with currency: "NGN", balances
report "NGN", and amounts are always kobo. The currency field exists on the wire
so the model is ready for more, but for now: naira, in kobo.
Convert at the boundaries: parse user input to kobo on the way in, format kobo to naira on the way out, and keep everything in between as integers.
/** ₦ string → integer kobo. Reject anything with sub-kobo precision. */
export function toKobo(naira: number): number {
const kobo = Math.round(naira * 100);
if (Math.abs(naira * 100 - kobo) > 1e-6) {
throw new Error("amount has sub-kobo precision");
}
return kobo;
}
/** integer kobo → ₦ display string. Format at the edge only. */
export function formatNaira(kobo: number): string {
return `₦${(kobo / 100).toLocaleString("en-NG", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}`;
}
formatNaira(500000); // "₦5,000.00"
toKobo(5000); // 500000