vault

05 — External Providers

docs/05-External Providers.mdupdated: 2026-08-09

External Providers

All providers are abstracted behind interfaces in lib/providers/types.ts so the live cutover swaps implementations only. The selector is lib/providers/index.ts.

Provider Interface Mock state Live state Open question
Custody (Fireblocks) FireblocksProvider MockFireblocks 🟡 LiveFireblocks implemented (live-fireblocks.ts, live-fireblocks-client.ts, fireblocks-network.ts) — selected by FIREBLOCKS_MODE=live 09-Open Questions#Custody
Chain ChainProvider MockChain ❌ not started — CHAIN_MODE=live throws 09-Open Questions#Custody
AML + sanctions (Elliptic) EllipticProvider MockElliptic (24 h cache) ❌ not started — ELLIPTIC_MODE=live throws vendor resolved, integration open
Card acquirer + on/off ramp (Mercuryo) AcquirerProvider MockAcquirer (test cards) ❌ not started — ACQUIRER_MODE=live throws. An inbound /api/webhooks/mercuryo route exists. vendor resolved, integration open
Rate oracle OracleProvider MockOracle 🟢 CexioOracle — live against CEX.IO (https://cex.io/api/last_prices/EUR, 30 s batch TTL) decided — CEX.IO
Risk RiskProvider MockRisk ❌ not started — RISK_MODE=live throws
Email EmailProvider MockEmail writes mock_emails rows 🟢 Resend adapter implemented 09-Open Questions#Email deliverability
KYC KycProvider MockKycService 🟢 SumsubService implemented — selected by KYC_MODE=live; /api/webhooks/kyc ingests reviews resolved in practice — Sumsub
Ledger / accounting (TRES) TresProvider MockTres ❌ not started — TRES_MODE=live throws. Payload builder exists (lib/providers/tres.ts). 09-Open Questions#TRES alignment

| Ledger / accounting (TRES) (planned) | not yet defined | n/a — Postgres ledger_entries is current source of truth | not started | 09-Open Questions#TRES alignment | | Edge (AWS — ALB + WAF now, CloudFront + Shield later) | n/a — infra layer | MVP uses public ALB + regional AWS WAF | partial | 09-Open Questions#AWS edge hardening | | Admin entry (AWS IAM Identity Center) (planned) | n/a — infra layer | n/a — MVP uses NextAuth-only admin login | not started | 09-Open Questions#AWS edge cutover |

Provider switching

Provider mode is per-provider via <NAME>_MODE env var, falling back to PROVIDER_MODE. Email is selected directly via EMAIL_PROVIDER=mock or EMAIL_PROVIDER=resend; legacy EMAIL_MODE=live maps to Resend when EMAIL_PROVIDER is absent. Other providers still throw when switched to live until their live implementations land.

Planned dependencies (target architecture)

These appear in 11-Target-Architecture but have no abstraction in lib/providers yet — they're planned for the AWS refactor, not the current MVP.

TRES — internal ledger / accounting layer

  • Current state: the ledger lives in Postgres (ledger_entries). Reconciliation is SELECT-driven.
  • Target state: TRES becomes the accounting interpretation layer; Postgres remains the operational system-of-record for transactions. Why this split: see 11-Target-Architecture#Why TRES (not just Postgres).
  • Risk: vendor lock-in for accounting + reconciliation. Trade-off vs continuing to grow Postgres-side accounting tooling. Tracked at 09-Open Questions#TRES alignment.

AWS edge — ALB + AWS WAF now; CloudFront + Shield + Route 53 later

  • Current state: the MVP dev stack runs on AWS with a public ALB and regional AWS WAF. It starts on the ALB service hostname until a domain is registered.
  • Target state: AWS-native edge stack in front of API Gateway: Route 53 for DNS, CloudFront for HTTPS termination + caching, AWS WAF for managed rules + rate-based rules, AWS Shield for DDoS. Admin entry uses AWS IAM Identity Center (SSO) + a dedicated AWS WAF web ACL with IP allowlist + geo restriction. See 13-Network-Model.
  • Why architectural, not just infra: the edge is part of the security boundary. The whole regulated stack lives in a single AWS account family — no third-party network in the production data path.
  • Hardening tracked at 09-Open Questions#AWS edge hardening.

Mercuryo — card acquirer + on/off ramp

  • Decision: Mercuryo is the contracted partner for card acquiring (Buy) and on/off-ramp (Buy / Sell). PCI scope sits with Mercuryo — we do not handle card PANs.
  • Current state: MockAcquirer covers the Buy flow; Mercuryo iframe is referenced in product docs but not integrated.
  • Target state: iframe integration on the Buy/Sell flow under 03-Domains#Buy / 03-Domains#Sell using a signed payload. The platform interprets resulting activity through its own transaction model — Mercuryo events are not the source of truth.

Elliptic — AML + sanctions

  • Decision: Elliptic is the contracted vendor for both wallet-AML risk scoring and address-level sanctions (OFAC / EU / UK / UN). One product, one integration path.
  • Current state: MockElliptic returns deterministic risk scores; sanctions are not screened.
  • Target state: live Elliptic for risk + sanctions; cache-aside for 24 h per (address, network, currency).

CEX.IO — rate oracle

  • Decision: CEX.IO is the rate source — quotes, invoice rate-freeze, conversion previews.
  • Current state: live. CexioOracle (lib/providers/cexio.ts) calls https://cex.io/api/last_prices/EUR with a 5 s timeout and a 30 s batch TTL, with stale-on-failure fallback. MockOracle remains the default for local development.
  • Consideration: CEX.IO quotes USDT/USDC without network differentiation — USDT_ETH and USDT_TRX resolve to the same ticker. Acceptable for pricing; worth stating explicitly if a quote is ever contested.
  • Re-evaluate against Kaiko / CryptoCompare / Chainlink at GA based on uptime, regional pair coverage, and data-licensing terms.

Durable provider-callback inbox

Inbound provider traffic no longer terminates in the request handler.

/api/webhooks/fireblocks, /api/webhooks/kyc and /api/webhooks/mercuryo each persist to provider_webhook_events (migration 0007), then hand off to the provider.webhook.process pg-boss queue. A crash mid-processing replays the event instead of dropping it.

Local development enqueues the same shape through dev-only.provider.webhook.dispatch, so emulation exercises the production ingestion path rather than a shortcut.

Mocked Fireblocks behaviour

  • createSubVault({ parentRef, paymentOrderId, asset }) — deterministic per-key sub-vault.
  • createTransaction({ … }) — synthetic mock_<24hex> txId stored in-process map.
  • getTransaction(txId)1% chance of returning FAILED to exercise payout failure paths.

Mocked Elliptic

  • getRiskScore(...) — deterministic SHA-based score in [0.0, 10.0] (1-decimal precision).
  • 25 ms artificial latency.
  • The 24 h cache lives in lib/aml, not the mock — provider swap doesn't change that.

Drawbacks

Open Questions