Skip to content

Dealing with currency

Traditional accounting software often treats multiple currencies as an afterthought. Much of the workflow then feels like single-entry bookkeeping, which makes it very hard to track the flow of money, particularly when you convert from one currency to another.

So far, most examples used a single currency, such as USD. Beancount is different because every transaction is double-entry by design. Foreign cash, crypto, brokerage positions, and plain old domestic currency all show up as commodities moving between accounts, with explicit amounts and, when needed, explicit exchange rates. That makes multi-currency work feel less like a corner case and more like a natural feature. In practice it is one of the strongest reasons people choose plain-text double-entry ledgers. Beancount models and validates those flows with the same rules it uses for everyday cash, and it does so extremely well. In a world where international travel, cryptocurrency, and global interconnectedness are becoming more and more common, multi-currency bookkeeping is becoming more and more important, and Beancount is the best tool for the job. That is the superpower of Beancount.

In Beancount, that three-letter code (or another symbol you define) names a commodity. Money in different currencies is treated like different kinds of “things,” and every posting states how much of which commodity moved.

Commodity syntax in Beancount

In postings, the pattern is always a number, a space, then the commodity symbol, for example 5.00 USD, 100 EUR, or 0.25 BTC. Symbols are usually short tickers: ISO-style codes for cash (USD, JPY), broker symbols for securities (VTI, AAPL), or whatever label you choose for crypto and other instruments. Beancount does not reserve special syntax per asset class; a currency and a share certificate are both commodities with the same “amount + symbol” shape.

You can optionally declare a commodity with a commodity directive dated like any other entry. Declarations are not required for the ledger to load, but they are the right place for metadata such as a display name so reports read nicely.

2014-07-16 commodity USD
  name: "US Dollar"

2014-07-16 commodity BTC
  name: "Bitcoin"

The Beancount language syntax manual spells out the rules: the symbol is up to 24 characters, must start with a capital letter and end with a capital letter or digit, and the characters in between may be capitals, digits, or only these punctuation marks: apostrophe, period, underscore, hyphen ('._-). There must be no spaces in the symbol. A ticker with a period, such as BRK.B, is valid:

2014-07-16 commodity BRK.B
  name: "Berkshire Hathaway Class B"

If you need a label that breaks those rules, map it to a symbol you define (for example when a bank exports an odd code).

One commodity per posting

Each line in a transaction names an amount and its commodity:

2024-07-31 * "Coffee"
    Assets:Cash           -5.00 USD
    Expenses:Food          5.00 USD

The transaction balances because the same commodity appears on both sides and the numbers add to zero. If you mix commodities in one transaction without a proper conversion, Beancount will not accept the entry as balanced.

Accounts and multiple commodities

An account is not locked to one currency. Your Assets:US:Chase:Checking might hold only USD, while Assets:EU:Revolut might hold EUR, or both over time. What matters is that each transaction is internally consistent: you record what left one place, what arrived elsewhere, and at what rate when two commodities meet.

When money changes currency

Typical situations are: you spend in euros while the money leaves an account you track in dollars, or you move cash from a dollar balance to a euro balance. Either way, you write one transaction that names both currencies (two commodities) and states the rate between them so the transaction balances.

Per-unit price (@): how much of the second commodity you pay for one unit of the first.

2024-08-01 * "Convert cash while traveling"
    Assets:Cash:EUR      -100.00 EUR @ 1.10 USD
    Assets:Cash:USD       110.00 USD

Here you give up 100 EUR and receive 110 USD, as if each euro cost 1.10 USD.

Total price (@@): the total amount in the other commodity for the whole posting (useful when the statement gives you a total conversion amount).

2024-08-02 * "ATM withdrawal abroad"
    Assets:Cash:USD       -200.00 USD @@ 185.00 EUR
    Assets:Cash:EUR       185.00 EUR

You can also write cross-currency entries without @ / @@ by spelling out both legs with numbers you know are correct. The important part is that Beancount’s balancing rules are satisfied for the commodities and costs involved.

Transaction fees (very common)

Banks, card networks, brokers, and exchanges often charge a fee in the settlement currency, or they net it out of what you receive. You still use one transaction: keep the conversion posting honest to the statement, add an expense leg for the fee, and put the net into cash (or the destination account). All legs must sum to zero in Beancount’s balancing sense.

Your statement might read: sold 100 EUR, bank credited 104 USD to you, and separately shows a 3 USD wire or foreign exchange fee. At 1.07 USD per EUR, the conversion is 107 USD of value; 104 + 3 matches that.

2024-08-03 * "EUR to USD conversion, bank fee"
    Assets:Cash:EUR           -100.00 EUR @ 1.07 USD
    Expenses:Bank:Fees           3.00 USD
    Assets:Cash:USD           104.00 USD

The same pattern applies whenever a fee sits beside a currency move: card foreign-transaction fees, broker spreads on currency conversion shown as a line item, or crypto network costs paid in the coin you are sending. Split the fee into Expenses:… (or a sub-account you prefer) instead of burying it inside an incorrect conversion rate.

Prices and investments

For stocks, funds, or crypto, you often buy units of one commodity (shares, coins) using another (USD). Curly braces {… USD} record the per-unit cost in your functional currency, which helps with cost basis and later reporting:

2024-08-15 * "Buy index fund"
    Assets:US:Banks:Checking     -1_000.00 USD
    Assets:US:Investments:VTI       4 VTI {250 USD}

price directives tell Beancount how to value holdings over time for reports. They are especially useful for assets whose market prices change, not only for exchange rates between currencies.

Operating currency and tidy books

You can set an operating currency in your Beancount options so reports default to that commodity. That does not stop you from recording EUR, JPY, or anything else; it just picks a lens for summaries.

Small rounding differences sometimes appear when you copy rates from a bank. You may post immaterial imbalances to equity accounts (for example conversion or rounding adjustments). The exact pattern depends on how you structure the transaction. The goal is the same: every transaction should balance according to the rules you choose.

Practical habits

  • Pick clear commodity codes (USD, EUR) and use them consistently.
  • Match the bank statement when you enter foreign exchange: use the bank’s rate or amounts so your ledger agrees with reality.
  • Separate sub-accounts by currency when it keeps reports readable (for example Assets:Cash:USD vs Assets:Cash:EUR), even though Beancount does not require it.

Once you are comfortable with single-currency postings, the core of dealing with currency in Beancount is to treat each currency as its own commodity and to use explicit conversion postings when they interact.