vault
05 — External Providers
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 |
|
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 isSELECT-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:
MockAcquirercovers 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:
MockEllipticreturns 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) callshttps://cex.io/api/last_prices/EURwith a 5 s timeout and a 30 s batch TTL, with stale-on-failure fallback.MockOracleremains the default for local development. - Consideration: CEX.IO quotes USDT/USDC without network differentiation —
USDT_ETHandUSDT_TRXresolve 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({ … })— syntheticmock_<24hex>txId stored in-process map.getTransaction(txId)— 1% chance of returningFAILEDto 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.