Skip to content

feat(fiat): multi-provider resilience, best-execution quoting, rate-drift protection - #327

Merged
robertocarlous merged 3 commits into
Neurowealth:mainfrom
sammajayi:feat/fiat-multi-provider-313
Aug 17, 2026
Merged

feat(fiat): multi-provider resilience, best-execution quoting, rate-drift protection#327
robertocarlous merged 3 commits into
Neurowealth:mainfrom
sammajayi:feat/fiat-multi-provider-313

Conversation

@sammajayi

@sammajayi sammajayi commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

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.

  • Multi-provider registry with health tracking + failover (src/fiat/registry.ts): per-provider circuit breaker (closed → open after N consecutive failures → half-open after a reset window, matching utils/http-client.ts semantics), 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.
  • Second provider: a deterministic 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.
  • Best-execution quoting: GET /api/v1/fiat/quotes queries 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-boxed FiatQuoteLock (default 60s) so POST /fiat/orders can reference a quoteId to lock in that exact rate — an expired lock is rejected with quote_expired rather than silently re-pricing.
  • Fee transparency: providers return a structured FeeBreakdown (provider fee / network fee / FX spread) or explicit fees: null + unpriced: true — never an assumed zero.
  • Rate-drift protection: FiatOrder gains quoteRate, 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 new fiat.order.rate_mismatch webhook. Over-delivery is credited to the user, not capped — still reported for audit visibility.
  • Reconciliation hardening: reconcileFiatOrders now 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 concurrent PROCESSING orders 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.
  • Admin ops: GET /api/admin/fiat/providers (health snapshot) and POST /api/admin/fiat/providers/:name/failover (manual circuit override) behind new fiat:read/fiat:write admin scopes, audited via AdminAuditLog.
  • Observability: Prometheus metrics for quote latency, quote failure rate, order outcomes, circuit state, and rate drift, all labeled by provider.
  • Docs: docs/openapi.yaml updated (new GET /fiat/quotes path, extended CreateFiatOrderRequest/FiatOrder schemas, FeeBreakdown/RankedQuote/BestExecutionQuoteResult schemas, new error responses). Validated with redocly lint / redocly bundle.

Settlement correctness is unchanged/preserved: on-chain confirmation via a CONFIRMED Transaction row owned by the same user remains the sole path to SETTLED; a provider "completed" webhook still only advances an order to PROCESSING.

Test plan

  • npx tsc --noEmit — clean
  • npx eslint on all touched files — clean
  • redocly lint docs/openapi.yaml / redocly bundle — valid (only pre-existing warning categories)
  • New unit tests: provider registry (health/circuit breaker/selection policy/admin failover), sandbox provider, best-execution quote ranking + partial-failure/timeout handling, quote-lock consumption/expiry/mismatch rejection, quoted-vs-settled drift alerting (including over-delivery), and a dedicated multi-provider cross-linking regression test (two providers, same user, same asset)
  • Extended tests/integration/fiat.integration.test.ts for GET /fiat/quotes and the new order-creation error paths (quote_expired, no_healthy_providers, provider preference passthrough)
  • Full suite: npm test → 59 suites / 656 tests passing (also re-verified by the repo's pre-push hook)

Closes #313

sammajayi and others added 3 commits August 17, 2026 11:42
…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).
@robertocarlous
robertocarlous merged commit 96a47e8 into Neurowealth:main Aug 17, 2026
8 checks passed
@sammajayi
sammajayi deleted the feat/fiat-multi-provider-313 branch August 17, 2026 11:14
@sammajayi
sammajayi restored the feat/fiat-multi-provider-313 branch August 17, 2026 11:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multi-Provider Fiat Aggregation with Best-Execution Quoting & Fee Transparency

2 participants