feat: identify the VASP that custodies a crypto wallet account - #829
Conversation
|
Preview deployment for your docs. Learn more about Mintlify Previews.
|
|
The latest updates on your projects. Learn more about Vercel for GitHub. 2 Skipped Deployments
|
✱ Stainless preview builds for gridThis PR will update the cli go kotlin openapi php python ruby typescript
|
# Conflicts: # mintlify/openapi.yaml # openapi.yaml # openapi/components/schemas/errors/Error400.yaml
## Summary Crypto-wallet external accounts were the only account family without a `beneficiary`. This PR adds one, following the fiat pattern — an `INDIVIDUAL`/`BUSINESS` `oneOf` discriminated by `beneficiaryType`. The beneficiary identifies who owns the wallet — the counterparty identity needed for Travel Rule data, independent of custody. This is PR 2 of 3 for VASP counterparty support (Travel Rule): 1. #827 — `/vasps` directory 2. **This PR** — `beneficiary` on crypto-wallet external accounts 3. #829 — `custodyType`/`vaspName` on external accounts ### Field requirements: exactly what is transmitted The individual variant is a new `WalletIndividualBeneficiary` with only **`fullName` + `countryOfResidence`** (both required) — the exact set transmitted as Travel Rule counterparty info. The generic `IndividualBeneficiary` couldn't be reused because it *requires* `birthDate`/`nationality`, which are never transmitted for wallets and would force platforms to collect a third party's date of birth (OpenAPI composition can't relax `required`). Optional PII fields were deliberately omitted: adding optional fields later is non-breaking, while accepting-but-ignoring PII invites needless collection. The `BUSINESS` variant reuses the existing `BusinessBeneficiary` (`legalName` required). ### Semantics (one deliberate divergence from fiat) - Fiat accounts require `beneficiary`. For wallets it is **optional for `FIRST_PARTY`** — when omitted, the customer's verified identity is used, so the dominant own-wallet case sends nothing extra. - **Required for `THIRD_PARTY`** wallets on platforms subject to counterparty requirements (e.g., EU Travel Rule and similar regimes) — enforced at runtime with `400 INVALID_INPUT`, not in the schema, since the requirement is platform-dependent. ### Changes - New `WalletBeneficiaryFields` fragment (the `beneficiary` property) composed into all seven wallet variants: `BASE_WALLET`, `ETHEREUM_WALLET`, `POLYGON_WALLET`, `PLASMA_WALLET`, `SOLANA_WALLET`, `SPARK_WALLET`, `TRON_WALLET` - New `WalletBeneficiaryOneOf` — the named individual/business union (matches the `*OneOf` house convention) - New `WalletIndividualBeneficiary` schema (`fullName` + `countryOfResidence`) - Stainless model entries for all three ### Out of scope - `LIGHTNING` external accounts — Travel Rule counterparty identity for Lightning flows in-band (payment-level), not via a stored account beneficiary. Flagging in case reviewers feel otherwise. ## Testing `make build` bundles cleanly; `redocly lint` and `spectral lint` match the pre-existing baseline on `main` exactly (no new findings). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Summary
Adds a **VASP directory**: `GET /vasps`, cursor-paginated. Each entry is
`{ vaspName, url }` — `vaspName` is the value a platform passes back
when declaring a VASP-hosted counterparty, and `url` lets a picker UI
confirm the right entity.
This is PR 1 of 3 for VASP counterparty support (Travel Rule):
1. **This PR** — `/vasps` directory
2. #828 — `beneficiary` on crypto-wallet external accounts
3. #829 — `custodyType`/`vaspName` on external accounts
### Shape decisions
- **No `search` param.** Matches the sibling directory endpoints
(`/discoveries`, `/uma-providers`), which are paginated and filtered
client-side. Integrators can cache the directory and search it locally.
- **Names, not opaque IDs.** Consistent with `/discoveries`, where the
returned `bankName` is the value passed back on account creation. The
provider's declare-counterparty call takes only its own identifier, so
the exposed name is a Grid-side key that the backend maps back — which
also keeps the surface portable if the provider set changes.
- **A single name field.** The provider returns both a common name and a
registered legal name; the legal name earns its place only as a
uniqueness tiebreaker, and it is worse for display ("Payward, Inc." for
Kraken). Ships one recognizable name plus `url`.
- **No `totalCount`.** The upstream search returns no total, so it could
never be populated.
### Changes
- `GET /vasps` (`limit`, `cursor`) under a new **VASPs** tag
- `Vasp` and `VaspListResponse` schemas
- Stainless `vasps` resource block (`list` method) so the endpoint flows
into the documented spec and SDKs
- Sidebar icon (globe) for the VASPs group in the Mintlify API reference
## Testing
`make build` bundles cleanly; `redocly lint` and `spectral lint` match
the pre-existing baseline on `main` exactly (no new findings).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude <noreply@anthropic.com>
They were on the shared account schemas, where they applied to 38 fiat variants and Lightning. Moves them into a WalletCustodyFields fragment composed into the seven wallet variants, matching how the wallet beneficiary is scoped.
# Conflicts: # mintlify/openapi.yaml # openapi.yaml # openapi/components/schemas/external_accounts/BaseWalletExternalAccountInfo.yaml # openapi/components/schemas/external_accounts/EthereumWalletExternalAccountInfo.yaml # openapi/components/schemas/external_accounts/PlasmaWalletExternalAccountInfo.yaml # openapi/components/schemas/external_accounts/PolygonWalletExternalAccountInfo.yaml # openapi/components/schemas/external_accounts/SolanaWalletExternalAccountInfo.yaml # openapi/components/schemas/external_accounts/SparkWalletExternalAccountInfo.yaml # openapi/components/schemas/external_accounts/TronWalletExternalAccountInfo.yaml
|
custodyType was redundant with vaspName and made two contradictory states expressible (VASP_HOSTED without a name, SELF_CUSTODY with one). Presence of vaspName now means the wallet is VASP-held. Defers the transfer-time beneficiary error code.
Greptile SummaryAdds an optional
Confidence Score: 4/5The PR should not merge until the custody marker rejects empty values that cannot identify a VASP. The creation contract currently accepts a present but empty Files Needing Attention: openapi/components/schemas/external_accounts/WalletCustodyFields.yaml
|
| Filename | Overview |
|---|---|
| openapi/components/schemas/external_accounts/WalletCustodyFields.yaml | Introduces the presence-based custody marker but permits an empty VASP name. |
| openapi/components/schemas/external_accounts/EthereumWalletExternalAccountInfo.yaml | Composes the custody fragment into the Ethereum wallet account shape consistently with the other wallet variants. |
| .stainless/stainless.yml | Registers the custody fragment for SDK generation using the established model-mapping convention. |
| openapi.yaml | Regenerated bundle consistently exposes the new fragment across all seven intended wallet variants. |
| mintlify/openapi.yaml | Mirrors the regenerated root OpenAPI bundle for documentation consumption. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
A[External account create request] --> B{vaspName present?}
B -- No --> C[Self-custody wallet]
C --> D[Ownership verification may apply]
B -- Yes --> E[VASP-hosted wallet]
E --> F[Record named custodian]
Prompt To Fix All With AI
### Issue 1
openapi/components/schemas/external_accounts/WalletCustodyFields.yaml:3-4
**Empty VASP name breaks custody classification**
When a client creates a wallet with `vaspName: ""`, the schema accepts a present value that names no VASP, causing the account either to be misclassified as VASP-hosted or to fail undocumented server-side validation.
```suggestion
vaspName:
type: string
minLength: 1
```
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (1): Last reviewed commit: "Infer custody from vaspName and drop the..." | Re-trigger Greptile
| vaspName: | ||
| type: string |
There was a problem hiding this comment.
Empty VASP name breaks custody classification
When a client creates a wallet with vaspName: "", the schema accepts a present value that names no VASP, causing the account either to be misclassified as VASP-hosted or to fail undocumented server-side validation.
| vaspName: | |
| type: string | |
| vaspName: | |
| type: string | |
| minLength: 1 |
Prompt To Fix With AI
This is a comment left during a code review.
Path: openapi/components/schemas/external_accounts/WalletCustodyFields.yaml
Line: 3-4
Comment:
**Empty VASP name breaks custody classification**
When a client creates a wallet with `vaspName: ""`, the schema accepts a present value that names no VASP, causing the account either to be misclassified as VASP-hosted or to fail undocumented server-side validation.
```suggestion
vaspName:
type: string
minLength: 1
```
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
Summary
Lets a platform declare that a crypto-wallet external account is held at an exchange rather than self-custodied — the missing counterparty dimension for Travel Rule support.
Adds a single field,
vaspName, to the seven crypto wallet account types. Its presence means the wallet is held at that VASP; its absence means self-custody. Ownership verification (#806) applies only to self-custody wallets.This is the last of three PRs for VASP counterparty support (Travel Rule), after #827 (
/vaspsdirectory, merged) and #828 (beneficiaryon wallet accounts, merged).Why one field instead of a
custodyTypeenumAn earlier revision had
custodyType: SELF_CUSTODY | VASP_HOSTEDalongsidevaspName. That was two fields for one fact, and it made two contradictory states expressible —VASP_HOSTEDwith no name, andSELF_CUSTODYwith one — which then need validating. Inferring custody from presence makes both unrepresentable.Scoping
vaspNamelives in aWalletCustodyFieldsfragment composed into the seven wallet variants, not on the shared account schemas — it is meaningless for the 38 fiat account types. Same approach as the walletbeneficiaryin #828.It stays orthogonal to
ownershipType: your own exchange account isFIRST_PARTYwith avaspName; a friend's self-custody wallet isTHIRD_PARTYwith none.Deferred
COUNTERPARTY_USER_INFO_REQUIREDused vocabulary that doesn't appear anywhere else in the Grid schema; leaving it out until the naming and the creation-time-vs-transfer-time enforcement question are settled.LIGHTNINGaccounts are excluded, consistent with feat: add beneficiary to crypto wallet external accounts #828.Testing
make buildbundles cleanly;redocly lintandspectral lintmatch the pre-existing baseline onmainexactly (no new findings).🤖 Generated with Claude Code