Dashboard

How to Add Multi-Currency Support to an AI-Built App

Display currency and settlement currency are not the same thing. Confuse them and your checkout looks right while your ledger quietly isn't.

Steve Jefferson
Steve Jefferson
Developer Advocate
23 September 20261 min read

How to add multi-currency support to an AI-built app comes down to one habit: keep the currency you show the customer separate from the currency you actually charge and reconcile against. Conflate them and you will ship a checkout that looks correct in the interface while being wrong in the ledger. Display and settlement have to stay separate everywhere that a number touches money, the pricing table, the checkout call, the webhook handler, and your accounting export. Get that separation right first, the rest of this is mostly discipline about how you store a number.

Display currency versus settlement currency

Display currency is what the customer sees: a price converted to their local currency for readability, usually from a rate that is minutes or hours old. Settlement currency is what you actually charge the card and what shows up in your payout, fixed at the moment of checkout and never recalculated afterward. An app based in the US, settling in USD, can still show prices in EUR, GBP, or JPY to visitors browsing from those regions, provided every one of those displayed prices is clearly an estimate and the actual charge amount is confirmed before the card is run.

Display currency

Settlement currency

Purpose

Readable price for the shopper

What is actually charged and reconciled

Rate freshness

Cached, minutes to hours old is fine

Locked at the moment of checkout

Where it lives

Pricing page, cart, email previews

Payment intent, webhook, ledger, payout

Risk if treated as the same thing

None on its own

Charged amount silently drifts from the price shown

The float-storage bug

Storing a price as a floating point number, 19.99, is the single most common mistake in this whole feature. Floating point cannot represent most decimal fractions exactly, and multiplying by an exchange rate compounds the error instead of averaging it out.

  • `price_usd = 19.99`

  • `rate = 0.9123`

  • `price_eur = price_usd * rate`

  • `# 18.234877, and the rounding differs by language, database, and even CPU`

The fix is minor-units integer storage: store 1999 as an integer, not 19.99 as a float, do every calculation in that integer minor unit, and round exactly once, at the final boundary where you display or charge the number. Never store or transmit a bare decimal price without the currency code sitting right next to it.

  • `CREATE TABLE prices (`

  • ` id bigserial primary key,`

  • ` product_id bigint not null,`

  • ` amount_minor_units bigint not null,`

  • ` currency_code char(3) not null,`

  • ` minor_unit_exponent smallint not null default 2`

  • `);`

That minor_unit_exponent column earns its place the first time you sell in Japanese yen. JPY has zero minor units, there is no fractional yen, so amount_minor_units = 1999 means 1999, not 19.99. Bahraini dinar goes the other way, three minor units instead of two. Hardcoding a divide-by-100 anywhere in the codebase will quietly overcharge or undercharge every JPY transaction by a factor of 100 the day someone enables that currency.

Converting for display without touching settlement

The simplest way to display prices in local currency is to convert at read time, for display only, using a currency conversion API pulled on a schedule. Cache the rates aggressively and stamp the displayed price with when the rate was pulled. Display accuracy to the nearest hour is completely fine for a browsing customer and does not require hitting a rate provider on every page view.

The stale FX rate risk almost nobody mentions

The risk is not that your cached display rate is stale, that is expected and harmless. The risk is reusing that same cached rate to actually convert the amount you charge. A rate cached for display an hour ago, applied to a real settlement, creates a gap between what the customer was shown and what they were charged, and on a volatile currency pair that gap is large enough to generate support tickets and chargebacks. The safest pattern: settle in as few currencies as you can, ideally one, and let your processor convert at the moment of charge using its own live rate rather than one you looked up yourself.

For accounting, always store the rate your processor actually used on that specific transaction, taken from the charge object itself, never recomputed later from a rate table. Your own historical rate table can be corrected, backfilled, or simply not go back far enough, and none of those situations should ever change what a closed transaction is recorded as having cost.

How to add multi-currency support to an AI-built app: wiring it to Stripe

Stripe multi-currency support lets you charge a customer in their currency while your account settles in its own default currency, with Stripe handling the actual conversion at the exchange rate current at the moment of the charge. Two details trip people up: the currency parameter on the PaymentIntent must be the lowercase three-letter ISO code, and the amount must already be an integer in that currency's own minor unit, not dollars, not a float, and not automatically divided by 100 for currencies like JPY that do not use one.

Rounding errors get worse, not better, once you move from a flat price to metered usage. Usage-based billing, which has the same rounding traps, needs the identical minor-units discipline applied to every individual usage event before those events are summed, since rounding each one separately and rounding the total almost never produce the same number.

The same amount-and-currency-code pairing has to survive every layer of the app, including a subscription paywall for the same kind of app, where a plan's price needs to be re-quoted correctly if a customer switches their display currency in the middle of a billing cycle rather than silently keeping the old converted number.

If currency support is one of the first payment features you are adding, the complete guide to building an app with AI is worth reading first, for how a payments layer typically slots into the rest of an AI-built app before you start worrying about exchange rates.

None of this is a reason to store raw card data yourself to work around a processor's currency limits. What can go wrong when payment details are handled carelessly explains why that trade is never worth making, no matter how much it would simplify a multi-currency edge case.

Refunds across currencies

A refund has to go back in the same currency and against the same charge it came from, at the amount that was actually settled, not a fresh conversion at today's rate. If a customer paid 1850 yen and the yen has since strengthened against your settlement currency, the refund is still 1850 yen, computed by your processor against the original charge object, never recalculated from a current rate table. Trying to reconstruct a refund amount yourself from stored rates is exactly the kind of extra logic this feature does not need, and it is the most common place a homegrown multi-currency implementation quietly loses or invents money.

It is fine, and often expected, to show the customer their refund converted back into whatever currency they were originally shown at checkout, purely as a courtesy figure in an email or order history page. Just label it clearly as an estimate and keep the authoritative refund record in the original settlement currency and amount, exactly as your processor issued it.

A currency table worth keeping on hand

Minor-unit exponents are not uniform, and assuming they are is how the float-storage bug above sneaks back in through the side door. A short reference table saves you from relearning this the hard way in production.

Currency

ISO code

Minor unit exponent

1999 minor units equals

US Dollar

USD

2

$19.99

Euro

EUR

2

19.99

Japanese Yen

JPY

0

1999

Bahraini Dinar

BHD

3

1.999

Frequently asked questions

Frequently asked questions

Should I store prices in dollars or cents?

Cents, or whatever the relevant currency's smallest unit is, stored as an integer alongside an explicit currency code and its minor-unit exponent. Never store a bare decimal amount without both of those next to it.

What currency conversion API should I use to display local prices?

Any reputable rate provider works for display purposes, since accuracy to the hour is enough. The choice matters far less than the discipline of never using that same cached rate to compute an actual charge.

Does Stripe support multi-currency prices?

Yes. Stripe can charge a customer in their own currency while settling into your account's default currency, converting at its own live rate at the moment of the charge, provided the amount you send is already an integer in that currency's minor unit.

How often should exchange rates update for a pricing page?

Hourly is generally plenty for a page a customer is browsing. The rate only needs to be materially accurate at the moment of purchase, and that final number should come from your payment processor, not your own cached display rate.

Can I let customers pay in their local currency but settle everything in USD?

Yes, and it is often the simplest setup. Show localized prices for readability, then let the processor convert the real charge into your settlement currency, so your own accounting only ever has to deal with one currency.

How did this land?

About the author

Steve Jefferson
Steve Jefferson

Developer Advocate

Steve builds something with Swarmz every week and writes up what worked, what broke, and what he'd do differently. Tutorials and hands-on guides are his lane.

Share

Get the next post in your inbox

One email a month. Product updates, engineering posts, and the best of Built with Swarmz.

I agree to receive emails about AI building tips and Swarmz product news. Unsubscribe any time.