8.3 KiB
Capability Spec: currency-provider-coingecko
Package: @suaveplan/currency-provider-coingecko
Status: SHIPPED (2026-05-17 — add-currency-provider-coingecko)
Folded from
openspec/changes/add-currency-provider-coingecko/specs/currency-provider-coingecko/spec.mdon 2026-05-17.
Requirements
Requirement: CoinGeckoProvider implements the CurrencyProvider contract
The package SHALL export CoinGeckoProvider, a class implementing the
CurrencyProvider contract from @suaveplan/currency. It SHALL expose the
static identity fields name = "coingecko", supportsCrypto = true, and
supportsFiat = true. The constructor SHALL accept an optional
configuration object of shape NoKeyConfig & { readonly apiKey?: string };
omitting the config (free public tier) SHALL be a valid call.
Scenario: provider declares stable metadata at construction
- WHEN a caller instantiates
new CoinGeckoProvider()with no config - THEN the returned instance carries
name === "coingecko",supportsCrypto === true, andsupportsFiat === true
Verified by: src/coingecko/coingecko.test.ts::"exposes stable name and supports flags"
Requirement: fetchRates uses the shared httpRequest helper with default base URL
fetchRates(options?) SHALL build the request URL from the configured base
URL (default https://api.coingecko.com/api/v3), append
/simple/price?ids=<curated-ids>&vs_currencies=<base-lower> query
parameters, and issue the request through httpRequest() from
@suaveplan/currency-providers. The injected FetchLike from
ProviderConfigBase.fetch (default globalThis.fetch) SHALL be used. The
caller's RateFetchOptions.signal SHALL be forwarded to httpRequest so
abort-signal composition propagates.
Scenario: keyless call hits the default endpoint with vs_currencies derived from base
- WHEN
new CoinGeckoProvider().fetchRates({ base: "USD" })runs with an injectedFetchLikethat records the request URL - THEN the recorded URL SHALL begin with
https://api.coingecko.com/api/v3/simple/price?ids=and SHALL end with&vs_currencies=usd
Scenario: custom baseUrl overrides the default
- WHEN
new CoinGeckoProvider({ baseUrl: "https://custom.example/v3", fetch }).fetchRates({ base: "EUR" })runs - THEN the recorded URL SHALL begin with
https://custom.example/v3/simple/price
Verified by: src/coingecko/coingecko.test.ts::"issues request to default endpoint with vs_currencies from base" AND src/coingecko/coingecko.test.ts::"honours custom baseUrl override"
Requirement: Pro-tier API key is attached as x-cg-pro-api-key header
When the constructor receives apiKey, every outbound request SHALL carry
the header x-cg-pro-api-key: <key>. When apiKey is absent, the
provider SHALL NOT attach the header (keeping the request shape
indistinguishable from a public-tier call).
Scenario: Pro-tier configuration attaches the header
- WHEN
new CoinGeckoProvider({ apiKey: "secret", fetch }).fetchRates()runs with a recordingFetchLike - THEN the recorded
init.headersSHALL contain"x-cg-pro-api-key": "secret"
Scenario: free-tier configuration omits the header
- WHEN
new CoinGeckoProvider({ fetch }).fetchRates()runs with a recordingFetchLike - THEN the recorded
init.headersSHALL NOT contain ax-cg-pro-api-keyentry (the entireheadersfield SHALL be absent or empty)
Verified by: src/coingecko/coingecko.test.ts::"attaches x-cg-pro-api-key header when apiKey is provided" AND src/coingecko/coingecko.test.ts::"omits x-cg-pro-api-key header when apiKey is absent"
Requirement: Successful responses invert prices into the converter rate convention
CoinGecko returns prices as "1 crypto = X fiat"; the package SHALL invert
those into "1 base = X crypto" so they match the RateFetchResult
convention every other provider uses. The result SHALL set base to the
caller-supplied base (default "USD"), timestamp to Date.now() at
parse time, and rates to a record keyed by uppercase crypto symbol with
inverted numeric values. When options.targets is supplied, the result
SHALL include only the requested symbols.
Scenario: rates are inverted from upstream price
- WHEN the upstream returns
{ bitcoin: { usd: 67000 }, ethereum: { usd: 3500 } }andfetchRates({ base: "USD" })runs - THEN the returned
rates.BTCSHALL equal1 / 67000andrates.ETHSHALL equal1 / 3500
Scenario: target filtering narrows the result set
- WHEN the upstream returns rates for multiple coins and
fetchRates({ base: "USD", targets: ["BTC", "ETH"] })runs - THEN
Object.keys(result.rates)SHALL be exactly["BTC", "ETH"](in some order) and SHALL NOT include any other coin
Verified by: src/coingecko/coingecko.test.ts::"inverts upstream prices into base→crypto rates" AND src/coingecko/coingecko.test.ts::"filters rates by options.targets"
Requirement: HTTP failures raise CurrencyProviderError(HTTP_ERROR)
Non-2xx responses SHALL surface as
CurrencyProviderError(CURRENCY_PROVIDER_CODES.HTTP_ERROR) with
providerName === "coingecko" and status populated from the upstream
response. The raise SHALL be performed by the shared httpRequest() helper
(this package does not re-implement the check).
Scenario: 429 from upstream surfaces as HTTP_ERROR with status 429
- WHEN the injected
FetchLikereturns aResponsewith status429 - THEN
fetchRates()SHALL reject with aCurrencyProviderErrorwhosecode === CURRENCY_PROVIDER_CODES.HTTP_ERROR,providerName === "coingecko", andstatus === 429
Verified by: src/coingecko/coingecko.test.ts::"raises HTTP_ERROR on non-2xx response"
Requirement: Malformed responses raise PARSE_ERROR or UPSTREAM_ERROR
When the response body is not valid JSON, fetchRates() SHALL raise
CurrencyProviderError(PARSE_ERROR). When the body parses as JSON but is
not the expected Record<string, Record<string, number>> shape (e.g.
upstream returns an array or null or any non-object), fetchRates()
SHALL raise CurrencyProviderError(UPSTREAM_ERROR). Both errors SHALL
carry providerName === "coingecko".
Scenario: invalid JSON surfaces as PARSE_ERROR
- WHEN the upstream returns a 200 response with body
"not json" - THEN
fetchRates()SHALL reject withcode === CURRENCY_PROVIDER_CODES.PARSE_ERRORandproviderName === "coingecko"
Scenario: wrong-shape JSON surfaces as UPSTREAM_ERROR
- WHEN the upstream returns a 200 response with body
"[]"(a JSON array — wrong shape) - THEN
fetchRates()SHALL reject withcode === CURRENCY_PROVIDER_CODES.UPSTREAM_ERRORandproviderName === "coingecko"
Verified by: src/coingecko/coingecko.test.ts::"raises PARSE_ERROR when body is not valid JSON" AND src/coingecko/coingecko.test.ts::"raises UPSTREAM_ERROR when body is the wrong shape"
Requirement: getCoinGeckoId exposes a case-insensitive symbol lookup
The package SHALL export getCoinGeckoId(symbol: string) returning the
CoinGecko ID for the curated symbol set (BTC, ETH, SOL, USDT, USDC, BNB,
XRP, ADA, DOGE, DOT, AVAX, MATIC, LINK, UNI, ATOM, LTC). The lookup
SHALL be case-insensitive and SHALL return undefined for unknown
symbols. When fetchRates() receives options.targets containing a
symbol with no mapping, it SHALL raise
CurrencyProviderError(UNKNOWN_CURRENCY) proactively (before issuing
the HTTP request) carrying the offending symbol in the message.
Scenario: known symbol resolves regardless of case
- WHEN
getCoinGeckoId("btc")andgetCoinGeckoId("BTC")are called - THEN both SHALL return
"bitcoin"
Scenario: unknown target raises UNKNOWN_CURRENCY before any HTTP call
- WHEN
fetchRates({ targets: ["BTC", "ZZZ"] })runs with a recordingFetchLike - THEN the call SHALL reject with
code === CURRENCY_PROVIDER_CODES.UNKNOWN_CURRENCYAND the recordingFetchLikeSHALL NOT have been invoked
Verified by: src/coin-ids/coin-ids.test.ts::"returns the CoinGecko id for known symbols regardless of case" AND src/coin-ids/coin-ids.test.ts::"returns undefined for unknown symbols" AND src/coingecko/coingecko.test.ts::"raises UNKNOWN_CURRENCY when a target has no CoinGecko mapping"