Transaction currency versus base currency

Why every foreign transaction is recorded twice over, and where FX gains come from.

About 16 minutes

TheoryWhy this exists

A customer in Singapore agrees to pay you USD 1,000 for a piece of work. Your ledger is in ringgit. You have to record this, and both of the obvious approaches fail.

Record USD 1,000. Now your receivables account contains RM8,000 from local customers and USD 1,000 from this one, and the total is meaningless. You cannot add ringgit to dollars any more than you can add kilometres to litres. Every report that sums receivables produces a number that is not a quantity of anything.

Convert to ringgit and store only that. At 4.20 you record RM4,200 and throw the dollars away. Two months later the customer asks what they owe. You cannot tell them. Dividing RM4,200 by today's rate of 4.35 gives USD 965.52, which is not the agreement — they owe USD 1,000, and always did. You have destroyed the fact you most needed to keep.

Both, because they answer different questions

Neither figure is optional, so both are recorded on the same line:

  • The transaction currency amount — USD 1,000 — preserves the agreement. It is what the customer owes, what the contract says, what you will chase, and it does not change when exchange rates move.
  • The base currency amount — RM4,200 — makes the ledger summable. Every line in the ledger carries a base amount in the same unit, so totals, trial balances and statements are arithmetic on comparable quantities.

Along with these you store the rate used and the date it applied. That rate is not a lookup you can redo later; it is a historical fact about the transaction, and the next section is about why that matters.

A trial balance must be in base currency for the same reason. Its whole purpose is to prove that the sum of debits equals the sum of credits, and a sum across mixed units cannot prove anything. Multi-currency does not weaken the arithmetic guarantee; it means the guarantee is stated in one chosen unit.

Where FX gains come from

Now the interesting part. You invoiced at 4.20 and posted:

DebitCredit
Accounts receivable (USD 1,000 @ 4.20)4,200
Sales revenue4,200

Two months later the customer pays USD 1,000. The rate is now 4.35, so RM4,350 lands in your bank. Post the obvious two lines and the entry does not balance: cash up RM4,350, receivables down RM4,200, RM150 unaccounted for.

The RM150 is real — it is in your bank account. So where does it belong?

Not in revenue. The sale earned USD 1,000, exactly as agreed, exactly as delivered. Nothing about the work changed. Put the RM150 in sales and your revenue figure now moves with the currency market, and you can no longer tell whether a good month came from selling more or from the ringgit weakening. Trading performance becomes noise.

It belongs in a foreign exchange gain, which is its own income account, separate from revenue:

DebitCredit
Cash at bank4,350
Accounts receivable4,200
FX gain150

The entry balances, receivables clears to zero, and your P&L now says two distinct things: you earned RM4,200 from working, and RM150 from holding a dollar claim while the dollar rose. Those are different activities with different risk, and a business that conflates them cannot manage either.

Had the rate fallen to 4.10 you would collect RM4,100 and post a RM100 FX loss on the debit side. Same mechanism, opposite direction.

FX gains and losses are not revenue. They measure exposure to rate movement, and they belong in accounts that say so.

Realised and unrealised

The RM150 above is a realised gain. The money arrived, the rate is settled, the fact is final.

But suppose the invoice is still outstanding on 31 December and you have to produce a balance sheet. Receivables carries RM4,200 at the old rate, while the claim is worth RM4,350 at the year-end rate of 4.35. Report RM4,200 and the balance sheet understates what you are owed.

So you revalue: restate the open balance at the reporting date's rate and post the RM150 difference as an unrealised gain. The claim is still USD 1,000; only its ringgit measurement moved.

The word unrealised is doing real work. Nothing has been collected. If the rate slips back to 4.18 in January, the gain reverses, and next month's statements show a loss where this month showed a gain. This is not an error — it is an honest report of a position that genuinely fluctuates. Keeping realised and unrealised amounts in separate accounts lets a reader tell the difference between money earned and money merely measured, which is exactly the distinction a lender or an auditor will want.

Which is the theme of the whole course, one last time. The ledger stores facts. Currency means each fact needs two measurements and the rate that connected them — and once you keep all three, everything downstream still derives cleanly.

PracticeDoing it in Cynco

Your base currency is set once, when the business is set up, and does not change afterwards — every historical figure in the ledger is measured in it. For a Malaysian business, ringgit.

Invoicing in a foreign currency

When you create an invoice in Sales → Invoices, the currency is a property of the invoice. Choose USD and everything on the document — line amounts, tax, total — is in dollars, because that is what the customer agreed to and what they will read.

Underneath, the posting carries both measurements. Open Accounting → Journal and you will find the entry with the transaction amount, the rate applied, and the ringgit that entered the ledger. Bills work identically. The customer's record keeps the foreign amount, so ageing shows what they owe in their own currency, while receivables on the trial balance is in ringgit — the only unit the ledger can total.

Where rates come from

Cynco supplies a rate for the transaction date, and you can override it. Both matter.

The supplied rate is right for most transactions and saves you typing. The override exists because the market rate is sometimes not the rate that applied to you. If you bought dollars at a contracted bank rate, or a customer paid through a provider that converted at its own rate, that is the rate the transaction happened at, and substituting a mid-market figure puts a fictitious FX difference into your books.

The rule: use the rate that actually applied to the money, and when you override, say why in the narration.

Settlement and revaluation

When the customer pays, Cynco matches the payment to the invoice and posts the FX difference — the gap between the ringgit recorded at invoicing and the ringgit that arrived. It goes to an FX gain or loss account, not to revenue, and appears on Reports → Profit and loss as its own line below your trading results. Read it as the cost or benefit of being owed money in a currency you do not spend. A large FX line relative to revenue is worth a conversation about invoicing currency — a signal you would never see if the amounts were buried in sales.

Open foreign balances at a period end need restating at that date's rate, or the balance sheet reports last month's measurement of a current position. Do this before closing the period, alongside the bank reconciliation. It posts unrealised gains or losses, which can reverse next period — expected, not a mistake.

Try it yourself

  1. In Sales → Invoices, create an invoice for USD 1,000 to any customer. Note the rate Cynco applies.
  2. Issue it, then open Accounting → Journal and find the resulting entry.
  3. Look at the receivables line: the USD 1,000, the rate, and the ringgit amount that hit the ledger — three related facts on one line.
  4. Run Reports → Trial balance. The receivables figure is in ringgit, including this invoice at the rate above.

Step three is the point. Once you have seen both amounts and the rate sitting together on one posting, the rest of multi-currency accounting is bookkeeping on top of that single idea.

TechnologyHow it is built

Multi-currency is where a ledger schema either holds together or quietly starts producing figures nobody can reproduce. Almost all of it comes down to what you store on a line.

Four fields, not two

Every journal line in a multi-currency ledger carries:

ts
type Line = {
    accountId: AccountId;
    amount: Decimal;      // in the transaction currency
    currency: CurrencyCode;
    rate: Decimal;        // transaction currency -> base, as applied
    baseAmount: Decimal;  // the ringgit figure that enters the ledger
};

The field people try to remove is baseAmount, on the reasonable-sounding grounds that it is derivable from the other three. It is not derivable — it is reproducible only if you still have the exact rate and the exact rounding policy, and the point of storing it is that you should not have to trust that you do.

The deeper reason is what kind of data this is. The rate you used is a historical fact, not a current value. You converted at 4.20 on 14 October; that happened. A system that recomputes on read will, the moment the rate table is refreshed or the source changes its methodology, silently restate every historical transaction. Last year's audited revenue changes. Nothing errors, no migration runs, and there is no record of the number that used to be there.

Anything that depends on a value at a point in time must store the value, not a way to look it up again. The lookup will answer differently later.

Rounding is a policy, not a rounding

USD 1,000 at 4.20 is exactly RM4,200. Real invoices are not that tidy. USD 333.33 at 4.2015 is RM1,400.485995, which is RM1,400.49 to the sen, and the discarded fraction has to go somewhere.

Take a three-line invoice, each line USD 333.33, total USD 999.99. Convert each line and sum three rounded figures: RM4,201.47. Convert the total once — USD 999.99 at 4.2015 is RM4,201.457985 — and round: RM4,201.46. One sen apart, and neither is wrong. They answer slightly different questions.

So this cannot be left to whichever code path gets there first. If invoicing rounds per line and payment allocation rounds per total, the two disagree by amounts that are individually trivial and collectively a reconciliation problem that never resolves. And if the policy changes from one year to the next, period comparisons stop being like-for-like and nobody remembers why. Cynco treats base-currency rounding as an explicit, documented policy applied in one place — the only arrangement that stays consistent as a system grows.

The classic bug

Here is the failure everyone writes once:

ts
// Balances in USD. Does not necessarily balance in MYR.
const lines = usdLines.map((l) => ({
    ...l,
    baseAmount: round2(l.amount.times(rate)),
}));

Convert each line independently, round each one, and the base debits and credits can differ by a sen or two, because rounding is not distributive over addition. The posting engine validates balance in base currency, so the entry is rejected — and every amount looks correct to anyone reading it. Three debits of RM1,400.49 sum to RM4,201.47, while the receivable converted once from USD 999.99 is RM4,201.46, and there is nothing in the numbers to explain the sen.

Three standard fixes, all in use in real systems:

  • Convert the total, then allocate. Round once at the entry level and distribute across lines so the parts sum to the whole. The remainder goes to the largest line, where a sen distorts least.
  • Absorb the residual on the balancing line. In a two-sided entry, derive one side's base amount by subtraction rather than conversion. It balances by construction.
  • Post an explicit rounding line. Send the difference to a dedicated FX rounding account. Auditable, at the cost of a small permanent balance somewhere.

What you must not do is loosen the balance check with a tolerance. A tolerance that admits a one-sen rounding residual also admits a one-sen real error, and the invariant from module 1 stops being one.

Rate sourcing and staleness

Rates come from a feed, and feeds have properties worth designing around.

  • They go stale. A weekend, a holiday, a provider outage. Silently using Friday's rate on Tuesday is a defect; the system should know the age of the rate it applied.
  • They are ambiguous. Mid, bid and ask differ, and so do daily-close and intraday. Pick one, document it, apply it everywhere.
  • The override is the important case. A contracted bank rate is the rate that applied, so a user-supplied value must be storable and distinguishable from the feed value.
  • They must be kept. Storing the rate on the line is not redundancy with the rate table. It is the audit record of what you used, and it has to survive that table being rebuilt or deleted.

Every one is the same principle in a different costume: a financial record has to stay explicable years after the systems that produced it have moved on.