openrate
Open exchange-rate engine

Every rate,
with its receipts.

Most rate APIs hand you a number and ask you to trust it. openrate hands you the walk it took through the currency graph, the individual source quotes behind every hop, how far apart those sources are, and a grade for the lot — built from central-bank files and free public venues, never a resold paid API.

43currencies
4open sources
48policy areas
1binary, no database
Each source keeps its own base — ECB in EUR, SARB in ZAR — as edges in one graph. ZAR → USD → AED, two hops, highlighted.
The model

A graph, not a base.

Most rate APIs pick one base currency and derive everything through it, so every number inherits that one feed's problems. openrate keeps each source's quotes in its native base — ECB in EUR, SARB in ZAR, Coinbase in USD — as edges in a currency graph. Any pair is the product of the rates along the shortest path.

You can see the walk

Open any rate and the path is drawn: each node a currency, each hop labelled with its own rate, the source that quoted it, and how old that quote is. If a number is a cross-rate, you can see exactly which two numbers it was multiplied from.

  • Direct quotes win. Breadth-first search reaches a pair by the fewest hops first, so a quoted pair always beats a triangulated one.
  • Freshest breaks ties. Among equal-length paths, the most recent edge is used.
  • Any base is free. ZAR is the default view, not a privileged position — pass ?base= for any other.
  • No single point of contamination. A bad edge only affects paths that cross it, not every pair in the set.
0.0543×3.6725=0.1995 ZAR → AED, no direct quote exists
ZAR → USD → AED, two hops, drawn — each leg multiplied, not averaged

And you can see the disagreement

Where more than one source quotes a pair directly, openrate plots them on a scale zoomed to the quotes themselves, marks the mean, and reports the standard deviation and spread in basis points. Sources disagreeing by half a percent is normal across a market close — a daily central-bank fix against a live exchange tick — and the point is that you can see it rather than average it away silently.

Two sources, 53 bps apart — shown, not smoothed over
Accuracy

Graded, and reproducible.

Every price carries a letter grade and a 0–1 confidence. The confidence is a plain product of five factors — freshness, directness, source authority, cross-source agreement and currency caveats — and each factor is returned with the rate, so you can re-derive the score instead of taking it on faith.

A≥ 0.90
Trust it

Fresh, directly quoted, and corroborated by independent sources that agree tightly.

B≥ 0.78
Good

One weak link — usually a single source, or one triangulation hop.

C≥ 0.60
Use with care

Stale, multi-hop, or a currency whose official rate is not the rate you would trade at.

D< 0.60
Flagged

Something is materially wrong with the provenance. Read the caveats before using it.

Confidence is a product, not an opinion. This is the same USD → ZAR quote shown in the API example below — every factor it was built from, multiplied out in the open.

Honest about its own limits

A lone quote, however fresh, is multiplied by 0.88 and can never reach an A — there is nothing to check it against. NGN and EGP carry a parallel-market caveat and CNY a managed-regime one, each capping confidence at 0.7, so those pairs top out at C no matter how good the feed is. Currencies that no longer exist are removed from the graph rather than graded down: HRK is not a bad rate, it is not a rate.

Live board

Sort by what you can't trust.

The whole board, every currency against the anchor, sortable by grade or by age — because the useful question is rarely "what is 1 ZAR in AED", it is "which of these numbers should I not lean on". Hop pips show at a glance which rates are quoted and which are derived. Any row opens into the full working in place.

Currency1 ZAR =GradeHopsAgeNote
ZWL0.8700 D 14h ago 1 source, thin liquidity
NGN84.19 C 1h ago parallel-market caveat
CNY0.3910 C 40m ago managed-regime caveat
AED0.1995 B 48s ago via USD, 2 hops
USD0.0543 A 28s ago direct, 2 sources agree
Central banks

Policy rates, same contract.

The same binary carries a second engine for central-bank policy rates — 48 areas from the BIS and SARB, with history. They are flat series rather than a currency graph, so they get their own store and a slower refresh, but the identical A–D grading. That grade earns its keep here: the BIS still publishes legacy pre-euro national series whose last observation is from the 1990s, and the grade plus an explicit date is what stops a 1998 number passing for today's.

Stepped sparklines — policy rates hold flat and jump at a meeting, so interpolating between them would draw moves that never happened
Self-host

One binary. No database.

The engine, the JSON API and the web UI are one Go binary with the interface embedded. Nothing to sign up for, no API key, no account — and no telemetry phoning home.

terminal
# clone & run — serves the API and the UI on :8080
$ git clone https://github.com/vul-os/openrate
$ cd openrate
$ go run ./cmd/openrate

# or install the command directly
$ go install github.com/vul-os/openrate/cmd/openrate@latest
$ openrate -base ZAR -refresh 5m

# or in Docker
$ docker build -t openrate . && docker run -p 8080:8080 openrate
  1. 01
    Nothing to configureThe default source set — ECB, SARB, Coinbase, Luno — is free and needs no keys. It works on first run.
  2. 02
    Bring your own feeds if you wantDrop an OXR, Twelve Data, Polygon or TraderMade key into .env and that source auto-enables. More overlapping sources means better corroboration, which means better grades.
  3. 03
    Hardened by defaultPer-IP rate limiting, a restrictive robots.txt, no-store on the API, and X-Forwarded-For trusted only for proxies you name.
  4. 04
    Use it as a Go libraryThe graph, the quality model and the source adapters are importable packages — the server is one consumer of them.
The surface

Seven endpoints. All read-only.

All JSON, all CORS-enabled, all rate-limited per IP. The embedded UI is not.

PathWhat it returns
GET /api/v1/convert?from=USD&to=ZAR&amount=100 — the converted amount plus the rate, its path, per-leg calculation, contributing source quotes and quality block
GET /api/v1/rates?base=ZAR — every reachable currency against a base, each carrying the same rate object
GET /api/v1/metaSource status, per-source edge counts, and the currency list
GET /api/v1/interest/rates?area=ZA&type=policy — latest policy rate per series, graded
GET /api/v1/interest/series?id=za.policy — the full observation history for one series
GET /api/v1/interest/metaAreas, the series catalogue and interest-source status
GET /healthzLiveness probe
GET /api/v1/convert?from=USD&to=ZAR&amount=100
{
  "result": 1668.77,
  "rate": {
    "rate": 16.687657,
    "hops": 1,
    "path": ["USD", "ZAR"],
    "sources": ["coinbase"],
    "quotes": [
      { "source": "coinbase", "rate": 16.687657, "age_sec": 28 },
      { "source": "sarb",     "rate": 16.777,    "age_sec": 67470 }
    ],
    "quality": {
      "grade": "B",
      "confidence": 0.89,
      "freshness": "realtime",
      "directness": "direct",
      "source_class": "exchange",
      "corroboration": { "sources": 2, "spread_bps": 53.54, "agree": false }
    }
  }
}