# Design: @suaveplan/currency-provider-kraken > Non-normative. The normative requirements live in > `./specs/currency-provider-kraken/spec.md`. The cross-cutting > foundation-with-sibling-plugins architecture lives in > `openspec/archive/2026-05-17-slim-currency-providers/design.md` — read it > first; this document covers only the Kraken-specific deltas. ## Shape ```text @suaveplan/currency-provider-kraken ├── src/symbols/symbols.ts # KRAKEN_SYMBOL_MAP + pair→symbol reverse ├── src/provider/provider.ts # KrakenProvider class + DEFAULT_KRAKEN_BASE_URL └── src/index.ts # explicit named barrel ``` Two feature modules. Each carries a sibling `.md`, a `.test.ts`, and a feature-folder `index.ts` (named-exports only — `export *` is banned per AGENTS.md §6a). ## Wire shape The Kraken public ticker returns: ```json { "error": ["EGeneral:Too many requests"], "result": { "XXBTZUSD": { "c": ["67000.00", "0.1"], "..." }, "XETHZUSD": { "c": ["3500.00", "1.0"], "..." } } } ``` - `error` is **always** present (empty array on success). A non-empty array is the canonical upstream-failure signal and maps to `CurrencyProviderError(UPSTREAM_ERROR)`. The first entry is carried on `upstreamMessage` so consumers can dispatch on rate-limit / endpoint / pair-unknown messages. - `result` is keyed by Kraken's non-standard pair name (`XXBTZUSD`, not `BTCUSD`). The symbol map normalises both directions; the reverse map lets us go from the wire pair back to a Suaveplan symbol in O(1). - `c` is `[price, lotVolume]` — `c[0]` is the last-trade-closed price. ## Symbol mapping Twelve symbols are mapped, matching the upstream coverage. The list is intentionally small (rather than dynamic) because: - Kraken's full pair list rotates frequently (new listings, delistings) and pulling the live `AssetPairs` list per request would double the round trips. - The mapped set is the high-volume subset most consumers actually want. - Callers who need an additional symbol can supply a custom symbol-to-pair override via the constructor (`config.extraSymbols`) without forking the package. The override map is merged into `KRAKEN_SYMBOL_MAP` at construction time. ```ts const KRAKEN_SYMBOL_MAP: Readonly> = Object.freeze({ BTC: "XXBTZUSD", ETH: "XETHZUSD", SOL: "SOLUSD", USDT: "USDTZUSD", DOT: "DOTUSD", ADA: "ADAUSD", LINK: "LINKUSD", AVAX: "AVAXUSD", ATOM: "ATOMUSD", LTC: "XLTCZUSD", DOGE: "XDGUSD", XRP: "XXRPZUSD", }); ``` ## Rate model Kraken returns the USD price of one unit of the crypto (e.g. `1 BTC = 67000 USD`). The `CurrencyProvider` contract returns rates as "1 base = N target" where base defaults to `USD`. The rate for `USD → BTC` is therefore the reciprocal: `1 / 67000`. This matches the upstream behaviour verbatim and is what every downstream `CurrencyConverter` assumes. If `c[0]` does not parse to a positive finite number, the entry is dropped (skipped) rather than raising — Kraken occasionally returns `"0.00"` for freshly-listed pairs that haven't yet traded. If the entire mapped set drops, the provider raises `UNKNOWN_CURRENCY` so the caller sees the failure rather than a silent empty rate map. ## Targets filtering When `options.targets` is non-empty, the provider filters to those targets after building the full rate map. Any target that is not in `KRAKEN_SYMBOL_MAP` is a no-op (it simply doesn't appear in the result). If the filtered set is empty AND `options.targets` was non-empty, the provider raises `UNKNOWN_CURRENCY` — this is the "the caller asked for X / Y / Z and Kraken can't price any of them" signal, distinct from "Kraken is down" (`HTTP_ERROR`), "Kraken returned an error array" (`UPSTREAM_ERROR`), and "the response body is malformed" (`PARSE_ERROR`). ## Error path matrix | Wire condition | Code | `providerName` | Other discriminants | |---|---|---|---| | Non-2xx response | `HTTP_ERROR` | `"kraken"` | `status` | | `data.error.length > 0` | `UPSTREAM_ERROR` | `"kraken"` | `upstreamMessage = data.error[0]` | | JSON parse failure | `PARSE_ERROR` | `"kraken"` | (cause: original error) | | `data.result` not an object | `PARSE_ERROR` | `"kraken"` | (cause: shape-check failure) | | All requested targets unmapped | `UNKNOWN_CURRENCY` | `"kraken"` | — | `HTTP_ERROR` is raised by the shared `httpRequest()` helper (we don't re-implement it); the other three are raised inline after the response body is read. ## Test strategy - `symbols.test.ts` — asserts the 12-symbol map is exported, immutable (`Object.isFrozen`), and that the reverse map is consistent. - `provider.test.ts` — uses a recording `FetchLike` closure (the same pattern the slim core's `http.test.ts` established). Covers: - happy path with a real-shape Kraken response (`error: []`, `result: {…}`) - `supportsCrypto` / `supportsFiat` metadata - rates are reciprocals of `c[0]` - `options.base` default of `"USD"` - `options.targets` filtering (subset) - `options.targets` filtering with one unmapped → still returns mapped ones - `options.targets` all unmapped → `UNKNOWN_CURRENCY` - `data.error` non-empty → `UPSTREAM_ERROR` with `upstreamMessage` - non-2xx response → `HTTP_ERROR` with `status` (via the shared helper) - JSON-parse failure → `PARSE_ERROR` - shape-check failure (missing `result`) → `PARSE_ERROR` - `c[0]` non-positive / non-finite → entry skipped, others retained - custom `baseUrl` is honoured - custom `fetch` (FetchLike) is used in place of `globalThis.fetch` - `options.signal` is forwarded into the `httpRequest` call (abort propagates) All tests import runner primitives from `@suaveplan/testing/runner`. No `vi.mock`, no `vi.stubGlobal` — only injection through the constructor. ## Tier purity Universal-tier. The Kraken endpoint is reachable from any runtime that has `fetch`: Node ≥ 18, Bun, modern browsers (subject to Kraken's CORS policy — at time of writing the public ticker endpoint serves `Access-Control-Allow-Origin: *`), Web Workers. No `node:`/`bun:`/DOM imports.