Skip to content

feat: identify the VASP that custodies a crypto wallet account - #829

Merged
shreyav merged 6 commits into
mainfrom
shreyav/wallet-custody-type
Aug 20, 2026
Merged

feat: identify the VASP that custodies a crypto wallet account#829
shreyav merged 6 commits into
mainfrom
shreyav/wallet-custody-type

Conversation

@shreyav

@shreyav shreyav commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

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 feat: add beneficiary to crypto wallet external accounts #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

@mintlify

mintlify Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
Grid 🟢 Ready View Preview Aug 13, 2026, 9:16 PM

@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
grid-flow-builder Ignored Ignored Preview Aug 20, 2026 12:33am
grid-wallet-demo Ignored Ignored Preview Aug 20, 2026 12:33am

Request Review

@github-actions github-actions Bot added the breaking-change Introduces a breaking change to the OpenAPI spec label Aug 13, 2026
@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

✱ Stainless preview builds for grid

This PR will update the grid SDKs with the following commit messages.

cli

feat(api): add custody_type and vasp_id to agent/customer external accounts

go

feat(api): add custody_type and vasp_id fields to external accounts

kotlin

feat(api): add custodyType and vaspId fields to external accounts

openapi

feat(api): add custodyType/vaspId to external accounts, error code

php

feat(api): add custodyType and vaspID to external accounts

python

feat(api): add custody_type and vasp_id to external accounts

ruby

feat(api): add custody_type/vasp_id fields and CustodyType enum to external accounts

typescript

feat(api): add custodyType and vaspId fields to external accounts
⚠️ grid-openapi studio · code

Your SDK build had at least one "warning" diagnostic.
generate ⚠️

⚠️ grid-ruby studio · code

Your SDK build had at least one "warning" diagnostic.
generate ⚠️build ✅lint ✅test ✅

⚠️ grid-go studio · code

Your SDK build had a failure in the lint CI job, which is a regression from the base state.
generate ⚠️build ⏭️lint ❗test ❗

go get github.com/stainless-sdks/grid-go@4ffde45de8d88d5f5e956e65c017a9744deed169
⚠️ grid-kotlin studio · code

Your SDK build had a failure in the test CI job, which is a regression from the base state.
generate ⚠️build ✅lint ✅test ❗

⚠️ grid-typescript studio · conflict

Your SDK build had at least one warning diagnostic.

⚠️ grid-python studio · code

Your SDK build had a failure in the lint CI job, which is a regression from the base state.
generate ⚠️build ✅lint ❗test ❗

pip install https://pkg.stainless.com/s/grid-python/3f6ba36b0fd4df69659fea4e4388de15315927ad/grid-0.0.1-py3-none-any.whl
⚠️ grid-php studio · code

Your SDK build had at least one "warning" diagnostic.
generate ⚠️lint ✅test ✅

⚠️ grid-cli studio · code

Your SDK build had a failure in the test CI job, which is a regression from the base state.
generate ⚠️build ⏭️lint ⏭️test ❗


This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push.
If you push custom code to the preview branch, re-run this workflow to update the comment.
Last updated: 2026-08-20 04:22:49 UTC

# Conflicts:
#	mintlify/openapi.yaml
#	openapi.yaml
#	openapi/components/schemas/errors/Error400.yaml
@github-actions github-actions Bot removed the breaking-change Introduces a breaking change to the OpenAPI spec label Aug 14, 2026
shreyav added a commit that referenced this pull request Aug 14, 2026
## 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)
shreyav added a commit that referenced this pull request Aug 19, 2026
## 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
@github-actions github-actions Bot added the breaking-change Introduces a breaking change to the OpenAPI spec label Aug 20, 2026
@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

⚠️ Breaking OpenAPI changes detected

oasdiff reports 28 error / 0 warning changes to openapi.yaml.
This PR will need approval from an API reviewer before merge.

Errors (28)

  • POST /agents/me/external-accounts — added #/components/schemas/WalletCustodyFields to the allOf[subschema #1]/accountInfo/oneOf[subschema #39: Base Wallet]/ request property allOf list [request-property-all-of-added].
  • POST /agents/me/external-accounts — added #/components/schemas/WalletCustodyFields to the allOf[subschema #1]/accountInfo/oneOf[subschema #40: Ethereum L1 Wallet]/ request property allOf list [request-property-all-of-added].
  • POST /agents/me/external-accounts — added #/components/schemas/WalletCustodyFields to the allOf[subschema #1]/accountInfo/oneOf[subschema #42: Polygon Wallet]/ request property allOf list [request-property-all-of-added].
  • POST /agents/me/external-accounts — added #/components/schemas/WalletCustodyFields to the allOf[subschema #1]/accountInfo/oneOf[subschema #43: Plasma Wallet]/ request property allOf list [request-property-all-of-added].
  • POST /agents/me/external-accounts — added #/components/schemas/WalletCustodyFields to the allOf[subschema #1]/accountInfo/oneOf[subschema #44: Solana Wallet]/ request property allOf list [request-property-all-of-added].
  • POST /agents/me/external-accounts — added #/components/schemas/WalletCustodyFields to the allOf[subschema #1]/accountInfo/oneOf[subschema #45: Spark Wallet]/ request property allOf list [request-property-all-of-added].
  • POST /agents/me/external-accounts — added #/components/schemas/WalletCustodyFields to the allOf[subschema #1]/accountInfo/oneOf[subschema #46: Tron Wallet]/ request property allOf list [request-property-all-of-added].
  • POST /customers/external-accounts — added #/components/schemas/WalletCustodyFields to the allOf[subschema #1]/accountInfo/oneOf[subschema #39: Base Wallet]/ request property allOf list [request-property-all-of-added].
  • POST /customers/external-accounts — added #/components/schemas/WalletCustodyFields to the allOf[subschema #1]/accountInfo/oneOf[subschema #40: Ethereum L1 Wallet]/ request property allOf list [request-property-all-of-added].
  • POST /customers/external-accounts — added #/components/schemas/WalletCustodyFields to the allOf[subschema #1]/accountInfo/oneOf[subschema #42: Polygon Wallet]/ request property allOf list [request-property-all-of-added].
  • POST /customers/external-accounts — added #/components/schemas/WalletCustodyFields to the allOf[subschema #1]/accountInfo/oneOf[subschema #43: Plasma Wallet]/ request property allOf list [request-property-all-of-added].
  • POST /customers/external-accounts — added #/components/schemas/WalletCustodyFields to the allOf[subschema #1]/accountInfo/oneOf[subschema #44: Solana Wallet]/ request property allOf list [request-property-all-of-added].
  • POST /customers/external-accounts — added #/components/schemas/WalletCustodyFields to the allOf[subschema #1]/accountInfo/oneOf[subschema #45: Spark Wallet]/ request property allOf list [request-property-all-of-added].
  • POST /customers/external-accounts — added #/components/schemas/WalletCustodyFields to the allOf[subschema #1]/accountInfo/oneOf[subschema #46: Tron Wallet]/ request property allOf list [request-property-all-of-added].
  • POST /platform/external-accounts — added #/components/schemas/WalletCustodyFields to the accountInfo/oneOf[subschema #39: Base Wallet]/ request property allOf list [request-property-all-of-added].
  • POST /platform/external-accounts — added #/components/schemas/WalletCustodyFields to the accountInfo/oneOf[subschema #40: Ethereum L1 Wallet]/ request property allOf list [request-property-all-of-added].
  • POST /platform/external-accounts — added #/components/schemas/WalletCustodyFields to the accountInfo/oneOf[subschema #42: Polygon Wallet]/ request property allOf list [request-property-all-of-added].
  • POST /platform/external-accounts — added #/components/schemas/WalletCustodyFields to the accountInfo/oneOf[subschema #43: Plasma Wallet]/ request property allOf list [request-property-all-of-added].
  • POST /platform/external-accounts — added #/components/schemas/WalletCustodyFields to the accountInfo/oneOf[subschema #44: Solana Wallet]/ request property allOf list [request-property-all-of-added].
  • POST /platform/external-accounts — added #/components/schemas/WalletCustodyFields to the accountInfo/oneOf[subschema #45: Spark Wallet]/ request property allOf list [request-property-all-of-added].
  • POST /platform/external-accounts — added #/components/schemas/WalletCustodyFields to the accountInfo/oneOf[subschema #46: Tron Wallet]/ request property allOf list [request-property-all-of-added].
  • POST webhook:external-account-status — added #/components/schemas/WalletCustodyFields to the allOf[subschema #2]/data/allOf[subschema #1]/accountInfo/oneOf[subschema #38: Base Wallet]/ request property allOf list [request-property-all-of-added].
  • POST webhook:external-account-status — added #/components/schemas/WalletCustodyFields to the allOf[subschema #2]/data/allOf[subschema #1]/accountInfo/oneOf[subschema #39: Ethereum L1 Wallet]/ request property allOf list [request-property-all-of-added].
  • POST webhook:external-account-status — added #/components/schemas/WalletCustodyFields to the allOf[subschema #2]/data/allOf[subschema #1]/accountInfo/oneOf[subschema #41: Polygon Wallet]/ request property allOf list [request-property-all-of-added].
  • POST webhook:external-account-status — added #/components/schemas/WalletCustodyFields to the allOf[subschema #2]/data/allOf[subschema #1]/accountInfo/oneOf[subschema #42: Plasma Wallet]/ request property allOf list [request-property-all-of-added].
  • POST webhook:external-account-status — added #/components/schemas/WalletCustodyFields to the allOf[subschema #2]/data/allOf[subschema #1]/accountInfo/oneOf[subschema #43: Solana Wallet]/ request property allOf list [request-property-all-of-added].
  • POST webhook:external-account-status — added #/components/schemas/WalletCustodyFields to the allOf[subschema #2]/data/allOf[subschema #1]/accountInfo/oneOf[subschema #44: Spark Wallet]/ request property allOf list [request-property-all-of-added].
  • POST webhook:external-account-status — added #/components/schemas/WalletCustodyFields to the allOf[subschema #2]/data/allOf[subschema #1]/accountInfo/oneOf[subschema #45: Tron Wallet]/ request property allOf list [request-property-all-of-added].

Detected by oasdiff. Full report: job summary or the oasdiff-report artifact.

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.
@shreyav shreyav changed the title feat: add custody type and VASP link to external accounts feat: identify the VASP that custodies a crypto wallet account Aug 20, 2026
@shreyav
shreyav marked this pull request as ready for review August 20, 2026 00:46
@greptile-apps

greptile-apps Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds an optional vaspName custody marker to seven crypto-wallet external-account variants and exposes the fragment to Stainless-generated SDKs.

  • Composes WalletCustodyFields into Base, Ethereum, Plasma, Polygon, Solana, Spark, and Tron wallet schemas.
  • Regenerates the root and Mintlify OpenAPI bundles.
  • Uses field presence to distinguish VASP-hosted wallets from self-custody wallets.

Confidence Score: 4/5

The 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 vaspName, undermining the field-presence invariant used to distinguish hosted wallets from self-custody wallets.

Files Needing Attention: openapi/components/schemas/external_accounts/WalletCustodyFields.yaml

Important Files Changed

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]
Loading
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

Comment on lines +3 to +4
vaspName:
type: string

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Suggested change
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.

@shreyav
shreyav merged commit 47ae1e0 into main Aug 20, 2026
12 checks passed
@shreyav
shreyav deleted the shreyav/wallet-custody-type branch August 20, 2026 04:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking-change Introduces a breaking change to the OpenAPI spec

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants