Pricing and currency conversion
Most providers we route to publish their per-token prices in USD rather than EUR. To keep accounting and balances simple, every balance and every charge is in EUR, and we convert non-EUR provider prices to EUR once a day before they reach the catalogue.
This page explains how that conversion works, why the displayed price on a USD-billed provider isn’t quite the same as the headline USD figure, and what happens when our FX feed is unavailable.
The conversion buffer described here is the only thing added to a provider’s per-token price. LowRouter’s own fees apply at credit top-up, not per token, and are published with worked examples on the pricing page.
How the conversion is computed
For each non-EUR provider price we ingest, the stored EUR value is:
stored_eur_per_1m_tokens =
source_per_1m_tokens
× source_to_eur_rate
× (1 + fx_buffer_percent / 100)source_per_1m_tokens: the price the provider publishes in their billing currency (USD for most providers).source_to_eur_rate: the daily rate that converts the provider’s currency into EUR. The primary source is Airwallex; if Airwallex is unavailable, we fall back to the reference rates published by the European Central Bank at the eurofxref-daily.xml endpoint.fx_buffer_percent: a fixed conversion markup applied on top of the FX rate, defaulting to 1 %. The markup covers FX-spread drift between the day we fetched the rate and the day we settle with the provider, plus payment-rail conversion fees.
For example, a provider publishing $0.15 per 1M tokens at a
reference rate of 1 EUR = 0.90 USD (so 1 USD = 1.111 EUR) and a
1 % markup becomes:
0.15 × 1.111 × 1.01 ≈ 0.1683 EUR per 1M tokensThat is what your balance is debited for every 1M tokens you spend on that model.
Where you see the dual figures
Providers that already bill in EUR (e.g. /providers/scaleway)
render a single figure: their published EUR price, shown as-is with
no conversion. Providers that bill in USD (most of them: OpenAI,
Anthropic, Bedrock, Vertex, Groq, …) render both numbers:
$0.15/M (€0.17 billed)The first figure is the upstream price you’d see on the provider’s own pricing page. The second is the EUR value your balance is debited at, computed with the formula above. Hovering the price shows the FX rate, the markup, and when the conversion was last refreshed.
Long-context price bands
Some routes charge more for long prompts: gpt-5.4 past 272K tokens, or
Claude Haiku 5.5 past 100K. GET /v1/models lists these bands under
regions[].pricing.context_tiers, ordered by threshold_tokens. A band
applies to a prompt of strictly more than threshold_tokens tokens, with
cache reads and cache writes counted in the prompt. Once it applies, the
whole request is billed at the band’s prices, input, output and cache
alike, and not only the tokens over the threshold. Every price in a band
is the price you are billed above its threshold. When the provider raises
only some prices (input and output but not cache reads, for example), the
band repeats the unchanged base price, so you never have to work it out.
At or below the lowest threshold, the flat prices apply. A route without
bands has no context_tiers field.
For an auto/ model, auto_routing.pricing_range.context_tiers has one
entry for each threshold at which any candidate route changes price. Each
entry bounds what every candidate bills for a prompt above that threshold.
A candidate without a band at that length counts at the price it bills
there.
Cache prices
regions[].pricing gives a route’s cache prices where the provider
publishes them. cache_read_per_1m_tokens is the price of a cached prompt
token read back. cache_write_per_1m_tokens is the price of a prompt token
written to the cache with the default lifetime (cache_control: {"type": "ephemeral"}). cache_write_1h_per_1m_tokens is the price of a
write with "ttl": "1h". A route without a price for one of these omits
that field.
For an auto/ model, auto_routing.pricing_range bounds the same three
prices across the candidate routes. Each candidate counts at the price it
bills. A route with no cache-read price bills cache reads at its prompt
price, and a route with no cache-write price bills default-lifetime
writes at its prompt price, so those routes count at their prompt price
and widen the range. cache_write_1h_per_1m_tokens is left out when any
candidate has no 1-hour price, because such a route refuses a 1-hour
write.
Catalogue prices in USD
GET /v1/models?currency=usd (and GET /v1/models/{id}?currency=usd)
returns the same catalogue with its prices expressed in USD, for tools
that list one USD figure per model. It changes the numbers in
regions[].pricing and auto_routing.pricing_range and nothing else.
Each price is expressed per route, from the provider’s own quote:
- USD-billed providers: the upstream list price, as the provider publishes it. No conversion, so no FX buffer either.
- EUR-billed providers: the EUR price converted at the ECB monthly fixing, rounded to 4 decimals per 1M tokens, with no buffer. The fixing is the ECB reference rate on the first TARGET business day of the month. It is re-fixed mid-month only if the daily ECB rate moves more than 2% away from it, so the USD figures change about once a month rather than every day.
Cache, batch and long-context band prices follow the same rule as
prompt and completion prices. A band whose USD quote is not yet known is
left out rather than derived from the EUR price. Every entry returned with ?currency= carries an fx array
naming each conversion applied (pair, rate, date, source), and it is
empty when every price on the entry is the provider’s own USD quote.
For an auto/ model, the range is taken after each candidate route is
expressed in USD.
These USD figures are display values; your balance is still debited in EUR. For a USD-billed provider, the EUR debit is the conversion described above: the daily rate plus the conversion buffer. It is therefore not the USD figure multiplied by the monthly fixing.
Request cost in USD
Every response reports what the request cost in usage.cost, in your
billing currency (usage.currency, EUR by default). Clients that assume
usage.cost is in USD (most OpenRouter-compatible ones do) can add
?currency=usd to POST /v1/chat/completions, /v1/messages,
/v1/responses or /v1/embeddings:
"usage": {
"cost": 0.00117453,
"currency": "USD",
"billed_cost": 0.00105,
"billed_currency": "EUR",
"remaining_balance": 4.95,
"fx": [{ "pair": "EURUSD", "rate": 1.1186, "as_of": "2026-10-01", "source": "ecb", "fixing": "monthly" }]
}cost is the billed amount converted at the same monthly ECB fixing
GET /v1/models?currency=usd uses, with no further rounding: the debit
is already rounded up to the millionth.
billed_cost is the amount actually debited, and remaining_balance
stays in billed_currency. Streaming responses carry the same fields in
their final usage chunk. On /v1/messages they sit in
lowrouter_metadata, since the Messages usage object has a fixed shape.
If you are already billed in USD, or no fixing is available yet, cost
stays the billed amount with its own currency, and fx is empty. A EUR
figure is never labelled USD.
Every other USD cost figure follows the same rule: the
x-litellm-response-cost header on non-streaming responses and the
gen_ai.usage.cost tracing attribute are the billed amount at this same
fixing, so with ?currency=usd they carry the same figure as usage.cost
(the body rounds it to 7 significant digits; the header and attribute keep
full precision). They are left out while no fixing is available.
Refresh cadence
We pull FX rates once per ingest run, which runs daily. When the fallback ECB feed is in use, note that ECB publishes on TARGET business days at ~16:00 CET, and Saturday and Sunday reuse Friday’s published rate, which is the standard ECB convention.
Your debit at request time uses the most recent stored EUR value. This means the price you see in the catalogue today may differ slightly from what was billed yesterday for the same number of tokens, by the size of the FX move plus markup drift.
When the FX feed is unavailable
If we cannot reach our FX sources (both Airwallex and the ECB
fallback) during an ingest run, we soft-disable the affected
providers (the USD-billed ones that need conversion) until the feed
recovers. While soft-disabled, those providers are visible in the
catalogue but won’t accept new requests, and an internal alert
(FX_INGEST_STALE) notifies the team.
We deliberately do not fall back to a hard-coded rate. A guessed rate can silently under-charge our margin or over-charge customers, so we prefer the providers to be visibly unavailable.
What we don’t claim
- We don’t offer a rate lock. The price you see today is the price we charge today; tomorrow’s rate may differ. If you need a fixed price, the model’s EUR figure is the one we honour at the moment of the request, not a quote held in advance.
- We don’t pass through every provider price change in real time. The catalogue reflects the most recent successful ingest, which is daily. A provider price change mid-day will land in the next run.
- We don’t bill in any currency other than EUR. EUR is our single operating currency: balances, charges, the price list, and checkout are all EUR. Providers that publish in USD are converted once a day as described above, which keeps a single ledger and a single set of accounting rules.
Configuration
For self-hosted operators, the conversion markup can be tuned via the
LOWROUTER_FX_BUFFER_PERCENT environment variable (default 1.0,
clamped to [0, 20]). The change takes effect on the next ingest
run.
