Currency and FX Handling
Activate this skill when the user is building multi-currency pricing or handling foreign exchange in international payments: choosing presentment versus settlement currency, understanding FX exposure and basic hedging, rounding minor units correctly for currencies like JPY, KWD and BHD, displaying amounts per locale, and storing money without floating-point loss. Triggers on "multi-currency," "presentment currency," "settlement currency," "FX markup," "exchange rate," "zero-decimal currency," "minor units," "rounding," "money type," "DCC," "hedging," "FX exposure," or "how should I store money in the database."
You are a payments engineer who has integrated local payment rails on four continents and run reconciliation for a multi-market marketplace. You have traced a month-end discrepancy to a JPY amount multiplied by 100, watched a refund lose 3 percent to two conversions, and rebuilt a pricing service after a float rounded a Kuwaiti dinar. Money is integers with a currency attached, FX is a dated fact with a source, and every conversion is a booking, not a display step.
## Key Points
2. **The number of minor units is a property of the currency, not a constant.** ISO 4217 defines exponents: JPY 0, KWD 3, EUR 2. Your money type must look the exponent up.
3. **An FX rate is a fact with a timestamp, a source, a direction and a purpose.** Store rates as you use them. A converted amount you cannot reproduce from stored inputs is an unreconcilable amount.
4. **Convert as late and as few times as possible.** Every conversion costs a spread. Presentment, settlement and payout currency choices decide how many conversions your money suffers.
5. **Rounding is a business rule you write down.** Which mode, at which step, per line or per total. Tax authorities and card schemes have opinions; your ledger must agree with theirs.
- **Half up** (`ROUND_HALF_UP`): 2.345 to 2.35. Common for consumer prices.
- **Half even** (bankers' rounding, `ROUND_HALF_EVEN`): 2.345 to 2.34, 2.355 to 2.36. Reduces bias over many roundings; used in some accounting contexts.
- **Half away from zero**: symmetric for negatives; matters for refunds and credits.
- **Cash rounding**: Switzerland rounds cash to 0.05 CHF; Canada rounds cash to 0.05 CAD; electronic payments are exact. Apply only to the cash tender.
1. Define a `Money` value type: `{ amount_minor: int64, currency: char(3) }`. Refuse construction when the exponent is unknown.
3. Forbid arithmetic between different currencies at the type level. Conversion is an explicit function that takes a rate record.
5. Keep the original presentment amount immutable on the order. Refunds, disputes and tax reporting reference it.
2. Conversion: `converted_minor = round(amount_minor * rate * 10^(exp_quote - exp_base), exp_quote)` in decimal arithmetic. Round once, at the end.
## Quick Example
```text
booked at capture: 100.00 EUR x 1.0800 = 108.00 USD (expected)
settled at clearing: 100.00 EUR x 1.0650 x (1 - 0.015) = 104.90 USD
realised FX loss: 108.00 - 104.90 = 3.10 USD (2.87%)
refund 30 days later at 1.0900 with reconversion: PSP debits 100.00 x 1.0900 = 109.00 USD
net cost of a refunded order: 109.00 - 104.90 = 4.10 USD, before any refund fee
```skilldb get international-payments-skills/currency-and-fx-handlingFull skill: 197 linesCurrency and FX Handling
You are a payments engineer who has integrated local payment rails on four continents and run reconciliation for a multi-market marketplace. You have traced a month-end discrepancy to a JPY amount multiplied by 100, watched a refund lose 3 percent to two conversions, and rebuilt a pricing service after a float rounded a Kuwaiti dinar. Money is integers with a currency attached, FX is a dated fact with a source, and every conversion is a booking, not a display step.
Core Principles
- Money is a pair: integer minor units plus an ISO 4217 code. Never store or compute money as a binary float.
0.1 + 0.2is not0.3, and a 0.5 percent FX error compounding across a marketplace is a real loss. - The number of minor units is a property of the currency, not a constant. ISO 4217 defines exponents: JPY 0, KWD 3, EUR 2. Your money type must look the exponent up.
- An FX rate is a fact with a timestamp, a source, a direction and a purpose. Store rates as you use them. A converted amount you cannot reproduce from stored inputs is an unreconcilable amount.
- Convert as late and as few times as possible. Every conversion costs a spread. Presentment, settlement and payout currency choices decide how many conversions your money suffers.
- Rounding is a business rule you write down. Which mode, at which step, per line or per total. Tax authorities and card schemes have opinions; your ledger must agree with theirs.
Frameworks
The Three Currencies of Any Transaction
| Currency | Who decides | Where it appears |
|---|---|---|
| Presentment (pricing) currency | You, shown to the customer at checkout | Order, receipt, dispute evidence |
| Transaction / scheme currency | The rail; for cards, the currency submitted to the network | Authorisation, clearing |
| Settlement (payout) currency | Your PSP contract and bank account | Settlement report, bank statement, ledger |
Card networks add a fourth: the cardholder's billing currency, converted by the issuer or network at their rate. When you present in the cardholder's currency (multi-currency pricing, MCP) the issuer converts nothing and the customer sees no surprise. When you present in your own currency the customer pays issuer FX and a markup. Dynamic Currency Conversion (DCC) is the point-of-sale variant where the acquirer offers conversion at its rate; it is regulated and must be disclosed, and in the EU the markup over the ECB reference rate must be shown for intra-EU card payments and credit transfers (Regulation (EU) 2019/518 amending 924/2009).
Like-for-like settlement means settling in the same currency you presented, into a same-currency account. It removes one conversion and lets you refund at the original amount without FX loss. Ask your PSP which currencies it can settle like-for-like and what it costs to hold them.
ISO 4217 Exponents That Bite
| Exponent | Currencies (examples) | Notes |
|---|---|---|
| 0 | JPY, KRW, CLP, ISK, VND, UGX, PYG, XAF, XOF, XPF, RWF, GNF, DJF, KMF, BIF | Amount in minor units equals amount in major units |
| 3 | KWD, BHD, OMR, JOD, TND, LYD, IQD | 1.000 KWD = 1000 fils; a 2-decimal assumption loses a digit |
| 4 | CLF, UYW | Units of account; rare in checkout |
| 2 but market prices in whole units | HUF, TWD, IDR | PSPs differ: some accept two decimals but require a whole-unit amount, some treat them as zero-decimal; read your PSP's currency table |
| Non-decimal subunits | MGA (5 iraimbilanja), MRU (5 khoums) | ISO lists exponent 2; subunits are not decimal, so price in whole units |
Card schemes also set per-currency rules on allowed minor units for authorisation; a JPY authorisation with a fractional amount is rejected.
Rounding Modes
- Half up (
ROUND_HALF_UP): 2.345 to 2.35. Common for consumer prices. - Half even (bankers' rounding,
ROUND_HALF_EVEN): 2.345 to 2.34, 2.355 to 2.36. Reduces bias over many roundings; used in some accounting contexts. - Half away from zero: symmetric for negatives; matters for refunds and credits.
- Cash rounding: Switzerland rounds cash to 0.05 CHF; Canada rounds cash to 0.05 CAD; electronic payments are exact. Apply only to the cash tender.
Pick one mode for pricing, one for tax (often dictated: the EU VAT invoice can round per line or per invoice total but must be consistent and the invoice total must foot), and one for FX. Document them next to the code.
Allocation Without Losing Cents
Splitting 100.00 three ways is 33.33 + 33.33 + 33.34, not 3 x 33.33. Use an allocation routine that distributes the remainder deterministically (largest remainder or first-N-shares), and never compute each share independently.
Procedures
Storing Money
- Define a
Moneyvalue type:{ amount_minor: int64, currency: char(3) }. Refuse construction when the exponent is unknown. - In SQL, store
amount_minor BIGINT NOT NULLandcurrency CHAR(3) NOT NULL. If you must use decimals,DECIMAL(19,4)with the currency code alongside, and still round to the currency's exponent at boundaries. - Forbid arithmetic between different currencies at the type level. Conversion is an explicit function that takes a rate record.
- Serialize in APIs as a string or integer minor units plus currency, never as a JSON number with decimals:
{"amount": "1290", "currency": "JPY"}or{"amount_minor": 1290, "currency": "JPY"}. Check what your PSP expects: some take minor units, some take decimal strings. - Keep the original presentment amount immutable on the order. Refunds, disputes and tax reporting reference it.
Storing and Applying FX Rates
- Rate record:
base,quote,rate(decimal with at least 8 significant digits, stored as a string or scaled integer),source(ECB reference, PSP settlement rate, treasury desk, market data vendor),as_oftimestamp,valid_from/valid_to,kind(mid, bid, ask, applied),markup_bps. - Conversion:
converted_minor = round(amount_minor * rate * 10^(exp_quote - exp_base), exp_quote)in decimal arithmetic. Round once, at the end. - Persist the rate id on every converted amount. A ledger line that says "EUR 9.13 from USD 10.00" must point at the rate that produced it.
- Distinguish pricing rates (what you charge the customer; can include your markup and can be held stable for a day) from booking rates (what actually happened at settlement, from the PSP report) and reporting rates (month-end rate for consolidation, usually a central bank or group treasury rate).
- Recompute realised FX gain/loss at settlement as
settled_amount_minor - booked_amount_minorin the settlement currency, and post it to an FX gain/loss account.
Deciding Presentment and Settlement per Market
- List each market's local currency and whether your PSP can present and settle in it like-for-like.
- For each: estimate volume, refund rate, and your ability to spend the currency locally (payouts to sellers, local costs). Spendable currency is a natural hedge; hold it. Unspendable currency should be converted on a schedule, not per transaction.
- Set a price book per currency with rounded, psychologically sensible prices (JPY 1,280 not JPY 1,283.47); refresh on a cadence (weekly or monthly), not on every rate tick, and version the price book.
- Define the refund rule: refund the original presentment amount in the presentment currency, at the original rate for your books. Confirm with your PSP whether it returns the original settlement amount or reconverts at today's rate; the difference is your FX loss on refunds and belongs in the fee model.
- Write down the exposure window per rail: cards convert at clearing (T+1 to T+2 after capture), wallets at their settlement, push rails not at all if settled like-for-like.
Worked Examples
A Money Type
from decimal import Decimal, ROUND_HALF_UP, ROUND_HALF_EVEN
from dataclasses import dataclass
EXPONENT = {"EUR": 2, "USD": 2, "SGD": 2, "INR": 2, "JPY": 0, "KRW": 0,
"KWD": 3, "BHD": 3, "OMR": 3, "JOD": 3, "TND": 3}
@dataclass(frozen=True)
class Money:
amount_minor: int
currency: str
@classmethod
def from_decimal(cls, value: Decimal, currency: str, mode=ROUND_HALF_UP) -> "Money":
exp = EXPONENT[currency] # KeyError is the correct failure
q = Decimal(1).scaleb(-exp) # 0.01, 0.001, or 1
return cls(int(value.quantize(q, rounding=mode).scaleb(exp)), currency)
def to_decimal(self) -> Decimal:
return Decimal(self.amount_minor).scaleb(-EXPONENT[self.currency])
def allocate(self, ratios: list[int]) -> list["Money"]:
total = sum(ratios)
shares = [self.amount_minor * r // total for r in ratios]
remainder = self.amount_minor - sum(shares)
for i in range(remainder): # largest-first is also acceptable; be deterministic
shares[i] += 1
return [Money(s, self.currency) for s in shares]
def convert(m: Money, rate: Decimal, quote: str, mode=ROUND_HALF_EVEN) -> Money:
return Money.from_decimal(m.to_decimal() * rate, quote, mode)
# KWD 12.345 is 12345 fils; USD conversion at 3.2500 is USD 40.12 (half-even)
print(convert(Money(12345, "KWD"), Decimal("3.2500"), "USD")) # Money(amount_minor=4012, currency='USD')
print(Money(10000, "EUR").allocate([1, 1, 1])) # 3334, 3333, 3333
FX Rate Table
CREATE TABLE fx_rate (
id BIGSERIAL PRIMARY KEY,
base CHAR(3) NOT NULL,
quote CHAR(3) NOT NULL,
rate NUMERIC(20,10) NOT NULL,
kind TEXT NOT NULL CHECK (kind IN ('mid','bid','ask','applied')),
source TEXT NOT NULL, -- 'ECB', 'PSP_SETTLEMENT', 'TREASURY'
markup_bps INTEGER NOT NULL DEFAULT 0,
as_of TIMESTAMPTZ NOT NULL,
valid_from TIMESTAMPTZ NOT NULL,
valid_to TIMESTAMPTZ,
UNIQUE (base, quote, kind, source, valid_from)
);
-- Every converted amount points at the rate that made it
ALTER TABLE ledger_line ADD COLUMN fx_rate_id BIGINT REFERENCES fx_rate(id);
Displaying Amounts
// Locale formatting from CLDR data; the symbol and grouping follow the viewer's locale,
// the exponent follows the currency.
new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" }).format(1290.5); // "1.290,50 €"
new Intl.NumberFormat("ja-JP", { style: "currency", currency: "JPY" }).format(1290); // "¥1,290"
new Intl.NumberFormat("en-KW", { style: "currency", currency: "KWD" }).format(12.345); // "KWD 12.345"
new Intl.NumberFormat("en-US", { style: "currency", currency: "CAD", currencyDisplay: "code" }).format(45); // "CAD 45.00"
Use currencyDisplay: "code" wherever $ is ambiguous (USD, CAD, AUD, SGD, HKD, NZD, MXN all use it). Never format from the float; format from the decimal built from minor units.
Exposure Arithmetic
A EUR-priced order of 100.00 captured on a USD-settling account, EURUSD 1.0800 at capture, 1.0650 at clearing two days later, PSP markup 1.5 percent:
booked at capture: 100.00 EUR x 1.0800 = 108.00 USD (expected)
settled at clearing: 100.00 EUR x 1.0650 x (1 - 0.015) = 104.90 USD
realised FX loss: 108.00 - 104.90 = 3.10 USD (2.87%)
refund 30 days later at 1.0900 with reconversion: PSP debits 100.00 x 1.0900 = 109.00 USD
net cost of a refunded order: 109.00 - 104.90 = 4.10 USD, before any refund fee
Like-for-like EUR settlement makes both lines zero and moves the decision to when you convert the EUR balance.
Checklist
- Money type with currency-aware exponent, no float anywhere in the money path, including analytics exports.
- PSP currency table reviewed for zero-decimal and three-decimal handling and for HUF/TWD/IDR quirks.
- Presentment and settlement currency decided per market; like-for-like where volume justifies it.
- Price book per currency, versioned, with refresh cadence and rounding rules documented.
- Rate records stored with source, timestamp, kind and markup; every converted amount references one.
- Refund policy states currency and rate; PSP's refund reconversion behaviour confirmed in writing.
- Realised and unrealised FX gain/loss accounts exist and are posted at settlement and month-end.
- Displays use locale formatting with explicit currency codes where symbols collide.
- Tests include JPY, KWD, HUF, negative amounts, allocation of 1 minor unit across 3 parties, and rates below 0.01.
Common Mistakes
- Multiplying every amount by 100 for the PSP, then charging JPY 129,000 for a JPY 1,290 order.
- Rounding per line and per total inconsistently so that invoices do not foot and tax reports drift by cents.
- Storing a rate as a float or with 4 decimals; INR/JPY cross rates need more precision.
- Refreshing prices on live rates, producing EUR 9.87 today and EUR 9.91 tomorrow for the same plan.
- Booking wallet or card settlements at your own reference rate instead of the rate the PSP actually applied, then carrying a permanent unexplained difference.
- Treating DCC as free revenue without meeting disclosure obligations.
- Hedging a currency you do not actually receive in net (for example, forgetting that seller payouts in BRL offset BRL collections).
Limits and When Not to Use This
This skill covers the engineering and ledger mechanics of currency handling. Hedging strategy, whether to use forwards or hold balances, and accounting treatment of FX gains under IFRS or local GAAP are decisions for a treasurer and an accountant. Regulatory disclosure rules for currency conversion (the EU cross-border payments regulation, local consumer protection rules) and any restrictions on holding or converting a currency are questions for a payments lawyer in that jurisdiction; check the current text with the European Commission, your national competent authority, or the relevant central bank. Nothing here is legal, tax or investment advice, and nothing here addresses evading capital controls or sanctions. It is not legal, tax or regulatory advice: whether your platform needs a payments, e-money or money-transmission licence in a market, and what its rules require, is a question for payments counsel in that jurisdiction.
Install this skill directly: skilldb add international-payments-skills
Related Skills
Fraud and Authentication by Market
Triggers when the user is designing authentication and fraud controls for international payments across local payment methods: Strong Customer Authentication and 3-D Secure in the EU and UK, OTP and additional-factor norms in India for cards and UPI, risk rules per rail, chargeback exposure by method including SEPA Direct Debit and iDEAL, and velocity and device signals. Keywords: "SCA," "PSD2," "3DS2," "3-D Secure," "frictionless," "challenge," "TRA exemption," "soft decline," "OTP," "RBI AFA," "UPI PIN," "chargeback," "friendly fraud," "card testing," "velocity rules," "device fingerprint," "liability shift," "VAMP," "APP fraud."
Local Payment Rails Overview
Activate this skill when the user is choosing or explaining local payment methods for international payments and needs a map of the rails: cards, bank transfers, wallets, cash vouchers and QR schemes, and how each one settles, refunds and reconciles. Triggers on "local payment methods," "international payments," "PayNow," "iDEAL," "UPI," "SEPA," "Pix," "Bizum," "Alipay," "WeChat Pay," "Konbini," "which payment methods for Singapore/Netherlands/India/Brazil/Japan," "settlement time," "push vs pull payment," "refund on bank transfer," or "payment method coverage." Covers rail families, market-by-market defaults, settlement and refund behaviour, and how to choose a method mix by product and market.
Payment Reconciliation
Activate this skill when the user must match PSP payouts, bank statements and scheme reports to orders for international payments: fee lines, refunds, chargebacks, FX differences, timing gaps, building a double-entry ledger, running exception queues and closing the month. Triggers on "reconciliation," "settlement report," "payout reconciliation," "three-way match," "camt.053," "unmatched transactions," "ledger," "exception queue," "month-end close," "chargeback accounting," "SEPA returns," "UPI settlement file," or "why doesn't the payout equal the orders."
PayNow, iDEAL, UPI and SEPA Side by Side
Triggers when the user is integrating or comparing PayNow, iDEAL, UPI and SEPA as local payment methods and needs the flows, identifiers, settlement times, refund paths, reconciliation artefacts, fee shapes and failure modes laid out side by side, plus what a checkout needs for each. Trigger keywords: "PayNow QR," "SGQR," "FAST transfer," "iDEAL redirect," "iDEAL transaction ID," "UPI intent," "VPA," "RRN," "UPI AutoPay," "SEPA Credit Transfer," "SCT Inst," "SEPA Direct Debit," "mandate," "R-transaction," "camt.053," "what does my checkout need for UPI/iDEAL," or "international payments for Singapore, the Netherlands, India and the euro area."
Payouts and Mass Payments
Activate this skill when the user is paying sellers, creators, drivers or suppliers in other countries and needs to design international payments out: KYC/KYB onboarding, choosing payout rails such as SEPA, UPI, PayNow, Pix, ACH and SWIFT, timing and cut-offs, paying in local currency versus a hub currency, fee handling, tax forms and platform reporting, and failure handling for returned or misdirected payouts. Keywords: "payouts," "mass payments," "seller payouts," "creator payments," "disbursements," "KYB," "beneficiary verification," "SWIFT OUR/SHA," "correspondent fees," "W-8BEN," "DAC7," "1099-K," "return codes," "payout failure," "negative balance."
PSP Selection and Integration
Triggers when the user is selecting, integrating or migrating a payment service provider for international payments and local payment methods such as iDEAL, PayNow or UPI: coverage and licensing by market, card and network tokenization, webhook design with idempotency, retries, sandbox limitations, PSP-to-PSP migration of tokens and mandates, and the contract terms that matter. Keywords: "PSP," "payment gateway," "acquirer," "orchestration," "tokenization," "network tokens," "webhook idempotency," "idempotency key," "sandbox," "PSP migration," "interchange++," "rolling reserve," "settlement delay," "payment aggregator," "which PSP for market X."