Store money as integer cents to stop prototype billing bugs
Store money as integer cents to avoid rounding errors, set currency rules, and prevent billing disputes as your prototype turns into a real product.

Why prototypes get money wrong
A prototype can look perfect on screen and still charge the wrong amount. The price label says $19.99, the cart shows $39.98, but the payment confirmation comes back as $39.97 or $39.99. Nobody catches it in testing, then real users do.
This usually happens because the prototype treats money like any other number. It adds, divides, and applies percentages using decimals that aren’t always represented exactly. The UI rounds one way, the backend rounds another way, and the payment provider might round differently again. Those tiny gaps turn into “Why was I charged more?” messages.
One cent sounds harmless, but it stacks up:
- Support tickets and refunds eat time
- Chargebacks bring fees and account risk
- Accounting gets messy when totals don’t match reports
- Trust drops when receipts and screens disagree
The goal is boring predictability: the same inputs should produce the same totals every time, across cart, checkout, receipt, invoices, and refunds.
You don’t need a complex finance system to get there. You need a few practical choices: store amounts safely (the simplest pattern is integer cents), decide where rounding is allowed, and agree on currency rules before you ship.
The hidden problem with decimals and floating point math
Prices look simple on screen: $9.99, $19.00, $0.50. The trouble is that many programming languages store these as floating point numbers. Floating point is built for speed, not for exact money math. Some decimal values can’t be represented perfectly in binary, so the stored value ends up slightly off.
That’s how $9.99 can quietly become something like 9.9899999997 or 9.9900000003 inside your app. You usually don’t notice it when you print a single number because formatting rounds it for display. But the tiny error is still there.
Those errors show up in normal billing work:
- Adding many line items
- Applying tax (multiply, then round)
- Applying percentage discounts
- Splitting or prorating charges
A common scenario: a cart totals $49.95 (five items at $9.99). The UI shows $49.95. Then you add 8.25% tax. If the underlying values are slightly off, the tax can round differently depending on how you calculate it (per item vs at the end). The customer sees one total, but the payment processor gets another, sometimes off by $0.01.
That mismatch is where disputes start. Users don’t care that the difference is “just rounding.” They care that the checkout screen, the receipt, and the card charge don’t match.
Decide currency and rounding rules before you write code
Many billing bugs aren’t caused by “bad math.” They happen because nobody agreed on the rules.
Start by picking a pricing currency. For early testing, one currency is usually enough. You can still display a symbol in the UI, but the product should have one official currency for stored prices and charges.
Then decide when rounding is allowed. Rounding each line item can produce a different result than rounding only at the final total, especially once discounts and taxes get involved. Either approach can work, but you need one approach everywhere.
A small set of rules prevents most disputes:
- One pricing currency for stored prices
- One rounding moment (per line item or final total)
- A fixed order of operations (for example: discounts, then tax, then fees)
- A clear promise about display vs charge (what users see should match what you send to the processor)
- One source of truth when numbers disagree (your saved totals, or the processor total)
If you later switch to storing integer cents, these rules tell you what to store, what to calculate, and what to compare against processor receipts.
The safest model: integers for amounts, currency as a code
If you want pricing to behave the same in every environment, the safest choice is simple: store money as integer minor units (cents for USD) and store the currency as a separate code.
When you save an amount as a decimal like 9.99, many systems can’t represent it exactly. Even if the UI prints “$9.99,” the stored value might be slightly above or below. That difference can change totals after taxes, discounts, or repeated calculations. If you store 999 instead, the math stays exact.
A clean storage model has two parts:
- Amount in minor units (integer)
- Currency code (like “USD” or “EUR”)
Use field names that make mistakes hard to miss:
- amount_cents (integer)
- currency (string)
- description (optional, for receipts and logs)
- created_at (for audits)
- metadata (optional, kept separate from money fields)
Keep formatted strings out of storage. “$9.99” is a display choice, not a value. Format at the UI boundary using amount + currency.
Plan for negatives early. Refunds, credits, and chargebacks are normal. A signed integer makes a refund clearly -999 in the same currency.
Step by step: implement integer-cent pricing in a prototype
Pick one base currency and write it down: what you charge in, what symbol you show, and how you round. That single decision avoids a lot of “we thought it was USD” surprises.
Then store money as integer cents everywhere that matters. In your database, keep amount_cents as an integer (999 means $9.99) and currency as a short code like USD. In your code, pass amounts around as integers too.
A simple flow that stays predictable:
- Keep price lists in cents (plan_price_cents = 999)
- Multiply and add using integers (quantity, add-ons, usage units)
- Apply discounts with integer math, using one rounding rule
- Add tax and fees using that same rule
- Save results as
subtotal_cents,tax_cents,total_cents, plus currency
Only round where your rules say rounding is allowed. A common approach is to calculate the subtotal in cents, compute tax from that subtotal, then round tax once to cents.
For auditability, log inputs (prices, quantities, discount, tax rate, currency) and outputs (subtotal_cents, tax_cents, total_cents). When a total looks wrong, those logs make the problem obvious instead of mysterious.
Taxes, discounts, and fees without rounding surprises
If you store base prices as integer cents, most problems show up later: tax, tips, fees, and percentage discounts. Those steps create fractions of a cent, so you need consistent rules.
Pick one rounding rule and use it everywhere
Choose a rounding method and stick to it. Two common choices are half-up (0.5 rounds up) and half-even (banker’s rounding). Either can be fine. Mixing them is what causes “your receipt differs from mine” arguments.
Also decide when rounding is allowed. A practical rule is: do calculations in minor units, and only round when you must convert a percentage result back into cents.
Percentage discounts: avoid the one-cent trap
A 10% discount on 999 cents is 99.9 cents. That tenth of a cent has to go somewhere, and it must go the same way every time.
A dependable sequence:
- Compute discount_cents from the original subtotal
- Round discount_cents once using your chosen method
- Subtract discount_cents from subtotal_cents
- Calculate tax from the discounted subtotal (if that matches your policy)
This avoids double-rounding, where you round along the way and then round again at the end.
Make fees explicit
Fees are easier to understand and debug when they’re separate fields, not buried in totals. Use clear names like shipping_cents, service_fee_cents, platform_fee_cents, and tip_cents. Your receipt can then mirror your database.
If you can explain every cent on the invoice, you usually avoid disputes.
Multi-currency: what to do now, what to postpone
Most prototypes don’t need true multi-currency on day one. If your users pay in one country and you settle in one currency, keep it single-currency and get the basics right. You can still show a converted estimate, but treat it as display only, not the amount you charge.
If you do support multiple currencies, every amount must be paired with a currency code (USD, EUR, GBP). The integer-minor-unit pattern still applies, but “cents” aren’t universal. Some currencies have 0 minor units (JPY) and some have 3 (KWD). So store:
- Integer amount in minor units
- Currency code
- The currency’s minor-unit precision (derived from the code)
Also accept that exchange rates aren’t reversible. Converting USD to EUR and back won’t reliably return the same integer amount. That’s normal. The mistake is pretending conversions are lossless, then arguing over the missing cent.
If you support multiple currencies, write down:
- Where rates come from
- How long a rate is valid
- What you store (charged amount, rate used, any reference conversion)
- When you convert (checkout, invoice, settlement)
- How you round
Avoid mixing currencies in one total unless you also define the conversion step.
Common mistakes that lead to billing disputes
Most billing disputes start small: one screen shows $19.99, the receipt shows $20.00, and the card charge is $19.98. Users don’t care why. They care that it feels sloppy or dishonest.
A frequent root cause is storing “pretty” values instead of raw values. If you save “$10.00” or “10.00” (already formatted), different parts of the app will re-parse it, re-round it, or assume a currency. A harmless display choice turns into wrong totals.
Another root cause is multiple places computing totals. If the cart rounds each line item, the invoice rounds the subtotal, and the payment request rounds the final total, you can get three different answers.
Patterns that often create mismatched totals:
- Computing totals in the browser and trusting them without server verification
- Generating emails or invoices from a different calculation path than checkout
- Hard-coding a currency symbol and assuming all currencies behave like USD
- Applying discount-then-tax in one place and tax-then-discount in another
- Skipping edge tests (tiny amounts, lots of lines, repeated add/remove)
Quick checks before you ship payments
Before you put real cards behind a prototype, do a money sanity pass. Most payment bugs are small inconsistencies that create big support problems.
Start with storage: amounts should be integers in the smallest unit for that currency, including refunds and credits. If you see decimals in database amount columns, treat it as a red flag.
Then check currency handling: every amount should travel with a currency code. If an API returns amount: 1999 without currency: "USD", someone will guess wrong later.
Finally, pick one owner for totals. One function or service calculates subtotal, tax, discounts, fees, and grand total. Everyone else reads the saved results. If the checkout page and the webhook handler both recompute, they will eventually disagree.
A short checklist that catches most issues:
- Integer amounts everywhere that matters (including refunds)
- Currency code present on every money value
- Totals calculated in one place, saved, and reused
- Rounding rules documented and covered by tests
- Invoice/receipt numbers match the charged amount to the cent
Test with real edge cases, not just “$1.00” examples: percentage discounts plus tax, partial refunds after discounts, fees added before vs after tax, and carts with many small items.
A realistic example: where one cent goes missing
Say you sell a $9.99 plan, offer 20% off, and charge 8.25% sales tax. The plan is billed for 3 seats on one invoice.
With integer cents, each seat is 999 cents. The disagreement comes from when you round.
Two reasonable ways to round
Method A: round each seat after the discount
Each seat discount is 20% of 999 = 199.8 cents, rounded to 200 cents. Net per seat is 999 - 200 = 799 cents. For 3 seats: 799 x 3 = 2,397 cents ($23.97). Tax: 2,397 x 0.0825 = 197.7525 cents, rounded to 198 cents. Total: 2,397 + 198 = 2,595 cents ($25.95).
Method B: round once on the subtotal
Subtotal is 999 x 3 = 2,997 cents ($29.97). Discount is 20% of 2,997 = 599.4 cents, rounded to 599 cents. Net: 2,997 - 599 = 2,398 cents ($23.98). Tax: 2,398 x 0.0825 = 197.835 cents, rounded to 198 cents. Total: 2,398 + 198 = 2,596 cents ($25.96).
Both methods can be defended. They differ by one cent. If your UI shows one method and your backend charges the other, you’ve created a dispute.
On the invoice, spell it out in plain language so the math is easy to follow. For refunds, don’t recompute. Refund the exact charged cents, or you can create a mismatch later.
Log enough to replay the decision:
- Currency code, tax rate, discount rate
- Rounding rule and where rounding happens
- Line items, quantities, and final charged cents
- Key intermediate values (before and after rounding)
- Payment processor IDs for charge and refund
Next steps: make money handling boring and reliable
The goal isn’t clever billing code. It’s totals that always match: cart, invoice, receipt, refunds, and reports.
Write your money rules down in a short shared doc: supported currency, rounding method, where rounding is allowed, and the order of operations. Then add a small set of tests that lock those behaviors in place, especially around tiny discounts, many line items, and partial refunds.
If you’re inheriting an AI-generated prototype (especially from tools like Lovable, Bolt, v0, Cursor, or Replit), a quick cleanup is worth it before real customers use it. Float math often hides in helpers, UI formatting functions, or database columns with inconsistent precision.
FixMyMess (fixmymess.ai) helps teams turn “payments mostly work” prototypes into production-ready billing flows by diagnosing the math, normalizing money storage, and tightening rounding rules so the charged amount stays consistent across every step.