feat: add beneficiary to crypto wallet external accounts - #828
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 ✅ grid-ruby studio · code
|
# Conflicts: # .stainless/stainless.yml
…iaries Matches what is actually transmitted for Travel Rule counterparty info (name + country of residence) instead of the generic beneficiary's birthDate/nationality requirements, which are not transmitted and would force platforms to collect a third party's date of birth.
Greptile SummaryThe PR adds an optional, discriminated individual-or-business beneficiary to all seven non-Lightning crypto-wallet external-account variants and registers the new models for SDK generation.
Confidence Score: 5/5The PR appears safe to merge with no concrete blocking or independently actionable non-blocking issues identified. The create-request unions reach all seven changed wallet schemas, the discriminator mappings resolve consistently in the generated bundles, and the intentionally optional and excluded cases match the documented scope.
|
| Filename | Overview |
|---|---|
| openapi/components/schemas/common/WalletIndividualBeneficiary.yaml | Defines the wallet-specific individual beneficiary with the intended required identity fields and optional supporting details. |
| openapi/components/schemas/external_accounts/WalletBeneficiaryOneOf.yaml | Adds an internally consistent discriminated union whose mappings resolve correctly in the generated bundles. |
| openapi/components/schemas/external_accounts/WalletBeneficiaryFields.yaml | Defines the deliberately optional beneficiary property and documents the platform-dependent third-party requirement. |
| .stainless/stainless.yml | Registers all three new wallet beneficiary schemas as generated external-account SDK models. |
| openapi.yaml | The generated root bundle consistently includes the new schemas and all seven wallet compositions. |
| mintlify/openapi.yaml | The generated Mintlify bundle remains synchronized with the root OpenAPI bundle. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
C[External-account create request] --> U[ExternalAccountCreateInfoOneOf]
U --> W[Wallet external-account variant]
W --> F[WalletBeneficiaryFields]
F --> B{beneficiaryType}
B -->|INDIVIDUAL| I[WalletIndividualBeneficiary]
B -->|BUSINESS| O[BusinessBeneficiary]
W --> G[Bundled OpenAPI]
G --> S[Stainless-generated SDK models]
Reviews (1): Last reviewed commit: "Name the wallet beneficiary union Wallet..." | Re-trigger Greptile
| propertyName: beneficiaryType | ||
| mapping: | ||
| INDIVIDUAL: ../common/WalletIndividualBeneficiary.yaml | ||
| BUSINESS: ../common/BusinessBeneficiary.yaml |
There was a problem hiding this comment.
does this mean there'll be a business beneficiary as well?
There was a problem hiding this comment.
Er I mean is there a individual beneficiary we can resuse
There was a problem hiding this comment.
Huh i guess not I see a bunch of different types of beneficiaries depending on the country but not a generic one.
Do you think this one will be generic or is it only for wallets?
There was a problem hiding this comment.
yeah we went through the exercise of splitting up beneficiaries for individuals based on region reqs but we havent done that for business yet -- so reusing the existing business one and added a new one just for wallets w the minimum requirements
Drops the never-transmitted optional PII (birthDate, nationality, email, phoneNumber, address) from WalletIndividualBeneficiary — optional fields can be added back without breaking; collected-but-unused PII cannot be removed as easily. Documents the 400 INVALID_INPUT on missing required beneficiary.
## 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>
## 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 (`/vasps` directory, merged) and #828 (`beneficiary` on wallet accounts, merged). ### Why one field instead of a `custodyType` enum An earlier revision had `custodyType: SELF_CUSTODY | VASP_HOSTED` alongside `vaspName`. That was two fields for one fact, and it made two contradictory states expressible — `VASP_HOSTED` with no name, and `SELF_CUSTODY` with one — which then need validating. Inferring custody from presence makes both unrepresentable. ### Scoping `vaspName` lives in a `WalletCustodyFields` fragment 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 wallet `beneficiary` in #828. It stays orthogonal to `ownershipType`: your own exchange account is `FIRST_PARTY` with a `vaspName`; a friend's self-custody wallet is `THIRD_PARTY` with none. ### Deferred - A transfer-time error code for a destination missing beneficiary information. The earlier `COUNTERPARTY_USER_INFO_REQUIRED` used 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. - `LIGHTNING` accounts are excluded, consistent with #828. ## 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
Crypto-wallet external accounts were the only account family without a
beneficiary. This PR adds one, following the fiat pattern — anINDIVIDUAL/BUSINESSoneOfdiscriminated bybeneficiaryType.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):
/vaspsdirectorybeneficiaryon crypto-wallet external accountscustodyType/vaspNameon external accountsField requirements: exactly what is transmitted
The individual variant is a new
WalletIndividualBeneficiarywith onlyfullName+countryOfResidence(both required) — the exact set transmitted as Travel Rule counterparty info. The genericIndividualBeneficiarycouldn't be reused because it requiresbirthDate/nationality, which are never transmitted for wallets and would force platforms to collect a third party's date of birth (OpenAPI composition can't relaxrequired). Optional PII fields were deliberately omitted: adding optional fields later is non-breaking, while accepting-but-ignoring PII invites needless collection. TheBUSINESSvariant reuses the existingBusinessBeneficiary(legalNamerequired).Semantics (one deliberate divergence from fiat)
beneficiary. For wallets it is optional forFIRST_PARTY— when omitted, the customer's verified identity is used, so the dominant own-wallet case sends nothing extra.THIRD_PARTYwallets on platforms subject to counterparty requirements (e.g., EU Travel Rule and similar regimes) — enforced at runtime with400 INVALID_INPUT, not in the schema, since the requirement is platform-dependent.Changes
WalletBeneficiaryFieldsfragment (thebeneficiaryproperty) composed into all seven wallet variants:BASE_WALLET,ETHEREUM_WALLET,POLYGON_WALLET,PLASMA_WALLET,SOLANA_WALLET,SPARK_WALLET,TRON_WALLETWalletBeneficiaryOneOf— the named individual/business union (matches the*OneOfhouse convention)WalletIndividualBeneficiaryschema (fullName+countryOfResidence)Out of scope
LIGHTNINGexternal 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 buildbundles cleanly;redocly lintandspectral lintmatch the pre-existing baseline onmainexactly (no new findings).🤖 Generated with Claude Code