#DevOps#独立开发

Multi-Currency Pricing and Settlement: Minor Units, Rounding Allocation, and the FX Evidence Chain

Multiplying a USD price by a live exchange rate and retaining two decimal places creates unexplained discrepancies for zero-decimal currencies, provider-specific exponents, refunds, and settlement conversion. This article versions price books, FX quotes, and rounding points, then reconciles presentment, charge, and settlement currencies.

Key Points

  • Topic Focus: DevOps、独立开发
  • Suitable for: indie hackers, full-stack engineers, AI application developers
  • Core value: quickly understand background, practical paths, common pitfalls, and engineering advice
Author @admin·Published July 26, 2026·Updated July 26, 2026

The product multiplies USD 99 by an exchange rate to display a price in yen, while the backend still submits 10543.7 JPY to the payment platform using “two decimal places.” The refund service then converts it back to USD using the current day’s exchange rate. The customer, payment platform, and general ledger ultimately end up with three amounts that are all “approximately correct.” ISO 4217 provides the foundation for currency codes and minor units, but payment platforms may use different exponents or impose acquiring restrictions for a small number of currencies. The displayed amount, charge amount, and settlement proceeds are not the same fact.

AI Pricing Migration handles price versions and their effective cohorts, while Versioned Cost Attribution for AI Gross Margin attributes final revenue and costs to operating windows. This article defines the monetary identity of a purchase from the price book through the FX quote and rounding to provider settlement, without substituting floating-point approximations for a monetary ledger.

Price Books Must Declare Currency Semantics Directly

Publish pricing.currencyPriceBook.v1, binding each market/price to a currency code, integer minor amount, ISO exponent, provider exponent override, tax-inclusive mode, effective [start,end) interval, product/billing period, maximum discount, rounding mode/scale, rules for negative amounts and refunds, provider account, and API version. Storing only base USD and a live formula makes historical checkouts impossible to reproduce and leaves products without prices during exchange-rate outages.

yaml
schema: pricing.currencyPriceBook.v1
priceBook: pro-global@11
market: JP
price: pro-monthly
presentment: {currency: JPY, amountMinor: 10544, exponent: 0}
settlementCurrency: USD
rounding: HALF_EVEN_AT_FINAL_LINE
providerCurrencyTable: adyen-currency-codes@2026-07-01
validFrom: 2026-08-01T00:00:00Z

All monetary API boundaries use (currency, integerMinor). Intermediate ratios use bounded decimals or integer fractions, with the level at which rounding occurs explicitly defined. Rounding each line first and rounding the order total first can produce different results. The contract must choose one method and use a stable largest-remainder rule to allocate residual units by line ID, guaranteeing sum(lines)=orderTotal. Stripe’s rules for zero-decimal and special currencies, as well as Adyen’s minor-unit differences, must be pinned to a provider table generation rather than treating the ISO exponent as a permanent fact.

Dynamic localized pricing uses fxQuote.v1, including source/target, rate numerator/denominator, direction, provider, observedAt, expiresAt, spread, rounding, and quote ID. A quote affects only the cart digest bound to it; once it expires, the system cannot silently replace the rate in the background and continue charging. If the product promises a fixed local price, the integer amount comes directly from the price book. Changes in the settlement exchange rate become platform profit or loss and do not rewrite the customer’s order.

Dispatch Fence and Financial Acceptance Point

The checkout state machine is PRICED -> QUOTED -> CUSTOMER_CONFIRMED -> PREPARED -> DISPATCHING -> CHARGED -> SETTLEMENT_PENDING -> SETTLED -> RECONCILED, with explicit QUOTE_EXPIRED, CHARGE_UNKNOWN, CHARGE_REJECTED, SETTLEMENT_UNKNOWN, and MISMATCH states. The customer-confirmation receipt binds the price-book/quote/cart/discount/tax roots and exact presentment amount, but confirmation alone does not authorize an external charge.

The payment ledger uses CAS to verify an unexpired quote, the customer-confirmed digest, the current checkout generation, and the PREPARED state. It writes a stable charge operation before advancing to DISPATCHING; this is only the dispatch fence, not proof that the charge has occurred. Only the dispatcher that wins the CAS may send the same amount, currency, and idempotency key to the provider. A response timeout leaves the operation in CHARGE_UNKNOWN, to be reconciled through the provider payment ID or an idempotency lookup. Never infer success from balance changes or retry the charge with a different key. Correct Use of HTTP Idempotency Keys defines the request identity boundary.

The financial acceptance point occurs only after retrieving the authoritative provider payment receipt. The acceptance function matches the provider account, payment ID, charge operation, amount, currency, and idempotency key, then atomically writes the canonical charge receipt, balanced monetary ledger entries, and outbox record before advancing to CHARGED. A permanent rejection enters CHARGE_REJECTED; an outcome that cannot be proven successful or rejected remains in CHARGE_UNKNOWN. The charge receipt fixes the presentment amount, while the balance transaction separately records the settlement currency, provider FX rate, fee, net, and availableAt. Every stage preserves multi-currency entries instead of overwriting values in a single “amount” column. A refund references the original charge and original presentment currency. A partial refund creates new legs from the original line allocation, while the actual settlement reversal is recorded separately. If the account uses a prepaid balance, the Prepaid Credits Ledger still reserves funds within its own currency batches; numbers in different currencies cannot be added directly. The AI Usage Metering Ledger provides only the billable quantity and does not determine FX.

Inject Exponent, Expiry, and Settlement Mismatches

Fault fixtures cover zero-decimal currencies such as JPY, differences between provider and ISO exponents, a 1/3 exchange rate, negative credit, three lines each with a half-unit remainder, a quote expiring between the click and CAS, concurrent confirmation from two browser tabs, a timeout after provider acceptance, delayed settlement, settlement in an unexpected currency, partial refunds, chargebacks, and price-book boundaries. Assertions require zero binary floating-point values to enter any monetary path, 100% agreement between line and total summaries for the same fixture, and zero accepted currency/exponent mismatches.

Release gates also require 100% priceBook/quote root coverage across all charges, zero difference between the line sum and order total, zero unexplained provider charge amount/currency discrepancies, a 100% balance rate for settlement/fee entries, zero blind retries from CHARGE_UNKNOWN, and at least 99.9% reconciliation completion within 24 hours of settlement proceeds arriving. Rollout begins by shadowing the existing single-currency calculation, then opens one market without automatic FX conversion, and finally expands to 1% and 10% of checkouts. During an incident, stop issuing new quotes, switch the price alias back to the previous generation, and block unconfirmed orders from dispatching. Transactions that have already been charged can only use an original-currency refund or compensation; changing exchange-rate records cannot fabricate a rollback.

Related

Related Content