Skip to main content

D98 — Utila custody go-live — Custody→utila (all six), no Conduit fallback, accepted travel-rule originator drop (RESOLVED)

Architecture decision record. Status, thematic clusters, and how to record a new ADR: the decision log index. Program context + the custody build plan (the increment ladder, the go-live runbook, the unblock criteria) live in docs/tracking/multi-provider/20-custody.md §5a.

Custody flips to Utila across the whole corridor. After the entire provider path shipped registered-not-granted (2.2a–2.2f-3 "d-prep" — the adapter, the mirror, the webhooks, the Stage-0 approval quorum + the real forward, the asset map, the signer/quorum retirement), the go-live grant makes Utila the Custody PRIMARY for all six conduit-baseline jurisdictions (ZA/NG/KE/GB/EU/US). This is the second worked instance of the D95 one-liner (Sumsub Onboarding), and — unlike D95's two-jurisdiction corridor — it is a fleet-wide flip of a single capability group: every seeded jurisdiction's custody traffic now routes to Utila, conduit everything else.

The mechanism is one declaration. In FeaturesServiceProvider::registerFeatureDeclarations(), after registerConduitBaseline([...]) (which routes every CapabilityGroup to conduit) and the D95 Onboarding line, one later line overrides only the (*, Custody) tuples: $providers->assignIn(['ZA','NG','KE','GB','EU','US'], CapabilityGroup::Custody, ['utila','conduit'], 'utila'). features:sync realizes it into jurisdiction_provider_grants (central; no tenant-scoped migration — the flip is config, not schema). Every other capability group in those jurisdictions stays conduit-primary — the flip is Custody-only. Deploy, not code, is the live gate: the grant is a one-line switch; the runbook (20-custody.md §5a) enumerates the deploy steps that make it behave — vault provisioning + per-vault SA secrets, the config/utila.php assets contract-address publish, features:sync, and the sandbox HARD/soft gates.

No Conduit fallback — conduit is an override-only permitted key. conduit is listed as a PERMITTED (non-primary) key so a per-tenant tenant_provider_overrides back to Conduit needs no code change (the escape hatch for legacy Conduit-modeled signer/quorum/wallet flows). It is NOT an automatic custody-transaction fallback (the locked 2026-07-11 decision) — and it could not be one anyway: the Custody group is co-dependent (all five capabilities resolve to ONE provider) and Conduit does not implement ManagesCustodyTransactions, so a custody-transaction under a Conduit override fails loud rather than silently mis-routing money. This is why the PRIMARY must be a custody-tx-capable provider: only utila implements ManagesCustodyTransactions, so a non-tx primary would fail-loud AdapterMissingCapability (500) at the per-capability bind (20-custody.md §5a note (a)). Utila as primary satisfies that invariant.

data_region shipped null — residency is a production gate, not a code guarantee. Utila's data residency is EU per the vendor, but the grant carries no data_region/dpa_ref, exactly as D95 shipped. This is forced by the same structural limitation: the EU bloc jurisdiction has region = null (ISO region data covers member countries, not supranational blocs), and ProviderRouter::assertResidency refuses when the jurisdiction's region ≠ the grant's data_region — so a non-null data_region='Europe' on the EU-bloc grant would residency_conflict and refuse EU routing outright. Binding a real residency region (against the resolved member country's region, or by giving blocs a region — the deferred D95 fix) plus recording the DPA is a documented pre-production go-live gate, not a blocker for the flip.

Accepted regulatory risk — the travel-rule field drop (option C). Utila's address-book create (registerAddress) accepts only address/displayName/network, so three neutral travel-rule inputs are not transmitted to Utila's screening, in two kinds: selfCustodyAttestation + originatorDetails are DROPPED ENTIRELY (never transmitted, not retained), while custodyType is RETAINED LOCALLY (carried into our records via mapRegisterResult, so not lost from our side) but is still NOT transmitted — the self-hosted/VASP custody classification is itself a travel-rule determinant. The drop is UNCONDITIONAL across all six granted jurisdictions (ZA/NG/KE/GB/EU/US) — not conditioned on jurisdiction: EU/GB/US are the acute registration-mandate cases, while ZA (FSCA)/NG/KE have emerging FATF travel-rule regimes and are not unaffected. This is a live, knowingly ACCEPTED exposure per the 2026-07-11 owner decision — recorded honestly, not silently omitted, and made visible at the drop point (UtilaRailsProvider::registerAddress) and in the rails/payments READMEs. It is not a resolved gap: the remediation — local originator capture at registration + a fail-closed registration-mandate guard that refuses a Utila-routed registration for a mandate jurisdiction until captured — is DEFERRED and TRACKED as a go-live carry-over (20-custody.md §5a criterion (6)). Remediation owner: TBD — a named compliance approver + remediation owner is recorded before serving real EU/GB/US custody traffic.

Verify-in-place, pre-product. The flip is verified in place (no data migration; pre-product — the freeze is at the v1 guard): the grant is real, gate-green, and routes a corridor tenant's Custody to Utila in dev/test, driving the full money path — InitiateCustodyTransactionCommand → Stage-0 quorum → the SYSTEM forward (asset mapped via UtilaAssetMap, initiate + APPROVE co-sign on the real Utila endpoints, ref stamped, approved → forwarded). It is not serving real production custody movement until the deploy-time write-path steps land (20-custody.md §5a). One deploy-time subtlety recorded for the flip: appending the transaction tenant resolver in d-prep un-darkened INBOUND Utila transaction resolution at DEPLOY, not at the flip — safe only because no Utila wallet is mapped and no Utila transaction webhook arrives until go-live.

This decision builds on the router substrate (D94), the first non-Conduit grant (D95), the ceremony-less custody contract (D96), and the custody approval-quorum engine (D97); it does not restate them.


← Decision log index