Provider landscape — the vendor capability matrix
What this covers / who it's for. A cross-vendor evidence base — every finance-adjacent vendor with code in this repo, what it can serve against our 17 capability contracts, what is actually routed to it today, and what remains unverified. This is the evidence base for a future provider-selection decision, not the decision itself. The first distinction that matters throughout: ROUTED (a live
jurisdiction_provider_grantsrow can send it traffic) vs DORMANT (a transport SDK with zero app consumers). An honest ❔ UNVERIFIED cell is deliberate — this page would rather under-claim than guess.
1. Legend + evidence rules
| Symbol | Meaning |
|---|---|
| ✅ | Serves the capability as our contract models it |
| ◐ | Partial — serves it with a named gap (stubbed method, missing sub-shape, semantic mismatch) |
| ✗ | Does not serve it (confirmed absent from the adapter/spec/docs) |
| ❔ | UNVERIFIED — not established this pass; a vendor question, not a claim |
Evidence required per vendor class before a cell is anything but ❔:
- Routed (Conduit, Sumsub, Utila, Fingerprint, SEON) — the adapter class's
implementslist (§2, §3) plus anyProviderOperationNotSupportedExceptionstub inside it. Re-verify by reading the adapter class declaration. - Bridge (dormant SDK) — the committed
packages/stables/bridge-sdk/spec/openapi.jsonsnapshot for request/response shapes, and the liveapidocs.bridge.xyzpages for semantics (behavior a spec shape alone doesn't state). Every Bridge fact below carries its page URL and "fetched 2026-09-02". Re-verify by re-fetching the cited page, or runningmake bridge-spec-difffor a shape check — method per thebridge-developmentskill (llms.txt → fetch the.mdpage → cite URL + date). - Sovera (dormant SDK) —
packages/stables/sovera-sdk/README.md's resource list, "Spec ambiguities appendix" (README ~L420) and "Onboard an end user (Crypto)" section, since there is no committed OpenAPI spec for it (hand-authored against a vendor doc not in this repo). Re-verify against that README; gaps there are pending the vendor, not resolvable in-repo. - sFOX (dormant SDK) — not assessed this pass. Every cell is ❔ by policy, not by omission.
2. Vendor roster
| Vendor | In-repo package | Status | Adapter class | FinanceProvider case? | config/rails.php entry? | ADR |
|---|---|---|---|---|---|---|
| Conduit | packages/stables/conduit-sdk | ROUTED primary (FiatRails, Conversion, + Onboarding/Custody/Fraud outside their flips) | Conduit\ConduitRailsProvider | Yes | Yes | decisions D94 |
| Sumsub | packages/stables/sumsub-sdk | ROUTED primary (Onboarding, GB+EU only) | Sumsub\SumsubRailsProvider | Yes | Yes | D95 |
| Utila | packages/stables/utila-sdk | ROUTED primary (Custody, all six baseline jurisdictions) | Utila\UtilaRailsProvider | Yes | Yes | D98 |
| Fingerprint | packages/stables/fingerprint-sdk | ROUTED primary (Fraud, all six) | Fingerprint\FingerprintRailsProvider | Yes | Yes | D102 |
| SEON | packages/stables/seon-sdk | ROUTED permitted (Fraud fallback, all six) | Seon\SeonRailsProvider | Yes | Yes | D102 |
| Bridge | packages/stables/bridge-sdk | DORMANT SDK — zero app-modules/* imports | none | No | No | D114 (SDK drift-gate posture only, no adapter) |
| Sovera | packages/stables/sovera-sdk | DORMANT SDK — zero app-modules/* imports | none | No | No | none |
| sFOX | packages/stables/sfox-sdk | DORMANT SDK — zero app-modules/* imports | none | No | No | D111 (ingestion posture only, no adapter) |
Source: app-modules/rails/src/{Conduit,Sumsub,Utila,Fingerprint,Seon}/*RailsProvider.php class
declarations; app-modules/rails/config/rails.php (all five routed vendors enabled => true);
app-modules/foundation/src/ValueObjects/FinanceProvider.php (5 cases: Conduit, Sumsub, Utila,
Fingerprint, Seon); app-modules/rails/src/Providers/RailsServiceProvider.php (ProviderAdapterRegistry
seeded with the same 5).
3. The capability matrix
17 rows: 13 umbrella capabilities (Modules\Rails\Contracts\RailsProvider extends list) + 4
segregated (off-umbrella) capabilities. Grouped by the routing CapabilityGroup
(app-modules/rails/src/Support/CapabilityGroupMap.php). Column order matches the roster above.
| Group | Capability (method(s)) | Conduit | Sumsub | Utila | Fingerprint | SEON | Bridge | Sovera | sFOX |
|---|---|---|---|---|---|---|---|---|---|
| Onboarding | OnboardsCustomers (getOnboardingRequirements, submitApplication, getApplication) | ✅ | ✅ | ✗ | ✗ | ✗ | ◐ [1] | ◐ [2] | ❔ |
| Onboarding | ManagesCustomers (getCustomer) | ✅ | ✅ | ✗ | ✗ | ✗ | ✅ [3] | ◐ [4] | ❔ |
| Onboarding | ManagesDocuments (uploadDocument) | ✅ | ✗ [5] | ✗ | ✗ | ✗ | ◐ [6] | ❔ | ❔ |
| Onboarding | VerifiesIdentity (initiateVerification) | ✅ | ✅ | ✗ | ✗ | ✗ | ✅ [7] | ❔ | ❔ |
| Onboarding | FetchesVerification (fetchVerification) — segregated | ✗ | ✅ | ✗ | ✗ | ✗ | ✅ [7] | ❔ | ❔ |
| Onboarding | VerifiesBusinesses (fetchBusinessVerification, fetchBeneficialOwners) — segregated | ✗ | ✅ | ✗ | ✗ | ✗ | ◐ [8] | ❔ | ❔ |
| Fraud | AssessesFraudRisk (assess) — segregated | ✗ | ✗ | ✗ | ✅ | ✅ | ✗ [9] | ❔ | ❔ |
| Custody | ManagesWallets (getWallet, listWallets, createWallet, rotateWallet) | ✅ | ✗ | ◐ [10] | ✗ | ✗ | ◐ [11] | ◐ [12] | ❔ |
| Custody | ManagesWalletSigners (5 methods) | ✅ | ✗ | ◐ [13] | ✗ | ✗ | ❔ [14] | ❔ | ❔ |
| Custody | ManagesSigningQuorum (4 methods) | ✅ | ✗ | ◐ [13] | ✗ | ✗ | ❔ [14] | ❔ | ❔ |
| Custody | ManagesCustodyTransactions (getTransaction, listTransactions, initiateTransaction, voteOnTransaction) — segregated | ✗ | ✗ | ✅ | ✗ | ✗ | ❔ [15] | ❔ | ❔ |
| Custody | ManagesRegisteredAddresses (registerAddress, getRegisteredAddress, listRegisteredAddresses, revokeRegisteredAddress) | ✅ | ✗ | ◐ [16] | ✗ | ✗ | ◐ [17] | ❔ | ❔ |
| FiatRails | ManagesVirtualAccounts (getVirtualAccount, listVirtualAccounts) | ✅ | ✗ | ✗ | ✗ | ✗ | ◐ [18] | ❔ [19] | ❔ |
| FiatRails | ManagesPayouts (createPayout, getPayout, cancelPayout, payoutRequirements) | ✅ | ✗ | ✗ | ✗ | ✗ | ✅ [20] | ❔ [19] | ❔ |
| FiatRails | ManagesWhitelistRecipients (5 methods) | ✅ | ✗ | ✗ | ✗ | ✗ | ◐ [21] | ❔ | ❔ |
| FiatRails | ManagesTransactions (getTransaction, listTransactions, submitSenderInformation) | ✅ | ✗ | ✗ | ✗ | ✗ | ◐ [22] | ◐ [23] | ❔ |
| Conversion | ManagesOrders (createOrder, getOrder, listOrders, cancelOrder, orderRequirements) | ✅ | ✗ | ✗ | ✗ | ✗ | ◐ [24] | ◐ [25] | ❔ |
Footnotes (each cites the page/file checked this pass; Bridge dates are all 2026-09-02):
- Bridge
POST /customers+ KYC Links create the onboarding intent; no separate "requirements"/"application" resource — one customer-creation flow serves both individual and business.kyc_statusstates includeawaiting_questionnaire/awaiting_ubo. No analogue of our per-countrygetOnboardingRequirementsread was found on the fetched pages — the requirements-read half is UNVERIFIED, hence partial. Fetched 2026-09-02: https://apidocs.bridge.xyz/api-reference/customers/create-a-customer.md , https://apidocs.bridge.xyz/api-reference/kyc-links/get-all-kyc-links.md - Sovera: KYC is a "flow" via
users()->create/activate, not a distinct application resource; business end users are undocumented (see [4]).packages/stables/sovera-sdk/README.md"Onboard an end user (Crypto)" (~L250–299). - Bridge
GET /customers/{id}— same customer resource as onboarding. Fetched 2026-09-02: https://apidocs.bridge.xyz/api-reference/customers/create-a-customer.md - Sovera
users()->get(individual only, per [2]).packages/stables/sovera-sdk/README.md"Resources & methods" (~L342–361). Individual end users only — reading or creating a business end user is undocumented (UNVERIFIED), so the cell is partial, not full. Sumsub\SumsubRailsProviderdeliberately omitsManagesDocuments— Sumsub's own WebSDK collects documents, so there is nothing for our adapter to forward.app-modules/rails/src/Sumsub/SumsubRailsProvider.phpclass declaration.- Bridge has no upload endpoint. Documents ride as base64 fields on the customer payload:
identifying_information[].image_front/image_backanddocuments[].file(with apurposesenum). A structurally different shape from a document-upload capability — not a 1:1 fit. Fetched 2026-09-02: https://apidocs.bridge.xyz/api-reference/customers/create-a-customer.md - Bridge's
kyc_statusis queryable on the same customer resource both at initiation and as a pull — no separate "initiate" vs "fetch" split like ours, so both cells map to the one read/poll surface. Fetched 2026-09-02: https://apidocs.bridge.xyz/api-reference/customers/create-a-customer.md - Bridge KYB rides
type: businesscustomers +associated_persons(UBO threshold 25%+,has_control: truefor a control person) — a UBO graph is readable via associated persons, but whether it is walkable the same way ourfetchBeneficialOwnersneeds (cycle-safe, depth-bounded) is unconfirmed — mark the graph-walk semantics UNVERIFIED. Fetched 2026-09-02: https://apidocs.bridge.xyz/platform/customers/compliance/businesses/overview.md , .../ownership.md - No fraud-scoring/device-risk endpoint in the spec. The only
fraud/riskhits inpackages/stables/bridge-sdk/spec/openapi.jsonare a booleanfraudflag +FraudParameterquery param on ACH returns, andrisk_rejection_reasonon funds requests — return-side flags, not an assessment/scoring surface. Greppedspec/openapi.json(lines ~13, 3195, 7705, 8833) this pass. - Utila stubs
rotateWalletwithProviderOperationNotSupportedException(terminal 422); read + create are served.app-modules/rails/src/Utila/UtilaRailsProvider.php~L193–206. - Bridge custodial wallets exist (
platform/wallets/overview.md) with create/list/get, but no rotate/replace-key operation is documented. Fetched 2026-09-02: https://apidocs.bridge.xyz/platform/wallets/overview.md - Sovera
wallets()->create, list, balance, transactions— no rotate.packages/stables/sovera-sdk/README.md"Resources & methods" (~L342–361). - Utila advertises both interfaces (Console-managed policy engine) but every one of their
5+4=9 methods throws
ProviderOperationNotSupportedException— advertised-but-stubbed, not a genuine partial.app-modules/rails/src/Utila/UtilaRailsProvider.php~L453–496. - No wallet co-signer/quorum-management endpoint found in the Bridge spec or wallets docs — mark
❔ rather than ✗ because the wallets docs describe
initiation_required/SCA metadata whose full semantics were not chased down this pass. Fetched 2026-09-02: https://apidocs.bridge.xyz/platform/wallets/move-money.md - Bridge custodial wallets support movement via Orchestration APIs, but no approver/vote model or transaction-list-with-approval-state resource is documented — UNVERIFIED whether a transaction read/list surface maps to this capability at all. Fetched 2026-09-02: https://apidocs.bridge.xyz/platform/wallets/overview.md
- Utila stubs
revokeRegisteredAddress; register/read/list are served.app-modules/rails/src/Utila/UtilaRailsProvider.php~L323–332. - Bridge liquidation addresses are a distinct concept — a permanent route FROM a blockchain
address TO a fiat or blockchain destination, not a withdrawal-address allow-list on a wallet.
Mapping liquidation addresses onto
ManagesRegisteredAddresseswould be a semantic stretch; treat as ◐ pending a real mapping exercise, not ✅. Fetched 2026-09-02: https://apidocs.bridge.xyz/platform/orchestration/liquidation_address/liquidation_address.md - Bridge virtual accounts exist but are transport-only: no page states a VA holds a balance,
and destinations are
currency/payment_rail/address/bridge_wallet_id— no fiat destination in the enum, i.e. auto-convert-to-crypto only. OurManagesVirtualAccountsmodels a balance-bearing account (Conduit shape) — a semantic mismatch, not a clean fit. Fetched 2026-09-02: https://apidocs.bridge.xyz/api-reference/virtual-accounts/create-a-virtual-account.md , https://apidocs.bridge.xyz/platform/orchestration/virtual_accounts/virtual-account.md - Sovera has no virtual-accounts or fiat-rails resource in its Crypto or Cards resource lists —
absent, not gapped-with-a-note.
packages/stables/sovera-sdk/README.md"Resources & methods" (~L342–361) and "Cards resources & methods" (~L507–555). - Bridge transfers (with a
developer_fee/conversion path) serve payout-shaped money movement to an external account; states/lifecycle documented. Fetched 2026-09-02: https://apidocs.bridge.xyz/platform/orchestration/transfers/transfer-states.md - Bridge external accounts (USD routing+account, EUR IBAN+BIC, MXN CLABE, BRL Pix key, GBP sort code, COP BreB key) are the closest analogue to a whitelist recipient, but there is no dedicated "recipient" resource separate from "external account" — a naming/shape difference, not a 1:1 capability match. Fetched 2026-09-02: https://apidocs.bridge.xyz/platform/orchestration/external-accounts/external-accounts-api.md
- Bridge transfer reads exist;
travel_rule_dataon transfers covers the sender-information piece, but the shape differs from our deposit-sidesubmitSenderInformation(Conduit-specific RFI flow) — same intent, different mechanics. Fetched 2026-09-02: https://apidocs.bridge.xyz/get-started/guides/move-money/crypto-travel-rule.md - Sovera has
transfers()(crypto wallet-to-wallet) andwithdrawals(), but no travel-rule sender-information submission surface documented.packages/stables/sovera-sdk/README.md"Resources & methods" (~L342–361). - Bridge has no first-class "order" resource: fiat-to-fiat conversion rides
POST /v0/transfers(the "Stablecoin Sandwich" — Bridge executes the FX at market rate and pays out on the destination rail), and a "fixed output" transfer lets the destination amount be specified upfront with no separate quote/lock step; cancellation is only documented for transfers still inawaiting_funds, and no requirements read exists — so cancel + requirements semantics of ourManagesOrdersare not matched. Fetched 2026-09-02: https://apidocs.bridge.xyz/get-started/guides/move-money/fiat_to_fiat.md , https://apidocs.bridge.xyz/get-started/guides/move-money/fixed_outputs_integration_guide.md , https://apidocs.bridge.xyz/platform/orchestration/transfers/transfer-states.md - Sovera
trading()->requestQuote, placeOrder— quote+order only, no get/list/cancel documented (ambiguity #8).packages/stables/sovera-sdk/README.md"Spec ambiguities appendix" (~L444).
Tally: Conduit 13✅ / 0◐ / 4✗ / 0❔ (full umbrella, none of the 4 segregated). Sumsub 5✅ / 0◐ / 12✗ / 0❔ (5 onboarding capabilities exactly). Utila 1✅ / 4◐ / 12✗ / 0❔ (custody-tx clean; wallets + registered-addresses genuinely partial; signers/quorum advertised-but-fully-stubbed). Fingerprint 1✅ / 0◐ / 16✗ / 0❔. SEON 1✅ / 0◐ / 16✗ / 0❔. Bridge 4✅ / 9◐ / 1✗ / 3❔ (✅ ManagesCustomers, VerifiesIdentity, FetchesVerification, ManagesPayouts; ✗ AssessesFraudRisk; ❔ ManagesWalletSigners, ManagesSigningQuorum, ManagesCustodyTransactions). Sovera 0✅ / 5◐ / 0✗ / 12❔. sFOX 0✅ / 0◐ / 0✗ / 17❔ (not assessed, by policy).
4. Live routing state
Grants realized by features:sync from app-modules/features/src/Providers/FeaturesServiceProvider.php
(~L145–193); all shipped data_region null (no residency enforcement — a documented pre-product
deferral, not an oversight):
| Jurisdictions | CapabilityGroup | Permitted keys | Primary | ADR |
|---|---|---|---|---|
| ZA, NG, KE, GB, EU, US | every group (baseline) | conduit | conduit | D94 |
| GB, EU | Onboarding | sumsub, conduit | sumsub | D95 |
| ZA, NG, KE, GB, EU, US | Custody | utila, conduit | utila | D98 |
| ZA, NG, KE, GB, EU, US | Fraud | fingerprint, seon | fingerprint | D102 |
Net effective primaries: Onboarding = sumsub (GB/EU) / conduit (ZA/NG/KE/US); Custody = utila
everywhere (conduit override-only — it lacks ManagesCustodyTransactions, so it is never an
automatic custody-tx fallback); Fraud = fingerprint everywhere (seon permitted; conduit demoted,
not permitted — it implements no fraud capability at all); FiatRails + Conversion + the 6
placeholder CapabilityGroup cases with no capability contract yet = conduit everywhere.
Fail-closed refusal reasons (ProviderNotRoutableException, 403, machine-readable reason,
app-modules/features/src/Support/ProviderRouter.php): seven reasons total —
blank_jurisdiction, inactive_jurisdiction, no_grant, override_not_permitted, no_primary,
residency_conflict (the grant's data_region doesn't match the jurisdiction's region) and
missing_dpa (a distinct reason: the grant's data_region is non-null and the residency check
passed, but its dpa_ref is empty) — both moot today since every grant ships data_region null —
plus BusProviderGuard's no_tenant_context. Never a silent fallback.
Routing is orthogonal to the money-path bus gates: every money-movement command also carries the
account-freeze gate and the D113 RequiresApprovedOnboarding gate
(decision log) regardless of which provider a capability routes to.
Override mechanism. tenant_provider_overrides (tenant-RLS, model
Modules\Features\Models\TenantProviderOverride): capability_group, provider_key,
valid_until (nullable). The router accepts an override only among the jurisdiction's permitted
keys for that group. No bus command, artisan command, or backoffice surface writes this table
today — rows are written directly (ops tooling only).
RequiresProvider marker coverage (bus-gated commands, grepped this pass): 12 command files —
9 accounts wallet/signer/quorum makers (CreateWallet, RotateWallet, AddSigner,
RemoveSigner, PromoteSigner, DemoteSigner, SetQuorumThreshold, OverrideWalletQuorum,
ClearWalletQuorumOverride), 2 onboarding makers (SubmitOnboardingApplicationCommand,
InitiateVerificationCommand), 1 payments maker (RegisterAddressCommand). The custody-controls
InitiateCustodyTransactionCommand deliberately carries no RequiresProvider (it dispatches
into the approval-quorum platform, not directly to rails); the same fail-closed check is
re-implemented off-bus, at real-forward time, in CustodyTransactionSubmitter via
ProviderGuard::ensureResolvable — a SYSTEM-context analogue of the marker, not a gap. FiatRails,
Conversion, and funding makers carry no gate yet — the bind-time resolver there is
fail-SAFE→default, not fail-closed. Built-vs-deferred detail:
docs/tracking/multi-provider/00-provider-router.md.
5. Webhook / event parity
| Vendor | Validator scheme | Events registered today | Consuming mirror domain(s) |
|---|---|---|---|
| Conduit | HMAC-SHA256, single header, ~300s replay tolerance | customer.*, application.*, wallet.*, virtual_account.*, signer.*, quorum.updated, account.freeze, transaction.* | onboarding, accounts, funding, payments |
| Sumsub | Two-header HMAC (X-Payload-Digest + -Alg, HMAC-256/512/1) | applicantCreated, applicantReviewed, applicantPending, applicantOnHold, applicantWorkflowCompleted, applicantWorkflowFailed, companyStructureChanged | onboarding (pull-authoritative — signal only, then re-pull) |
| Utila | Asymmetric RSASSA-PSS/SHA-512 | TRANSACTION_CREATED, TRANSACTION_STATE_UPDATED, TRANSACTION_AML_SCREENING_RESULT_READY, TEST | accounts custody_transactions mirror (pull-to-hydrate) |
| Fingerprint | Single-header HMAC-SHA256 | identification (synthesised type) | fraud (not a mirror domain — device-risk verdicts, no mirror table) |
| SEON | none (webhook-less, sync post-and-score) | — | — |
| Bridge | None built. Docs describe X-Webhook-Signature: t=<ts>,v0=<base64>, RSA+SHA-256 PKCS1v15 over timestamp.payload, reject >10min old, per-endpoint PEM public key. event_categories: customer, kyc_link, liquidation_address.drain, static_memo.activity, transfer, virtual_account.activity, bridge_wallet.activity, card_account, card_transaction, card_withdrawal, posted_card_account_transaction, external_account, rfi. Retry policy not stated on any fetched page — ❔ vendor question. Fetched 2026-09-02: https://apidocs.bridge.xyz/get-started/introduction/quick-start/setting-up-webhooks.md , https://apidocs.bridge.xyz/api-reference/webhooks/create-a-webhook-endpoint.md | none | |
| Sovera | None built. SDK models webhook CRUD + verify() only; event schemas undocumented by the vendor (packages/stables/sovera-sdk/README.md "Spec ambiguities appendix" #1, ~L425). | none |
Today the Conduit + Sumsub + Utila events above are consumed by onboarding, accounts, funding, and payments (payments builds on the same mirror framework for its payout/order mirrors); treasury, the fourth mirror-domain module, is table-less (rates only) and consumes no webhook. See Provider integration for the full pipeline
mechanics — not restated here.
6. Asset & chain coverage
| Vendor | Chains | Fiat rails | Stablecoins (where evidenced) |
|---|---|---|---|
Foundation Chain (our vocabulary) | 10: Ethereum, Polygon, Arbitrum, Base, Optimism, Avalanche, Solana, Tron, Stellar, Bitcoin (bsc deliberately excluded, D96 notes) | n/a (fiat rides Conduit/other adapters, not this enum) | n/a |
Utila (UtilaChainMap) | All 10, incl. Bitcoin → networks/bitcoin-mainnet | n/a | n/a (custody rail; asset ticker mapping via UtilaAssetMap, config-driven, fail-loud) |
| Conduit | ❔ UNVERIFIED — the mapper does Chain::tryFrom(...) ?? throw ProviderValidationException against the neutral enum; the exact set of chains the Conduit SDK itself advertises was not extracted this pass (would need the Conduit SDK's own chain enum, out of the rails module this page cites). | n/a | n/a |
| Fingerprint / SEON | n/a (fraud-scoring only, not chain/asset-bearing) | n/a | n/a |
| Bridge | 10+ chains, verbatim: EVM — Arbitrum, Avalanche C-Chain, Base, Celo, XDC, HyperEVM, Ethereum, Linea, Monad, Optimism, Polygon, World Chain; non-EVM — Solana, Stellar, Sui, Aptos, Tron, Plasma, Tempo. Bitcoin is NOT listed on any fetched page (chains page, payment-routes page, transfer payment_rail enum, liquidation-address chain enum, Bridge-wallet chain enum). Custodial wallets specifically: Base, Ethereum, Solana, Tempo, Tron (+xdc in the API enum). Fetched 2026-09-02: https://apidocs.bridge.xyz/get-started/guides/move-money/supported-blockchains.md , https://apidocs.bridge.xyz/get-started/introduction/what-we-support/payment-routes.md , https://apidocs.bridge.xyz/platform/wallets/overview.md | USD (ACH/FedNow/Wire), EUR (SEPA), MXN (SPEI), BRL (Pix), GBP (Faster Payments), COP (Bre-B + bank transfer). Fetched 2026-09-02: https://apidocs.bridge.xyz/get-started/introduction/what-we-support/payment-routes.md | USDC, USDT, USDB, PYUSD, EURC, USDP, USDG, CASH, USDSUI, USDCBL, PATHUSD ("USDC & EURC are the only stablecoins supported for users in the EEA"). Same source. |
| Sovera | ❔ UNVERIFIED-IN-REPO — no chain enum found in the SDK source; wallets are vault-network-keyed (vault().networks()), not chain-enum-keyed. packages/stables/sovera-sdk/README.md "Resources & methods" (~L354). | ❔ (no fiat-rails resource — see matrix §3 footnote 19) | ❔ |
| sFOX | ❔ not assessed | ❔ | ❔ |
7. Semantic deltas a tick hides
A ✅/◐ cell above says a vendor has a resource in the same neighborhood — not that it behaves the same way. These are the deltas worth knowing before routing anything real to a new vendor.
(a) Virtual accounts. In-app (Conduit): balance-bearing deposit accounts, mirrored in
accounts (the virtual_accounts mirror + the wholesale-overwritten account_balances
projection). Bridge: transport-only — "permanent, reusable fiat deposit addresses that convert
incoming fiat into crypto and deliver it to your specified destination"; destinations carry
currency/payment_rail/address/bridge_wallet_id, no fiat destination in the enum; no
page states a VA holds a balance. Fetched 2026-09-02:
https://apidocs.bridge.xyz/platform/orchestration/virtual_accounts/virtual-account.md ,
https://apidocs.bridge.xyz/api-reference/virtual-accounts/create-a-virtual-account.md . Sovera: ❔
(no VA resource — §3 footnote 19).
(b) Custody / co-sign. In-app: an N-of-M in-app approval-quorum engine (custody-controls,
D97) — amount-tiered N-eyes consensus among the tenant's own people, BEFORE the request ever
forwards to a provider; provider-agnostic by design. Conduit's older model was a
provider-hosted co-sign ceremony (a verificationUrl a human visits) — dropped in D96 because
it had no neutral representation once MPC vendors (Utila, and the Fireblocks/Fordefi set it was
evaluated against) proved signing is an API vote, not a hosted URL. Utila: signing is
voteOnTransaction, an API call, consistent with the D96 rewrite. Bridge: no
approver/co-signer/key-management model documented for custodial wallets; initiation_required
and an SCA initiation object exist but read as metadata, not an approval workflow. Fetched 2026-09-02:
https://apidocs.bridge.xyz/platform/wallets/move-money.md . Sovera: ❔.
(c) Documents vault. In-app (D43): Stables stores its own encrypted copy first, then
ForwardDocumentToProviderJob streams it to the rail via ManagesDocuments::uploadDocument
(Conduit only — Sumsub collects via its own WebSDK, so the forward is skipped for that route, not
500'd, per ProviderCapabilityInspector). Bridge: no upload endpoint at all — documents ride
as base64 fields (identifying_information[].image_front/image_back, documents[].file) on the
customer create/update payload, with a purposes enum. A different shape entirely — a
document-forward job built for Conduit's model would need a real rewrite, not a new translator.
Fetched 2026-09-02: https://apidocs.bridge.xyz/api-reference/customers/create-a-customer.md . Sovera: ❔.
(d) Travel rule. In-app: ManagesTransactions::submitSenderInformation (Conduit,
deposit-side, the funding module's RFI flow). Utila: registerAddress drops
originatorDetails + selfCustodyAttestation unconditionally, across all six granted
jurisdictions (D98's accepted regulatory risk; UtilaRailsProvider ~L207–269; remediation
tracked in docs/tracking/multi-provider/20-custody.md, not restated here). Bridge:
travel_rule_data attachable to transfers/VAs/liquidation addresses, or a one-off
POST /v0/travel_rule_data/:id; quoted thresholds — EEA "no threshold" on the rail itself but
"exceeding €1,000" triggers self/hosted-wallet attestation, US "above USD 3,000"; the FAQ states
Bridge will not hold or freeze funds for missing Travel Rule data. Fetched 2026-09-02:
https://apidocs.bridge.xyz/get-started/guides/move-money/crypto-travel-rule.md and its FAQ page.
(e) Developer fees. In-app: no such concept — fees are entitlement-driven (e.g. the payments
payments.payout-volume-fee meter), not a per-vendor mechanism. Bridge: a documented per-
transaction mechanism — dedicated ledger, monthly USD settlement on the 5th, developer_fee /
developer_fee_percent fields on transfers, developer_fee_percent/beta fee_config on VAs, an
account-default + per-address override on liquidation addresses. Fetched 2026-09-02:
https://apidocs.bridge.xyz/platform/orchestration/fees-and-mins/devfees.md . Sovera/sFOX: ❔
(sFOX's README headline lists configurable per-user fees — packages/stables/sfox-sdk/README.md
L16; capability mapping not assessed this pass).
(f) Onboarding subject / eligibility. In-app (D110): the client declares the onboarding
SUBJECT explicitly — individual|business, no silent default. Bridge: type is
individual|business on customer create; the high_risk_activities enum verbatim lists
gambling as an enumerated high-risk activity, but no fetched page states whether a
gambling-flagged business is eligible to onboard at all — UNVERIFIED, a vendor question, not
an assumption either way. Fetched 2026-09-02:
https://apidocs.bridge.xyz/api-reference/customers/create-a-customer.md . Sovera: business
end-user creation is undocumented entirely (§3 footnote 4).
8. Dormant SDK inventory + maintenance
bridge-sdk (packages/stables/bridge-sdk/README.md) — pure Saloon v4 transport, no domain
logic, no app consumer. 24 tag resources (README "Resources", L129–159: Cards, Customers,
AssociatedPersons, KycLinks, Transfers, ExternalAccounts, Plaid, VirtualAccounts, StaticMemos,
PrefundedAccounts, LiquidationAddresses, Wallets, Webhooks, Developers, ApiKeys,
FiatPayoutConfiguration, Sandbox, CryptoReturnPolicies, Rewards, Lists, BatchSettlements,
FundsRequests, TravelRule, ExchangeRates). README states "128 operations across 24 tag
resources" (README L15); an independent grep of the committed spec/openapi.json this pass
(grep -oE '"(get|post|put|patch|delete|head|options)":' spec/openapi.json | wc -l) agrees: 128.
spec/openapi.json snapshot last touched 2026-09-02 (README
"Known live drift" note, L296–299 — a real drift-loop exercise already absorbed two schema
changes). spec/exclusions.json is empty ([]). D114 drift loop: make bridge-spec-diff [ARGS="--update"]; tests/SpecCoverageTest.php reddens only on added/removed operations, never
on changed-shape or schema-only drift (README "Tracking upstream API changes", L260–299). Inbound
webhook ingestion is design-only — README "Inbound webhooks — the future ingestion slice
(design, not built)" (L317). KYC-error redaction note: exception messages from customer/KYC
endpoints can echo submitted field values back — route through this app's redaction/DLP path
before logging (README "Error handling").
sovera-sdk (packages/stables/sovera-sdk/README.md) — two connectors, one facade: Crypto
(Stables\Sovera\Crypto\*, ~50 endpoints, 3 auth tiers — SX Customers session, SX Connect
x-api-key, SX Users bearer) and Cards (Stables\Sovera\Cards\*, ~45 endpoints, single x-api-key).
No committed OpenAPI spec — hand-authored against a vendor document cited as "spec §1…§9", not
itself in the repo. "Spec ambiguities appendix" (README L420) and "Cards spec ambiguities
appendix" (L600) carry the gaps: webhook event schemas undocumented, account_type enum conflict
kept as plain string, a users/ path oddity in trading, a wallet-balance response shape conflict,
/currency-pairs referenced but unspecified, fiat withdrawals with error codes but no endpoint,
production base URL undocumented (defaults sandbox). Open vendor item: business end-user creation
is "not a documented flow" (README L294–299) — an open integration-team question with the vendor.
sfox-sdk (packages/stables/sfox-sdk/README.md) — Saloon connector for sFOX Connect
(partner master account + end-user sub-accounts, trading, custody, funding, withdrawals,
transfers, per-user fees; REST + a WebSocket message layer). Pure plumbing, no adapter, no
consumer. ADR D111 covers only its ingestion posture (REST polling first) — not a capability
commitment. Not assessed against the capability matrix this pass (§3, §6 both ❔ throughout).
Turning any of these three dormant SDKs into a routed provider means writing an adapter behind
the same neutral capability seam this page's §2–§4 describe — see the
multi-provider-development
skill
for the seam's conventions (adapter/RailsProvider capability interfaces, exception translation,
ProviderAdapterRegistry, the router).
9. Questions for the vendor
Bridge:
- Is there a BTC roadmap? Every fetched chain-related page omits it entirely.
- What is the webhook retry schedule, and what auto-disables an endpoint? (resolves §5's Bridge webhook row retry-policy ❔)
- What is the wallet signing/key model, and is there any customer-side approver/quorum concept? (resolves §3 rows ManagesWalletSigners/ManagesSigningQuorum/ManagesCustodyTransactions and §7(b))
- Does the DocSend "Bridge Business Onboarding (KYB) Guide" fully replace manual document
submission, or is
documents[]a subset? (resolves §3 ManagesDocuments footnote 6) - Is there a per-country onboarding-requirements read (an analogue of our
getOnboardingRequirements)? (resolves §3 OnboardsCustomers footnote 1) - Can a virtual-account destination ever be fiat, or is auto-convert-to-crypto the only mode? (resolves §7(a))
- Which endorsements gate which rails for a business, and is a
gambling-flagged business eligible to onboard at all? (resolves §7(f)) - What is the
Idempotency-Keyretention window, and what is the GA status offee_config/ non-USD fee settlement?
Sovera:
- How does a business (non-individual) end user get created on the Crypto side? (resolves §3 footnote 2, §7(f))
- What are the webhook event schemas (types, payload shapes, signature scheme, retry policy)? (resolves §5's Sovera webhook row)
- Is there a virtual-accounts or fiat-rails resource at all, on either Crypto or Cards? (resolves §3 footnote 19, §6, §7(a))
- What is the production base URL, and how are rate-limit headers named?
- Are trading list/cancel and fiat withdrawal endpoints planned?
sFOX: not assessed this pass — the first open question is simply which of the 17 capabilities it can serve at all; no matrix cell here should be treated as anything but ❔ until that pass runs.
Routed-vendor unknowns: Conduit's exact supported-chain list was not extracted from the SDK this pass (§6) — worth resolving before citing Conduit chain coverage as complete anywhere else.
10. Re-verification checklist
Before trusting a cell on this page:
- Re-read the adapter class declaration (
implementslist) for any routed-vendor cell —app-modules/rails/src/{Conduit,Sumsub,Utila,Fingerprint,Seon}/*RailsProvider.php. - Re-check the
assignIn/registerConduitBaselinelines inapp-modules/features/src/Providers/FeaturesServiceProvider.phpfor §4's grant table — a new grant changes the "net effective primaries" summary. - Re-run
make bridge-spec-difffor any Bridge shape claim, and re-fetch the specificapidocs.bridge.xyzpage cited for any Bridge semantic claim — both can drift independently. - Re-read
packages/stables/sovera-sdk/README.md's ambiguities appendices for Sovera cells — they are pending the vendor's own spec, not resolvable from this repo alone. - Re-grep
RequiresProviderimplementers if a new provider-sensitive command ships, before restating §4's coverage count.