Why we freeze exchange rates

Live FX rates quietly rewrite your spending history. Here is the frozen at-the-time rate model moniya uses instead, and the worked example behind it.

The moniya team6 min read

If you build a multi-currency expense tracker, you will hit this decision within the first week: when a user spends ¥1,240 and views their history in dollars, which exchange rate do you use?

Most tools reach for the obvious answer — the current rate — and quietly ship a bug that takes users months to notice. This post is about that bug, and the model we chose instead.

The bug: your past keeps moving

Say you buy a coffee (fine, a coffee and two onigiri) at a Tokyo konbini for ¥1,240 on July 23. The USD/JPY rate that day is 150.12, so the purchase costs you $8.26. That is a fact. It happened. The money left your account and the amount it represented in dollars is fixed forever.

Now suppose your tracker converts with the current rate every time it renders your history:

When you lookUSD/JPY rateThat same coffee shows as
July 23 (purchase day)150.12$8.26
Three months later138.40$8.96
A year later161.75$7.67

The coffee's price now depends on when you ask. Your July food budget retroactively changes in November. A monthly report you exported in August disagrees with the same report regenerated in December. Your past drifts.

For someone with two foreign transactions a year, this is a rounding error. For an expat paid in one currency and spending in another, or a traveler with a three-week trip across four countries, it is hundreds of line items that never stop moving. Every question you ask your own history — did I overspend in July? what did that trip actually cost? — gets a different answer depending on today's market.

The deeper problem is conceptual: an expense is a historical event, not a live position. Your brokerage account should be marked to market. Your coffee should not. Applying live rates to spending history treats a receipt like an open FX trade, which it is not — the trade closed the moment you paid.

The model: freeze the rate at spend time

moniya's rule is simple to state:

  1. The original amount is the source of truth. The transaction stores ¥1,240, verbatim, forever. Conversion is derived data; the original is never overwritten.
  2. The exchange rate is captured once, at spend time. ¥1,240 on July 23 at 150.12 is stored as exactly that — amount, currency, rate, and the date the rate was frozen.
  3. Every future view derives from the frozen rate. Whether you open the app tomorrow or in 2031, that coffee is $8.26. Reports are reproducible. Exports don't rot.

Internally, USD is the anchor currency: every transaction carries its frozen original-to-USD rate, and any other display currency is derived through the anchor. Anchoring means we store one rate per transaction instead of one per currency pair, and cross-currency views stay consistent with each other — the same frozen fact viewed from different angles, rather than N independently drifting conversions.

On top of that sits a two-currency display toggle. You pick a primary and a secondary currency — say USD and JPY, or EUR and GBP — and flip your whole ledger between them with one tap. The toggle changes presentation only. Nothing is recalculated against today's market; you are always looking at the same frozen history through a different lens.

Daily reference rates come from the European Central Bank via the open Frankfurter API, and moniya supports 18 currencies with this model.

The worked example, end to end

Here is the full lifecycle of that konbini receipt in moniya:

  1. You snap a photo of the receipt. The AI reads the merchant (ローソン 渋谷店 — kept in the original alongside "Lawson Shibuya"), the line items, and the total: ¥1,240.
  2. The draft freezes the day's rate: 150.12 JPY per USD, stamped July 23.
  3. You approve it. The ledger now holds: original ¥1,240 · rate 150.12 · frozen 2025-07-23 · anchor value $8.26.
  4. By November, USD/JPY has moved to 138.40. Your July report still says $8.26, because July did not change.
  5. You flip the display toggle to JPY. The coffee reads ¥1,240 — the verbatim original, not a round-trip through two conversions.

Step 5 is worth underlining. A subtle failure mode in multi-currency systems is storing only the converted value and reconstructing the original later: ¥1,240 becomes $8.26 becomes ¥1,239.87 on the way back. Keeping the original amount verbatim makes the round trip exact by construction. Your ledger always matches the paper receipt.

What freezing costs you (yes, there are trade-offs)

An honest engineering post lists the downsides, so:

  • Totals mix rates. A monthly sum across a three-week trip aggregates transactions frozen at slightly different daily rates. That is correct behavior for spending history — each purchase costs what it cost that day — but it means the total is not "this month's yen spending at one clean rate." It is better than that, but it can surprise people who expect a single conversion factor.
  • Reference rates are not your card's rate. The frozen rate is the day's ECB reference rate, not the rate your card network applied, which includes its own spread and lands a day or two later. For expense tracking (as opposed to accounting reconciliation), the day-of reference rate is the right trade: it is available instantly, it is unbiased, and it is within a fraction of a percent of what your card did.
  • It is more data to store. One rate and one date per transaction. In 2026, this is not a real cost, but it is a schema commitment: the rate becomes part of the immutable record, like the amount itself.

What you get in exchange is the property that makes an expense tracker trustworthy: your history is append-only in value, not just in rows. Numbers you saw once, you will see again.

Who feels this most

If all your spending is in one currency, frozen rates are invisible — which is exactly how infrastructure should be. The model earns its keep for:

  • Expats — paid in euros, spending in pounds, sending money home in a third currency. Their entire ledger is cross-currency; live-rate drift would make every historical report meaningless.
  • Travelers — a two-week trip through Japan, Korea, and Vietnam produces dozens of transactions in three currencies. Frozen rates mean "what did this trip cost?" has one answer, permanently.
  • Anyone importing history — the model is import-ready by design: a transaction from 2024 gets 2024's rate, not today's, so migrated data lands with correct historical values instead of being repriced at the door.

The principle underneath

The design rule that produced all of this: record facts, derive views, and never let a view rewrite a fact. The fact is ¥1,240 on July 23 at 150.12. Everything else — the dollar figure, the toggle, the monthly report — is a projection of that fact, cheap to compute and impossible to corrupt.

Live rates answer "what is this worth now?" That is a great question about assets and a nonsense question about a coffee you drank in July. For spending history, the only honest rate is the one from the moment the money moved.

So we froze it.

Keep reading

Read enough. Try it.

Snap a receipt or say it out loud — moniya drafts the transaction, you approve it.

Start tracking free

No card required.

  • Original receipts kept
  • Rates frozen at spend time
  • CSV export anytime