fxrates is a small Go service/library for ingesting exchange rates from pluggable providers and exposing them over
HTTP / WS (REST + GraphQL).
- Runs as a server out of the box: start it and query rates via REST/GraphQL.
- Works as a library: import it and register your own providers, routes, or integrations.
- Configurable storage layer: the storage layer is completely abstracted away, so you can swap implementations without touching ingest/providers. Want to use Mongo, or something else? It should be easy.
- PostgreSQL (production)
- In-memory (tests / local dev)
Providers are pluggable fetchers (scrapers, APIs, etc.) scheduled by the ingestor/orchestrator and persisted through the storage interface.
fxrates serve sql --config ./config.yamlfxrates serve memory --config ./config.yamlBase path: /v1
All endpoints are read-only.
as_of(optional, RFC3339) - Returns the latest rate at or before this timestamp. Defaults to "now".source(optional) - Filter by data source (e.g. BCV, different banks, etc).type(optional) - Filter by rate type: MID, BUY, SELL.limit(optional) - Page size. Defaults to 100. Clamped to a max (e.g. 500).offset(optional) - Number of rows to skip. Defaults to 0.
Each rate returned looks like:
{
"as_of": "2026-01-13T04:00:00Z",
"fetched_at": "2026-01-10T15:43:04Z",
"base": "USD",
"target": "VES",
"rate_type": "MID",
"source": "BCV",
"rate": 321.1234
}Rate endpoints return:
{
"results": [
/* exchange rates */
],
"total": 123
}Errors are JSON (surprise):
{
"error": "invalid as_of (must be RFC3339 UTC)"
}Returns rates for a currency pair, as-of a point in time.
If source/type are omitted, you can get multiple results (one per (source, rate_type) bucket). Paginated via limit/offset.
Example:
curl "http://localhost:8080/v1/rates/USD/VES?as_of=2026-01-10T00:00:00Z&limit=100&offset=0"Filter by source/type:
curl "http://localhost:8080/v1/rates/USD/VES?source=BCV&type=MID"Returns rates for a base currency across targets, as-of a point in time.
- If target is not specified (it isn't part of this path), the response can include many targets.
- If source/type are omitted, you can get multiple results per target (one per (source, rate_type) bucket).
Paginated via limit/offset.
Example:
curl "http://localhost:8080/v1/rates/USD?limit=100&offset=0"Filter:
curl "http://localhost:8080/v1/rates/USD?source=BCV&type=MID"Lists distinct sources currently present in storage.
Response:
{
"results": [
"BBVA Provincial",
"BCV",
"Banco Exterior",
"Banco Nacional de Crédito BNC",
"Banco Sofitasa",
"Otras Instituciones",
"R4"
]
}Example:
curl "http://localhost:8080/v1/sources"Lists distinct currencies currently present in storage.
Response:
{ "results": ["USD", "VES", "EUR"] }Example:
curl "http://localhost:8080/v1/currencies"- Spec:
GET /openapi.yaml - UI:
GET /
GraphQL is available at:
- Endpoint:
POST /graphql - Playground:
GET /playground
rates mirrors the REST rate endpoints, with optional filters and pagination.
query {
rates(base: "USD", target: "VES", type: MID, source: "BCV", limit: 10, offset: 0) {
total
results {
as_of
fetched_at
base
target
rate_type
source
rate
}
}
}You can omit target, source, and type to get all matching buckets:
query {
rates(base: "USD", limit: 50, offset: 0) {
total
results {
target
source
rate_type
rate
as_of
}
}
}query {
sources
currencies
}