A customer in Berlin arrives from ChatGPT on 2 January and pays €100. You report in dollars. What was that order worth?
Converting at today's rate gives one answer and converting at January's rate gives another. The difference looks like rounding until you notice what it does to a chart: a revenue line built on today's rate moves every day, for every day in its history, whenever the euro does. Nothing about January changed, yet January's number did. This post sets out the three rules that keep multi-currency revenue stable, the precision traps underneath them, and what to do with a currency that has no rate at all.
Rule one: the rate belongs to the order
The order happened on a date, so it is converted at that date's rate. That is the whole rule, and most analytics tools agree on it: PostHog converts revenue using the exchange rate at the time of the event, and GA4 uses the prior day's exchange rate. The sources differ; the principle does not.
Traceten converts each payment to US dollars at the European Central Bank reference rate published on or before the payment's UTC date. The "on or before" matters, because the ECB publishes around 16:00 CET on working days and not on TARGET closing days, so an order on a Saturday is converted at Friday's rate.
The ECB quotes everything against the euro, so any other currency reaches dollars through the euro. The easy mistake is to invert the ratio. For yen, the dollar value of one unit is the euro-to-dollar rate divided by the euro-to-yen rate. With the euro at 1.16 dollars and 171 yen, ¥1,000 is $6.78. Get the division the wrong way round and the same order is worth $147,414, which is the kind of number nobody questions until the quarter closes.
Every revenue record then keeps its receipt: the original amount, its currency, the rate used and the date of that rate. The native amount times the rate gives the dollar figure, with no lookup, so any single number can be checked by hand. A dollar payment needs no rate at all, which means dollar revenue keeps flowing even if the rate table is empty.
Rule two: convert for display one day at a time
Storing a single currency keeps the arithmetic simple. Most teams do not want to read their revenue in it, so the dashboard shows a display currency of your choosing. This is where the second trap sits.
A monthly total in the display currency must be built as: group by date, convert each date at that date's rate, then sum. Converting the month's dollar total at one rate gives a figure that changes whenever the rate does.
The same month, converted two ways
Illustrative rates. Day by day, the month's figure is fixed once the month is over. At one rate, it moves every time the rate moves.
| Day | Revenue in USD | EUR per USD | Revenue in EUR |
|---|---|---|---|
| 1 March | $1,000 | 0.92 | €920 |
| 15 March | $1,000 | 0.90 | €900 |
| 31 March | $1,000 | 0.88 | €880 |
| March, converted day by day | $3,000 | each day's own | €2,700 |
| March, converted at today's rate | $3,000 | 0.86 | €2,580, and different tomorrow |
In Traceten this is enforced by the shape of the code rather than by care. The display conversion only accepts a series of per-day amounts, so a caller holding a single collapsed total has nothing to pass in. It costs nothing, because every revenue rollup is already kept by day. Conversion runs on the dollar amount rather than whole cents and rounds once at the end, so small errors do not accumulate across thousands of rows.
The same rule applies to refunds, in the other direction. A refund is converted at the original order's rate, not the refund day's, so refunding a euro order in full brings it back to exactly zero. The dating side of that is covered in refunds belong on the day they happen.
Not every currency has cents
The quiet bug in multi-currency code is the assumption that every amount has two decimal places.
Japanese yen, South Korean won and Icelandic krona have no minor unit. ¥1,234 is 1,234 yen, not 12.34, and a routine that divides by 100 understates it a hundredfold. Traceten keeps these exponents in one table rather than special cases scattered through the code, so the next zero-decimal currency is a one-line change.
Processors add their own wrinkles. Stripe's documentation lists Icelandic krona as a special case: it became zero-decimal, but for backward compatibility Stripe still represents it with two decimals that are always zero, so a charge of 5 krona is sent as 500. Read that with the standard exponent and the order is overstated a hundredfold. The fix is to parse each processor's amounts with that processor's rules, and to display with the standard ones.
Case is the smallest trap. Some processors send currency codes in lowercase and others in uppercase, and a database compares text exactly. Traceten normalises every code to uppercase at the boundary, before it is checked or stored.
When a currency has no rate
Traceten converts 30 currencies, pinned to the ECB's daily reference feed and including the euro itself. Some currencies merchants use are not in that feed, such as the UAE dirham, the Saudi riyal, the New Taiwan dollar and the Vietnamese dong.
Silently dropping those orders is the worst outcome, because the missing revenue leaves no trace. Writing them as zero is nearly as bad, because a zero inside a sum is indistinguishable from a real zero. So there are three layers:
- Convert the native amount when the ECB publishes a rate for it, which is the common case.
- Use the provider's own conversion when it gives one. Paddle also reports the order in the merchant's payout currency, and Lemon Squeezy in US dollars. That figure is used only when the native currency cannot be priced.
- Record the order unpriced when neither works. The order still counts as an order and keeps its native amount and currency, contributes nothing to revenue totals, and is flagged in the dashboard as an order missing a price rather than hidden inside a zero.
One failure is treated differently. If the currency is supported but our own rate table has a gap for that date, the fault is ours, not the merchant's, so the payment is refused and logged as an error for us to fix, rather than being written as zero.
A third number, on purpose
A converted figure built this way will not match your processor to the cent. Processors convert at their own rates, banks settle at theirs, and the ECB itself says its reference rates are published for information purposes only. That is fine for attribution, whose job is to compare assistants and pages on one consistent scale. For accounting, the processor's payout report remains the record. The attribution figure should agree with it closely, move for explainable reasons, and never change after the fact.
Frequently asked
01Which exchange rate should analytics use for revenue in another currency?
02Why does my multi-currency revenue total change every day?
03What are zero-decimal currencies?
04What happens to an order in a currency with no exchange rate?
Sources and further reading
- 01Euro foreign exchange reference rates, European Central Bank
- 02Currency conversion, Google Analytics Help
- 03Revenue analytics troubleshooting, PostHog Docs
- 04Supported currencies, Stripe Docs

