Why you should not use floating point for money (and what AI code does instead)
By Seaworthy · July 7, 2026
Photo by Vardan Papikyan on Unsplash
The number that broke your checkout
Open a browser console and type 0.1 + 0.2. The result comes back as 0.30000000000000004. This is not a JavaScript quirk. IEEE 754, the floating point standard, works this way in every language: Python, Java, Go, Ruby. Decimal fractions do not map cleanly onto binary representations, so the runtime stores the closest approximation it can. For most uses, the error is irrelevant. For money, it compounds.
What AI-generated payment code looks like
AI assistants reach for parseFloat almost by reflex. This kind of thing appears in generated checkout handlers constantly:
const price = parseFloat(req.body.price);
const quantity = parseInt(req.body.quantity, 10);
const total = price * quantity;
On round numbers this passes casual inspection. On fractional amounts it is silently wrong.
Take a product priced at £9.99 and a quantity of 3:
const price = parseFloat("9.99");
const qty = 3;
const total = price * qty;
console.log(total); // 29.97 — looks fine
console.log(total * 100); // 2996.9999999999995
That second line is the problem. When you convert to pence for Stripe, Math.round(total * 100) could give you 2997 or 2996 depending on which way the rounding falls. One penny off per transaction. Across thousands of orders at different prices, the errors are inconsistent and direction-dependent. Some customers overpay, some underpay. Your reconciliation is broken.
What to do instead
Integer math
Store and compute in the smallest currency unit. Pence for GBP, cents for USD. Convert to display units only at render time, never during calculation.
// Store price as 999 (representing £9.99)
const priceInPence = 999;
const qty = 3;
const totalPence = priceInPence * qty; // 2997, exact
// Convert only for display
const display = (totalPence / 100).toFixed(2); // "29.97"
Stripe's API requires this. The amount parameter is an integer in the smallest currency unit. If AI-generated code is calling stripe.paymentIntents.create({ amount: 29.97 }), that is a bug. Stripe will reject non-integer amounts or silently truncate them, depending on the version of the API call.
Decimal.js or big.js
When your business logic needs fractional intermediate values, such as tax rates, proration, or currency conversion, use a library that does decimal arithmetic rather than binary floating point.
import Decimal from "decimal.js";
const price = new Decimal("9.99");
const total = price.times(3);
console.log(total.toString()); // "29.97" — exact
Pass the string from your request body directly to new Decimal. Do not pass a float: new Decimal(9.99) starts from the already-imprecise float value and you lose precision before any calculation runs.
The currency package
For simpler scenarios where you want a convenient API around integer math, currency.js wraps the integer approach with formatting built in.
import currency from "currency.js";
const total = currency(9.99).multiply(3);
console.log(total.value); // 29.97
console.log(total.intValue); // 2997 (cents, ready for Stripe)
Database schema matters too
Do not store prices as FLOAT or DOUBLE in your database. Those types have the same representation problem. Use INTEGER and store the smallest currency unit, or use DECIMAL/NUMERIC with explicit precision. PostgreSQL's NUMERIC(10, 2) stores the decimal exactly. SQLite's REAL is a float, which means if you are using SQLite for a payments service your storage layer is also broken.
AI-generated migrations frequently use FLOAT for price columns because it seems like the obvious type. It is not. The schema is a source of error long before any arithmetic runs.
Watch the entry point
The problem starts at the boundary where user input enters your system. parseFloat on a price string is the source of every downstream rounding error. If you inherit a codebase with parseFloat in checkout handlers, treat every subsequent money calculation as suspect until you can verify otherwise. Do not trust the output just because it renders correctly in spot tests on tidy integers. The errors surface on fractional amounts under load, in aggregate reports, and in production reconciliation, not in your happy-path unit tests.
Seaworthy flags parseFloat usage in payment-related code as part of its resilience checks.
This article was generated by AI and summarises publicly available sources.
Seaworthy scans your repo for the issues covered in articles like this one.
Security gaps, exposed secrets, and misconfigurations — caught before you deploy. Free to run, no account needed.