FX Rates
How exchange rates are fetched, cached, and applied to payments.
How Rates Work
Each partner has a fiat currency (e.g. USD, BRL, COP) and a stored FX rate expressed as fiat per USDC. When a merchant creates a payment request for a fiat amount, JoPay divides the fiat amount by this rate to calculate the USDC equivalent:
usdcAmount = fiatAmount / fx_rate_fiat_per_usdcThe rate is stored on the partner record with high precision (up to 18 decimal places) to minimize rounding errors.
Rate Sources
JoPay uses two FX rate sources, with automatic fallback:
- Primary: ExchangeRate-API — the default source. JoPay calls
v6.exchangerate-api.comwith yourFX_API_KEY_EXCHANGERATE_APIto fetch USD-based rates for all supported currencies. The partner's fiat currency rate is extracted from theconversion_ratesresponse. - Fallback: Trails Token Prices — if the primary source fails (network error, bad response), JoPay falls back to Sequence Trails
GetTokenPricesto get USDC pricing data. Thefx_sourcefield records which source was used.
Refresh Mechanisms
Admin Manual Refresh
A JoPay admin can trigger a rate refresh from the admin panel by calling the POST /api/admin/fx-refresh endpoint. This updates all partners (or a specific partner if partner_id is passed). The action is rate-limited to 1 refresh per 10-minute window per partner and is logged in the admin audit trail.
Cron Refresh
A scheduled cron job can call the same endpoint on a regular interval (e.g. every 30 minutes) to keep rates fresh without manual intervention. This is the recommended approach for production environments.
Dry Run
Pass ?dry_run=1 to the refresh endpoint to see which partners would be updated and what rates would be applied, without actually writing to the database.
Caching and Staleness
Request-Time Cache
When merchants create payment requests, the FX rate lookup uses an in-memory cache with a 5-minute TTL. If the cache expires and the FX API is temporarily unreachable, JoPay serves the stale cached rate for up to 30 minutes to prevent request creation from failing during transient outages.
If the cache is older than 30 minutes and the API is still down, rate resolution returns null and the request cannot be created.
Recurring Billing Staleness
The recurring billing cron job applies a stricter rule: if the partner's fx_last_refreshed_at is older than 24 hours, the charge is rejected. This prevents recurring charges from using severely outdated rates.
Display vs. Payment
JoPay supports a concept where the display currency shown to the customer can differ from the partner's base currency. For example, a partner may operate in USD, but a merchant can set their display currency to EUR. In this case:
- The payment page shows the amount in the merchant's display currency (EUR).
- The USDC conversion uses the display currency's rate, not the partner's base rate.
- If the display currency matches the partner currency, no cross-rate calculation is needed.
Stored Fields
| Field | Description |
|---|---|
fx_rate_fiat_per_usdc | Decimal rate: how many fiat units per 1 USDC (e.g. 1.00 for USD, 4850.00 for COP). |
fx_source | Which source provided the rate: exchangerate-api or trails-token-prices. |
fx_last_refreshed_at | Timestamp of the last successful rate update. |
Frozen Rate on Requests
When a payment request is created, the FX rate at that moment is frozen onto the request record as fx_rate_fiat_per_usdc_frozen along with fx_timestamp. This ensures the USDC amount shown to the customer does not change after the request is created, even if the live rate moves.