Skip to main content

FX-rate resolution

When you top up your wallet in a currency other than your wallet currency, Orbit must pick one exchange rate to convert the payment at credit time. That pick is not a guess: it is the result of an ordered pipeline of rate sources, and the winning source’s identity is stamped on the transaction so finance can audit it later. This page models that pipeline end to end. The person-level walkthrough of a converted top-up lives on Wallet top-up currency conversion; this page explains the mechanism behind it.

The four sources in precedence

For every converted top-up, Orbit asks each source in turn and takes the first usable answer:
  1. Pinned rate — an explicit per-pair rate set by the deployment operator. Used to lock a rate during a launch or in a test environment. Always wins when set.
  2. Commercial provider feed — the paid Open Exchange Rates subscription, refreshed hourly. This is the same rate pipeline Orbit uses to price metered usage, so a converted top-up and your ongoing spend use the same rates and never drift against each other.
  3. Public live feed — a free public exchange-rate endpoint, read on demand with a short timeout. Any error, timeout, or malformed response simply moves resolution down the chain.
  4. Maintained reference table — a curated table built on published central-bank reference rates, updated by the Orbit team between releases. In a default deployment sources 1–3 are off, so this table resolves every converted top-up — it is the default, not a rare fallback.
Sources 1–3 resolve only when an operator has enabled them; the reference table is always available, up to its staleness ceiling. Whichever source supplied the rate is recorded on the transaction, so you never have to guess which tier applied:

The 60-day staleness ceiling

The reference table is a maintenance artifact — it carries a refresh date, and Orbit refuses to resolve a rate from it once the table is 60 days old. Inside that window the table is fine as the everyday default. Past it, an old rate may no longer reflect the market, so Orbit treats the table as unusable rather than credit money at an unreliable rate. The ceiling matters only in a deployment where none of the live sources are enabled — in that configuration the table is the last source in the chain, and when it expires the chain runs out. Deployments with the commercial feed or live fetching enabled never depend on the table, so the ceiling never gates them. For operators, the expiry is announced in advance: as the window closes, the service logs a warning and raises a monitoring alert, escalating in the final week, so the refresh lands before resolution starts failing.

Failure mode — FX_RATE_UNAVAILABLE

If every usable source returns nothing — for example the reference table is past its ceiling and no live source is enabled — Orbit fails the conversion with the FX_RATE_UNAVAILABLE error instead of guessing:
The retry split between the two top-up entry points:
  • Hosted card checkout. The completed payment is credited by the payment webhook asynchronously, so a failed conversion does not block the checkout. The credit is retried automatically until a usable rate resolves; your wallet is never credited at a bad rate.
  • API-initiated crypto top-ups (POST /balance/top-up-crypto). The endpoint returns the error to your integration, so you can treat the top-up as not-yet-complete and retry later, instead of interpreting a bad-rate credit as success.
Either way, no money moves at an unreliable rate. An operator can clear the condition permanently by enabling one of the live sources, or by pinning a rate for the affected pair.

Provenance — fx_rate_source and fx_rate_source_ref

Every converted credit and refund row carries two provenance fields on its transaction metadata:
  • fx_rate_source — which tier of the pipeline resolved the rate (env_override, oxr, live, or fallback).
  • fx_rate_source_ref — the exact resolution inside that tier: the per-pair override name for a pinned rate, the provider snapshot timestamp for the commercial feed, the feed endpoint plus its update time for the public live feed, or the reference date of the table (for example ecb_ref:2026-08-21).
Together with the applied rate itself, these freeze the full decision at credit time. The rate is stored in fixed point (a million-scaled integer) so the ledger round-trips without floating-point drift, and the audit record timestamps when the resolution ran. A refund of a converted top-up re-reads the stored rate from the original transaction row and reverses it at exactly that rate — the refund never samples today’s rate, so a credit and its later refund always reconcile to zero spread. You can inspect both fields from Billing → Transaction history in the dashboard or via GET /billing/transactions.

Relationship to the voice FX lock

Top-ups are not the only place FX resolution runs. The outbound billing gate chain holds funds at call start and settles at call end, and the rate resolved at hold time is reused at settlement so the two computations never disagree on currency. The top-up pipeline reuses the same resolution order against the same hourly-refreshed commercial snapshot, which keeps three surfaces consistent: held voice spend, settled voice spend, and converted top-ups all resolve from one rate family rather than three drifting ones.

Same-currency top-ups bypass the chain

A top-up paid in your wallet currency is credited 1:1 with no rate lookup at all. The pipeline steps are skipped, the provenance fields simply record the identity resolution, and the 60-day ceiling and FX_RATE_UNAVAILABLE failure mode cannot apply. This is why the supported top-up currencies list is deliberately small — each addition needs a reference-table entry and an auditable source of rate.

Worked example — pinned rate vs provider rate

Say a €100.00 card top-up lands on a deployment where the operator has pinned EUR → USD = 1.0000 and also configured the commercial provider feed, whose snapshot currently says 1.0789:
  • The pinned rate wins. The pipeline stops at source 1, the wallet is credited exactly $100.00, and the transaction records fx_rate_source: env_override with a ref naming the pinned-pair override. The provider feed is never consulted — an enabled pinned override outranks everything below it for that pair, every time.
  • The operator then removes the override. The next €100.00 top-up resolves at source 2 instead: it credits $107.89 and records fx_rate_source: oxr with a ref naming the provider snapshot timestamp.
The recorded fields are what make this auditable: given any converted top-up, you can tell not only the numeric rate but which tier of the pipeline produced it and the exact input it read.