Skip to main content
Version: 0.4.7

Rate Provider

Overview

The zkStables Rate Provider is the price + fee authority for EBSwap swaps. It is a small stateless HTTP service (crates/rate-provider) that answers two questions about a symbol pair: what is the rate right now (getQuote, used by a client to price a swap) and what was the canonical rate at a past instant (getQuoteAt, used by the Market Maker — and any auditor — to cross-check a submitted quote).

Everything is keyed by asset symbol (EUR, USDC, zkEUR), never by contract address. Token addresses are not canonical — they differ per chain and per deployment — so mapping an on-chain token to its symbol is the caller's job (the Market Maker knows its own tokens); the rate provider is chain-agnostic and address-free.

Quotes are not signed. Both endpoints serve from one constant HTTPS URL, and getQuoteAt lets any verifier read the canonical value directly, so transport integrity is sufficient and there is no signature to manage. The Market Maker exact-matches a client's quote against getQuoteAt — there is no slippage band on the authenticity check, because the value is canonical.

Pricing model

The rate provider tracks the USD spot price of every quotable symbol and derives a pair rate by cross:

price(symbolIn → symbolOut) = usd(symbolIn) / usd(symbolOut) // symbolOut per symbolIn

USD as the common reference (numéraire)

Every symbol is priced as USD per 1 unit of that symbol — its rate against USD. So usd("EUR") is the EUR/USD spot rate (≈ 1.08 USD per EUR), and usd("USD") = 1.0 by definition. There is no direct per-pair feed: a pair rate is always derived by dividing the two USD prices.

This single-reference design is deliberate:

  • No N² feeds. Any pair is a cross of two USD prices, so adding a symbol needs only its USD price — not a quote against every other symbol.
  • Asset classes compose. A fiat leg (EUR, from the fiat feed) and a crypto leg (EURC, from the crypto feed) are both expressed in USD, so they cross cleanly even though different feeds produce them.
  • Direction is just reciprocal. getQuote(A,B) and getQuote(B,A) are inverses of each other, and equal-symbol legs cross to 1.0.

The contract every feed must honour: usd_price(symbol) returns USD per 1 unit, normalized to that convention. FX "EUR/USD" already means USD-per-EUR and maps directly, but a feed whose upstream returns the inverse (units-per-USD) or quotes against a non-USD base must convert before returning — otherwise the cross silently inverts. This normalization is the feed's responsibility, not the cross's.

feeBps is a flat configured fee returned alongside the price; the Market Maker folds it into one leg (see Market Maker › Pricing & fee). The rate provider itself does no amount math — it only publishes { price, feeBps, timestamp }.

Symbol categories

Quotable symbols fall into three kinds, which determine how the USD price is sourced:

CategorySymbolsUSD price is
fiatUSD, EUR, GBP, PLNthe fiat's FX rate vs USD (EUR → EUR/USD; USD = 1.0)
cryptoUSDC, USDT, EURCthe token's own market price in USD
zk-stablezkUSD, zkEUR, zkGBP, zkPLN1:1 with the underlying fiatusd(zkEUR) = usd(EUR)

A zk-stable is valued as the fiat itself: zk<FIAT> pegs to <FIAT> at exactly 1.0, so getQuote(zkEUR, EUR) is 1 and getQuote(zkEUR, zkUSD) equals EUR/USD. A crypto stablecoin is not assumed equal to its peg: EURC is valued at its own market price, so if it depegs, getQuote(EUR, EURC) reflects it (the two legs come from different feeds).

Price feeds (fiat and crypto)

USD prices come from two feeds behind a shared PriceFeed trait, one per priced asset class:

  • a fiat feed — FX rates for USD, EUR, GBP, PLN; and
  • a crypto feed — market prices for the on-chain stablecoins in use (USDC, USDT, EURC), not volatile assets like BTC/ETH.

There is no zk-stable feed: for every fiat symbol F the fiat feed prices, the provider publishes a zk<F> symbol at the same USD price. So zk-stables are derived, never fetched, and the 1:1 peg is structural rather than something a feed has to report.

Keeping the two feeds separate is what lets a pair cross asset classes — e.g. EUR (fiat feed) against EURC (crypto feed) — since both legs reduce to USD. Only mock feeds ship today: static in-memory price tables for tests and local/stage runs. The real HTTP-backed fiat and crypto feed clients land in a later PR; swapping a mock for a real feed is a one-line change at startup and nothing else in the service depends on the concrete feed.

Quote history (getQuoteAt)

History is an in-memory ring buffer. On a fixed cadence the service fetches a full price snapshot and records it stamped at a bucket boundary — a multiple of the quote interval.

  • getQuote returns the latest snapshot.
  • getQuoteAt(t) returns the snapshot for the bucket containing t.

Round-trip guarantee. Whenever getQuote hands a caller a { price, feeBps, timestamp }, getQuoteAt(timestamp) returns the identical quote to that caller and to anyone else — the canonical / exact-match property the Market Maker and auditors rely on. This holds because:

  1. getQuote's timestamp is itself a bucket boundary, so getQuoteAt looks up that exact bucket; and
  2. a published bucket is immutable — once recorded it is never overwritten (first write wins), so the answer cannot drift even if the refresh cadence differs from the bucket size.

The guarantee is bounded only by retention: the buffer is trimmed to a window (which must cover the MM's freshness window, longer for the audit use case), and a lookup for an evicted bucket returns a no quote error. History is not persisted — it is lost on restart, after which getQuoteAt can only answer for buckets observed since. Reproducibility beyond the retention window (or across restarts) would require the persistent store, which is deferred.

Endpoints

Transport is JSON-RPC 2.0 over a single POST /, plus GET /api/health. Symbols are case-insensitive strings (upper-cased internally); feeBps is an integer; other integer fields are decimal strings, matching the SDK serialization. All timestamp fields are Unix time in seconds (never milliseconds) — both the getQuoteAt parameter and the timestamp in every RateQuote.

getQuote

Params { symbolIn: string, symbolOut: string } Result RateQuote = { price: string, feeBps: number, timestamp: string } — the current quote; price is symbolOut per symbolIn, timestamp is the bucket time.

getQuoteAt

Params { symbolIn: string, symbolOut: string, timestamp: string } Result RateQuote — the canonical quote for the bucket containing timestamp (equal to what getQuote returned at that instant). Errors if the bucket is outside the retained history window.

health (GET /api/health)

Result { status: "ok", symbols: string[] } — every quotable symbol (sorted).

Errors

JSON-RPC error codes: -32001 unsupported pair (a symbol is not quotable, or the two symbols are equal), -32002 no quote available (still warming up, or timestamp outside history), -32600 bad request (malformed params / unknown method), -32000 internal error.

Configuration

Read from the environment at startup (fail-fast: at least one feed must provide a symbol):

Env varDefaultMeaning
RATE_PROVIDER_FEE_BPS10Flat fee returned with every quote, in basis points.
RATE_PROVIDER_QUOTE_INTERVAL_SECS15Refresh cadence and getQuoteAt bucket size.
RATE_PROVIDER_HISTORY_RETENTION_SECS3600How long quote history is retained.
HOST / PORT0.0.0.0 / 3002Listen address.