Files
skills/docs/money/finance/currency-provider-open-exchange-rates/src/provider/provider.md
2026-05-29 08:20:56 +01:00

5.9 KiB

OpenExchangeRatesProvider

(no submodules)

Purpose

OpenExchangeRatesProvider is the @suaveplan/currency provider for openexchangerates.org. It wraps the /latest.json bulk-fetch endpoint, validates the upstream payload, and surfaces every failure mode through the typed CurrencyProviderError hierarchy from @suaveplan/currency-providers. The provider is universal — it relies only on globalThis.fetch (or an injected FetchLike) and the slim-core httpRequest() helper, so it runs unchanged in Node, Bun, browsers, and Web Workers.

Features

  • Implements CurrencyProvider with name === "open-exchange-rates", supportsFiat === true, supportsCrypto === false.
  • Constructor takes the OXR app_id as apiKey. Empty string raises CurrencyProviderError(MISSING_CONFIG) synchronously.
  • Free-tier safe by default: any RateFetchOptions.base !== "USD" raises CurrencyProviderError(UNSUPPORTED_BASE) before any network call. Paid-plan callers opt in with allowNonUsdBase: true.
  • Routes every request through @suaveplan/currency-providers httpRequest() so non-2xx responses surface CurrencyProviderError(HTTP_ERROR) with status and providerName populated. Caller AbortSignal from RateFetchOptions.signal is forwarded.
  • Validates the upstream { base, rates, timestamp } shape. Mismatches raise CurrencyProviderError(PARSE_ERROR).
  • Detects the OXR 2xx error-payload shape ({ error: true, status, message }) and raises CurrencyProviderError(UPSTREAM_ERROR) with upstreamMessage carrying the upstream message field.
  • Filters the rates map to RateFetchOptions.targets. Missing target codes raise CurrencyProviderError(UNKNOWN_CURRENCY).
  • Converts the OXR Unix-seconds timestamp to milliseconds.
  • Injectable FetchLike for offline testing; defaults to globalThis.fetch.

Usage: Basic Example

import { OpenExchangeRatesProvider } from "@suaveplan/currency-provider-open-exchange-rates";

const provider = new OpenExchangeRatesProvider({
    apiKey: process.env.OXR_APP_ID ?? "",
});

const result = await provider.fetchRates({ base: "USD" });
console.log(result.base, result.rates.EUR, result.timestamp);

Usage: Advanced Example

Wiring the provider into CurrencyConverter with a paid-tier app_id, custom baseUrl for staging, and AbortSignal-driven cancellation:

import { CurrencyConverter } from "@suaveplan/currency";
import { OpenExchangeRatesProvider } from "@suaveplan/currency-provider-open-exchange-rates";

const oxr = new OpenExchangeRatesProvider({
    apiKey: process.env.OXR_APP_ID!,
    allowNonUsdBase: true,
    baseUrl: process.env.OXR_BASE_URL ?? "https://openexchangerates.org/api",
});

const converter = new CurrencyConverter({
    provider: oxr,
    staleDurationMs: 30 * 60 * 1000, // 30 minutes
});

const controller = new AbortController();
setTimeout(() => controller.abort(new Error("timeout")), 5000);

const rates = await oxr.fetchRates({
    base: "EUR",
    targets: ["USD", "GBP", "JPY"],
    signal: controller.signal,
});

The slim-core CurrencyProviderRegistry is also supported when the consumer wires multiple providers behind a single lookup surface — see @suaveplan/currency-providers README.

API Reference

OpenExchangeRatesProviderConfig

Extends ApiKeyConfig from @suaveplan/currency-providers:

  • apiKey: string — required. The OXR app_id.
  • baseUrl?: string — defaults to "https://openexchangerates.org/api".
  • fetch?: FetchLike — defaults to globalThis.fetch.
  • allowNonUsdBase?: boolean — defaults to false. When false, any non-USD base raises UNSUPPORTED_BASE before any network call.

class OpenExchangeRatesProvider

Properties

  • readonly name: "open-exchange-rates"
  • readonly supportsFiat: true
  • readonly supportsCrypto: false

constructor(config: OpenExchangeRatesProviderConfig)

Throws CurrencyProviderError(MISSING_CONFIG) when config.apiKey === "".

fetchRates(options?: RateFetchOptions): Promise<RateFetchResult>

Issues GET ${baseUrl}/latest.json?app_id=<apiKey>&base=<base> via httpRequest(). Returns { base, rates, timestamp } with timestamp in milliseconds.

Throws:

  • MISSING_CONFIG — from the constructor when apiKey === "".
  • UNSUPPORTED_BASE — when options.base !== "USD" and allowNonUsdBase is not set to true.
  • HTTP_ERROR — via httpRequest() for any non-2xx response.
  • PARSE_ERROR — when the response body is not JSON, not an object, or is missing one of base / rates / timestamp in the expected shape.
  • UPSTREAM_ERROR — when the response is 2xx but the body is the OXR error shape ({ error: true, status, message }).
  • UNKNOWN_CURRENCY — when options.targets contains a code not present in the upstream rates map.

Implementation Notes

  • The OXR free tier restricts the base currency to USD; the provider enforces this client-side so the failure surfaces as a typed UNSUPPORTED_BASE rather than a generic upstream error. Paid plans pass allowNonUsdBase: true to opt out.
  • OXR returns timestamp in Unix seconds; the provider multiplies by 1000 so every CurrencyProvider answer is in milliseconds, matching RateFetchResult.timestamp's contract.
  • OXR's error-payload shape is returned with a 2xx HTTP status, which would otherwise pass httpRequest()'s non-2xx check silently. The provider detects { error: true, … } after parsing and surfaces UPSTREAM_ERROR with status and upstreamMessage populated.
  • The provider does not retry, dedupe, or rate-limit by itself — the slim core's ## Out of scope defers those concerns to a cross-cutting follow-up wave so every sibling provider gains them in lockstep.
  • apiKey is encodeURIComponent-wrapped in the query string to prevent injection when callers pass keys containing reserved characters.