# Fx Accounting Rates

> Use whenever code touches multi-currency invoicing, bookkeeping, expense reports, revenue recognition, or financial reporting — choosing which FX rate to record a foreign-currency transaction at, converting an invoice to the home currency for the books, computing realized/unrealized FX gain or loss, or running month-end revaluation of open AR/AP balances. Trigger even for a one-off "convert this invoice to USD for the books". Covers rate-of-record date conventions and audit-trail fields, not just the raw conversion math.

- Skill: `nusantara-ventures/fx-accounting-rates` (Agent Skill)
- Install (CLI): `npx skillmds@latest add nusantara-ventures/fx-accounting-rates`
- Raw SKILL.md: https://api.skillmd.com/api/skills/nusantara-ventures/fx-accounting-rates/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- License: MIT
- Author: nusantara-ventures (https://skillmd.com/u/nusantara-ventures)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/nusantara-ventures/fx-accounting-rates

---


# FX accounting rates

Converting a number is the easy part. The question that actually matters — **which rate, dated which day, from which source** — is a policy decision, not a math problem. Get the convention wrong and every downstream FX gain/loss line is wrong too, silently.

**Confirm the required rate source with your accountant or jurisdiction before treating any of this as your policy.** This skill shows common conventions and how to fetch them correctly with exchangerate.dev; it is not tax or legal advice.

## 1. Every FX transaction needs a rate-of-record AND a date convention

A foreign-currency transaction touches the books at least twice — recognition and settlement — and each convention names a different rate date (examples, confirm with your accountant):

- **Transaction/invoice-date rate** — the rate on the issue date; most common for revenue recognition.
- **Payment-date rate** — the rate on the day cash actually moved.
- **Realized FX gain/loss** — the difference between those two rates on the same amount, booked when cash settles.
- **Month-end closing rate** — for balances still open at period end, revalue at the closing rate; the delta from the booked rate is **unrealized** gain/loss, reversed next period.

The load-bearing rule isn't the definitions above (your accountant owns those) — it's that each convention pins a specific **date**, so the whole skill is really about fetching the rate for the right date from the right `source`. Pick one convention per transaction type and don't mix mid-ledger, or reconciliations stop tying out.

## 2. Fetching the rate for a specific date

```bash
curl "https://api.exchangerate.dev/v1/2024-01-15?base=EUR&symbols=USD"
```

```json
{
  "result": "success",
  "base": "EUR",
  "date": "2024-01-15",
  "source": "ecb_daily",
  "sources": { "USD": "ecb_daily" },
  "market_session": "open",
  "data_updated_at": "2024-01-15T00:00:00Z",
  "is_forward_filled": false,
  "rates": { "USD": 1.0945 },
  "derived_symbols": [],
  "notice": "Indicative rates, not for settlement. Source: incl. ECB statistics."
}
```

The `rates` object is keyed by the requested `symbols`. **Request the rate in the direction you actually need** (base = the invoice currency here) rather than fetching the inverse and dividing — see §4.

For bookkeeping, prefer `source: ecb_daily` (or `fred_daily` for currencies ECB doesn't fix, e.g. NZD) values. These are the published central-bank daily reference series — many tax authorities and accounting policies name them explicitly as an acceptable bookkeeping reference rate. `source: live` is a ~60s intraday estimate meant for dashboards and operational display, not for booking a ledger entry — see §7.

**Always check `is_forward_filled`.** An invoice dated Saturday requests a date with no published fix:

```json
{
  "date": "2024-01-13",
  "source": "ecb_daily",
  "is_forward_filled": true,
  "data_updated_at": "2024-01-12T15:05:00Z",
  "rates": { "USD": 1.09512 }
}
```

The rate returned is Friday's fix, not a Saturday rate — none exists. **Record the rate's actual date (`data_updated_at`), not the requested date**, in the ledger row. Storing this as "the 2024-01-13 rate" misstates the audit trail even though the number itself is correct policy (most policies say: weekend invoice uses prior business day's fix).

## 3. Worked example — invoice, payment, realized gain/loss

Invoice issued 2024-01-15 for €1,000, home currency USD. Request the rate directly in the direction you need — EUR base, USD symbol — rather than fetching USD/EUR and inverting:

```bash
curl "https://api.exchangerate.dev/v1/2024-01-15?base=EUR&symbols=USD"
# → rates.USD = 1.0945  (i.e. 1 EUR = 1.0945 USD that day)
```

Booked at invoice date: €1,000 × 1.0945 = **$1,094.50** (rounded to USD's 2 minor units).

Payment received 2024-02-20 at that day's rate:

```bash
curl "https://api.exchangerate.dev/v1/2024-02-20?base=EUR&symbols=USD"
# → rates.USD = 1.0802
```

Cash received: €1,000 × 1.0802 = **$1,080.20**. The realized FX loss is just the difference the two rates produce on the same foreign amount — $1,094.50 − $1,080.20 = **$14.30** — booked when cash settles (the euro weakened against the dollar between invoice and payment). The API's only job here is giving you the *right two rates* — invoice-date and payment-date, each `ecb_daily`, each requested EUR-base; the debit/credit shape is your accounting system's, not the FX layer's.

## 4. Inversion and precision rules

- **Request the rate in the direction you need.** `base=EUR&symbols=USD` gives you EUR→USD directly. Don't fetch `base=USD&symbols=EUR` and divide `1/rate` — inverting client-side and re-rounding introduces a second rounding step that a reviewer can't reproduce from the stored number alone.
- **Round once, at the end**, to the home currency's `minor_units` — pull this from `GET /v1/currencies` rather than hard-coding (USD/EUR = 2, JPY = 0, KWD/BHD = 3).
- **Store the rate at full precision** (4–5 dp for most pairs, 2–3 dp for JPY pairs) even though the resulting amount is rounded — you need full precision to reproduce the calculation later.
- **Amounts: integer minor units or fixed-point decimals, never binary floats** — float drift is a real-money bug in a ledger.

## 5. Month-end revaluation pattern

At period close, revalue every open (unsettled) AR/AP balance at the closing rate and book the unrealized difference:

```bash
curl "https://api.exchangerate.dev/v1/2024-01-31?base=EUR&symbols=USD"
```

(or `GET /v1/latest` if your policy defines "closing rate" as the rate on the actual close date/time rather than a specific historical snapshot — confirm which with your accountant).

Pattern:

1. Pull one rate per closing (one API call per period, not one per invoice — apply the same closing rate to all open balances in that currency).
2. For each open AR/AP line, compute `current_value = original_amount_fc × closing_rate` and diff against its currently booked value.
3. Book the delta as unrealized FX gain/loss; reverse it next period (either automatically on the first day of the new period, or when the item settles, whichever your policy specifies).
4. Make the job **idempotent**: key it by `(period, currency, closing_date)` and store the rate + `source` + `date` used, so re-running the same month-end job doesn't double-book if it's retried.

## 6. Audit trail — persist more than the number

Every converted amount should carry enough metadata to be reproduced and defended without re-fetching:

```json
{
  "amount_fc": 1000.00,
  "amount_hc": 1094.50,
  "rate": 1.0945,
  "source": "ecb_daily",
  "requested_date": "2024-01-15",
  "data_updated_at": "2024-01-15T00:00:00Z",
  "is_forward_filled": false
}
```

This matters because history is stable, not because you'll need it as a backup: exchangerate.dev's historical series is stable back to **1999-01-04** — re-fetching the same date returns the same fix. Reproducibility from the stored fields beats re-fetching anyway, because it survives even if the rate source ever changes providers.

## 7. What NOT to do

- **Don't book a `source: live` rate as your rate-of-record.** Live is a ~60s intraday operational estimate for dashboards and checkout displays, not a daily reference fix — it isn't designed to be reproducible the way `ecb_daily`/`fred_daily` are. This is doubly true for a weekend `live` value, which is last trading-week's consensus, not a fresh quote.
- **Don't settle or reconcile actual money movement against any indicative rate.** All exchangerate.dev rates are indicative (aggregated market data, ~5–15 bps band) — "not for settlement, regulated trading, or use as a source of record." The bank or payment processor's statement rate is the settlement truth; reconcile against that, not against an API call.
- **Don't mix rate directions mid-ledger.** If invoices book EUR→USD requested directly, don't have another process computing USD→EUR and inverting — the rounding won't match and reconciliation will show phantom variance.

## Related skills in this repo

- `exchangerate-dev` — API reference: endpoints, auth, freshness fields.
- `fx-rates-correctness` — weekend gaps, forward-fill, precision, and rounding pitfalls that apply here too.
- `multi-currency-pricing` — customer-facing price display, a different use case from books-of-record accounting.

