feat(fiat): multi-provider resilience, best-execution quoting, rate-drift protection - #327
Merged
robertocarlous merged 3 commits intoAug 17, 2026
Conversation
…nd best-execution quoting Turns the fiat on-ramp/off-ramp layer from a single hardcoded MoonPay integration into a resilient, price-transparent, multi-provider system (Neurowealth#313): - Registry now tracks per-provider health (circuit breaker semantics matching utils/http-client.ts) and supports DEFAULT/BEST_QUOTE/ROUND_ROBIN_HEALTHY/ PREFER_PROVIDER selection policies with automatic failover for new orders. - Adds a deterministic sandbox provider proving the abstraction supports more than one vendor end-to-end (quote, order, webhook, reconciliation). - New GET /api/v1/fiat/quotes queries every healthy provider in parallel (Promise.allSettled + per-provider timeout), ranks results, and persists a 60s-default time-boxed FiatQuoteLock per quote so order creation can reference a locked rate instead of re-quoting. - FiatOrder gains quoteRate/quotedCryptoAmount/fees/providerQuoteId/ rateLockExpiresAt/settledRate/settledCryptoAmount so the crypto amount is never an unexplained number and quoted-vs-settled drift is always inspectable. Drift beyond a configurable tolerance raises an alert and a new fiat.order.rate_mismatch webhook; over-delivery is credited, not capped. - Reconciliation now matches candidate on-chain transactions against an order's quoted crypto amount (within tolerance) instead of "most recent unlinked", closing a cross-provider mis-link risk when two providers have concurrent PROCESSING orders for the same user + asset. - Providers report a structured fee breakdown (provider fee / network fee / FX spread) or explicit `fees: null` + `unpriced: true` — never an assumed zero. - New admin endpoints (fiat:read/fiat:write scopes, audited) to inspect provider health and force manual failover. - Prometheus metrics for quote latency, quote failures, order outcomes, circuit state, and rate drift per provider. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ider reconciliation Adds unit coverage for the Neurowealth#313 multi-provider changes: registry health/circuit breaker/selection policy, the sandbox provider, best-execution quote ranking + parallel timeout handling, quote-lock consumption/expiry/mismatch rejection on order creation, quoted-vs-settled rate-drift alerting (including over-delivery credited-not-capped), and the reconciliation fix that stops two providers' concurrent orders for the same user+asset from cross-linking. Extends the fiat integration test for GET /fiat/quotes and the new order-creation error paths. Updates docs/openapi.yaml: GET /api/v1/fiat/quotes, extended CreateFiatOrderRequest (provider/quoteId) and FiatOrder (quote/settlement fields) schemas, FeeBreakdown/ RankedQuote/BestExecutionQuoteResult schemas, and new error responses. Validated with redocly lint/bundle. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- prisma/migrations/20260728000000_add_sub_accounts was missing its rollback.sql (pre-existing gap on main, unrelated to this PR's changes) and was failing the "Require rollback.sql per migration" CI check. - npm audit --audit-level=high was failing on 4 pre-existing transitive vulnerabilities unrelated to this PR's dependency set: brace-expansion (new advisory beyond the prior 5.0.8 pin, bumped to 5.0.9), ip-address (via express-rate-limit, bumped to ^10.5.0), and js-yaml 4.x pinned inside eslint's/istanbuljs's dependency trees (scoped override to ^4.3.1; the direct js-yaml@^5.2.1 dependency was already unaffected).
8 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Turns the fiat on-ramp/off-ramp layer from a single hardcoded MoonPay integration into a resilient, price-transparent, multi-provider system, per the problem statement in #313.
src/fiat/registry.ts): per-provider circuit breaker (closed → open after N consecutive failures → half-open after a reset window, matchingutils/http-client.tssemantics), and a documented selection policy (DEFAULT,BEST_QUOTE,ROUND_ROBIN_HEALTHY,PREFER_PROVIDER). Failover only ever picks a provider for a new order/quote — an order already in flight stays pinned to the provider that created it.SandboxProvider(src/fiat/providers/sandbox.ts) proves the abstraction supports more than one vendor end-to-end (quote, order, webhook signature verification, reconciliation), registered by default outside production.GET /api/v1/fiat/quotesqueries every healthy provider in parallel (Promise.allSettled+ per-provider timeout), ranks results, and returns the best executable quote plus the full ranked list and any excluded providers with reasons. Each quote is persisted as a time-boxedFiatQuoteLock(default 60s) soPOST /fiat/orderscan reference aquoteIdto lock in that exact rate — an expired lock is rejected withquote_expiredrather than silently re-pricing.FeeBreakdown(provider fee / network fee / FX spread) or explicitfees: null+unpriced: true— never an assumed zero.FiatOrdergainsquoteRate,quotedCryptoAmount,fees,providerQuoteId,rateLockExpiresAt,settledRate,settledCryptoAmount. On settlement, quoted-vs-settled drift is computed; beyond a configurable tolerance (default 2%) it raises an operational alert and a newfiat.order.rate_mismatchwebhook. Over-delivery is credited to the user, not capped — still reported for audit visibility.reconcileFiatOrdersnow matches a candidate on-chain transaction against an order's quoted crypto amount (within tolerance) instead of "most recent unlinked for this user+asset", closing a cross-provider mis-link risk when two providers have concurrentPROCESSINGorders for the same user + asset. Provider-keyed webhook idempotency ((provider, providerOrderId)) is unchanged, so a provider's webhook still can never mutate another provider's order.GET /api/admin/fiat/providers(health snapshot) andPOST /api/admin/fiat/providers/:name/failover(manual circuit override) behind newfiat:read/fiat:writeadmin scopes, audited viaAdminAuditLog.docs/openapi.yamlupdated (newGET /fiat/quotespath, extendedCreateFiatOrderRequest/FiatOrderschemas,FeeBreakdown/RankedQuote/BestExecutionQuoteResultschemas, new error responses). Validated withredocly lint/redocly bundle.Settlement correctness is unchanged/preserved: on-chain confirmation via a
CONFIRMEDTransactionrow owned by the same user remains the sole path toSETTLED; a provider "completed" webhook still only advances an order toPROCESSING.Test plan
npx tsc --noEmit— cleannpx eslinton all touched files — cleanredocly lint docs/openapi.yaml/redocly bundle— valid (only pre-existing warning categories)tests/integration/fiat.integration.test.tsforGET /fiat/quotesand the new order-creation error paths (quote_expired,no_healthy_providers, provider preference passthrough)npm test→ 59 suites / 656 tests passing (also re-verified by the repo's pre-push hook)Closes #313