# Add a customer to a fee-wrapper allowlist
Source: https://docs.groundtech.co/api-reference/add-fee-wrapper-allowlist
swagger/swagger-combined.yaml PUT /v2/fee-wrappers/{id}/allowlist/{address}
Call this from your backend before the customer deposits into the fee wrapper.
Keep the Ground API key on the server.
# Add an address to a gtoken allowlist
Source: https://docs.groundtech.co/api-reference/add-gtoken-allowlist
swagger/swagger-combined.yaml PUT /v2/gvaults/{gVaultId}/allowlist/{address}
Allow a wallet using its signed authorization challenge. The verified manager retains removal rights.
Grants an EOA, multisig, or contract permission to deposit into an existing
Ground vault backing a gtoken. Call this from your backend and keep the API key on the server.
# Add a MicroVault shareholder
Source: https://docs.groundtech.co/api-reference/add-microvault-shareholder
swagger/swagger-combined.yaml PUT /v2/microvaults/vaults/{vaultId}/shareholders/{walletAddress}
Adds a wallet to the shareholder list so it can subscribe or receive shares after the updated list is confirmed onchain.
Approve one wallet to subscribe to and hold this MicroVault's shares.
# Allocate wallet funds
Source: https://docs.groundtech.co/api-reference/allocate
swagger/swagger-combined.yaml POST /v2/wallets/{id}/allocate
Moves settled, unreserved funds between cash and yield. Open rebalances keep reservations and destination commitments; changed plans require a new preview.
# Compute yield
Source: https://docs.groundtech.co/api-reference/compute-yield
swagger/swagger-combined.yaml GET /v2/wallets/{id}/yield
Returns cumulative yield data and supporting metrics for a wallet. Use this to display "You've earned $X" and yield source APY breakdowns.
# Create address book entry
Source: https://docs.groundtech.co/api-reference/create-address-book-entry
swagger/swagger-combined.yaml POST /v2/address-book/entries
Adds a withdrawal destination to the organization's address book. Requires an organization owner or admin.
Add a withdrawal destination to the authenticated organization's address book.
# Provision a fee wrapper
Source: https://docs.groundtech.co/api-reference/create-fee-wrapper
swagger/swagger-combined.yaml POST /v2/fee-wrappers
Provisions a revenue-sharing fee wrapper around the Ground vault backing an existing gtoken.
Use the returned fee-wrapper ID to check when the contract is ready.
# Create a gtoken webhook
Source: https://docs.groundtech.co/api-reference/create-gtoken-webhook
swagger/swagger-combined.yaml POST /v2/gvaults/webhooks
Register an HTTPS endpoint for gtoken and fee-wrapper lifecycle events. Store
the returned signing secret securely; Ground returns it only once.
# Create a MicroVault
Source: https://docs.groundtech.co/api-reference/create-microvault
swagger/swagger-combined.yaml POST /v2/microvaults/vaults
Deploys a MicroVault and ERC-20 share token with the selected base asset, token metadata, and fee policy. Returns a task while provisioning.
Create a vault and its ERC-20 share token. The response contains a task that tracks deployment.
# Create Turnkey activity approval request
Source: https://docs.groundtech.co/api-reference/create-turnkey-activity-approval-request
swagger/swagger-combined.yaml POST /v2/turnkey/activity-approval-request
Creates the Turnkey approval or rejection request that the customer signs before submitting their vote.
Returns the Turnkey approve or reject request payload that the customer signer must stamp locally before submitting it to the vote endpoint.
# Create a portfolio wallet
Source: https://docs.groundtech.co/api-reference/create-wallet
swagger/swagger-combined.yaml POST /v2/wallets
Creates a wallet that either invests deposits automatically or keeps them in cash. Reusing `requestId` returns the same wallet.
# Create a webhook
Source: https://docs.groundtech.co/api-reference/create-webhook
swagger/swagger-combined.yaml POST /v2/webhooks
Creates a webhook for `portfolio_wallet.*` events. The signing secret is shown only once, in this response.
# Delete address book entry
Source: https://docs.groundtech.co/api-reference/delete-address-book-entry
swagger/swagger-combined.yaml DELETE /v2/address-book/entries/{id}
Permanently removes an address book entry. Deleting the last entry turns withdrawal address enforcement off for the organization.
Delete a withdrawal destination from the authenticated organization's address book.
# Remove a customer from a fee-wrapper allowlist
Source: https://docs.groundtech.co/api-reference/delete-fee-wrapper-allowlist
swagger/swagger-combined.yaml DELETE /v2/fee-wrappers/{id}/allowlist/{address}
Removing a customer blocks new deposits but preserves withdrawals from an
existing position.
# Remove an address from a gtoken allowlist
Source: https://docs.groundtech.co/api-reference/delete-gtoken-allowlist
swagger/swagger-combined.yaml DELETE /v2/gvaults/{gVaultId}/allowlist/{address}
Removes your organization's grant. Onchain access is removed after the final
organization grant is removed, and existing withdrawals remain available.
# Delete a gtoken webhook
Source: https://docs.groundtech.co/api-reference/delete-gtoken-webhook
swagger/swagger-combined.yaml DELETE /v2/gvaults/webhooks/{id}
Delete a gtoken webhook registration and stop future deliveries to its URL.
# Delete a webhook
Source: https://docs.groundtech.co/api-reference/delete-webhook
swagger/swagger-combined.yaml DELETE /v2/webhooks/{id}
Deletes a webhook registration by its ID. No further events are delivered to its URL once it is deleted.
# Get accounting yield period summary
Source: https://docs.groundtech.co/api-reference/get-accounting-yield-period-summary
swagger/swagger-combined.yaml GET /v2/accounting/yield/period-summary
Shows how a wallet or your organization performed over a selected period. Performance values can be negative.
# Get a deposit
Source: https://docs.groundtech.co/api-reference/get-deposit
swagger/swagger-combined.yaml GET /v2/wallets/{id}/deposits/{depositId}
Returns a single deposit by its ID.
Processing deposits are keyed by the observed onchain event while they wait for finality, and that ID remains stable after the deposit completes. They are informational only: balances and withdrawable value update only after the finalized deposit posts to the ledger. Ground only surfaces pre-finality processing deposits for workflow-idle wallets; wallets with active Ground-managed workflows may wait until final attribution.
# Get a fee wrapper
Source: https://docs.groundtech.co/api-reference/get-fee-wrapper
swagger/swagger-combined.yaml GET /v2/fee-wrappers/{id}
Wait for `status: ready` before allowing deposits. Ready responses include the
wrapper address, underlying gtoken source, fee configuration, TVL, and liquidity.
# Check a customer's fee-wrapper access
Source: https://docs.groundtech.co/api-reference/get-fee-wrapper-allowlist
swagger/swagger-combined.yaml GET /v2/fee-wrappers/{id}/allowlist/{address}
Wait for `allowed: true` before submitting the customer's deposit transaction.
# Check an address's gtoken access
Source: https://docs.groundtech.co/api-reference/get-gtoken-allowlist
swagger/swagger-combined.yaml GET /v2/gvaults/{gVaultId}/allowlist/{address}
Wait for `allowed: true` before submitting the user's deposit transaction.
# Get a wallet authorization challenge
Source: https://docs.groundtech.co/api-reference/get-gtoken-allowlist-authorization
swagger/swagger-combined.yaml GET /v2/gvaults/{gVaultId}/allowlist/{address}/authorization
Get organization-bound EIP-712 typed data to sign for wallet admission. The authorization expires in five minutes.
Request this challenge using your Ground API key. Have the wallet sign the returned `typedData.domain`, `typedData.types`, and `typedData.value`, then send its signature together with `issuedAt` and `expiresAt` to the allowlist endpoint. The challenge expires after five minutes.
# Get a gtoken webhook
Source: https://docs.groundtech.co/api-reference/get-gtoken-webhook
swagger/swagger-combined.yaml GET /v2/gvaults/webhooks/{id}
Retrieve one gtoken webhook registration. The signing secret is never returned
after creation.
# Get a MicroVault
Source: https://docs.groundtech.co/api-reference/get-microvault
swagger/swagger-combined.yaml GET /v2/microvaults/vaults/{vaultId}
Returns the vault's identity, share-token metadata, current state, asset balances, and latest NAV observation.
Read vault configuration, balances, valuation, and automation status.
MicroVaults are in private preview for enabled sandbox organizations on Ethereum Sepolia.
# Get MicroVault pause status
Source: https://docs.groundtech.co/api-reference/get-microvault-pause-status
swagger/swagger-combined.yaml GET /v2/microvaults/vaults/{vaultId}/pause-status
Returns which subscription, redemption, transfer, source-entry, and source-exit capabilities are currently paused for the vault.
Read the current pause controls for a MicroVault.
MicroVaults are in private preview for enabled sandbox organizations on Ethereum Sepolia.
# Get MicroVault performance series
Source: https://docs.groundtech.co/api-reference/get-microvault-performance-series
swagger/swagger-combined.yaml GET /v2/microvaults/vaults/{vaultId}/performance-series
Returns chronological lifetime-earned, NAV, and trailing-APY observations for charting and performance analysis.
Read historical NAV, earnings, and trailing APY checkpoints.
MicroVaults are in private preview for enabled sandbox organizations on Ethereum Sepolia.
# Get MicroVault performance summary
Source: https://docs.groundtech.co/api-reference/get-microvault-performance-summary
swagger/swagger-combined.yaml GET /v2/microvaults/performance-summary
Returns lifetime investment earnings across the organization's MicroVaults. Use `status` to determine whether every vault was included in the aggregate.
Read aggregate lifetime earnings across your organization's MicroVaults.
MicroVaults are in private preview for enabled sandbox organizations on Ethereum Sepolia.
# Get a requested redemption
Source: https://docs.groundtech.co/api-reference/get-microvault-redemption
swagger/swagger-combined.yaml GET /v2/microvaults/vaults/{vaultId}/redemptions/{requestId}
Returns liquidation progress, actual attributable proceeds, the fixed receiver, and underlying source operations for one asynchronous redemption request.
Read liquidation, claim availability, and actual attributable proceeds for a requested redemption.
# Get shareholder proof
Source: https://docs.groundtech.co/api-reference/get-microvault-shareholder-proof
swagger/swagger-combined.yaml GET /v2/microvaults/vaults/{vaultId}/shareholders/{walletAddress}/proof
Returns the proof an approved wallet uses to subscribe or receive shares. A `404` means no proof exists on the confirmed onchain list.
Get the proof a shareholder wallet supplies to the MicroVault contract.
MicroVaults are in private preview for enabled sandbox organizations on Ethereum Sepolia.
# Get MicroVault shareholders
Source: https://docs.groundtech.co/api-reference/get-microvault-shareholders
swagger/swagger-combined.yaml GET /v2/microvaults/vaults/{vaultId}/shareholders
Returns wallets in the current shareholder list and whether the list containing each wallet has been confirmed onchain.
Read the active shareholder root and the addresses represented by it.
MicroVaults are in private preview for enabled sandbox organizations on Ethereum Sepolia.
# Get a sandbox mock USDT request
Source: https://docs.groundtech.co/api-reference/get-sandbox-usdt-request
swagger/swagger-combined.yaml GET /v2/sandbox/faucets/usdt/{id}
Returns the current status of a mock USDT faucet transfer owned by the authenticated organization.
Poll this endpoint until the faucet request reaches `confirmed` or `failed`.
# Get a portfolio wallet
Source: https://docs.groundtech.co/api-reference/get-wallet
swagger/swagger-combined.yaml GET /v2/wallets/{id}
Returns the wallet with balances, strategy, positions, and deposit addresses.
# Get wallets overview
Source: https://docs.groundtech.co/api-reference/get-wallets-overview
swagger/swagger-combined.yaml GET /v2/wallets/overview
Returns aggregate balance and yield totals across the authenticated organization's wallets.
# Get a webhook
Source: https://docs.groundtech.co/api-reference/get-webhook
swagger/swagger-combined.yaml GET /v2/webhooks/{id}
Returns a single webhook registration by its ID. The signing `secret` is never included; it is only returned once, in the create response.
# Get a withdrawal
Source: https://docs.groundtech.co/api-reference/get-withdrawal
swagger/swagger-combined.yaml GET /v2/wallets/{id}/withdrawals/{withdrawalId}
Returns a single withdrawal by its ID.
# List accounting activity
Source: https://docs.groundtech.co/api-reference/list-accounting-activity
swagger/swagger-combined.yaml GET /v2/accounting/activity
Shows accounting activity for a wallet or your organization. Records with the same `journalEntryId` are part of the same entry.
# List activity
Source: https://docs.groundtech.co/api-reference/list-activity
swagger/swagger-combined.yaml GET /v2/activity
Returns customer-facing wallet activity across the authenticated organization. Pass one or more `walletId` query parameters to filter to specific wallets.
Deposit activity may appear with `status: "processing"` before finality for workflow-idle wallets. Processing deposit activity means Ground has detected the inbound onchain transfer; it is not yet credited to wallet balances.
# List address book entries
Source: https://docs.groundtech.co/api-reference/list-address-book-entries
swagger/swagger-combined.yaml GET /v2/address-book/entries
Returns the organization's withdrawal address book entries. A non-empty address book means withdrawals are restricted to listed destinations.
List withdrawal address book entries for the authenticated organization.
# List deposits
Source: https://docs.groundtech.co/api-reference/list-deposits
swagger/swagger-combined.yaml GET /v2/wallets/{id}/deposits
Lists a wallet's deposits one page at a time. Incoming transfers are not added to the balance until processing finishes.
Deposits can appear with `status: "processing"` as soon as Ground detects a likely supported-token transfer into a workflow-idle wallet. A processing deposit is not final, not included in balances, and has `completedAt: null`.
After finality and ledger posting, the same onchain deposit is returned as a completed deposit with the same deposit `id` and `completedAt` populated. Wallets with active Ground-managed workflows may wait until final attribution before showing a deposit.
# List fee wrappers
Source: https://docs.groundtech.co/api-reference/list-fee-wrappers
swagger/swagger-combined.yaml GET /v2/fee-wrappers
Lists fee wrappers provisioned for your Ground organization.
# List gtoken webhook events
Source: https://docs.groundtech.co/api-reference/list-gtoken-webhook-events
swagger/swagger-combined.yaml GET /v2/gvaults/webhooks/events
Inspect delivery status and payloads across the organization's gtoken webhook
registrations.
# List events for a gtoken webhook
Source: https://docs.groundtech.co/api-reference/list-gtoken-webhook-events-by-webhook
swagger/swagger-combined.yaml GET /v2/gvaults/webhooks/{id}/events
Inspect delivery status and payloads for one gtoken webhook registration.
# List gtoken webhooks
Source: https://docs.groundtech.co/api-reference/list-gtoken-webhooks
swagger/swagger-combined.yaml GET /v2/gvaults/webhooks
List the organization's active gtoken webhook registrations. Signing secrets
are not included.
# List gtoken yield sources
Source: https://docs.groundtech.co/api-reference/list-gtoken-yield-sources
swagger/swagger-combined.yaml GET /v2/gvaults/yield-sources
Returns the unpaginated integration manifest for existing Ground gtokens.
Returns the gtoken sources available for direct onchain integration
in the current environment. Use a returned `id` as the `gVaultId` in the direct
access endpoints or as the `gVaultId` when Ground provisions a fee
wrapper.
# List MicroVault activity
Source: https://docs.groundtech.co/api-reference/list-microvault-activity
swagger/swagger-combined.yaml GET /v2/microvaults/vaults/{vaultId}/activity
Returns the vault's user-visible money-movement history in reverse chronological order, including current operation status and known transaction hashes.
List subscription, rebalance, and redemption activity.
MicroVaults are in private preview for enabled sandbox organizations on Ethereum Sepolia.
# List MicroVault source positions
Source: https://docs.groundtech.co/api-reference/list-microvault-sources
swagger/swagger-combined.yaml GET /v2/microvaults/vaults/{vaultId}/sources
Returns each source attached to the vault with its permission mode, current position value, APY, and pending or claimable operations.
Read registered source modes, current positions, and in-flight source amounts.
MicroVaults are in private preview for enabled sandbox organizations on Ethereum Sepolia.
# List MicroVault yield sources
Source: https://docs.groundtech.co/api-reference/list-microvault-yield-sources
swagger/swagger-combined.yaml GET /v2/microvaults/yield-sources
Returns the yield sources admitted for MicroVault use on a chain, including APY, processing-time estimates, and synchronous or asynchronous capabilities.
Discover the environment-specific yield sources admitted for MicroVault allocation.
MicroVaults are in private preview for enabled sandbox organizations on Ethereum Sepolia.
# List MicroVaults
Source: https://docs.groundtech.co/api-reference/list-microvaults
swagger/swagger-combined.yaml GET /v2/microvaults/vaults
Returns active MicroVaults and creations still provisioning. Use each active vault's `id` in API paths and its `address` for onchain share-token interactions.
List active MicroVaults and vault deployments that are still provisioning.
MicroVaults are in private preview for enabled sandbox organizations on Ethereum Sepolia.
# List pending Turnkey approval activities
Source: https://docs.groundtech.co/api-reference/list-pending-turnkey-activities
swagger/swagger-combined.yaml GET /v2/turnkey/activities/pending
Lists approval requests that are waiting for the customer, with the related wallet and withdrawal details.
Returns pending customer-approval activities for the authenticated organization.
# List Portfolio Wallet yield sources
Source: https://docs.groundtech.co/api-reference/list-portfolio-wallet-yield-sources
swagger/swagger-combined.yaml GET /v2/wallets/yield-sources
Lists available yield sources for this environment, along with each source's deposit token and current status.
Returns the live yield-source catalog available for Portfolio Wallet strategy
allocations. The catalog is environment-specific: sandbox exposes only the
currently supported sandbox yield sources, while production may expose
additional entries.
# List portfolio wallets
Source: https://docs.groundtech.co/api-reference/list-wallets
swagger/swagger-combined.yaml GET /v2/wallets
Returns a cursor-paginated list of wallets for the authenticated organization.
# List webhook events
Source: https://docs.groundtech.co/api-reference/list-webhook-events
swagger/swagger-combined.yaml GET /v2/webhooks/events
Returns a cursor-paginated delivery event feed across all active webhooks for the organization.
# List events for a webhook
Source: https://docs.groundtech.co/api-reference/list-webhook-events-by-webhook
swagger/swagger-combined.yaml GET /v2/webhooks/{id}/events
Returns a cursor-paginated delivery event feed for one webhook.
# List webhooks
Source: https://docs.groundtech.co/api-reference/list-webhooks
swagger/swagger-combined.yaml GET /v2/webhooks
Lists active webhooks one page at a time. Signing secrets are not included.
# List withdrawals
Source: https://docs.groundtech.co/api-reference/list-withdrawals
swagger/swagger-combined.yaml GET /v2/wallets/{id}/withdrawals
Returns a cursor-paginated list of withdrawals for a specific wallet.
# Lookup a wallet deposit by tx hash
Source: https://docs.groundtech.co/api-reference/lookup-deposit
swagger/swagger-combined.yaml GET /v2/wallets/{id}/deposits/lookup
Fetch a single deposit by its onchain transaction hash. `chain` is optional; if provided, it must match the deposit's recorded chain.
Lookup can return a `processing` deposit before chain finality when the transaction hash has been observed and normalized for a workflow-idle wallet. `completedAt` remains `null` until the deposit finalizes and posts. The deposit ID remains stable when the row transitions from `processing` to `completed`.
# Preview wallet allocation
Source: https://docs.groundtech.co/api-reference/preview-allocation
swagger/swagger-combined.yaml POST /v2/wallets/{id}/allocate-preview
Checks a proposed movement between cash and yield positions and shows the resulting balances. Moving cash to cash is not supported.
# Preview a MicroVault redemption
Source: https://docs.groundtech.co/api-reference/preview-microvault-redemption
swagger/swagger-combined.yaml POST /v2/microvaults/vaults/{vaultId}/redemptions/preview
Returns an instant route when liquidity is available, otherwise a requested route that settles at actual attributable proceeds.
Determine whether a redemption can execute immediately or requires underlying source liquidation.
# Remove a MicroVault shareholder
Source: https://docs.groundtech.co/api-reference/remove-microvault-shareholder
swagger/swagger-combined.yaml DELETE /v2/microvaults/vaults/{vaultId}/shareholders/{walletAddress}
Removes a wallet from the shareholder list after onchain confirmation. Existing shares are not burned and remain redeemable.
Remove one wallet from this MicroVault's shareholder allowlist.
# Request sandbox mock USDT
Source: https://docs.groundtech.co/api-reference/request-sandbox-usdt
swagger/swagger-combined.yaml POST /v2/sandbox/faucets/usdt
Requests mock USDT for an Ethereum Sepolia wallet. Available only in sandbox. Check the request until it is `confirmed` or `failed`.
Submit a faucet request for the wallet's `depositAddresses.ethereum_sepolia`
address, then poll the returned request ID until it is `confirmed` or `failed`.
Use a new UUID v4 `Idempotency-Key` for each intended transfer. Reusing the key
returns the original request without sending another transfer.
# Update a fee wrapper's performance fee
Source: https://docs.groundtech.co/api-reference/update-fee-wrapper-fees
swagger/swagger-combined.yaml PATCH /v2/fee-wrappers/{id}/fees
Update management fees separately from other fee fields. Caps are immutable. Recipient changes are unsupported while management fees are enabled.
Updates the fee recipient, performance-fee rate, or both without exceeding the
wrapper's creation-time maximum.
# Update MicroVault fees
Source: https://docs.groundtech.co/api-reference/update-microvault-fees
swagger/swagger-combined.yaml PUT /v2/microvaults/vaults/{vaultId}/fees
Replaces the vault's management fee, performance fee, and fee-share recipient. The complete fee policy must be supplied on every update.
Set the fee recipient and management and performance fees.
# Update MicroVault pause controls
Source: https://docs.groundtech.co/api-reference/update-microvault-pause
swagger/swagger-combined.yaml PUT /v2/microvaults/vaults/{vaultId}/pause
Blocks or restores one vault capability without changing the other pause controls. Use this for incident response or planned operational restrictions.
Pause or resume one supported vault operation.
# Update MicroVault source mode
Source: https://docs.groundtech.co/api-reference/update-microvault-source-mode
swagger/swagger-combined.yaml PUT /v2/microvaults/vaults/{vaultId}/sources/{sourceId}/mode
Controls whether the vault may enter, exit, both, or stop using a source. It does not change the target or erase an existing position.
Control whether a yield source may receive subscriptions, service redemptions, do both, or remain disabled.
# Update MicroVault strategy
Source: https://docs.groundtech.co/api-reference/update-microvault-strategy
swagger/swagger-combined.yaml PUT /v2/microvaults/vaults/{vaultId}/strategy
Sets the vault's target allocation across idle base assets and admitted yield sources. Ground automation subsequently moves assets toward the new targets.
Set target allocations across yield sources and idle USDC. Targets must total 10,000 basis points.
# Update wallet strategy
Source: https://docs.groundtech.co/api-reference/update-strategy
swagger/swagger-combined.yaml PATCH /v2/wallets/{id}/strategy
Updates selected token strategies. Open rebalances keep their reservations; new plans use unreserved funds. Open withdrawals block edits.
Wallets created with `autoRebalance: false` use
[Preview wallet allocation](/api-reference/preview-allocation) and
[Allocate wallet funds](/api-reference/allocate) instead of percentage targets.
# Update a portfolio wallet
Source: https://docs.groundtech.co/api-reference/update-wallet
swagger/swagger-combined.yaml PATCH /v2/wallets/{id}
Updates the wallet label.
Update the wallet label. The `label` field is required; send `null` or an empty string to clear it. The response is HTTP 200 with the full updated wallet. See [Update a Wallet](/docs/portfolio-wallets/update-wallet) for an example.
# Vote on Turnkey activity
Source: https://docs.groundtech.co/api-reference/vote-turnkey-activity
swagger/swagger-combined.yaml POST /v2/turnkey/activities/{activityId}/vote
Submits the customer's signed approval or rejection after Ground checks that it matches the pending Turnkey activity.
Approve or reject a pending Turnkey activity with a customer-generated Turnkey stamp.
# Withdraw funds
Source: https://docs.groundtech.co/api-reference/withdraw
swagger/swagger-combined.yaml POST /v2/wallets/{id}/withdrawals
Starts a withdrawal. Reusing `requestId` returns the same withdrawal. If automatic rebalancing is off, you can choose the source positions.
# Preview a withdrawal
Source: https://docs.groundtech.co/api-reference/withdrawal-preview
swagger/swagger-combined.yaml POST /v2/wallets/{id}/withdrawal-preview
Shows how much can be withdrawn and checks an optional amount, destination, token, and set of source positions.
# API Changelog
Source: https://docs.groundtech.co/docs/changelog
Breaking changes and notable updates to the Ground API.
API contract changes are listed newest first. Breaking entries include the fields and client updates required to migrate.
## October 6, 2026
### Automatic TRON deposit addresses
Every Portfolio Wallet receives a TRON USDT address automatically. Wait for address readiness before depositing. Ground funds activation and gas when an outgoing transaction needs them.
**Breaking change:** `PATCH /v2/wallets/{id}` now accepts only `label`. Remove `enabledChains` from update requests; it returns HTTP 400. Address setup requires no enable request.
## October 4, 2026
### TRON Portfolio Wallets and JustLend V1 USDT
Portfolio Wallets support native TRON USDT in production and Shasta test USDT in sandbox. TRON addresses are provisioned automatically; poll the wallet for readiness before depositing. TRON uses the independent `usdt:tron` allocation group; Ethereum USDT remains in `usdt`. USDT does not bridge between them.
JustLend V1 USDT (`justlend-v1-usdt`) is available through the environment-specific catalog. Its underlying allocation represents 100% exposure to the USDT lending market. Shasta uses a simulated market with test assets. Ground pays the transaction resource costs for Ground-managed operations.
### JustLend current APY preview
JustLend V1 yield-source responses now include an optional `currentApy` preview
with `apyBps`, `asOf`, and `validUntil`. Label this value **Current APY** and stop
displaying it at `validUntil`; it expires two hours after observation. It is a
variable supply-rate estimate excluding rewards, using JustLend's annualization
and display precision without additional compounding. The existing `apyBps` remains
the historical 30-day rate and stays `null` until enough history is available.
Responses also include a link to JustLend's USDT market overview.
## September 1, 2026
### Strategy optimization endpoint removed
**Breaking change**
`POST /v2/wallets/strategy/optimize` is no longer available. Fetch the current
yield-source catalog from `GET /v2/wallets/yield-sources`, apply your allocation
policy in your application, and submit explicit allocations through
`POST /v2/wallets` or `PATCH /v2/wallets/{id}/strategy`.
## August 14, 2026
### Cash-to-cash allocations are rejected
`POST /v2/wallets/{id}/allocate-preview` and
`POST /v2/wallets/{id}/allocate` now reject movements whose source and
destination are both cash positions. Allocate between cash and a yield source,
or between two yield sources.
## August 10, 2026
### Portfolio wallet webhooks identify their wallet
Portfolio wallet deposit, withdrawal, payout, strategy, and rebalance webhooks
now include a top-level `walletId`. USD amounts in deposit activity and webhooks
are returned with exactly six decimal places. Retrying a workflow no longer
emits a rebalance event solely because its worker claim changed.
## August 9, 2026
### Allocation responses include accepted instructions
`POST /v2/wallets/{id}/allocate` now returns the allocation's `requestId`,
`createdAt`, and accepted `allocations` alongside its `rebalanceId` and current
`status`. Allocation create and preview responses return dollar strings with
exactly six decimal places.
## July 21, 2026
### JAAA USDT available in production
Production now returns `ground-jaaa-usdt-vault` from
`GET /v2/wallets/yield-sources`. Put this source under
`strategy.allocations.usdt`; continue using `ground-jaaa-usdc-vault` under
`strategy.allocations.usdc` for the USDC-funded lane.
This availability update does not change the response schema or introduce new
identifiers. The JAAA-USDT metadata now correctly describes its AAA-rated
collateralized loan obligation exposure.
## July 20, 2026
### Stablecoin-explicit JAAA yield source IDs
**Breaking change**
The existing USDC-funded JAAA source moved from `ground-jaaa-vault` to
`ground-jaaa-usdc-vault`. Update stored strategy templates and API clients to
use the stablecoin-explicit USDC ID.
Sandbox also exposes `ground-jaaa-usdt-vault` for the USDT-funded JAAA lane.
Place it under `strategy.allocations.usdt` only when it is present in the live
environment-specific response from `GET /v2/wallets/yield-sources`. Production
clients must not assume the USDT row is available until the production catalog
returns it.
Historical completed JAAA-USDC transactions retain their economics and remain
readable after the identity migration.
## July 17, 2026
### Canonical list responses and webhook event casing
**Breaking change**
Update clients that read the removed response aliases before calling the affected endpoints after this release.
We completed the response-pattern migration introduced on July 9. The following endpoints now return list items only under `data` and pagination only through `nextCursor`:
* `GET /v2/wallets/yield-sources`
* `GET /v2/activity`
* `GET /v2/webhooks`
* `GET /v2/webhooks/events`
* `GET /v2/webhooks/{id}/events`
#### List envelope migration
| Removed field | Read instead |
| - | - |
| `yieldSources` | `data` |
| `activity` | `data` |
| `endpoints` | `data` |
| `events` | `data` |
| `hasMore` | `nextCursor !== null` |
All affected list responses now use this shape:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"data": [],
"nextCursor": null
}
```
When `nextCursor` is non-null, pass it as the next request's `cursor` query parameter. A null value means there are no more pages.
#### Webhook event field migration
Webhook event feed items and their nested `latestEmission` object now use camelCase exclusively. Update clients to read:
* `registrationId`, `eventType`, `attemptCount`, `deliveredAt`, `lastError`, `createdAt`, and `updatedAt` on each event;
* `eventId`, `attemptNumber`, `requestedAt`, `completedAt`, `responseStatus`, `responseMs`, and `errorMessage` from `latestEmission`.
The equivalent snake\_case fields are no longer returned.
#### Required client changes
1. Read every affected list from `response.data`.
2. Replace `response.hasMore` checks with `response.nextCursor !== null`.
3. Replace snake\_case webhook event and delivery-attempt field access with the camelCase names above.
# Handle asynchronous gtoken operations
Source: https://docs.groundtech.co/docs/gtokens/async-operations
Track API submissions, onchain requests, confirmations, processing, and claims without losing state.
gtoken integrations have two independent asynchronous boundaries: Ground API
mutations that submit onchain administration transactions, and vault operations
whose underlying RWA settlement completes later.
## Keep the state layers separate
| Layer | Durable evidence | Complete when |
| - | - | - |
| API request | `requestId` or resource ID | Ground accepts or rejects the request |
| Transaction submission | `transactionHash` | The transaction confirms successfully |
| Effective access | `allowed` from a read endpoint | The address has the desired effective state |
| Vault request | Onchain `requestId` plus transaction hash | The request can be claimed or is terminal |
| Asset settlement | Claim receipt and balance change | Assets or shares reach the intended address |
| Webhook delivery | `Ground-Event-Id` | Your handler processes the event idempotently |
A `202` response or transaction hash is not final confirmation. A confirmed
request transaction is not the same as completed RWA settlement.
## Fee-wrapper provisioning
Generate one UUID v4 per logical wrapper and persist it before the POST. If the
request outcome is uncertain, retry the same payload with the same `requestId`
or query `GET /v2/fee-wrappers?requestId=...`. Never create a new ID merely
because the client timed out.
Treat `provisioning` as pending, `ready` as usable, and `failed` as terminal for
that request. Do not use `contractAddress` until readiness is confirmed.
## Access and fee updates
When an allowlist or fee mutation returns `submitted`, retain its transaction
hash and poll the associated read endpoint or consume the status-change webhook.
Before retrying after a timeout, read the current resource to avoid submitting a
duplicate or contradictory change.
## Mutation statuses
| Status | Meaning |
| - | - |
| `submitted` | Ground broadcast an onchain transaction, but its requested state is not yet confirmed |
| `confirmed` | The requested state is effective onchain |
| `failed` | The transaction or resulting state change reached a terminal failure |
Do not infer `confirmed` from elapsed time. Read the resource, verify the
transaction receipt and resulting onchain state, or process the confirming
webhook.
## Onchain request/claim flows
Persist the contract address, chain ID, owner, transaction hash, returned
onchain request ID, requested amount, and minimum-output parameters. Use onchain
state or a successful call simulation to determine whether a claim is ready.
Elapsed time from `processingPolicies` is an estimate, not proof of claimability.
For a claim, reconcile all three facts:
1. The claim transaction succeeded.
2. The expected assets or shares moved to the intended address.
3. Your local operation record is marked terminal exactly once.
See [Integrate gtoken webhooks](/docs/gtokens/webhooks) for event payloads and
delivery statuses.
# Understanding gtokens and fee wrappers
Source: https://docs.groundtech.co/docs/gtokens/concepts
Understand the contracts, shares, access controls, and API resources in a gtoken integration.
A **gtoken** is the receipt token a user receives by depositing a supported
asset into a Ground vault contract. Each backing vault holds or routes assets
into an underlying RWA yield source and issues gtokens representing a
proportional claim on that vault.
## The two contract layers
| Layer | Created by | What users hold | When to use it |
| - | - | - | - |
| Backing vault | Ground, before your integration | gtokens | Direct access without partner revenue sharing |
| Fee wrapper | Ground, for your organization | Fee-wrapper shares | Performance fees or a customer allowlist isolated from the backing vault |
The fee wrapper deposits into one existing backing vault. It does not create, clone,
or replace that vault.
## Direct access
In the direct path, your backend calls the gtoken allowlist API for the address
that will receive gtokens. That address can be an EOA, multisig, smart account,
or contract. After authorization confirms, deposits, share accounting, and
redemptions happen onchain against the backing vault.
The authorization controls new deposits. Removing it does not confiscate shares
or prevent the holder from exiting an existing position.
## Access through a fee wrapper
In the revenue-sharing path, Ground provisions a fee wrapper over the selected
gtoken's backing vault and authorizes the wrapper against that vault. Your backend
then manages customer access on the wrapper's own allowlist.
Customers receive fee-wrapper shares. The wrapper owns the underlying gtokens,
accrues the configured performance fee above its high-water mark, and mints fee
shares to the configured recipient.
## API IDs and contract addresses
The gtoken API uses the `/v2/gvaults` namespace. Use the returned source ID as
`gVaultId`. Do not interchange these identifiers:
* `gVaultId` identifies a gtoken yield source in Ground's API.
* The catalogue's `contractAddress` is used for direct onchain calls.
* `id` identifies your organization's wrapper in Ground's API.
* `contractAddress` is the deployed wrapper contract used for onchain calls.
* `requestId` makes fee-wrapper provisioning safely retryable.
## Synchronous and asynchronous operations
Some backing vaults expose synchronous ERC-4626-style deposits and withdrawals. Other
vaults use request/claim methods because settlement depends on an underlying
RWA rail. Use the catalogue's `interface` field to select the correct contract
methods, and document settlement expectations in your application.
A submitted API request, an onchain transaction hash, and final settlement are
different states. Persist the durable IDs for each layer until the operation is
confirmed or claimed.
Read the catalogue and select the correct interface.
Receive status-change events and reconcile asynchronous updates.
# Configure revenue sharing on gtokens
Source: https://docs.groundtech.co/docs/gtokens/configure-revenue-sharing
Have Ground provision a fee wrapper and integrate its customer and fee lifecycle.
Ground can provision a fee wrapper around a gtoken's existing backing vault. The
wrapper holds the underlying gtokens, issues its own shares to your users, and
accrues a performance fee above its high-water mark.
## 1. Create a server-side API key
Create an API key in the
[Ground Developer Portal](https://portal.groundtech.co) and keep it on your
backend.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export GROUND_API_KEY=""
```
## 2. Select the gtoken source
Call `GET /v2/gvaults/yield-sources` and select the gtoken yield source. Pass that
row's `id` as `gVaultId` when provisioning the fee wrapper.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export GVAULT_ID="ground-ustb-vault"
```
## 3. Provision the fee wrapper
Generate a UUID v4 for `requestId`, then submit:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST https://sandbox.groundtech.co/v2/fee-wrappers \
-H "Authorization: Bearer $GROUND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"requestId": "a1b2c3d4-0000-4000-8000-000000000001",
"gVaultId": "'$GVAULT_ID'",
"feeRecipientAddress": "0x3333333333333333333333333333333333333333",
"performanceFeeBps": 2000
}'
```
`performanceFeeBps` is expressed in basis points. `2000` means that 20% of
gains above the wrapper's high-water mark accrue to `feeRecipientAddress`.
To also charge an annual management fee, include `managementFeeBps` in the
request. For example, `10` means 0.10% annually; the supported range is 0 through
100 bps. See [Manage fee wrappers](/docs/gtokens/manage-fee-wrappers) for fee caps
and update rules.
The initial performance-fee rate becomes the wrapper's permanent maximum. A
wrapper created with `performanceFeeBps: 0` cannot enable a performance fee
later.
Use the same `requestId` when retrying an uncertain request. Reusing it with
different parameters returns `409 request_id_conflict`.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"id": "abababab-abab-4bab-8bab-abababababab",
"requestId": "a1b2c3d4-0000-4000-8000-000000000001",
"gVaultId": "ground-ustb-vault",
"status": "provisioning",
"contractAddress": null,
"feeRecipientAddress": "0x3333333333333333333333333333333333333333",
"performanceFeeBps": 2000,
"maxPerformanceFeeBps": 2000,
"managementFeeBps": 0,
"maxManagementFeeBps": 100
}
```
## 4. Wait for onchain readiness
Set `FEE_WRAPPER_ID` to the `id` returned by the create response, then poll that
resource:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export FEE_WRAPPER_ID="abababab-abab-4bab-8bab-abababababab"
```
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl \
"https://sandbox.groundtech.co/v2/fee-wrappers/$FEE_WRAPPER_ID" \
-H "Authorization: Bearer $GROUND_API_KEY"
```
Wait for `status: ready`. Store `id` for future API calls and
`contractAddress` for onchain transactions. Ground provisions the wrapper and
authorizes it to use the selected backing vault before reporting it ready.
You can also recover a wrapper by its provisioning request:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl \
"https://sandbox.groundtech.co/v2/fee-wrappers?requestId=a1b2c3d4-0000-4000-8000-000000000001" \
-H "Authorization: Bearer $GROUND_API_KEY"
```
## 5. Allowlist each customer
Before a customer's first deposit, call:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X PUT \
"https://sandbox.groundtech.co/v2/fee-wrappers/$FEE_WRAPPER_ID/allowlist/0x2222222222222222222222222222222222222222" \
-H "Authorization: Bearer $GROUND_API_KEY"
```
Poll the corresponding `GET` endpoint until it returns `allowed: true`. This
allowlist belongs to the fee wrapper and is separate from the underlying
vault's address allowlist.
## 6. Integrate deposits
Read `asset()` from `contractAddress`, approve that token for the wrapper, and
call:
```solidity theme={"theme":{"light":"github-light","dark":"github-dark"}}
deposit(uint256 assets, address receiver)
```
Set `receiver` to the allowlisted customer address. The customer receives fee
wrapper shares, while the wrapper holds the underlying gtokens.
Useful read methods include:
```solidity theme={"theme":{"light":"github-light","dark":"github-dark"}}
asset() external view returns (address);
previewDeposit(uint256 assets) external view returns (uint256 shares);
convertToAssets(uint256 shares) external view returns (uint256 assets);
totalAssets() external view returns (uint256 assets);
```
## 7. Integrate withdrawals
For an immediate exit, read `maxWithdraw(owner)` or `maxRedeem(owner)`, then
call:
```solidity theme={"theme":{"light":"github-light","dark":"github-dark"}}
withdraw(uint256 assets, address receiver, address owner)
redeem(uint256 shares, address receiver, address owner)
```
For a larger exit, use:
```solidity theme={"theme":{"light":"github-light","dark":"github-dark"}}
requestRedeem(address owner, uint256 shares)
claimRedeem(address owner, uint256 requestId)
```
The wrapper translates its share amount into underlying gtokens and
manages the underlying queued redemption. Persist the returned `requestId`
until the claim confirms.
## 8. Update revenue sharing
You can change the fee recipient, reduce the fee, or restore it up to the
creation-time maximum:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X PATCH \
"https://sandbox.groundtech.co/v2/fee-wrappers/$FEE_WRAPPER_ID/fees" \
-H "Authorization: Bearer $GROUND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"feeRecipientAddress": "0x4444444444444444444444444444444444444444",
"performanceFeeBps": 1500
}'
```
Omit either field to leave it unchanged. The wrapper accrues fees under the old
configuration before applying the update. A rate above
`maxPerformanceFeeBps` is rejected.
## 9. Remove customer deposit access
Call `DELETE /v2/fee-wrappers/{id}/allowlist/{address}`. Removal blocks new deposits but preserves
the customer's ability to exit an existing position.
For annual management fees, add `managementFeeBps` (0–100, default 0) when creating
the wrapper. Ground fixes the cap at 100 bps; it is not a request parameter.
Management fees use the same recipient as performance fees. To update the annual
rate, PATCH the fees endpoint with only `managementFeeBps`, within the original
cap. Zero disables management fees. Performance updates use a separate request;
recipient changes remain unavailable while management fees are enabled.
# gtoken contracts
Source: https://docs.groundtech.co/docs/gtokens/contracts
Verified gtoken backing vaults and fee-wrapper contracts by environment.
The tables below list the deployed vault proxies backing gtokens. Call each vault at its
proxy address; Etherscan exposes the verified implementation ABI and read
methods through the proxy page.
For gtoken sources enabled through the public API, use
`GET /v2/gvaults/yield-sources` as the environment-specific integration
manifest. The contract directory is broader than the set of gtoken sources currently
returned by that endpoint.
## Production gtoken contracts
| gtoken | Funding asset | Address |
| - | - | - |
| gUSTB | USDC | [`0x5a68cfE0DCCcfA4991e5a92d2c382b15B41C4119`](https://etherscan.io/address/0x5a68cfE0DCCcfA4991e5a92d2c382b15B41C4119#readProxyContract) |
| gUSCC | USDC | [`0x6fd0f5224d571250A830ADb09bC8cD21A0d64BEf`](https://etherscan.io/address/0x6fd0f5224d571250A830ADb09bC8cD21A0d64BEf#readProxyContract) |
| gJAAA | USDC | [`0xA0FC15E1c879acd4036d912425997811fA49A195`](https://etherscan.io/address/0xA0FC15E1c879acd4036d912425997811fA49A195#readProxyContract) |
| gJTRSY-USDC | USDC | [`0x668c8B417314cC231bB18D948F3835e79c85Fe43`](https://etherscan.io/address/0x668c8B417314cC231bB18D948F3835e79c85Fe43#readProxyContract) |
## Sandbox gtoken contracts
| gtoken | Funding asset | Address |
| - | - | - |
| gUSTB | Sepolia USDC | [`0x7DB59Ed654aE4b3756eA81803Ff32528eA907DCa`](https://sepolia.etherscan.io/address/0x7DB59Ed654aE4b3756eA81803Ff32528eA907DCa#readProxyContract) |
| gUSCC | Sepolia USDC | [`0x70B2458EB62EC019b53494E3C214730549E5B558`](https://sepolia.etherscan.io/address/0x70B2458EB62EC019b53494E3C214730549E5B558#readProxyContract) |
| gJAAA | Sepolia USDC | [`0xf13E207d7F0CFAc068Bd34cA13c062c66772541d`](https://sepolia.etherscan.io/address/0xf13E207d7F0CFAc068Bd34cA13c062c66772541d#readProxyContract) |
| gJTRSY-USDC | Sepolia USDC | [`0xfF5155c2389f8552795C0fA12fa3005260F417ec`](https://sepolia.etherscan.io/address/0xfF5155c2389f8552795C0fA12fa3005260F417ec#readProxyContract) |
| gJTRSY-USDT | Mock USDT | [`0xCAa3b4C38Ee8EA43aAa3a44b50dACB4B3032C1CA`](https://sepolia.etherscan.io/address/0xCAa3b4C38Ee8EA43aAa3a44b50dACB4B3032C1CA#readProxyContract) |
## Fee-wrapper infrastructure
Ground provisions each fee wrapper as a separate proxy. The
`contractAddress` returned by the [fee-wrapper API](/api-reference/create-fee-wrapper)
is the address your integration reads from and transacts with.
| Environment | Contract | Address | Use |
| - | - | - | - |
| Sandbox | Fee-wrapper inner gUSTB | [`0x13Acb61A5F8daefF8F941c58BE754a888d780a9F`](https://sepolia.etherscan.io/address/0x13Acb61A5F8daefF8F941c58BE754a888d780a9F#readProxyContract) | Underlying gUSTB deployment targeted by the fee-wrapper factory. |
| Sandbox | Fee-wrapper factory | [`0x731a6F0CCbb13ad14F88aa49C321C9368eF880b2`](https://sepolia.etherscan.io/address/0x731a6F0CCbb13ad14F88aa49C321C9368eF880b2#readContract) | Deploys and registers customer fee-wrapper proxies for gUSTB. |
| Sandbox | Fee-wrapper implementation | [`0xe534EA2D96Cb597457dce2Dc3Da20d1fC6F63D07`](https://sepolia.etherscan.io/address/0xe534EA2D96Cb597457dce2Dc3Da20d1fC6F63D07#code) | Verified template used by fee-wrapper proxies. Do not send customer transactions to this address. |
Production fee-wrapper factory infrastructure is not currently deployed. Ground
will add the verified addresses here when production provisioning is enabled.
Never copy contract addresses between sandbox and production. Resolve the
backing vault from the yield-source response and resolve a provisioned fee wrapper
from its API response before constructing a transaction.
# Deposit and redeem onchain
Source: https://docs.groundtech.co/docs/gtokens/deposit-and-redeem
Deposit supported assets to receive gtokens and redeem through synchronous or request/claim vault flows.
Deposit supported assets into a gtoken's backing vault to receive gtokens.
Redeem those tokens through the same contract using its supported exit flow.
Use the `interface` returned by `GET /v2/gvaults/yield-sources` to select the
correct deployed interface. Do not assume every backing vault implements the same
operation lifecycle.
## Before any deposit
1. Resolve `contractAddress` from the current environment's catalogue.
2. Confirm the receiving address is allowlisted.
3. Call `asset()` and verify the returned token and chain.
4. Convert human-readable amounts to the asset's native units.
5. Estimate the result and set application-level slippage or minimum-output policy.
## USTB-style synchronous deposit
USTB-style backing vaults accept an ERC-20 approval followed by a deposit:
```solidity theme={"theme":{"light":"github-light","dark":"github-dark"}}
address token = gVault.asset();
uint256 limit = gVault.maxDeposit(receiver);
uint256 expectedShares = gVault.convertToShares(assets);
IERC20(token).approve(address(gVault), assets);
uint256 shares = gVault.deposit(assets, receiver);
```
The `receiver` receives gtokens and must be authorized for deposits.
Record the actual returned share amount; previews are estimates, not receipts.
## Immediate withdrawal
Check the owner's current synchronous capacity before submitting:
```solidity theme={"theme":{"light":"github-light","dark":"github-dark"}}
uint256 maxAssets = gVault.maxWithdraw(owner);
uint256 maxShares = gVault.maxRedeem(owner);
```
Within those limits, the owner can call:
```solidity theme={"theme":{"light":"github-light","dark":"github-dark"}}
uint256 sharesBurned = gVault.withdraw(assets, receiver, owner);
uint256 assetsOut = gVault.redeem(shares, receiver, owner);
```
If a caller other than `owner` is used, confirm the deployed contract's
authorization rules before building that transaction.
## Queued redemption
For an exit above current synchronous liquidity, submit:
```solidity theme={"theme":{"light":"github-light","dark":"github-dark"}}
uint256 requestId = gVault.requestRedeem(owner, shares);
```
Persist the chain, contract, transaction hash, `owner`, share amount, and
returned `requestId`. Once the request is processed, the same owner claims:
```solidity theme={"theme":{"light":"github-light","dark":"github-dark"}}
uint256 assetsOut = gVault.claimRedeem(owner, requestId);
```
Do not infer claimability from elapsed time. Read onchain request state or
simulate the claim against the current block before prompting the user.
## Centrifuge-style operations
Centrifuge-style backing vaults expose explicit request and claim methods:
```solidity theme={"theme":{"light":"github-light","dark":"github-dark"}}
requestDeposit(address owner, uint256 assets, uint256 minSharesOut)
claimDeposit(address owner, uint256 requestId)
requestRedeem(address owner, uint256 shares, uint256 minBasinAssetsOut)
claimRedeem(address owner, uint256 requestId)
```
Centrifuge-style deposit methods are restricted to the configured Ground
router. A user address cannot call them directly. For a fully direct customer
deposit, select a source with a synchronous backing vault; otherwise coordinate the router flow with
Ground.
See [gtoken contracts](/docs/gtokens/contracts) and
[Handle asynchronous gtoken operations](/docs/gtokens/async-operations).
# Discover gtoken yield sources
Source: https://docs.groundtech.co/docs/gtokens/discover-yield-sources
Use the gtoken catalogue to select a source, backing vault, network, asset, and contract interface.
Call the gtoken catalogue before configuring either a direct integration or a
fee wrapper. Each row identifies a gtoken yield source and the backing vault
contract used to obtain and redeem its tokens.
## List the catalogue
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl https://sandbox.groundtech.co/v2/gvaults/yield-sources \
-H "Authorization: Bearer $GROUND_API_KEY"
```
The response is an unpaginated integration manifest.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"data": [
{
"id": "ground-ustb-vault",
"name": "Ground USTB gVault",
"chain": "ethereum_sepolia",
"contractAddress": "0x5a68cfE0DCCcfA4991e5a92d2c382b15B41C4119",
"asset": {
"symbol": "USDC",
"contractAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"decimals": 6
},
"interface": "ground_ustb_v1"
}
]
}
```
## Persist the integration fields
Store these values per environment:
| Field | Use |
| - | - |
| `id` | `gVaultId` in access and fee-wrapper API calls |
| `chain` | Network selection and transaction validation |
| `contractAddress` | Direct onchain contract address |
| `interface` | Contract-interface and operation-flow selection |
| `asset` | Deposit-token symbol, contract address, and decimals |
Always verify `asset()` onchain against the manifest before requesting an
ERC-20 approval.
## Select by capability
Use the `interface` field to select the ABI for the returned contract. The
[gtoken contracts](/docs/gtokens/contracts) page links to the verified deployed
contracts, where Etherscan exposes the current ABI and read methods. An
USTB-style source can expose synchronous deposits and withdrawals plus queued
redemptions. A Centrifuge-style source exposes request/claim lifecycles and may
require the configured Ground router for deposits.
Never copy IDs or addresses between sandbox and production. Fetch and validate
the catalogue after changing API base URL or chain configuration.
See the complete schema in
[List gtoken yield sources](/api-reference/list-gtoken-yield-sources).
# Integrate gtokens directly onchain
Source: https://docs.groundtech.co/docs/gtokens/integrate-directly-onchain
Discover a gtoken source, authorize an EVM address, and integrate its backing vault's deposits and withdrawals.
In a direct integration, users deposit into an existing Ground vault and
receive gtokens in their existing wallets. Ground does not deploy a fee wrapper
for this path.
## 1. Create a server-side API key
Create an API key in the
[Ground Developer Portal](https://portal.groundtech.co). Store it only on your
backend.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export GROUND_API_KEY=""
```
Never expose this key in a browser or mobile client.
## 2. Discover available gtokens
Retrieve the environment's yield-source catalogue:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl https://sandbox.groundtech.co/v2/gvaults/yield-sources \
-H "Authorization: Bearer $GROUND_API_KEY"
```
Each returned row is an integration manifest. Its `id` is the `gVaultId` used
by the allowlist API, while `contractAddress` is the contract your application
calls onchain.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"id": "ground-ustb-vault",
"chain": "ethereum_sepolia",
"contractAddress": "0x1111111111111111111111111111111111111111",
"asset": {
"symbol": "USDC",
"contractAddress": "0x2222222222222222222222222222222222222222",
"decimals": 6
},
"interface": "ground_ustb_v1"
}
```
Do not copy production addresses into sandbox configuration. Resolve the
catalogue separately in each environment.
Set `GVAULT_ID` from the selected catalogue row and `CUSTOMER_ADDRESS` to the
address that will receive gtokens:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export GVAULT_ID="ground-ustb-vault"
export CUSTOMER_ADDRESS="0x2222222222222222222222222222222222222222"
```
## 3. Allowlist the user's address
Before the user's first deposit, follow
[Manage gtoken access](/docs/gtokens/manage-gtoken-access) to fetch Ground's
wallet authorization message, collect the address's signature, and submit the
`ownershipProof` from your backend. The message expires after five minutes.
Ground checks your organization's source access and screens the address before
admission.
A `202` response means processing is pending. Poll the allowlist endpoint and
enable deposits only after `allowed` is `true` and processing is complete.
## 4. Read the deposit asset
Read the ERC-4626-style `asset()` method from the backing vault contract:
```solidity theme={"theme":{"light":"github-light","dark":"github-dark"}}
function asset() external view returns (address);
```
Use the returned token address for the user's approval and express `assets` in
that token's native units.
## 5. Approve and deposit
The user first approves the backing vault contract to spend the deposit asset:
```solidity theme={"theme":{"light":"github-light","dark":"github-dark"}}
IERC20(asset).approve(gVaultAddress, assets);
```
Then the user deposits:
```solidity theme={"theme":{"light":"github-light","dark":"github-dark"}}
deposit(uint256 assets, address receiver)
```
Set `receiver` to the allowlisted address that should receive gtokens.
The backing vault checks the receiver's access, not merely the transaction sender.
Before submitting, you can estimate the output with:
```solidity theme={"theme":{"light":"github-light","dark":"github-dark"}}
previewDeposit(uint256 assets) external view returns (uint256 shares);
maxDeposit(address receiver) external view returns (uint256 assets);
```
## 6. Withdraw within current liquidity
Read the user's immediately available capacity:
```solidity theme={"theme":{"light":"github-light","dark":"github-dark"}}
maxWithdraw(address owner) external view returns (uint256 assets);
maxRedeem(address owner) external view returns (uint256 shares);
```
Within that capacity, the share owner can call:
```solidity theme={"theme":{"light":"github-light","dark":"github-dark"}}
withdraw(uint256 assets, address receiver, address owner)
redeem(uint256 shares, address receiver, address owner)
```
For a direct wallet transaction, set `owner` to the calling wallet. `receiver`
is the address that receives the deposit asset.
## 7. Request a larger redemption
When the requested exit exceeds current synchronous liquidity, submit an
asynchronous redemption:
```solidity theme={"theme":{"light":"github-light","dark":"github-dark"}}
requestRedeem(address owner, uint256 shares)
```
The transaction returns a `requestId`. Ground processes queued redemptions
onchain. After the request is processed, the same owner claims the proceeds:
```solidity theme={"theme":{"light":"github-light","dark":"github-dark"}}
claimRedeem(address owner, uint256 requestId)
```
Keep the original transaction hash and returned `requestId` in your operation
record until the claim confirms.
## 8. Remove deposit access
The managing organization can remove the address's deposit access:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X DELETE \
"https://sandbox.groundtech.co/v2/gvaults/$GVAULT_ID/allowlist/$CUSTOMER_ADDRESS" \
-H "Authorization: Bearer $GROUND_API_KEY"
```
Poll until `allowed` becomes `false`. Removal blocks future deposits and retains
your organization's management rights. It does not block redemption of an
existing position.
# gtokens
Source: https://docs.groundtech.co/docs/gtokens/introduction
Give users access to Ground RWA yield through gtokens or add revenue sharing with a fee wrapper.
gtokens give applications access to Ground RWA yield through tokens held in
their users' wallets. Users deposit a supported asset into the corresponding
Ground vault contract and receive receipt tokens. Your application can
integrate the contract directly or use a fee wrapper for revenue sharing.
## Choose an integration path
Use a direct gtoken integration when users should deposit into the backing
Ground vault and hold gtokens in their current wallets. Your backend uses
the Ground API only to manage deposit access. Deposits, withdrawals, and share
accounting happen onchain.
Use a fee wrapper when your application needs a performance fee, designated
fee recipient, or a customer allowlist isolated from the underlying vault.
Ground provisions the wrapper, but the underlying vault already exists.
`POST /v2/fee-wrappers` provisions a fee wrapper over an existing backing vault. It
never creates the underlying vault.
Allowlist EVM addresses and interact with a gtoken's backing Ground vault.
Provision a fee wrapper around a gtoken's backing vault and configure its performance fee.
## Start building
Complete a sandbox integration from discovery through the first deposit.
Learn how gtokens, backing vaults, fee-wrapper shares, and API resources fit together.
If you need Ground to provision wallets, manage allocations across several
yield sources, or automate rebalancing, use
[Portfolio Wallets](/docs/portfolio-wallets/introduction) instead.
# Manage fee-wrapper access
Source: https://docs.groundtech.co/docs/gtokens/manage-fee-wrapper-access
Authorize customer addresses to deposit into an organization fee wrapper.
Each fee wrapper has a customer allowlist separate from the gtoken's backing
vault allowlist. Ground authorizes the wrapper contract against that vault;
your backend authorizes customer addresses against the wrapper.
Use the wrapper `id` returned by the fee-wrapper API and the customer address
that will receive shares:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export FEE_WRAPPER_ID="8f4c52d7-62ab-4db9-a964-92f481f6b851"
export CUSTOMER_ADDRESS="0x3bf25a73a1f1033c2d50ac65f3c9d6a44123db81"
```
## Add an address
Wait for the wrapper to reach `status: ready`, then call:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X PUT \
"https://sandbox.groundtech.co/v2/fee-wrappers/$FEE_WRAPPER_ID/allowlist/$CUSTOMER_ADDRESS" \
-H "Authorization: Bearer $GROUND_API_KEY"
```
The path parameter is an address, which may be an EOA, smart account, multisig,
or contract.
## Verify access
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl \
"https://sandbox.groundtech.co/v2/fee-wrappers/$FEE_WRAPPER_ID/allowlist/$CUSTOMER_ADDRESS" \
-H "Authorization: Bearer $GROUND_API_KEY"
```
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"allowed": true
}
```
Only enable a deposit after the effective state is true. If the mutation returns
`202`, its transaction hash is only submission evidence; poll this endpoint or
consume the corresponding webhook event for confirmation.
## Remove access
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X DELETE \
"https://sandbox.groundtech.co/v2/fee-wrappers/$FEE_WRAPPER_ID/allowlist/$CUSTOMER_ADDRESS" \
-H "Authorization: Bearer $GROUND_API_KEY"
```
Removal blocks new deposits after confirmation. It preserves the address's
ability to redeem an existing fee-wrapper position.
API reference:
[add](/api-reference/add-fee-wrapper-allowlist),
[get](/api-reference/get-fee-wrapper-allowlist), and
[remove](/api-reference/delete-fee-wrapper-allowlist).
# Manage fee wrappers
Source: https://docs.groundtech.co/docs/gtokens/manage-fee-wrappers
Provision, retrieve, and update a fee wrapper over a gtoken's existing backing vault.
A fee wrapper is an organization-specific contract over a gtoken's existing backing vault. It
adds management and performance fees and its own customer allowlist. Provisioning a
wrapper does not create the underlying vault.
Set `GVAULT_ID` to the `id` selected from `GET /v2/gvaults/yield-sources`:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export GVAULT_ID="ground-ustb-vault"
```
## Provision a wrapper
Select a `gVaultId` from the gtoken catalogue and generate a UUID v4:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST https://sandbox.groundtech.co/v2/fee-wrappers \
-H "Authorization: Bearer $GROUND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"requestId": "2b8332ce-5fe8-4fa6-a1da-d30a78285e07",
"gVaultId": "'$GVAULT_ID'",
"feeRecipientAddress": "0x92e6a1ed742f0c8502f902c61775f57728985b42",
"performanceFeeBps": 2000
}'
```
`performanceFeeBps` is the share of gains above the high-water mark paid to the
fee recipient. `2000` is 20%. The initial rate is also the wrapper's permanent
maximum; creating it with zero prevents enabling a performance fee later.
Use the same `requestId` when retrying the same logical request. Reusing it with
different parameters returns `409 request_id_conflict`.
To charge an annual management fee, include `"managementFeeBps": 10` in the
create request for 0.10% annually. The optional rate defaults to zero and accepts
integers from 0 through 100. Ground sets the cap to 100 bps (1.00%); do not pass
`maxManagementFeeBps`. Both fee types use `feeRecipientAddress`.
GET and list responses include `managementFeeBps` and read-only
`maxManagementFeeBps`. Older wrappers retain their original deployed caps.
## Provisioning statuses
| Status | Meaning | Application action |
| - | - | - |
| `provisioning` | Ground accepted the request, but deployment or configuration is incomplete | Persist the IDs and wait |
| `ready` | The wrapper is deployed and authorized against its backing vault | Enable customer access and onchain use |
| `failed` | Provisioning reached a terminal failure | Surface the error and contact Ground before creating a replacement |
## Wait for readiness
The create call returns `202` while provisioning is in progress. Poll the
resource:
Set `FEE_WRAPPER_ID` to the `id` returned by the create response:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export FEE_WRAPPER_ID="8f4c52d7-62ab-4db9-a964-92f481f6b851"
```
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl \
"https://sandbox.groundtech.co/v2/fee-wrappers/$FEE_WRAPPER_ID" \
-H "Authorization: Bearer $GROUND_API_KEY"
```
Do not send customer transactions until `status` is `ready`. At that point,
persist both `id` for API calls and `contractAddress` for onchain calls.
Ground has also authorized the wrapper against its underlying vault.
If your original response was lost, recover by provisioning request:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl \
"https://sandbox.groundtech.co/v2/fee-wrappers?requestId=2b8332ce-5fe8-4fa6-a1da-d30a78285e07" \
-H "Authorization: Bearer $GROUND_API_KEY"
```
## Update the fee configuration
Update the annual management rate with a separate request containing only
`managementFeeBps`, for example `{"managementFeeBps": 20}`. The rate must be an
integer from 0 through `maxManagementFeeBps` (at most 100). Zero disables management
fees; they can be re-enabled within the original cap. Legacy wrappers with a
zero cap cannot enable management fees. The cap is immutable and is not accepted
as input.
Performance-rate updates remain available while management fees are enabled,
but recipient changes return `409 operation_not_supported`. Send management
updates separately from performance or recipient updates; combined requests
return `400 validation_error`. Each changed request submits one transaction.
The confirmed rate appears in GET/list and `fee_wrapper.fees.status_changed`.
Failed updates do not change stored fees; their webhook includes the requested rate.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X PATCH \
"https://sandbox.groundtech.co/v2/fee-wrappers/$FEE_WRAPPER_ID/fees" \
-H "Authorization: Bearer $GROUND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"feeRecipientAddress": "0x4444444444444444444444444444444444444444",
"performanceFeeBps": 1500
}'
```
Omit a field to leave it unchanged. The contract accrues fees under the old
configuration before applying the update. The new rate cannot exceed
`maxPerformanceFeeBps`.
API reference:
[provision](/api-reference/create-fee-wrapper),
[list](/api-reference/list-fee-wrappers),
[get](/api-reference/get-fee-wrapper), and
[update fees](/api-reference/update-fee-wrapper-fees).
# Manage gtoken access
Source: https://docs.groundtech.co/docs/gtokens/manage-gtoken-access
Authorize, verify, and remove an address for direct deposits into a gtoken backing vault.
The allowlist permits an address to deposit into a gtoken's backing vault. Your
organization must have access to the selected yield source. Keep your Ground API
key on your backend.
## Add an address
Use the catalogue's `id` as `gVaultId` and the deposit receiver as
`customerAddress`. From your backend, fetch the authorization:
```javascript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const baseUrl = 'https://sandbox.groundtech.co';
const gVaultId = 'ground-ustb-vault';
const customerAddress = '0x2222222222222222222222222222222222222222';
const path = `/v2/gvaults/${gVaultId}/allowlist/${customerAddress}`;
const headers = { Authorization: `Bearer ${process.env.GROUND_API_KEY}` };
const response = await fetch(`${baseUrl}${path}/authorization`, { headers });
if (!response.ok) throw new Error(`Authorization request failed: ${response.status}`);
const authorization = await response.json();
```
Send this authorization response to your wallet frontend. With an ethers v5
signer for `customerAddress` on the selected chain, sign its returned typed data:
```javascript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const { typedData, issuedAt, expiresAt } = authorization;
const signature = await signer._signTypedData(
typedData.domain, typedData.types, typedData.value,
);
const ownershipProof = { issuedAt, expiresAt, signature };
```
The message binds your organization, address, contract, and chain. It expires
after five minutes. EOAs and smart accounts with ERC-1271 signature validation
are supported. An address that cannot produce a valid authorization cannot
establish customer management through this endpoint.
Return `ownershipProof` to your backend and submit it before it expires:
```javascript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const result = await fetch(`${baseUrl}${path}`, {
method: 'PUT',
headers: {
...headers,
'Content-Type': 'application/json',
},
body: JSON.stringify({ ownershipProof }),
});
if (![200, 202].includes(result.status)) {
throw new Error(`Allowlist request failed: ${result.status}`);
}
```
A new admission requires a matching completed LOW-risk assessment. The operation is idempotent for the desired state:
* `200` means processing is complete. Check `allowed` before depositing.
* `202` means processing is pending; screening or transaction confirmation may
still be in progress. Retry or poll the state.
## Verify the effective state
```javascript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const response = await fetch(`${baseUrl}${path}`, { headers });
if (!response.ok) throw new Error(`Allowlist read failed: ${response.status}`);
const state = await response.json();
```
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"allowed": true,
"status": "confirmed",
"canManage": true
}
```
Enable deposits only when `allowed` is `true` and processing is complete. A
transaction hash alone does not establish access.
The organization that establishes verified management keeps removal rights.
Other organizations can observe an already-allowed address with
`canManage: false`. An onchain permission without verified customer management
is reported as `unmanaged`. Product and Ground infrastructure addresses are
managed through their product workflows.
## Remove an address
```javascript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const response = await fetch(`${baseUrl}${path}`, { method: 'DELETE', headers });
if (![200, 202].includes(response.status)) {
throw new Error(`Removal request failed: ${response.status}`);
}
```
Only the managing organization can request removal. Poll until `allowed` becomes
`false`. Removal blocks future deposits and retains your organization's management
rights. It does not seize shares or block redemption of an existing position.
## Handle failures
An invalid or expired signature returns `400`; obtain a fresh authorization and
sign it again. `403` means your organization or API key lacks access. `404` means
the source was not found. `409` means the operation is unsupported or conflicts
with an unresolved update. Read the current state before retrying a temporary
`503` failure.
API reference:
[add](/api-reference/add-gtoken-allowlist),
[get](/api-reference/get-gtoken-allowlist), and
[remove](/api-reference/delete-gtoken-allowlist).
# gtoken quickstart
Source: https://docs.groundtech.co/docs/gtokens/quickstart
Discover a sandbox gtoken source, authorize an address, and deposit into its backing vault.
This quickstart follows the direct integration path. Your backend uses Ground's
API to discover a gtoken source and authorize an address. The user then approves
the deposit asset and deposits into the backing vault onchain to receive gtokens.
## Prerequisites
You need a Ground sandbox API key, an EVM address controlled by your user or
application, and enough of the listed deposit token and native gas token to
submit sandbox transactions.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export GROUND_API_KEY=""
export CUSTOMER_ADDRESS="0x2222222222222222222222222222222222222222"
```
Keep the Ground API key on your server. Never place it in frontend code.
## 1. Discover a gtoken source
The gtoken API uses the `/v2/gvaults` namespace. Use the returned source ID as
`gVaultId`.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl https://sandbox.groundtech.co/v2/gvaults/yield-sources \
-H "Authorization: Bearer $GROUND_API_KEY"
```
Select a row and persist its `id`, `contractAddress`, `asset`, and `interface`.
Resolve these values independently in sandbox and production.
Set `GVAULT_ID` to the selected row's `id`:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export GVAULT_ID="ground-ustb-vault"
```
See [Discover gtoken yield sources](/docs/gtokens/discover-yield-sources) for
the complete response contract.
## 2. Authorize the address
Follow [Manage gtoken access](/docs/gtokens/manage-gtoken-access) to fetch the
wallet authorization message, have the address sign it, and submit its
`ownershipProof` from your backend. Ground checks your source access and screens
the address before admission.
A `202` response means processing is pending. Poll the allowlist endpoint and
enable deposits only after `allowed` is `true` and processing is complete.
## 3. Read the contract configuration
At the returned `contractAddress`, call `asset()` to resolve the ERC-20 deposit
token. Treat the catalogue's `interface` as the selector for the correct vault
interface; not every backing vault has the same synchronous or asynchronous methods.
```solidity theme={"theme":{"light":"github-light","dark":"github-dark"}}
address depositAsset = gVault.asset();
uint8 shareDecimals = gVault.decimals();
```
## 4. Submit the deposit
For a synchronous USTB-style backing vault, approve the exact asset amount and call:
```solidity theme={"theme":{"light":"github-light","dark":"github-dark"}}
IERC20(depositAsset).approve(gVaultAddress, assets);
uint256 shares = gVault.deposit(assets, customerAddress);
```
Set the deposit receiver to the authorized address. A successful synchronous
deposit sends gtokens to that address. For a Centrifuge-style backing vault,
deposits use a request/claim lifecycle instead. Follow
[Deposit and redeem onchain](/docs/gtokens/deposit-and-redeem) for the interface
returned by the selected yield source.
## 5. Record the result
Store the environment, `gVaultId`, contract address, chain ID, transaction hash,
asset amount, share amount, and receiving address. Treat the transaction as
complete only after the required chain confirmations.
Build the complete direct access flow.
Have Ground provision a fee wrapper over the selected gtoken's backing vault.
# Test a gtoken integration in sandbox
Source: https://docs.groundtech.co/docs/gtokens/sandbox-testing
Validate discovery, access, contract calls, asynchronous operations, and webhooks before production.
Use `https://sandbox.groundtech.co` for API calls and the chain returned by the
sandbox gtoken catalogue for transactions. Sandbox and production are separate
environments with separate IDs, contract addresses, API keys, and webhook
registrations.
## Build an environment manifest
At application startup, call `GET /v2/gvaults/yield-sources` and record:
* API base URL and environment
* `gVaultId`, `interface`, chain, and `contractAddress`
* asset contract address, symbol, decimals, and chain ID
Fail closed if the configured chain or contract does not match the manifest.
## Test the direct path
1. Add a fresh test address to the selected gtoken allowlist.
2. Wait for the effective state to become `allowed: true`.
3. Approve and deposit the smallest supported test amount.
4. Confirm the gtoken balance at the intended receiver.
5. Exercise a synchronous withdrawal when available.
6. Exercise request/claim redemption when supported.
7. Remove access and verify a new deposit is blocked while exit remains possible.
## Test the fee-wrapper path
1. Provision a wrapper with a new UUID v4 and nonzero test fee.
2. Retry the identical create request to validate idempotency.
3. Recover the wrapper by `requestId` and wait for `ready`.
4. Add a customer address to the wrapper allowlist.
5. Deposit and confirm that the customer receives wrapper shares, while the wrapper holds the underlying gtokens.
6. Reduce the performance fee and change its recipient.
7. Remove customer access and verify the existing position can still exit.
## Test failure handling
Exercise invalid addresses, unknown IDs, conflicting `requestId` payloads,
unconfirmed access, wrong-chain transactions, insufficient balances, and
temporary `429` or `503` responses. Retries should reconcile current state
before resubmission.
## Test webhooks
Register a sandbox-only HTTPS endpoint, preserve the one-time secret, verify
signatures from the raw body, deduplicate by `Ground-Event-Id`, and compare your
processed events with `GET /v2/gvaults/webhooks/events`.
Before production, fetch a fresh production catalogue and repeat the manifest
checks. Never promote sandbox contract addresses or webhook secrets.
# Integrate gtoken webhooks
Source: https://docs.groundtech.co/docs/gtokens/webhooks
Receive and verify gtoken access, fee-wrapper, and fee-configuration lifecycle events.
Use gtoken webhooks to react to asynchronous API and onchain state changes
without polling every resource. Webhooks complement authoritative API and
onchain reads; they do not replace them.
## Event types
| Event | Resource | Important `data` fields |
| - | - | - |
| `gvault.allowlist.status_changed` | Direct backing-vault deposit authorization | `gVaultId`, `address`, `status`, `transactionHash`, `error` |
| `fee_wrapper.status_changed` | Fee-wrapper provisioning | `feeWrapperId`, `status`, `contractAddress`, `error` |
| `fee_wrapper.fees.status_changed` | Fee configuration update | `feeWrapperId`, `status`, `transactionHash`, `error` |
| `fee_wrapper.allowlist.status_changed` | Wrapper customer authorization | `feeWrapperId`, `address`, `status`, `transactionHash`, `error` |
Every delivery uses `{ id, type, createdAt, data }`. Treat identifiers and the
current resource read as authoritative because webhook arrival order is not
guaranteed.
## 1. Create a registration
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST https://sandbox.groundtech.co/v2/gvaults/webhooks \
-H "Authorization: Bearer $GROUND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://ops.acme.com/webhooks/ground-gvaults",
"events": [
"gvault.allowlist.status_changed",
"fee_wrapper.status_changed",
"fee_wrapper.fees.status_changed",
"fee_wrapper.allowlist.status_changed"
],
"description": "Sandbox gVault lifecycle events"
}'
```
Store the returned `id` and `secret`. The secret is shown only in the create
response and is not returned by list or get calls.
## 2. Verify every delivery
Ground sends JSON with `Ground-Event-Id`, `Ground-Event-Type`, and
`Ground-Signature` headers. Verify the signature against the exact raw request
body before parsing JSON:
```javascript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const crypto = require('crypto');
function verifyGroundSignature(rawBody, header, secret, tolerance = 300) {
const parts = Object.fromEntries(header.split(',').map((part) => part.split('=')));
const timestamp = Number(parts.t);
if (!Number.isFinite(timestamp)) throw new Error('Invalid signature timestamp');
if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > tolerance) {
throw new Error('Signature timestamp outside tolerance');
}
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const actual = parts.v1 || '';
if (expected.length !== actual.length ||
!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(actual))) {
throw new Error('Invalid signature');
}
}
```
Return a `2xx` response promptly, then process the event asynchronously.
Deduplicate deliveries by `Ground-Event-Id`.
## 3. Reconcile the resource
Route on `type` or `Ground-Event-Type`, then use the identifiers in `data`
to read the current resource state. Events can be delayed or delivered more
than once, so update local state monotonically and never assume arrival order.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"id": "73a8e0c9-c20d-4b10-b9a4-0f0ff6b5cb9c",
"type": "fee_wrapper.status_changed",
"createdAt": "2026-09-03T17:35:39.000Z",
"data": {
"feeWrapperId": "8f4c52d7-62ab-4db9-a964-92f481f6b851",
"status": "ready",
"contractAddress": "0x7c4a1cb5a3d5c3c4e019c3f6ab29a0f98c21b814"
}
}
```
## 4. Inspect delivery history
List events across the organization with
`GET /v2/gvaults/webhooks/events`, or for one registration with
`GET /v2/gvaults/webhooks/{id}/events`. Use `status`, `attemptCount`,
`deliveredAt`, and `lastError` to reconcile your receiver.
## Delivery statuses
| Status | Meaning |
| - | - |
| `pending` | Delivery has not completed |
| `completed` | Your endpoint accepted the delivery |
| `failed` | Delivery did not complete; inspect `attemptCount` and `lastError` |
Delivery status is separate from the business-operation status inside `data`.
For example, a successfully delivered fee-update event can have delivery status
`completed` while its payload reports the fee update as `failed`.
## 5. Delete a registration
Call `DELETE /v2/gvaults/webhooks/{id}`. Deletion stops future
deliveries; retain your processed-event ledger for audit and deduplication.
API reference:
[create](/api-reference/create-gtoken-webhook),
[list registrations](/api-reference/list-gtoken-webhooks),
[get](/api-reference/get-gtoken-webhook),
[list events](/api-reference/list-gtoken-webhook-events), and
[delete](/api-reference/delete-gtoken-webhook).
# Introduction
Source: https://docs.groundtech.co/docs/introduction
Ground is cash management infrastructure for stablecoins. One API connects balances to
yield that institutions already use to hold value, and handles allocation, rebalancing,
withdrawals, and reporting on top of it.
## Who it is for
Fintechs, neobanks and exchanges putting customer balances to work; payment companies
holding float; and treasuries earning on their own reserves. Two integration guides walk
the common shapes end to end — a
[treasury portfolio](/docs/portfolio-wallets/integration-guides/treasury-portfolio-wallet)
and a
[neobank or exchange customer portfolio](/docs/portfolio-wallets/integration-guides/neobank-customer-portfolio-wallet).
## What you can hold
Four categories currently, all reached through the same endpoints. **Tokenized funds** cover
tokenized treasury and money-market exposure such as Invesco Short Duration US Government
Securities and Janus Henderson Anemoy Treasury. **Structured credit** covers the Janus
Henderson Anemoy AAA CLO fund alongside Figure PRIME and AUTO.
**Institutional credit** is available through Maple, and **onchain lending** through
curated vaults on Aave, Morpho and Kamino.
Stocks and fixed income products are coming soon.
The catalog is live and is updated often with new products. [Yield sources](/docs/portfolio-wallets/yield-sources)
describes yield sources, rate semantics, NAV freshness and liquidity terms.
`GET /v2/public/yield-sources` returns the public catalog; sources available to your
organization may differ.
## What comes with the API
* **Risk.** The Ground Risk Engine takes absolute or velocity-based rules on APY,
liquidity and utilization, and what should happen when one triggers. Portfolio Wallets
and MicroVaults carry it; gTokens do not.
* **Reporting.** Yield and activity accounting are generated for you and readable from
both the dashboard and the API —
[accounting activity](/api-reference/list-accounting-activity),
[yield period summaries](/api-reference/get-accounting-yield-period-summary) and
[computed yield](/api-reference/compute-yield). Each is scoped by `walletId`, so
figures resolve to an individual end user and not only to the organization total.
* **Custody.** Every implementation is non-custodial. Signing keys stay with your
organization or your users; Ground never holds them.
[Security](/docs/security/custody-model) covers the signing model, wallet types per
chain and the independent review.
* **Chains.** Ground handles routing and bridging across
[supported chains](/docs/portfolio-wallets/supported-chains), so you do not need to
move assets off the chain you are already on.
## Choose an implementation
Ground exposes the same yield sources through three implementations. The right one depends
on your stack.
| Implementation | Who holds the balance | Best for |
| - | - | - |
| [Portfolio Wallets](/docs/portfolio-wallets/introduction) | Your organization, in provisioned wallets | Building an earn product from nothing. |
| [MicroVaults](/docs/microvaults/introduction) | Multiple approved wallets, as ERC-20 shares | Building an earn feature into an existing wallet product. |
| [gTokens](/docs/gtokens/introduction) | Your users, in their own wallets | Getting easy, one-click access to our RWA yield products. |
MicroVaults are in private preview for enabled sandbox organizations on Ethereum Sepolia.
Production and Ethereum mainnet are not yet supported.
## Reach the API
[Sign up for the Ground Dashboard](https://portal.groundtech.co/signup) to create an
organization and issue an API key. Self-serve production access supports up to two
Portfolio Wallets and \$100 in cumulative withdrawals.
[Contact Ground](https://www.groundtech.co/contact) for full production access.
| Environment | Base URL |
| - | - |
| Sandbox | `https://sandbox.groundtech.co` |
| Production | `https://production.groundtech.co` |
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export GROUND_API_KEY=""
curl "https://sandbox.groundtech.co/v2/wallets" \
-H "Authorization: Bearer $GROUND_API_KEY"
```
Build against sandbox, then swap the base URL. The endpoint structure is identical, but
sandbox uses explicit testnet chain keys such as `ethereum_sepolia` and may expose a
smaller yield-source catalog than production.
[API Conventions](/docs/portfolio-wallets/api-conventions) covers authentication,
pagination, idempotency, rate limits, and error handling.
## API reference
Each implementation has a separate API reference tab. Endpoints are grouped by resource
and can be called against sandbox with your own key when your organization has access.
Wallets, deposits, withdrawals, approvals, accounting, and webhooks.
Vaults, strategies, fees, shareholders, redemptions, and performance.
Yield sources, direct access allowlists, fee wrappers, and webhooks.
## Start building
Create a wallet and initiate a deposit in five minutes.
The catalog behind every implementation, and how to build a strategy from it.
## Operating in production
* [Supported chains](/docs/portfolio-wallets/supported-chains) — where deposits and payouts settle.
* [Webhooks](/docs/portfolio-wallets/webhooks) and [polling](/docs/portfolio-wallets/polling) — how to follow state you do not control synchronously.
* [Custody model](/docs/security/custody-model) — who can move funds, and what Ground can and cannot do.
* [Changelog](/docs/changelog) — API and documentation changes.
# Synchronous and asynchronous operations
Source: https://docs.groundtech.co/docs/microvaults/async-operations
Understand when an operation settles immediately and when it requires later processing.
## Synchronous operations
Subscriptions are synchronous: the wallet transaction deposits the base asset and mints shares. A redemption can also settle synchronously when the vault can return the requested assets immediately.
## Asynchronous redemptions
When immediate liquidity is unavailable, redemption has three stages:
1. The shareholder signs a request that reserves shares for redemption.
2. The vault exits the required yield-source positions.
3. The shareholder claims the available base asset.
The claim amount is the idle assets assigned to the request plus the actual proceeds from its source exits. It is not guaranteed to equal the preview estimate.
The redemption plan identifies the mode before the wallet signs and includes expected timing when available. See [Redeem MicroVault shares](/docs/microvaults/redeem) for the complete request and claim procedure.
## Requested-redemption states
| State | Meaning |
| - | - |
| `pending` | Shares are reserved and one or more underlying source exits remain. |
| `fulfilling` | Source proceeds are being assigned to the redemption. |
| `claimable` | Liquidation is complete and the fixed receiver can receive the actual proceeds. |
| `claimed` | The base asset has been transferred to the receiver. |
| `cancelled` | The request was cancelled and its reserved shares were returned to the owner. |
Expected and maximum processing times come from the underlying sources. They are estimates for planning and UI messaging, not settlement guarantees.
## Balances during processing
Total assets continue to include assets in flight and amounts reserved for redemptions. A source position can therefore decrease before the shareholder can claim the proceeds. Use source in-flight amounts and redemption activity to explain the difference.
The shareholder signs the request and claim transactions. Ground manages the underlying source exit while it remains authorized to manage the vault.
# Concepts and control model
Source: https://docs.groundtech.co/docs/microvaults/concepts
## Vault and shares
Each MicroVault accepts one base asset and issues its own ERC-20 shares. The vault contract address is also the share-token address.
## Authority
The vault authority controls strategy, fees, shareholder access, pauses, and source permissions. By default, this is your organization's Turnkey authority and Ground submits the changes you authorize. You can export an individual vault to another wallet or smart-contract authority, after which that authority becomes responsible for operating it.
## Shareholder policy
Only approved shareholder wallets may subscribe or receive shares. The vault stores a compact onchain commitment to that list, called a Merkle root. The proof endpoint returns the data a wallet transaction needs to show that an address is on the current list. See [Manage shareholders](/docs/microvaults/shareholders) for the update procedure.
## Source modes and strategy
Target allocations describe where the vault should hold its assets. Source modes separately control whether the vault may enter or exit each source. Changing a mode does not erase an existing position or change its target allocation.
## Fees
The management fee is an annual percentage of shareholder capital. The performance fee applies only to gains above the vault's previous high-water NAV. Both fees accrue by minting shares to the configured fee recipient, which avoids removing base assets from the strategy.
## Ground management
While Ground manages the vault, automation moves assets toward the target strategy, responds to subscriptions and redemptions, and completes asynchronous source operations. Its onchain permissions limit which sources and vault actions it can use.
## Valuation
NAV per share reflects the vault's idle assets, yield-source positions, in-flight operations, and redemption liabilities. It determines how many shares a subscription receives and how much base asset a redemption returns.
# MicroVault contracts
Source: https://docs.groundtech.co/docs/microvaults/contracts
The current Ethereum Sepolia deployment is:
| Contract | Address |
| - | - |
| Factory | [`0x0F62A920980b03F2fd8fEc9EFAC6040a6595a910`](https://sepolia.etherscan.io/address/0x0F62A920980b03F2fd8fEc9EFAC6040a6595a910) |
| Policy registry | [`0x3C95322dC3f394dbfEa06d5c92CDae918b620ea5`](https://sepolia.etherscan.io/address/0x3C95322dC3f394dbfEa06d5c92CDae918b620ea5) |
| Vault registry | [`0x9D1BeEdf0c9F1b8F02D669093247DeeBF0496CEb`](https://sepolia.etherscan.io/address/0x9D1BeEdf0c9F1b8F02D669093247DeeBF0496CEb) |
| Yield-source registry | [`0x46dCe2209A3b2CE59073079842AcE59B895b0078`](https://sepolia.etherscan.io/address/0x46dCe2209A3b2CE59073079842AcE59B895b0078) |
| Manager | [`0x4E2e5EAFf29732D83e9087CE996570ce3A333F8C`](https://sepolia.etherscan.io/address/0x4E2e5EAFf29732D83e9087CE996570ce3A333F8C) |
| Async account factory | [`0x96a8C1bA900aE508DC7a801830785678d8fa7AEB`](https://sepolia.etherscan.io/address/0x96a8C1bA900aE508DC7a801830785678d8fa7AEB) |
The table lists shared MicroVault infrastructure. Most shareholder integrations do not call these contracts directly: fetch the MicroVault from `GET /v2/microvaults/vaults/{vaultId}` and use its `address` for subscriptions, redemptions, share balances, and ERC-20 token metadata.
Each MicroVault address is both the vault contract and its share-token address. Yield-source addresses vary by source and environment; fetch them from the yield-source endpoints rather than copying addresses from this page. No production MicroVault deployment is published.
# Create a MicroVault
Source: https://docs.groundtech.co/docs/microvaults/create-vault
Create a vault and track its deployment.
Creating a MicroVault deploys a non-upgradeable vault with its own ERC-20 share token. The vault contract address is also the share-token address.
## Prerequisites
* MicroVaults enabled for your organization
* A base asset returned by `GET /v2/microvaults/yield-sources?chainId=11155111`
* A fee recipient address
## 1. Submit the vault settings
Fees use basis points: `100` is `1%`. Use `0` for no fee.
| Field | Meaning |
| - | - |
| `baseAsset` | Token shareholders deposit and receive on redemption. It must match an available MicroVault yield source on the selected chain. |
| `name` | Full ERC-20 name shown for the share token. |
| `symbol` | Short ERC-20 ticker shown for the share token. |
| `feeRecipient` | Address that receives fee shares when fees accrue. |
| `managementFeeBps` | Annual management fee in basis points. |
| `performanceFeeBps` | Fee on gains above the vault's high-water NAV, in basis points. |
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -sS -X POST "https://sandbox.groundtech.co/v2/microvaults/vaults" \
-H "Authorization: Bearer $GROUND_API_KEY" \
-H 'Content-Type: application/json' \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"baseAsset":"0x1c7d4b196cb0c7b01d743fbc6116a902379c7238",
"name":"Acme Treasury MicroVault",
"symbol":"acmeUSDC",
"feeRecipient":"0x76f8fc6667e239f83a547d4e16225d6a34f6fa22",
"managementFeeBps":0,
"performanceFeeBps":0
}'
```
The response describes the asynchronous creation task. Save `task.id` to identify this creation attempt.
## 2. Track provisioning
```javascript theme={"theme":{"light":"github-light","dark":"github-dark"}}
async function waitForVault(name) {
for (;;) {
const response = await fetch(`${process.env.GROUND_API}/v2/microvaults/vaults`, {
headers: { Authorization: `Bearer ${process.env.GROUND_API_KEY}` },
});
if (!response.ok) throw new Error(await response.text());
const result = await response.json();
const vault = result.items.find((item) => item.shareName === name);
if (vault) return vault;
const pending = result.provisionings.find((item) => item.shareName === name);
if (pending?.status === "failed") throw new Error(pending.error);
await new Promise((resolve) => setTimeout(resolve, 3000));
}
}
```
## Confirm creation
Creation is complete when the vault appears in `items` with `status: "active"`. Save its `id` for API calls. Its `address` is both the vault contract address and the ERC-20 share-token address.
If the provisioning entry reports `failed`, do not create a duplicate vault with the same settings. Record its task ID and error, correct the reported cause, and retry with a new idempotency key.
# Use emergency controls
Source: https://docs.groundtech.co/docs/microvaults/emergency-controls
Pause or resume individual MicroVault operations.
Emergency controls let you stop one class of vault activity without disabling unrelated operations.
| Operation | Effect while paused |
| - | - |
| `subscriptions` | Blocks new base-asset subscriptions. |
| `redemptions` | Blocks new instant and requested redemptions. |
| `share_transfers` | Blocks transfers between shareholder wallets. |
| `source_entries` | Blocks moving base assets into yield sources. |
| `source_exits` | Blocks initiating exits from yield sources. |
Claims for completed requested redemptions remain available because the underlying obligation already exists.
## Prerequisites
* An active `VAULT_ID`
* The operation that should be paused or resumed
* `uuidgen` for the idempotency key
## 1. Read the current controls
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -sS "$GROUND_API/v2/microvaults/vaults/$VAULT_ID/pause-status" \
-H "$AUTH" | jq
```
Each response field is `true` when that capability is paused.
## 2. Pause one operation
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -sS -X PUT "$GROUND_API/v2/microvaults/vaults/$VAULT_ID/pause" \
-H "$AUTH" -H 'Content-Type: application/json' \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"operation":"subscriptions","paused":true}' | jq
```
The response contains a task for the onchain control change. Other operations retain their current settings.
## 3. Resume the operation
Submit the same operation with `paused: false` and a new idempotency key.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -sS -X PUT "$GROUND_API/v2/microvaults/vaults/$VAULT_ID/pause" \
-H "$AUTH" -H 'Content-Type: application/json' \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"operation":"subscriptions","paused":false}' | jq
```
## Confirm the control
After the task confirms, fetch `pause-status` again and verify the selected field. Treat the previous setting as active until the onchain update is confirmed.
# Export vault authority
Source: https://docs.groundtech.co/docs/microvaults/export-authority
Transfer one MicroVault from Ground management to an authority you control.
Export transfers administrative control of one vault. It does not move shareholder assets or shares.
## Prerequisites
* The destination EOA or ERC-1271 contract address
* A plan to operate strategy, fees, shareholders, pauses, and source movements after export
## 1. Open the vault settings
In the Ground Portal, open the MicroVault and select **Settings**. Find **Vault authority**, then select **Export MicroVault**.
## 2. Review the destination
Enter the new authority address and verify it carefully. Export does not move assets or shares, but the destination gains control over strategy, fees, shareholder access, pauses, and source movements.
Confirm the acknowledgement and submit the export. Ground sends the authority transfer for onchain confirmation.
## 3. Wait for confirmation
Keep the settings dialog open until the Portal reports that the export is complete. Do not attempt another export while the first transfer is pending.
## Confirm the export
The vault displays the destination as its authority and Ground-managed settings become read-only. Returning control requires the new authority to transfer it back.
After export, the new authority must manage strategy updates, shareholder-list changes, fees, pauses, source permissions, rebalances, and asynchronous source operations. Confirm that your authority tooling can perform these actions before exporting a vault that holds assets.
# Configure fees
Source: https://docs.groundtech.co/docs/microvaults/fees
Set management and performance fees and choose who receives fee shares.
MicroVault fees accrue by minting shares to the fee recipient. This preserves the vault's invested assets while diluting other shareholders by the fee amount.
The management fee accrues annually on shareholder capital. The performance fee applies to gains above the vault's previous high-water NAV, so the same performance is not charged twice.
## Prerequisites
* An active `VAULT_ID`
* The fee recipient's EVM address
* Management and performance fees expressed in basis points
* `uuidgen` for the idempotency key
`100` basis points is `1%`. Use `0` to disable either fee.
## 1. Read the current policy
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -sS "$GROUND_API/v2/microvaults/vaults/$VAULT_ID" -H "$AUTH" | jq '.vault'
```
Record the existing recipient and both fee rates. An update replaces the complete fee policy rather than changing one field in isolation.
## 2. Submit the new policy
This example sets a `0.25%` annual management fee and a `10%` performance fee.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -sS -X PUT "$GROUND_API/v2/microvaults/vaults/$VAULT_ID/fees" \
-H "$AUTH" -H 'Content-Type: application/json' \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"feeRecipient":"0x76f8fc6667e239f83a547d4e16225d6a34f6fa22",
"managementFeeBps":25,
"performanceFeeBps":1000
}' | jq
```
The response contains a task for the onchain configuration change. Fee shares minted before this update remain with the previous recipient.
## 3. Wait for confirmation
Refresh the vault until the task has confirmed and the new fee policy appears. Do not submit another fee update while the first change is pending.
## Confirm the fee policy
Verify all three values: fee recipient, management fee, and performance fee. A confirmed policy controls future accrual; it does not retroactively recalculate fees already accrued.
# MicroVaults
Source: https://docs.groundtech.co/docs/microvaults/introduction
Customer-owned onchain vaults with externally held ERC-20 shares.
MicroVaults are in private preview for enabled sandbox organizations on Ethereum Sepolia. Production and Ethereum mainnet are not yet supported.
A MicroVault pools one base asset and issues ERC-20 shares directly to shareholder wallets. Its strategy allocates the pooled assets across yield sources available through Ground.
The vault's NAV includes idle assets, yield-source positions, assets moving through asynchronous operations, and amounts reserved for redemptions.
## When to use a MicroVault
A MicroVault is useful when multiple wallets need proportional ownership of one managed onchain portfolio. Common uses include offering a branded stablecoin yield product, managing pooled treasury or operating liquidity, and giving approved partners or users shares in a dedicated investment strategy without creating and managing a separate portfolio for every wallet.
Use a Portfolio Wallet when one organization needs a dedicated account for its own assets. Use a MicroVault when multiple approved wallets need transferable units of ownership in the same strategy and should subscribe or redeem directly from their own wallets.
## Lifecycle
1. Create the vault and its ERC-20 share token.
2. Approve the wallets that may receive shares.
3. Set target allocations across idle base assets and yield sources.
4. Shareholders subscribe, hold shares, and redeem them for the base asset.
5. Monitor NAV, earnings, source positions, and activity.
## What you control
You create each vault, approve its shareholders, set fees and target allocations, and choose which yield sources it may enter or exit. You can transfer management of an individual vault at any time.
## What shareholders control
Approved external wallets subscribe with the base asset, receive the vault's ERC-20 shares, request redemptions, and claim asynchronous redemption proceeds.
# MicroVault quickstart
Source: https://docs.groundtech.co/docs/microvaults/quickstart
Create and inspect a sandbox MicroVault.
MicroVaults are available to enabled sandbox organizations on Ethereum Sepolia.
## Prerequisites
* A sandbox organization with MicroVaults enabled
* A Ground sandbox API key
* `curl`, `jq`, and `uuidgen`
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export GROUND_API="https://sandbox.groundtech.co"
export GROUND_API_KEY="..."
export AUTH="Authorization: Bearer $GROUND_API_KEY"
```
## 1. Check creation readiness
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -sS "$GROUND_API/v2/microvaults/vaults" -H "$AUTH" | jq '.creation'
```
Continue when `creation.status` is `accepted`. Ground completes account setup before enabling MicroVaults for your organization.
## 2. Create a vault
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -sS -X POST "$GROUND_API/v2/microvaults/vaults" \
-H "$AUTH" -H 'Content-Type: application/json' \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"baseAsset":"0x1c7d4b196cb0c7b01d743fbc6116a902379c7238",
"name":"Acme Treasury MicroVault",
"symbol":"acmeUSDC",
"feeRecipient":"0x76f8fc6667e239f83a547d4e16225d6a34f6fa22",
"managementFeeBps":0,
"performanceFeeBps":0
}' | jq
```
The response is a creation task. Keep its `id`; it identifies this operation while the vault is being provisioned.
## 3. Wait for the vault
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
until VAULT_ID=$(curl -sS "$GROUND_API/v2/microvaults/vaults" -H "$AUTH" \
| jq -r '.items[] | select(.shareName == "Acme Treasury MicroVault") | .id' | head -1) \
&& test -n "$VAULT_ID"; do sleep 3; done
echo "$VAULT_ID"
```
While deployment is in progress, the list response includes it in `provisionings`. Once active, the new item contains two identifiers: `id` is used in Ground API paths, while `address` is the onchain vault and ERC-20 share-token address.
## 4. Inspect performance and positions
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -sS "$GROUND_API/v2/microvaults/vaults/$VAULT_ID" -H "$AUTH" | jq
curl -sS "$GROUND_API/v2/microvaults/vaults/$VAULT_ID/sources" -H "$AUTH" | jq
curl -sS "$GROUND_API/v2/microvaults/vaults/$VAULT_ID/activity" -H "$AUTH" | jq
```
## Confirm the quickstart
The vault is ready when it appears in `items` with `status: "active"`. Continue with [shareholder access](/docs/microvaults/shareholders), [strategy configuration](/docs/microvaults/strategy), and [subscription](/docs/microvaults/subscribe).
# Redeem MicroVault shares
Source: https://docs.groundtech.co/docs/microvaults/redeem
Execute an instant redemption or request underlying liquidation.
Preview immediately before asking the shareholder to sign. The preview returns either an instant transaction or a requested redemption.
| Parameter | Meaning |
| - | - |
| `owner` | Share owner whose MicroVault shares will be redeemed and who submits the transaction returned by the preview. |
| `receiver` | Receiving wallet for the redeemed base asset. This is usually the same address as the share owner, but it may differ. |
| `shares` | Share amount in the share token's 18-decimal base units. |
| `minAssetsOut` | Minimum base-asset amount accepted for an instant redemption, expressed using the base asset's decimals. |
The receiver is fixed when a requested redemption is created. A later claim always sends the proceeds to that receiving wallet, even if another wallet submits the claim transaction.
## Prerequisites
* An active `VAULT_ID` and vault contract address
* A connected shareholder wallet on Ethereum Sepolia
* The share amount in 18-decimal base units
* A Viem `publicClient` and `walletClient`
## 1. Preview the redemption
```javascript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const response = await fetch(
`${process.env.GROUND_API}/v2/microvaults/vaults/${vaultId}/redemptions/preview`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.GROUND_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ owner, receiver: owner, shares, minAssetsOut: "990000" }),
},
);
if (!response.ok) throw new Error(await response.text());
const { plan } = await response.json();
```
`minAssetsOut` protects only the instant route. A requested redemption settles at the USDC and source proceeds attributable to that request; the preview is an estimate, not a guarantee.
| Preview mode | What the wallet signs | When the receiver gets the base asset |
| - | - | - |
| `instant` | One redemption transaction | In that transaction |
| `requested` | A request transaction, followed later by a claim transaction | When the claim transaction confirms |
## 2. Submit the wallet transaction
```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const hash = await walletClient.sendTransaction({
to: plan.directTransaction.to,
data: plan.directTransaction.data,
value: BigInt(plan.directTransaction.value ?? "0"),
});
const receipt = await publicClient.waitForTransactionReceipt({ hash });
if (receipt.status !== "success") throw new Error("Redemption transaction reverted");
```
An instant redemption transfers USDC in this transaction. A requested redemption escrows the shares while Ground exits the underlying sources.
## 3. Track a requested redemption
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -sS \
"$GROUND_API/v2/microvaults/vaults/$VAULT_ID/redemptions/$REQUEST_ID" \
-H "$AUTH" | jq
```
Poll until `status` is `claimable`. Use the source progress and expected timing in the response to explain what remains.
## 4. Claim actual proceeds
```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const claimHash = await walletClient.writeContract({
address: vaultAddress,
abi: microVaultAbi,
functionName: "claimRedeemRequest",
args: [BigInt(requestId)],
});
await publicClient.waitForTransactionReceipt({ hash: claimHash });
```
## Confirm the redemption
An instant redemption completes with its first receipt. A requested redemption completes after the claim receipt succeeds and the redemption endpoint reports `claimed`.
# Sandbox testing
Source: https://docs.groundtech.co/docs/microvaults/sandbox-testing
Validate the complete MicroVault lifecycle on Ethereum Sepolia.
Use `https://sandbox.groundtech.co`, chain ID `11155111`, and Sepolia USDC for this test plan.
## Prerequisites
* A sandbox organization with MicroVaults enabled
* An active vault created through the [quickstart](/docs/microvaults/quickstart)
* An external test wallet with Sepolia USDC and enough Sepolia ETH for wallet-signed transactions
* The vault's `VAULT_ID`
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export GROUND_API="https://sandbox.groundtech.co"
export GROUND_API_KEY="..."
export AUTH="Authorization: Bearer $GROUND_API_KEY"
export VAULT_ID="d9902b20-6499-4465-a4c3-5d2208182f79"
```
## 1. Record the starting state
Save the vault, source positions, and activity before moving assets. This gives each later balance change a known baseline.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -sS "$GROUND_API/v2/microvaults/vaults/$VAULT_ID" -H "$AUTH" | jq
curl -sS "$GROUND_API/v2/microvaults/vaults/$VAULT_ID/sources" -H "$AUTH" | jq
curl -sS "$GROUND_API/v2/microvaults/vaults/$VAULT_ID/activity" -H "$AUTH" | jq
```
## 2. Test synchronous flows
1. [Approve the test wallet](/docs/microvaults/shareholders).
2. Complete a first [subscription](/docs/microvaults/subscribe), then repeat it with a second amount.
3. [Set a strategy](/docs/microvaults/strategy) that moves assets into a synchronous source and wait for the rebalance to complete.
4. Request a [redemption preview](/docs/microvaults/redeem). When its mode is `instant`, submit it and verify that USDC reaches the wallet in the same transaction.
After each transaction, compare the wallet receipt with the latest activity and vault balances.
For a subscription, verify that wallet USDC decreases by the deposited amount, the share balance increases, and total assets increase by the same base-asset amount before strategy movement. For an instant redemption, verify that shares decrease and the receiver's USDC increases in the same transaction.
## 3. Test an asynchronous redemption
Allocate assets to an asynchronous source, request a redemption preview, and verify that its mode is `requested`. The preview should identify the source exits and show expected timing when available.
Submit the request, wait until activity reports it as claimable, and then claim the USDC from the test wallet. Confirm that the redemption becomes completed only after the claim transaction succeeds.
While it is pending, verify that total assets still account for assets in flight and that source progress identifies the liquidation being performed. When it becomes claimable, verify the actual proceeds and fixed receiving address before submitting the claim.
## 4. Test controls and retries
* Remove a shareholder and verify that the wallet can no longer obtain an active proof.
* Set a source to each supported mode and verify that entry and exit follow the mode table in [Configure strategy and source modes](/docs/microvaults/strategy).
* Retry an API request with the same idempotency key and identical body; verify that it returns the original operation rather than creating another one.
* Retry the same key with a different body; verify that the request is rejected.
Test [authority export](/docs/microvaults/export-authority) in the Ground Portal last because Ground-managed configuration stops after the export confirms.
## Confirm the test run
A complete run has confirmed activity for both subscriptions, the tested rebalances, the immediate redemption, and the requested redemption plus claim. Reconcile the final wallet USDC and share balances with the vault's total assets, idle assets, source positions, and redemption liabilities.
If a transaction receipt succeeds but the expected activity does not appear, keep the transaction hash and request ID, refresh the relevant read endpoint, and do not repeat the write until its status is known. If an operation reaches a failed state, correct its reported cause and retry with a new idempotency key.
# Security and trust model
Source: https://docs.groundtech.co/docs/microvaults/security
## Share ownership
MicroVault shares are ERC-20 tokens held in shareholder wallets. An approved wallet signs its own subscription and redemption transactions. Removing a wallet from the shareholder list does not take or strand shares it already holds; it can still redeem them.
## Vault authority
Your organization's Turnkey authority controls strategy, fees, shareholder access, source permissions, and emergency settings until you export the vault. Ground submits management transactions only within the permissions attached to that authority.
## Yield-source risk
Ground's automation permissions are bounded by operation, source, amount, allocation range, redemption capability, and validity period. Exporting the vault removes Ground's ability to manage it.
Only admitted yield sources can receive vault assets. Source contract behavior, liquidity, settlement time, and upgrade controls remain part of the vault's risk profile. A source mode can prevent new entries while preserving the ability to exit an existing position.
## Emergency controls
You can pause subscriptions, redemptions, share transfers, source entry, or source exit independently. Completed asynchronous claims and authority transfer remain available so an existing obligation or control change is not blocked.
## Export
Exporting transfers vault authority; it does not move assets or shares. After confirmation, the new authority is responsible for all configuration and source operations, and Ground-managed controls become read-only.
# Manage shareholders
Source: https://docs.groundtech.co/docs/microvaults/shareholders
Approve or remove wallets that may hold MicroVault shares.
Add a wallet to the shareholder list before allowing it to subscribe or receive shares. Removing it blocks future subscriptions and incoming share transfers, but does not prevent it from redeeming shares it already owns.
The proof endpoint returns the onchain membership proof required by a subscription or share-transfer transaction. Ground updates the vault's compact onchain allowlist when you add or remove an address.
## Prerequisites
* An active `VAULT_ID`
* The shareholder's EVM address
* `uuidgen` for idempotency keys
## 1. Add a shareholder
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -sS -X PUT \
"$GROUND_API/v2/microvaults/vaults/$VAULT_ID/shareholders/0x76f8fc6667e239f83a547d4e16225d6a34f6fa22" \
-H "$AUTH" -H "Idempotency-Key: $(uuidgen)" -H 'Content-Type: application/json' -d '{}'
```
The response contains a task. Only one shareholder update can be in progress for a vault at a time.
## 2. Wait for the proof
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -sS \
"$GROUND_API/v2/microvaults/vaults/$VAULT_ID/shareholders/0x76f8fc6667e239f83a547d4e16225d6a34f6fa22/proof" \
-H "$AUTH" | jq
```
A `404` means the wallet is not on the currently confirmed list. Poll until the proof is available before enabling subscription; the add task being accepted is not yet sufficient.
## 3. Remove a shareholder
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -sS -X DELETE \
"$GROUND_API/v2/microvaults/vaults/$VAULT_ID/shareholders/0x76f8fc6667e239f83a547d4e16225d6a34f6fa22" \
-H "$AUTH" -H "Idempotency-Key: $(uuidgen)"
```
Removing a wallet prevents new subscriptions and transfers to that wallet. It does not erase shares the wallet already owns or prevent redemption.
## Confirm access
The wallet is approved when the proof endpoint returns `200`. Confirm removal by checking that the wallet no longer appears in `GET /shareholders` and its proof endpoint returns `404`.
# Statuses, activity, and performance
Source: https://docs.groundtech.co/docs/microvaults/status-and-performance
## Activity status
Use `GET /v2/microvaults/vaults/{vaultId}/activity` to track subscriptions, rebalances, and redemptions. A wallet receipt confirms the submitted transaction; the activity record shows the resulting vault operation and whether more work or a shareholder claim remains.
## Performance
Use `GET /v2/microvaults/vaults/{vaultId}/performance-series` for cumulative earnings, NAV per share, and trailing APY. A `partial` status means the requested period does not contain enough comparable checkpoints to calculate every value. A missing valuation is returned as unavailable rather than zero.
| Metric | Meaning |
| - | - |
| Total assets | Current base-asset value of idle assets, source positions, and assets in flight, net of redemption liabilities. |
| Idle assets | Base asset held directly by the vault, including amounts that may already be reserved for redemptions. |
| Free idle assets | Idle assets still available for new allocations or immediate redemptions after reservations. |
| NAV | Base-asset value represented by one whole MicroVault share. |
| Lifetime earned | Cumulative change in vault value attributable to investment performance, excluding shareholder subscriptions and redemptions. |
| APY | Annualized NAV change over the available trailing period. It can be unavailable until enough comparable history exists. |
Amounts ending in `Units` are integers in the referenced token's smallest units. Use `baseAssetDecimals` for asset amounts; MicroVault shares use 18 decimals.
The organization performance summary combines lifetime earned only when the included vaults use the same base asset and have complete observations. A `partial` result means the aggregate should be displayed as unavailable rather than treated as zero.
## Operation availability
Use `GET /v2/microvaults/vaults/{vaultId}/pause-status` to determine whether subscriptions, redemptions, transfers, source entries, or source exits are currently allowed.
# Configure strategy and source modes
Source: https://docs.groundtech.co/docs/microvaults/strategy
Choose yield sources and set target allocations.
Source modes control permitted movement. Target weights tell Ground how to rebalance the vault.
These settings are independent. A target says how much the vault should hold; a mode says whether it may move into or out of the source to reach that target.
## Prerequisites
* An active `VAULT_ID`
* Source IDs from `GET /v2/microvaults/yield-sources?chainId=11155111`
* Targets that total exactly `10000` basis points, including idle USDC
## 1. Choose source modes
| Mode | Subscriptions | Redemptions |
| - | - | - |
| `active` | Allowed | Allowed |
| `subscription_only` | Allowed | Blocked |
| `redemption_only` | Blocked | Allowed |
| `disabled` | Blocked | Blocked |
Changing a mode does not remove an existing position. To wind down a source, set its target to `0`, use `redemption_only` while the position exits, and disable it after the position reaches zero.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -sS -X PUT \
"$GROUND_API/v2/microvaults/vaults/$VAULT_ID/sources/$SOURCE_ID/mode" \
-H "$AUTH" -H 'Content-Type: application/json' \
-H "Idempotency-Key: $(uuidgen)" -d '{"mode":"active"}'
```
## 2. Set target allocations
This example allocates `80%` to one source and keeps `20%` in USDC.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -sS -X PUT "$GROUND_API/v2/microvaults/vaults/$VAULT_ID/strategy" \
-H "$AUTH" -H 'Content-Type: application/json' \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"allocations":[{
"sourceId":"0x1111111111111111111111111111111111111111111111111111111111111111",
"targetBps":8000
}],
"idleTargetBps":2000
}'
```
## 3. Track the rebalance
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -sS "$GROUND_API/v2/microvaults/vaults/$VAULT_ID/activity" -H "$AUTH" | jq
curl -sS "$GROUND_API/v2/microvaults/vaults/$VAULT_ID/sources" -H "$AUTH" | jq
```
Async sources can remain in progress until the source settles.
The strategy update and the resulting rebalance are separate states. The update confirms the new targets; the rebalance activity confirms that assets have moved toward them.
## Confirm the strategy
Confirm the intended targets and modes in the vault and source responses. The rebalance is complete when its activity is confirmed and no source movement remains in progress.
# Subscribe from an external wallet
Source: https://docs.groundtech.co/docs/microvaults/subscribe
Approve USDC and mint MicroVault shares from a browser wallet.
Subscriptions are synchronous wallet transactions. The shareholder approves the vault to spend USDC, then deposits USDC and receives ERC-20 MicroVault shares.
| Role | Address or value |
| - | - |
| Transaction caller | Wallet supplying the base asset and signing the transaction. |
| Share receiver | Approved wallet that receives the newly minted shares. Usually the caller. |
| Assets | Base-asset amount deposited, expressed using the base asset's decimals. |
| Minimum shares | Lowest acceptable share amount, expressed in the share token's 18-decimal base units. |
## Prerequisites
* An active MicroVault and its contract address
* A shareholder wallet connected to Ethereum Sepolia (`chainId: 11155111`)
* Sepolia USDC in that wallet
* An active shareholder proof
* A Viem `publicClient` and `walletClient`
Fetch the proof immediately before constructing the wallet transaction.
## 1. Fetch the shareholder proof
```javascript theme={"theme":{"light":"github-light","dark":"github-dark"}}
export async function getShareholderProof(vaultId, walletAddress) {
const response = await fetch(
`${process.env.GROUND_API}/v2/microvaults/vaults/${vaultId}/shareholders/${walletAddress}/proof`,
{ headers: { Authorization: `Bearer ${process.env.GROUND_API_KEY}` } },
);
if (!response.ok) throw new Error(await response.text());
return (await response.json()).proof;
}
```
A `404` means the wallet is not yet on the active allowlist. Complete [shareholder approval](/docs/microvaults/shareholders) before enabling the subscribe action.
## 2. Define the required contract calls
```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const erc20Abi = [
{
type: "function", name: "approve", stateMutability: "nonpayable",
inputs: [{ name: "spender", type: "address" }, { name: "amount", type: "uint256" }],
outputs: [{ type: "bool" }],
},
] as const;
const microVaultAbi = [
{
type: "function", name: "convertToShares", stateMutability: "view",
inputs: [{ name: "assets", type: "uint256" }], outputs: [{ type: "uint256" }],
},
{
type: "function", name: "depositWithProof", stateMutability: "nonpayable",
inputs: [
{ name: "assets", type: "uint256" },
{ name: "receiver", type: "address" },
{ name: "minSharesOut", type: "uint256" },
{ name: "callerProof", type: "bytes32[]" },
{ name: "receiverProof", type: "bytes32[]" },
],
outputs: [{ type: "uint256" }],
},
] as const;
```
## 3. Approve USDC
Convert the human amount using the base asset's decimals. USDC uses six decimals.
```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { parseUnits, type Address, type Hex } from "viem";
const assets = parseUnits("100", 6);
const approvalHash = await walletClient.writeContract({
address: usdcAddress as Address,
abi: erc20Abi,
functionName: "approve",
args: [vaultAddress as Address, assets],
});
await publicClient.waitForTransactionReceipt({ hash: approvalHash });
```
Skip this transaction only when the existing allowance is at least `assets`.
## 4. Submit the subscription
Quote the expected shares immediately before submission and apply your minimum-output policy. This example permits up to `0.5%` quote movement.
```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const quotedShares = await publicClient.readContract({
address: vaultAddress as Address,
abi: microVaultAbi,
functionName: "convertToShares",
args: [assets],
});
const minSharesOut = quotedShares * 9950n / 10000n;
const depositHash = await walletClient.writeContract({
address: vaultAddress as Address,
abi: microVaultAbi,
functionName: "depositWithProof",
args: [
assets,
walletAddress as Address,
minSharesOut,
proof.proof as Hex[],
proof.proof as Hex[],
],
});
const receipt = await publicClient.waitForTransactionReceipt({ hash: depositHash });
if (receipt.status !== "success") throw new Error("Subscription reverted");
```
The example uses the connected wallet as both caller and share recipient, so the same proof is supplied twice. If they differ, fetch and supply an active proof for each address.
## Confirm the subscription
After the receipt succeeds, refresh the vault detail and activity endpoints. The new subscription should appear in activity, `totalAssetsUnits` and `idleAssetsUnits` should include the deposit, and the wallet's ERC-20 share balance should increase.
The MicroVault contract address is also its share-token address. Compatible wallets can add it with `wallet_watchAsset` using the vault address, share symbol, and `18` decimals.
If the USDC approval succeeds but the deposit fails, do not repeat the approval unless the allowance is insufficient. Re-fetch the active proof and share quote, then submit a new deposit transaction.
# Supported chains and yield sources
Source: https://docs.groundtech.co/docs/microvaults/supported-chains-and-sources
MicroVaults currently support Ethereum Sepolia, chain ID `11155111`. Query `GET /v2/microvaults/yield-sources?chainId=11155111` before configuring a strategy.
Each result includes the source ID used in strategy requests, the underlying contract addresses, provider and display names, current APY when available, supported synchronous or asynchronous operations, and expected processing time.
Availability in this endpoint means the source can be attached to a new MicroVault on that chain. A vault's source mode can further restrict entry or exit without changing the global catalogue. Use processing-time fields to set expectations for asynchronous redemption; they are estimates rather than deadlines.
For source entry and exit controls, see [Configure strategy and source modes](/docs/microvaults/strategy).
# Address Book
Source: https://docs.groundtech.co/docs/portfolio-wallets/address-book
Manage the organization withdrawal address whitelist via the API.
The address book is an organization-level whitelist of withdrawal destinations. Enforcement is **implicit**: as soon as the address book has at least one entry, every withdrawal from the organization must pay out to an address in the book — requests to any other destination are rejected with `address_book_whitelist_violation` (HTTP 403). An empty address book means withdrawals are unrestricted.
There is no separate enforcement toggle, no status endpoint, and no label editing — the list of entries is the single source of truth. To know whether the whitelist is active, list the entries: a non-empty list means enforcement is on.
**Roles.** Anyone in the organization can read the address book. All writes (adding and deleting entries) require the acting profile to be an organization **owner** or **admin**. Requests by other members fail with `forbidden` (HTTP 403).
Key behaviors, enforced server-side:
* **Implicit enforcement** — adding the first entry turns enforcement on; deleting the last entry turns it off.
* **Hard delete** — removing an entry deletes it permanently. Deleting the last entry is allowed and lifts all withdrawal restrictions for the organization.
* **Uniqueness** — one entry per `(chain, address)` pair. EVM addresses are matched case-insensitively; Solana addresses are matched exactly.
* **Immutable entries** — `address`, `chain`, and `label` are set at creation and cannot be changed. To change a destination or its label, delete the entry and create a new one.
Supported chains: `ethereum`, `ethereum_sepolia`, `polygon`, `arbitrum`, `base`, `solana`, `solana_devnet`.
The full surface is three endpoints: list, create, and delete.
## List entries
The entry list is the only read. Derive enforcement from it: the whitelist is active exactly when the list is non-empty.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET "$BASE_URL/v2/address-book/entries?limit=100" \
-H "Authorization: Bearer $API_TOKEN"
```
Entries are returned newest first. The response uses the same cursor pagination as other list endpoints — pass `nextCursor` back as `cursor` to fetch the next page (`limit` max 200).
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"entries": [
{
"id": "a1b2c3d4-0000-4000-8000-000000000001",
"organizationId": "0a1b2c3d-0000-4000-8000-000000000000",
"address": "0x52908400098527886E0F7030069857D2E4169EE7",
"chain": "arbitrum",
"label": "Ops treasury",
"addedBy": "9f8e7d6c-0000-4000-8000-000000000002",
"addedAt": "2026-06-09T12:00:00.000Z"
}
],
"nextCursor": null,
"hasMore": false
}
```
## Create an entry
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST "$BASE_URL/v2/address-book/entries" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"address": "0x52908400098527886E0F7030069857D2E4169EE7",
"chain": "arbitrum",
"label": "Ops treasury"
}'
```
`label` must be 1-120 characters. The address format is validated for the given chain (EVM hex format for EVM chains, base58 public key for Solana chains, or a checksummed TRON base58 address for `tron` and `tron_shasta`). Use the exact network key; an Ethereum entry does not authorize a TRON withdrawal.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"entry": {
"id": "a1b2c3d4-0000-4000-8000-000000000001",
"organizationId": "0a1b2c3d-0000-4000-8000-000000000000",
"address": "0x52908400098527886E0F7030069857D2E4169EE7",
"chain": "arbitrum",
"label": "Ops treasury",
"addedBy": "9f8e7d6c-0000-4000-8000-000000000002",
"addedAt": "2026-06-09T12:00:00.000Z"
}
}
```
Creating a duplicate entry for the same `(chain, address)` fails with `address_book_duplicate_entry` (HTTP 409). If this is the organization's first entry, withdrawal enforcement is now active — subsequent withdrawals must pay out to an address in the book.
## Delete an entry
Permanently removes an address from the whitelist. The response returns the removed entry.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X DELETE "$BASE_URL/v2/address-book/entries/$ENTRY_ID" \
-H "Authorization: Bearer $API_TOKEN"
```
Deleting the **last** entry is allowed and implicitly turns withdrawal enforcement off — withdrawals to any destination will be accepted until a new entry is added.
## Error reference
| Code | HTTP | Meaning |
| - | - | - |
| `validation_error` | 400 | Missing/invalid fields (e.g. label length, invalid id). |
| `unsupported_chain` | 400 | `chain` is not in the supported chain list. |
| `invalid_destination_address` | 400 | Address format is invalid for the given chain. |
| `forbidden` | 403 | Acting profile is not an organization owner or admin. |
| `address_book_entry_not_found` | 404 | Entry does not exist for this organization. |
| `address_book_duplicate_entry` | 409 | An entry already exists for this `(chain, address)`. |
| `address_book_whitelist_violation` | 403 | Returned by withdrawal creation when the address book has entries and the destination is not one of them. |
See [Withdraw Funds](/docs/portfolio-wallets/withdraw-funds) for how enforcement applies to withdrawal creation.
# Agentic Installation
Source: https://docs.groundtech.co/docs/portfolio-wallets/agentic-installation
Copy-paste prompts that guide an AI coding agent through a complete Ground Portfolio Wallets integration, tailored to your product type.
Use the prompts below to have an AI coding agent (Claude, Cursor, Copilot, etc.) integrate Ground's Portfolio Wallets API into your product. Each prompt is tailored to a specific customer type so the agent optimizes for your product's goals.
**How it works:** Each prompt runs a multi-step process. The agent will first analyze your codebase and ask clarifying questions before writing any code. This avoids hallucinations from incorrect assumptions about your stack, infrastructure, or architecture.
These prompts are designed for use against the **sandbox** environment (`https://sandbox.groundtech.co`). Do not point at production until you have validated the integration end-to-end in sandbox.
## Prerequisites
Before running any prompt, make sure you have:
1. A Ground sandbox API token (contact your Ground rep if you don't have one)
2. Your AI coding agent has access to your project's codebase
3. Access to [Ground's API documentation](https://docs.groundtech.co)
***
## Neobank / Savings Product
Use this if you are building a consumer or business banking product where end-users hold balances and expect to earn yield on idle funds.
```text Step 1: Discovery & Planning theme={"theme":{"light":"github-light","dark":"github-dark"}}
You are integrating Ground's Portfolio Wallets API into our product. Ground is
a yield infrastructure provider -- their Portfolio Wallets API lets us allocate
customer stablecoin deposits across multiple yield sources (e.g. overcollateralized
lending, delta-neutral strategies, stablecoin yield tokens) via a single REST API.
Ground handles allocation, rebalancing, withdrawals, and reporting.
We are a neobank / savings product. Our goal is to offer yield on customer deposits
with these priorities:
- Capital preservation and liquidity (customers expect to withdraw at any time)
- Transparent yield display (earned amounts, current APY)
- Webhook-driven architecture so our UI updates in real time
Before writing any code, do the following:
1. Read our existing codebase to understand:
- What language/framework we use for our backend
- How we currently handle user accounts and balances
- How we process deposits and withdrawals today
- Whether we have an existing webhook ingestion system
- How we store secrets and API credentials
- What database we use and how we manage migrations
2. Read Ground's API documentation using the specific links below:
- API conventions (auth, pagination, idempotency, error handling):
https://docs.groundtech.co/docs/portfolio-wallets/api-conventions
- The wallet lifecycle (create, deposit, earn, withdraw):
https://docs.groundtech.co/docs/portfolio-wallets/introduction
- Wallet creation guide:
https://docs.groundtech.co/docs/portfolio-wallets/create-wallet
- Deposits lifecycle:
https://docs.groundtech.co/docs/portfolio-wallets/deposits
- Yield sources and strategy construction:
https://docs.groundtech.co/docs/portfolio-wallets/yield-sources
- Balances and yield tracking:
https://docs.groundtech.co/docs/portfolio-wallets/balances-and-yield
- Withdrawal flow:
https://docs.groundtech.co/docs/portfolio-wallets/withdraw-funds
- The withdrawal approval flow (Turnkey signing):
https://docs.groundtech.co/docs/portfolio-wallets/transaction-approvals
- Webhook events and signature verification:
https://docs.groundtech.co/docs/portfolio-wallets/webhooks
https://docs.groundtech.co/docs/portfolio-wallets/webhook-signature-verification
https://docs.groundtech.co/docs/portfolio-wallets/webhook-payload-examples
- Supported chains:
https://docs.groundtech.co/docs/portfolio-wallets/supported-chains
3. Then present a written integration plan that covers:
- Which parts of our codebase you'll modify or create
- How you'll map Ground wallets to our user model (1:1 per user? pooled?)
- Which yield strategy you recommend and why, given our liquidity needs
- How you'll handle webhook ingestion and signature verification
- How you'll surface balances and yield to our users
- How you'll handle the withdrawal -> Turnkey approval -> completion flow
- What new database tables/columns are needed
- What new environment variables are needed
Do NOT write any code yet. Wait for my approval of the plan before proceeding.
```
```text Step 2: Implementation theme={"theme":{"light":"github-light","dark":"github-dark"}}
Proceed with the integration plan. Implement the following in order:
1. Environment setup
- Add GROUND_API_TOKEN and GROUND_WEBHOOK_SIGNING_SECRET to our secrets/env config
- Use sandbox base URL: https://sandbox.groundtech.co
- See API conventions for auth details:
https://docs.groundtech.co/docs/portfolio-wallets/api-conventions
2. Ground API client
- Create a thin HTTP client for Ground's API with:
- Bearer token auth
- Idempotent request IDs (UUID v4) on all POST requests
- Retry with backoff on 429 and 5xx
- Structured error handling using Ground's {error, code} response format
- Reference: https://docs.groundtech.co/docs/portfolio-wallets/api-conventions
3. Wallet creation flow
- When a user action triggers wallet creation, call POST /v2/wallets with
the chosen strategy
API ref: https://docs.groundtech.co/api-reference/create-wallet
Guide: https://docs.groundtech.co/docs/portfolio-wallets/create-wallet
- Store the wallet ID and deposit addresses in our database
- Build a strategy manually from the yield source catalog, prioritizing
liquidity where needed (for example, overweighting faster-withdrawal
sources for user balances that need quick access)
Guide: https://docs.groundtech.co/docs/portfolio-wallets/yield-sources
4. Deposit tracking
- After wallet creation, provide the user with deposit addresses
- Register a webhook for portfolio_wallet.deposit.status_changed
Webhook setup: https://docs.groundtech.co/docs/portfolio-wallets/webhooks
API ref: https://docs.groundtech.co/api-reference/create-webhook
- When deposits are confirmed, update the user's displayed balance
Guide: https://docs.groundtech.co/docs/portfolio-wallets/deposits
5. Balance and yield display
- Register webhooks for portfolio_wallet.deposit.status_changed,
portfolio_wallet.rebalance.status_changed, and
portfolio_wallet.withdrawal.status_changed
- On each lifecycle event, fetch GET /v2/wallets/:id and sync totalUsd,
withdrawableUsd, and earnedUsd to our database.
API ref: https://docs.groundtech.co/api-reference/get-wallet
Guide: https://docs.groundtech.co/docs/portfolio-wallets/balances-and-yield
- Expose these through our existing user-facing balance endpoints/UI
6. Withdrawal flow
- Expose a withdrawal action that calls POST /v2/wallets/:id/withdrawal-preview
first, then POST /v2/wallets/:id/withdrawals
Preview API ref: https://docs.groundtech.co/api-reference/withdrawal-preview
Withdraw API ref: https://docs.groundtech.co/api-reference/withdraw
Guide: https://docs.groundtech.co/docs/portfolio-wallets/withdraw-funds
- Register a webhook for portfolio_wallet.withdrawal.status_changed
- Handle the pending_customer_approval status by routing the Turnkey activity
to our signing flow
Guide: https://docs.groundtech.co/docs/portfolio-wallets/transaction-approvals
- Update withdrawal status in our system as it progresses
7. Webhook handler
- Create a POST endpoint to receive Ground webhooks
- Verify the Ground-Signature header using HMAC-SHA256 (see Ground's docs
for the exact scheme: t=,v1= over timestamp.rawBody)
Signature verification: https://docs.groundtech.co/docs/portfolio-wallets/webhook-signature-verification
Payload examples: https://docs.groundtech.co/docs/portfolio-wallets/webhook-payload-examples
- Route events to the appropriate handler by event type
- Respond with 200 quickly; process asynchronously
After implementation, list any manual steps I need to take (e.g. running
migrations, setting env vars, registering the webhook URL with Ground).
```
***
## Cross-Border Payments / Stablecoin Money Movement
Use this if you are building a cross-border payments, remittance, or stablecoin money movement product (similar to Bridge, OpenFX, etc.) where funds transit through your platform and you want to earn yield on float/held balances.
```text Step 1: Discovery & Planning theme={"theme":{"light":"github-light","dark":"github-dark"}}
You are integrating Ground's Portfolio Wallets API into our product. Ground is
a yield infrastructure provider -- their Portfolio Wallets API lets us allocate
stablecoin balances across multiple yield sources via a single REST API. Ground
handles allocation, rebalancing, withdrawals, and reporting.
We are a cross-border payments / stablecoin money movement platform. Our goal is
to earn yield on float -- the stablecoins that sit in our system between when we
receive funds and when we pay them out. Our priorities are:
- Instant or near-instant liquidity (we need to pay out on short notice)
- Multi-chain support (we operate across multiple EVM chains)
- Automated treasury management (minimal manual intervention)
- Yield is a secondary revenue stream, not our core product
Before writing any code, do the following:
1. Read our existing codebase to understand:
- What language/framework we use
- How we currently hold and move stablecoins
- Our payment/transfer lifecycle (receive -> hold -> payout)
- What chains we currently operate on
- How we manage treasury wallets today
- How long funds typically sit idle (minutes? hours? days?)
- Whether we have an existing webhook ingestion system
- How we store secrets and handle environment configuration
2. Read Ground's API documentation using the specific links below:
- API conventions (auth, pagination, idempotency, error handling):
https://docs.groundtech.co/docs/portfolio-wallets/api-conventions
- The wallet lifecycle (create, deposit, earn, withdraw):
https://docs.groundtech.co/docs/portfolio-wallets/introduction
- Wallet creation guide:
https://docs.groundtech.co/docs/portfolio-wallets/create-wallet
- Deposits lifecycle:
https://docs.groundtech.co/docs/portfolio-wallets/deposits
- Yield sources -- especially which have the fastest estimated withdrawals:
https://docs.groundtech.co/docs/portfolio-wallets/yield-sources
- Withdrawal preview and the withdrawal lifecycle:
https://docs.groundtech.co/docs/portfolio-wallets/withdraw-funds
- The Turnkey transaction approval flow:
https://docs.groundtech.co/docs/portfolio-wallets/transaction-approvals
- Webhook events and signature verification:
https://docs.groundtech.co/docs/portfolio-wallets/webhooks
https://docs.groundtech.co/docs/portfolio-wallets/webhook-signature-verification
https://docs.groundtech.co/docs/portfolio-wallets/webhook-payload-examples
- Supported chains and cross-chain bridging:
https://docs.groundtech.co/docs/portfolio-wallets/supported-chains
- Polling (alternative to webhooks for status tracking):
https://docs.groundtech.co/docs/portfolio-wallets/polling
- Balances and yield tracking:
https://docs.groundtech.co/docs/portfolio-wallets/balances-and-yield
3. Then present a written integration plan that covers:
- How you'll architect the Ground wallet(s) relative to our payment flows
(e.g. one treasury wallet vs. per-corridor wallets)
- Which yield strategy you recommend -- given our need for instant liquidity,
you should heavily weight or exclusively use instant-liquidity sources
- How you'll sweep idle funds into the Ground wallet
- How you'll trigger withdrawals when we need to pay out
- The timing budget: how withdrawal timing (unwind + bridge) fits within
our payout SLA
- How you'll handle the Turnkey approval step in the withdrawal flow
- How you'll use webhooks vs. polling for withdrawal status tracking
- What changes to our database schema are needed
- What new environment variables are needed
Do NOT write any code yet. Wait for my approval of the plan before proceeding.
```
```text Step 2: Implementation theme={"theme":{"light":"github-light","dark":"github-dark"}}
Proceed with the integration plan. Implement the following in order:
1. Environment setup
- Add GROUND_API_TOKEN and GROUND_WEBHOOK_SIGNING_SECRET to our secrets/env config
- Use sandbox base URL: https://sandbox.groundtech.co
- See API conventions for auth details:
https://docs.groundtech.co/docs/portfolio-wallets/api-conventions
2. Ground API client
- Create a thin HTTP client for Ground's API with:
- Bearer token auth
- Idempotent request IDs (UUID v4) on all POST requests
- Retry with backoff on 429 and 5xx
- Structured error handling using Ground's {error, code} response format
- Reference: https://docs.groundtech.co/docs/portfolio-wallets/api-conventions
3. Treasury wallet setup
- Create Ground wallet(s) with a strategy optimized for instant liquidity
API ref: https://docs.groundtech.co/api-reference/create-wallet
Guide: https://docs.groundtech.co/docs/portfolio-wallets/create-wallet
- Select wallet allocations directly from the yield source catalog. For
payout-sensitive flows, prefer sources with the fastest estimated
withdrawals and keep the strategy simple enough to meet your payout SLA.
Guide: https://docs.groundtech.co/docs/portfolio-wallets/yield-sources
- Store wallet ID(s) and deposit addresses in our database
4. Sweep-in flow
- Identify the point in our payment lifecycle where funds become idle
- Implement a sweep that sends idle stablecoins to the Ground wallet's
deposit address on the appropriate chain
- Track sweep deposits via the portfolio_wallet.deposit.status_changed webhook
Webhook setup: https://docs.groundtech.co/docs/portfolio-wallets/webhooks
API ref: https://docs.groundtech.co/api-reference/create-webhook
Guide: https://docs.groundtech.co/docs/portfolio-wallets/deposits
5. Withdrawal / payout-out flow
- When we need to pay out, call POST /v2/wallets/:id/withdrawal-preview to
confirm available balance and timing
Preview API ref: https://docs.groundtech.co/api-reference/withdrawal-preview
- Initiate withdrawal with POST /v2/wallets/:id/withdrawals to the destination
chain and address
Withdraw API ref: https://docs.groundtech.co/api-reference/withdraw
Guide: https://docs.groundtech.co/docs/portfolio-wallets/withdraw-funds
- Handle the Turnkey approval step (pending_customer_approval payout status)
Guide: https://docs.groundtech.co/docs/portfolio-wallets/transaction-approvals
- Track withdrawal completion via webhook or polling
Polling guide: https://docs.groundtech.co/docs/portfolio-wallets/polling
6. Balance monitoring
- Register webhooks for portfolio_wallet.deposit.status_changed,
portfolio_wallet.rebalance.status_changed, and
portfolio_wallet.withdrawal.status_changed
- Sync wallet balances to our system for treasury dashboarding after each
lifecycle event
API ref: https://docs.groundtech.co/api-reference/get-wallet
Guide: https://docs.groundtech.co/docs/portfolio-wallets/balances-and-yield
- Track earned yield separately for revenue reporting
7. Webhook handler
- Create a POST endpoint to receive Ground webhooks
- Verify the Ground-Signature header using HMAC-SHA256
Signature verification: https://docs.groundtech.co/docs/portfolio-wallets/webhook-signature-verification
Payload examples: https://docs.groundtech.co/docs/portfolio-wallets/webhook-payload-examples
- Route events by type; respond 200 immediately and process async
After implementation, list any manual steps I need to take (e.g. running
migrations, setting env vars, registering the webhook URL with Ground).
```
***
## Non-Custodial Wallet Provider
Use this if you are building a self-custody wallet, smart wallet, or MPC wallet product where users hold their own keys and you want to offer yield features without taking custody of funds.
```text Step 1: Discovery & Planning theme={"theme":{"light":"github-light","dark":"github-dark"}}
You are integrating Ground's Portfolio Wallets API into our product. Ground is
a yield infrastructure provider -- their Portfolio Wallets API lets us allocate
stablecoin deposits across multiple yield sources via a single REST API. Ground
handles allocation, rebalancing, withdrawals, and reporting. Ground wallets use
Turnkey for non-custodial key management -- private keys are never held by Ground.
We are a non-custodial wallet provider. Our goal is to offer yield-earning
features to our users while preserving our non-custodial architecture. Our
priorities are:
- Users remain in control of their funds (non-custodial UX)
- The Turnkey approval flow is critical -- users must approve withdrawals
- Clear separation between user funds (each user gets their own Ground wallet)
- Transparency: users should see exactly where their funds are allocated and
what yield they are earning
Before writing any code, do the following:
1. Read our existing codebase to understand:
- What language/framework we use for our backend and client apps
- How users currently manage their wallet(s) in our product
- Whether we already integrate with Turnkey or a similar key management system
- How we handle transaction signing and approval UX today
- How we display balances and transaction history to users
- Whether we have an existing webhook ingestion system
- How we store secrets and handle environment configuration
2. Read Ground's API documentation using the specific links below:
- API conventions (auth, pagination, idempotency, error handling):
https://docs.groundtech.co/docs/portfolio-wallets/api-conventions
- The wallet lifecycle (create, deposit, earn, withdraw):
https://docs.groundtech.co/docs/portfolio-wallets/introduction
- Wallet creation guide:
https://docs.groundtech.co/docs/portfolio-wallets/create-wallet
- Deposits lifecycle:
https://docs.groundtech.co/docs/portfolio-wallets/deposits
- The transaction approval flow -- specifically how Turnkey activity IDs
surface on withdrawal payout legs (pending_customer_approval status):
https://docs.groundtech.co/docs/portfolio-wallets/transaction-approvals
- Yield sources and strategy construction:
https://docs.groundtech.co/docs/portfolio-wallets/yield-sources
- Balances and yield tracking:
https://docs.groundtech.co/docs/portfolio-wallets/balances-and-yield
- Withdrawal flow:
https://docs.groundtech.co/docs/portfolio-wallets/withdraw-funds
- How deposit addresses work (per-chain addresses per wallet):
https://docs.groundtech.co/docs/portfolio-wallets/supported-chains
- Webhook events and signature verification:
https://docs.groundtech.co/docs/portfolio-wallets/webhooks
https://docs.groundtech.co/docs/portfolio-wallets/webhook-signature-verification
https://docs.groundtech.co/docs/portfolio-wallets/webhook-payload-examples
3. Then present a written integration plan that covers:
- How you'll create and manage Ground wallets per user (1:1 mapping)
- How the yield opt-in flow works from the user's perspective
- Which yield strategies to offer and whether to let users choose
- How users will fund their Ground wallet from their existing wallet
- How you'll surface positions, balances, and earned yield in the UI
- How you'll integrate the Turnkey approval into our existing signing UX --
this is the most important part for our architecture
- How you'll handle the full withdrawal lifecycle from user request through
Turnkey approval to fund delivery
- What new backend endpoints, database tables, and client UI are needed
- What new environment variables are needed
Do NOT write any code yet. Wait for my approval of the plan before proceeding.
```
```text Step 2: Implementation theme={"theme":{"light":"github-light","dark":"github-dark"}}
Proceed with the integration plan. Implement the following in order:
1. Environment setup
- Add GROUND_API_TOKEN and GROUND_WEBHOOK_SIGNING_SECRET to our secrets/env config
- Use sandbox base URL: https://sandbox.groundtech.co
- See API conventions for auth details:
https://docs.groundtech.co/docs/portfolio-wallets/api-conventions
2. Ground API client
- Create a thin HTTP client for Ground's API with:
- Bearer token auth
- Idempotent request IDs (UUID v4) on all POST requests
- Retry with backoff on 429 and 5xx
- Structured error handling using Ground's {error, code} response format
- Reference: https://docs.groundtech.co/docs/portfolio-wallets/api-conventions
3. Yield opt-in and wallet creation
- Create a user-facing flow where a user opts in to earning yield
- On opt-in, call POST /v2/wallets to create a dedicated Ground wallet
for that user
API ref: https://docs.groundtech.co/api-reference/create-wallet
Guide: https://docs.groundtech.co/docs/portfolio-wallets/create-wallet
- Let the user choose a strategy (or recommend a default) from the yield
source catalog, and explain the tradeoffs based on source APY and
estimated withdrawal time
Guide: https://docs.groundtech.co/docs/portfolio-wallets/yield-sources
- Store the Ground wallet ID mapped to the user in our database
- Display the wallet's per-chain deposit addresses to the user
4. Deposit flow
- Guide the user to send USDC from their existing wallet to the Ground
wallet deposit address
- Register a webhook for portfolio_wallet.deposit.status_changed
Webhook setup: https://docs.groundtech.co/docs/portfolio-wallets/webhooks
API ref: https://docs.groundtech.co/api-reference/create-webhook
- Show deposit status progression (processing -> completed, or failed)
in the user's transaction history
Guide: https://docs.groundtech.co/docs/portfolio-wallets/deposits
5. Portfolio display
- Register webhooks for portfolio_wallet.deposit.status_changed,
portfolio_wallet.rebalance.status_changed, and
portfolio_wallet.withdrawal.status_changed
- Display to the user:
- Total balance (totalUsd)
- Yield earned (earnedUsd from balance, or use GET /v2/wallets/:id
for detailed breakdown)
API ref: https://docs.groundtech.co/api-reference/get-wallet
Guide: https://docs.groundtech.co/docs/portfolio-wallets/balances-and-yield
- Position breakdown (which yield sources, allocation percentages, per-
position value)
- Withdrawable balance (withdrawableUsd)
6. Withdrawal flow with Turnkey approval
- User initiates a withdrawal from the UI
- Call POST /v2/wallets/:id/withdrawal-preview to show the user what's
available and estimated timing
Preview API ref: https://docs.groundtech.co/api-reference/withdrawal-preview
- On user confirmation, call POST /v2/wallets/:id/withdrawals
Withdraw API ref: https://docs.groundtech.co/api-reference/withdraw
Guide: https://docs.groundtech.co/docs/portfolio-wallets/withdraw-funds
- Register a webhook for portfolio_wallet.withdrawal.status_changed
- When a payout leg or external payout step reaches pending_customer_approval status:
- Open the pending approval in Ground Portal or your enabled approval integration and match the activity by withdrawal/leg
- Route this to our existing transaction signing / approval UX
- The user approves the Turnkey activity through our signing flow
Guide: https://docs.groundtech.co/docs/portfolio-wallets/transaction-approvals
- Track the withdrawal through to completion and update the UI
7. Webhook handler
- Create a POST endpoint to receive Ground webhooks
- Verify the Ground-Signature header using HMAC-SHA256
Signature verification: https://docs.groundtech.co/docs/portfolio-wallets/webhook-signature-verification
Payload examples: https://docs.groundtech.co/docs/portfolio-wallets/webhook-payload-examples
- Route events by type; respond 200 immediately and process async
- Update user-facing state on each event so the UI reflects real-time status
After implementation, list any manual steps I need to take (e.g. running
migrations, setting env vars, registering the webhook URL with Ground).
```
***
## Tips for best results
* **Run Step 1 first, review the plan, then run Step 2.** The two-step approach prevents the agent from making wrong assumptions about your codebase.
* **Answer the agent's questions.** During Step 1, the agent will likely ask clarifying questions about your architecture. Answer them -- this is by design and prevents hallucinated code.
* **Use sandbox first.** All prompts target `https://sandbox.groundtech.co`. Test with Sepolia USDC or Mock Sepolia USDT.
* **Check the webhook handler carefully.** Signature verification is security-critical. Review the generated `Ground-Signature` verification code against [our reference implementation](/docs/portfolio-wallets/webhook-signature-verification).
* **The Turnkey approval flow is product-specific.** The prompts intentionally leave room for the agent to ask how your signing infrastructure works rather than assuming a specific Turnkey integration pattern.
# API Conventions
Source: https://docs.groundtech.co/docs/portfolio-wallets/api-conventions
Authentication, environments, pagination, idempotency, rate limiting, and error handling.
This page covers the conventions shared across all Portfolio Wallet API endpoints.
## Base path
Wallet resources use the `/v2/wallets` path prefix. Supporting APIs use their own `/v2/*` prefixes, including `/v2/activity`, `/v2/accounting`, `/v2/webhooks`, `/v2/address-book`, `/v2/turnkey`, and `/v2/system`.
## Authentication
All requests require a Bearer token in the `Authorization` header. If the token is missing or invalid, the API returns `401`.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET "https://sandbox.groundtech.co/v2/wallets" \
-H "Authorization: Bearer $API_TOKEN"
```
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"data": [
{
"id": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
"label": "Core Yield Portfolio",
"status": "idle",
"balance": {
"totalUsd": "82500.000000",
"earnedUsd": "250.000000",
"reservedUsd": "0.000000",
"withdrawableUsd": "82500.000000"
},
"createdAt": "2026-02-05T08:15:00.000Z",
"positions": [
{ "id": "cash:ethereum_sepolia:usdc", "kind": "cash", "label": "Cash (Ethereum Sepolia)", "valueUsd": "0.000000" },
{ "id": "syrup-usdc", "pct": 40, "kind": "yield_source", "label": "Syrup USDC", "valueUsd": "33000.000000", "yieldSourceId": "syrup-usdc", "mode": "active", "apyBps": 491 },
{ "id": "morpho-gauntlet-usdc", "pct": 30, "kind": "yield_source", "label": "Morpho Gauntlet USDC Prime", "valueUsd": "24750.000000", "yieldSourceId": "morpho-gauntlet-usdc", "mode": "active", "apyBps": 500 },
{ "id": "morpho-steakhouse-usdc", "pct": 30, "kind": "yield_source", "label": "Morpho Steakhouse USDC Prime", "valueUsd": "24750.000000", "yieldSourceId": "morpho-steakhouse-usdc", "mode": "active", "apyBps": 500 }
],
"failureReason": null,
"depositAddresses": {
"solana_devnet": "DmaePN7kL6WjSecMaH4urdS8zN8FwNPTxbdTqFwAw6Fd",
"ethereum_sepolia": "0xFf71D07826d0aFcEcDF07Be9c499f9DDF3ee5cFa"
}
}
],
"nextCursor": null
}
```
## Environments
| Environment | Base URL |
| - | - |
| Sandbox | `https://sandbox.groundtech.co` |
| Production | `https://production.groundtech.co` |
Sandbox focuses on test networks while you integrate. Production processes live funds on supported chains. Swap the base URL to move between environments, but note that sandbox uses explicit testnet chain keys such as `ethereum_sepolia` and may expose a smaller yield-source catalog than production.
Sandbox is limited to a subset of networks for integration testing. Unlike production, sandbox chain keys include the network suffix to make the testnet explicit:
* `ethereum_sepolia` -- Ethereum Sepolia testnet
* `solana_devnet` -- Solana devnet
* `tron_shasta` -- TRON Shasta testnet, when enabled
Production uses canonical chain names (`arbitrum`, `base`, `ethereum`, `polygon`, `solana`, and `tron` when enabled). See [Supported Chains](/docs/portfolio-wallets/supported-chains) for details.
## Pagination
List endpoints use **cursor-based** pagination with `limit` and `cursor` parameters.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET "$BASE_URL/v2/wallets/$WALLET_ID/withdrawals?limit=25" \
-H "Authorization: Bearer $API_TOKEN"
```
The response includes a `nextCursor` field. Pass it as `cursor` to fetch the next page:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET "$BASE_URL/v2/wallets/$WALLET_ID/withdrawals?limit=25&cursor=$NEXT_CURSOR" \
-H "Authorization: Bearer $API_TOKEN"
```
When `nextCursor` is `null`, you have reached the end of the result set.
Endpoint-specific filters, sort options, limits, and status semantics vary by endpoint. Treat each API reference page's parameter table as authoritative for that endpoint.
## Idempotency
Create and withdrawal endpoints accept a `requestId` (UUID v4). These endpoints return **`200 OK`** for both the initial request and an idempotent replay with the same `requestId` — the existing resource state is returned on replay.
Wallet and withdrawal creation use `requestId` to find and return the existing resource on replay. Do not rely on these create endpoints to compare the replayed payload field-by-field.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST "$BASE_URL/v2/wallets" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"requestId": "f0a3afc3-1e67-4c0c-a8e1-3f5d8fb9a23b",
"label": "Core Yield Portfolio",
"strategy": {
"allocations": {
"usdc": [
{ "yieldSourceId": "syrup-usdc", "pct": 40 },
{ "yieldSourceId": "morpho-gauntlet-usdc", "pct": 30 },
{ "yieldSourceId": "morpho-steakhouse-usdc", "pct": 30 }
],
"usdt": [
{ "yieldSourceId": "cash", "pct": 100 }
]
}
}
}'
```
The public write shape uses token-keyed allocation groups with `yieldSourceId` and `pct`. Each included group independently totals 100%.
Allocation `pct` values support 0.1% increments and must sum to 100.
Replaying the same `requestId` with the **same** payload returns `200 OK` with the existing resource. Replaying a `requestId` with a **different** payload returns `409 request_id_conflict`:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"error": "requestId has already been used with a different payload",
"code": "request_id_conflict",
"details": "The requestId 1f1c3b2a-0000-4000-8000-000000000001 was first used with a different allocation."
}
```
When you receive a `409`, fetch the existing resource by `requestId` rather than retrying the create.
## Rate limiting
Each API key can make up to **300 authenticated requests per minute** across the API. Every request counts toward this overall limit. Endpoints with a lower limit must also stay within their listed limit.
Rate limits use two limit types:
* **Per API key** — one allowance is shared by all calls to that endpoint using the API key.
* **Per API key, per wallet** — each wallet gets its own allowance under the API key. For example, you can update Wallet A five times and Wallet B five times in the same minute. Both wallets' calls still count toward the API key's overall 300-request limit.
| Endpoint | Requests allowed | Limit type |
| - | - | - |
| All authenticated `/v2/*` endpoints | 300 per minute | Per API key |
| `GET /v2/activity` | 10 per minute | Per API key |
| `GET /v2/activity?walletId={walletId}` | 10 per minute | Per API key, per wallet |
| `GET /v2/accounting/activity` | 10 per minute | Per API key |
| `GET /v2/accounting/yield/period-summary` | 10 per minute | Per API key |
| `GET /v2/wallets/overview` | 30 per minute | Per API key |
| `GET /v2/wallets/{id}` | 30 per minute | Per API key, per wallet |
| `GET /v2/wallets/{id}/yield` | 30 per minute | Per API key, per wallet |
| `POST /v2/wallets` | 5 per minute | Per API key |
| `PATCH /v2/wallets/{id}/strategy` | 5 per minute | Per API key, per wallet |
| `POST /v2/wallets/{id}/allocate-preview` | 10 per minute | Per API key, per wallet |
| `POST /v2/wallets/{id}/allocate` | 5 per minute | Per API key, per wallet |
| `POST /v2/wallets/{id}/withdrawal-preview` | 10 per minute | Per API key, per wallet |
| `POST /v2/wallets/{id}/withdrawals` | 5 per minute | Per API key, per wallet |
| `POST /v2/webhooks` | 10 per minute | Per API key |
| `DELETE /v2/webhooks/{id}` | 10 per minute | Per API key |
| `POST /v2/address-book/entries` | 30 per minute | Per API key |
| `DELETE /v2/address-book/entries/{id}` | 30 per minute | Per API key |
If you exceed a limit, the API returns `429 Too Many Requests`. The `Retry-After` header tells you how many seconds to wait. Wait at least that long, add a small random delay, and then try again.
## Error handling
Business API errors are returned as JSON with a human-readable `error` string and a machine-readable `code` field.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"error": "Missing required fields: destinationAddress, amountUsd, destinationChain, requestId",
"code": "validation_error"
}
```
| HTTP status | Meaning | Action |
| - | - | - |
| `400` | Validation error | Fix the request and retry |
| `401` | Authentication failed | Check your Bearer token |
| `404` | Resource not found | Verify the wallet/withdrawal ID |
| `409` | `requestId` reused with a different payload (`request_id_conflict`) | Fetch the existing resource instead |
| `429` | Rate limited | Wait for `Retry-After`, add a small random delay, and try again |
| `500` | Server error | Log the response and retry with backoff |
Business API errors include both `error` and `code` fields. Authentication middleware can return `{ "error": "...", "message": "..." }` before the request reaches a business handler. Use `code` for programmatic branching when it is present, and use the `error` string for logging and debugging.
# How Auto Rebalancing Works
Source: https://docs.groundtech.co/docs/portfolio-wallets/auto-rebalancing
Choose automatic or manual allocation and understand when Ground moves funds toward your portfolio targets.
An automatically rebalanced portfolio wallet maintains the allocation strategy you choose. Ground compares current holdings with your targets and moves funds when the difference is large enough and the required transactions can execute. Deposits, withdrawals, changes in position value, and strategy updates can all create a difference from the target.
## Two ways to manage a portfolio wallet
Choose the wallet's allocation behavior when you [create it](/api-reference/create-wallet):
| | Automatically rebalanced | Manually allocated |
| - | - | - |
| Creation setting | `autoRebalance: true` with `strategy.allocations` | `autoRebalance: false`, with no `strategy` |
| Allocation instructions | Choose how much to hold in cash and each yield source, as percentages. Set allocations separately for each supported group (`usdc`, `usdt`, and `usdt:tron`); each group totals 100%. | Specify individual movements with `from`, `to`, and `amountUsd`. |
| New deposits | Arrive as cash, then become eligible for allocation toward the strategy. | Remain cash until you request an allocation. |
| Changes in holdings | Ground checks for drift and rebalances eligible amounts. | Ground does not automatically restore allocation percentages. |
| How to make changes | [Update Strategy](/docs/portfolio-wallets/update-strategy) | [Preview Allocation](/api-reference/preview-allocation), then [Allocate](/api-reference/allocate) |
| Wallet response | `autoRebalance: true` and populated `strategyAllocations` | `autoRebalance: false` and `strategyAllocations: null` |
If you provide a strategy and omit `autoRebalance`, automatic rebalancing is enabled. Choose automatic rebalancing when you want Ground to maintain a standing allocation; choose manual allocation when your application decides each movement.
Configured risk-exit actions are separate from ordinary allocation maintenance and can apply to either wallet type.
## How targets are calculated
Targets are grouped by the funds that can be allocated together. The `usdc` and `usdt` allocations each independently sum to 100%. When TRON is enabled for your wallet, `usdt:tron` is a third independent group for native TRON USDT, with its own 100% allocation. The `usdt` group continues to represent Ethereum USDT. A rebalance stays within its allocation group; Ground does not bridge USDT between Ethereum and TRON. You can include `cash` as a target to keep part of a group uninvested.
Use the yield-source catalog to discover sources available to your organization. JustLend V1 uses the same historical APY model as other sources; historical `apyBps` is unavailable until sufficient history exists. Its separate `currentApy` preview can still show the current supply rate. Yield is variable.
Ground values settled yield positions in USD using their position prices, then calculates the desired dollar value of each target. A movement already committed to a yield source counts toward that source's projected value while it executes. Funds reserved for ongoing work and funds in transit cannot fund another movement. A frozen holding that has been removed from the strategy is also excluded from the value that can be redistributed.
You can update a strategy while a rebalance is running. Existing movements continue toward their committed destinations; later rebalances use the updated targets and any settled, unreserved funds. A withdrawal can use unreserved funds during a rebalance, while an open withdrawal defers new rebalance commitments until it closes.
For example, with \$10,000 available in the USDC group and a 60% / 40% strategy, the desired position values are \$6,000 and \$4,000. Any USDT holdings are evaluated separately.
## When a rebalance happens
### Ordinary allocation drift
For each target, Ground calculates a dollar threshold:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Desired value = projected value in the token group × target percentage
Drift threshold = greater of (2% × desired value) and the minimum rebalance amount
```
An overweight position can fund a rebalance when its excess is **at least** its threshold. An underweight position can receive funds when its shortfall is **at least** its own threshold. Both sides must qualify, and each planned movement must meet the minimum rebalance amount.
The default minimum rebalance amount is **\$1**. This is a service configuration, so it can differ by environment. The examples below use the \$1 default. Individual yield sources can also impose deposit minimums or allocation limits.
The 2% band is relative to the target's dollar value. For a \$6,000 target, it is \$120. In a \$10,000 token group, that corresponds to 1.2 percentage points of allocation weight.
Once a movement qualifies, Ground plans toward the target amount. It does not move only the portion beyond the threshold. Small residual differences can remain because of rounding, minimums, or execution constraints.
### New cash and removed positions
Two common cases use a zero-percent target:
* **Cash with a 0% cash target:** available cash at or above the minimum can fund underweight targets without waiting for their usual 2% drift band. Each individual movement must still meet the minimum. If your strategy includes a positive cash target, ordinary drift thresholds apply to that cash allocation.
* **A yield position removed from the strategy or set to 0%:** a sellable balance at or above the minimum becomes eligible to exit. Receiving targets can accept the proceeds once their shortfalls meet the minimum, even if they are inside their usual drift bands.
A strategy update saves the new targets immediately. Actual movements happen later when eligible work is found; accepting the update does not mean funds have moved.
### Conditions that can defer or limit a rebalance
The periodic check considers active wallets with automatic rebalancing enabled and enough available value. It waits while a withdrawal or another rebalance remains open, and skips wallets whose rebalancing is frozen.
An eligible movement also needs usable position prices, a supported route, and permission to exit the source and enter the destination. Source controls and risk rules may block buying or selling. Deposit minimums and remaining allocation capacity may reduce the amount that can move or leave funds in cash.
Ground checks periodically; the default check interval is one minute. Wallets are processed in batches, so this is not a promise that every wallet starts or completes a rebalance within one minute. Completion also depends on chain confirmations, bridging, and the yield source's redemption or subscription process.
## Examples
These examples assume available funds, permitted routes, no active withdrawals or rebalances, and no additional source limits. Amounts are simplified to omit fees and rounding.
### Example 1: Drift in a 60% / 40% portfolio
A wallet has \$10,000 in its USDC group:
| Position | Target weight | Desired value | Drift threshold |
| - | - | - | - |
| Source A | 60% | \$6,000 | \$120 |
| Source B | 40% | \$4,000 | \$80 |
If A holds **\$6,100** and B holds **\$3,900**, no movement is planned. B's \$100 shortfall meets its \$80 threshold, but A's \$100 excess is below its \$120 threshold.
If A instead holds **\$6,150** and B holds **\$3,850**, both qualify. Ground can move approximately **\$150 from A to B**, bringing them toward \$6,000 and \$4,000.
### Example 2: Investing a new deposit
The same 60% / 40% wallet is on target at \$6,000 in A and \$4,000 in B, with a 0% cash target. You deposit **\$10 USDC**.
The group's value becomes \$10,010. Its new targets are **\$6,006 in A** and **\$4,004 in B**, creating shortfalls of \$6 and \$4.
Those shortfalls are below the ordinary drift bands. However, the \$10 cash balance has a zero-percent target, so Ground can allocate **\$6 to A and \$4 to B**. Both movements meet the \$1 minimum.
A **\$1 deposit** in the same situation would produce shortfalls of \$0.60 and \$0.40. Neither movement meets the minimum, so the cash remains available for a later check after the balances or targets change.
### Example 3: Removing a small position
A wallet holds **\$9,990 in A** and **\$10 in B**. You update the strategy to 100% A, removing B.
B now has a zero-percent target. Its \$10 balance meets the minimum for an exit. A can receive the **\$10** even though its shortfall is below its ordinary \$200 drift threshold. The movement remains subject to B allowing redemptions and A allowing deposits.
## How the risk engine affects rebalancing
Risk rules determine whether Ground can continue allocating to a yield source or should exit it. Your saved strategy remains unchanged: a risk restriction can keep actual holdings away from their target weights for as long as the restriction applies.
Each rule has a configured condition and one of three outcomes:
| Outcome | Effect on rebalancing |
| - | - |
| Alert only (`alert`) | Creates an alert. The alert itself does not block allocations or request an exit; ordinary rebalancing continues. |
| Prevent additional allocation (`prevent_additional_allocation`) | Blocks new rebalance buys into the affected source. Existing holdings can remain invested, and permitted exits can still proceed. |
| Exit position (`exit_position`) | Blocks new rebalance buys into the affected source and requests redemption of its available position into cash in the source's underlying stablecoin. |
The action depends on the rule's approval setting. An **automatic** block applies when the breach is detected, and an automatic exit is queued for execution. An **approval-required** block or exit waits for approval; that pending rule does not yet block new buys. Other active rules and source controls still apply.
### Risk exits take priority over allocation targets
A risk exit can act even when the portfolio is exactly on target. Its plan uses the affected source's available, unreserved balance rather than the ordinary 2% drift band or default \$1 rebalance minimum. Actual completion still depends on available funds, transaction execution, and the source's redemption process. A queued or started exit does not mean its proceeds have settled.
The exit sends proceeds to cash and leaves unrelated yield positions alone. It does not automatically distribute the exited source's target weight among the remaining sources. Subsequent ordinary rebalancing can still fund permitted sources toward their original targets; amounts that cannot be allocated remain in cash.
While an exit's buy restriction is active, ordinary rebalancing cannot buy back into the affected source. An approved exit that fails or needs a retry continues to block buys while its alert remains ongoing and its rule remains active.
Risk exits can also apply to manually allocated wallets. Those wallets still do not perform ordinary automatic allocation after the exit.
### Example: A blocked source receives no new allocation
Suppose an automatically rebalanced wallet holds **\$6,000 in A** and **\$4,000 in B**, matching its 60% / 40% USDC strategy, with a 0% cash target. An automatic rule now prevents additional allocation to A. You deposit **\$1,000 USDC**.
The new desired values are \$6,600 in A and \$4,400 in B. Assuming the other execution conditions are met, Ground can allocate **\$400 to B**. The **\$600 intended for A stays in cash** while A is blocked. A's existing \$6,000 remains invested; the block alone does not request its redemption.
Ground does not put the entire deposit into B. The saved 60% / 40% strategy still determines the targets.
If the same rule required approval, this block would take effect only after approval. If its outcome were alert only, it would not prevent the usual \$600 / \$400 allocation.
### Example: An on-target position is exited to cash
Start again with **\$6,000 in A** and **\$4,000 in B**, no cash, and the same 60% / 40% strategy. This time, an automatic rule requests an exit from A.
Even though allocation drift is zero, Ground requests redemption of A's available position. After settlement, and ignoring fees or price changes, the wallet holds **\$6,000 USDC cash** and **\$4,000 in B**. The exit does not sell B or move A's proceeds into B. The ongoing exit restriction prevents the ordinary rebalance process from buying A again.
### What happens when the risk condition recovers?
When a later risk evaluation observes that the condition has recovered, Ground resolves that alert and removes its associated buy restriction. Other active restrictions can still block the source.
For an automatically rebalanced wallet, the saved strategy becomes actionable again subject to the usual thresholds and execution conditions. In the exit example, if A becomes eligible again after the exit has settled, Ground can allocate the \$6,000 cash back into A. **A risk exit does not permanently remove a source from your strategy.** Update the strategy if you want to keep that source excluded after recovery.
For a manually allocated wallet, recovery permits future allocation requests but does not automatically reinvest the cash.
## Monitor the result
Use [Get Wallet](/api-reference/get-wallet) to compare `strategyAllocations` with actual `positions[].valueUsd`. Target weights are returned as `targetWeightBps`, where 10,000 basis points equals 100%.
While a rebalance runs, the wallet can report `rebalance_active` or `withdrawal_and_rebalance_active`. `balance.reservedUsd` identifies customer-owned value reserved for ongoing work. See [Balances and Yield](/docs/portfolio-wallets/balances-and-yield) and [Polling](/docs/portfolio-wallets/polling) for interpreting these fields.
An `idle` wallet can still be slightly off target because its differences are below the thresholds or no eligible movement is available. If a larger difference persists, check for active work, source availability, and applicable minimums or limits. Contact Ground support with the wallet ID if the reason is unclear.
## Manually allocate TRON USDT
For a wallet with `autoRebalance: false`, use the following body with [Preview Allocation](/api-reference/preview-allocation) to move TRON cash into JustLend:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"allocations": [
{ "from": "cash:tron:usdt", "to": "justlend-v1-usdt", "amountUsd": "10.000000" }
]
}
```
To redeem JustLend back to cash, reverse the endpoints:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"allocations": [
{ "from": "justlend-v1-usdt", "to": "cash:tron:usdt", "amountUsd": "10.000000" }
]
}
```
After a successful preview, submit the same `allocations` plus a fresh UUID `requestId` to [Allocate](/api-reference/allocate), then track the returned rebalance until completion. In sandbox, use `cash:tron_shasta:usdt`. Use the position IDs returned by your wallet and amounts within its available balance; no funds bridge between TRON and Ethereum.
# Balances and Yield
Source: https://docs.groundtech.co/docs/portfolio-wallets/balances-and-yield
Monitor wallet balances, positions, and yield metrics.
Once your wallet is funded, use these endpoints to track balances, positions, and yield.
## Fetch a wallet
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET "$BASE_URL/v2/wallets/$WALLET_ID" \
-H "Authorization: Bearer $API_TOKEN"
```
The response is a clean neobank-style shape:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"id": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
"label": "Core Yield Portfolio",
"status": "idle",
"balance": {
"totalUsd": "100250.000000",
"earnedUsd": "250.000000",
"reservedUsd": "0.000000",
"withdrawableUsd": "100250.000000"
},
"createdAt": "2026-02-05T08:15:00.000Z",
"positions": [
{ "id": "cash:ethereum_sepolia:usdc", "kind": "cash", "label": "Cash (Ethereum Sepolia)", "valueUsd": "0.000000" },
{ "id": "syrup-usdc", "pct": 40, "kind": "yield_source", "label": "Syrup USDC", "valueUsd": "40100.000000", "yieldSourceId": "syrup-usdc", "mode": "active", "apyBps": 491 },
{ "id": "morpho-gauntlet-usdc", "pct": 30, "kind": "yield_source", "label": "Morpho Gauntlet USDC Prime", "valueUsd": "30075.000000", "yieldSourceId": "morpho-gauntlet-usdc", "mode": "active", "apyBps": 500 },
{ "id": "morpho-steakhouse-usdc", "pct": 30, "kind": "yield_source", "label": "Morpho Steakhouse USDC Prime", "valueUsd": "30075.000000", "yieldSourceId": "morpho-steakhouse-usdc", "mode": "active", "apyBps": 500 }
],
"strategyAllocations": {
"usdc": [
{ "yieldSourceId": "syrup-usdc", "targetWeightBps": 4000 },
{ "yieldSourceId": "morpho-gauntlet-usdc", "targetWeightBps": 3000 },
{ "yieldSourceId": "morpho-steakhouse-usdc", "targetWeightBps": 3000 }
],
"usdt": [
{ "yieldSourceId": "cash", "targetWeightBps": 10000, "token": "usdt", "chain": "ethereum_sepolia" }
]
},
"failureReason": null,
"depositAddresses": {
"solana_devnet": "DmaePN7kL6WjSecMaH4urdS8zN8FwNPTxbdTqFwAw6Fd",
"ethereum_sepolia": "0xFf71D07826d0aFcEcDF07Be9c499f9DDF3ee5cFa"
}
}
```
Key fields in the response:
| Field | Description |
| - | - |
| `status` | `idle`, `withdrawal_active`, `rebalance_active`, `withdrawal_and_rebalance_active`, `creating`, or `failed` |
| `balance.totalUsd` | Gross customer-owned wallet value, including economically-owned funds that are still in transit |
| `balance.withdrawableUsd` | Conservative gross amount that can be withdrawn now, before plan-specific fees |
| `balance.reservedUsd` | Customer-owned value currently reserved by active withdrawal or rebalance work |
| `balance.earnedUsd` | Lifetime yield earned |
| `positions[]` | Per-position balances with USD values and target allocations |
| `strategyAllocations` | Desired targets grouped by stablecoin, expressed in basis points |
The `positions` array includes cash positions and yield sources in your strategy allocation.
* **`yield_source`** — a strategy position. These entries include `yieldSourceId` and `pct`.
* **`cash`** — idle USDC or USDT sitting in the wallet's deposit addresses. Cash positions include `token` and `chain`; configured cash targets may include `pct`. TRON USDT cash uses `cash:tron:usdt` in production or `cash:tron_shasta:usdt` in sandbox, with allocation group `usdt:tron`.
Configured zero-value cash and yield targets can appear so clients can render the current strategy. The sum of all `positions[].valueUsd` equals `balance.totalUsd`.
Balances are computed from refreshed onchain holdings + price feeds and cached for fast reads. They may briefly lag deposits or withdrawals.
### When idle stablecoins move into yield
Idle cash is swept into yield positions through rebalances. USDC follows `usdc`, Ethereum USDT follows `usdt`, and TRON USDT follows `usdt:tron`; Ground does not convert tokens or bridge USDT between these groups. Rebalances only run when the available idle balance can produce executable yield-entry legs.
Rebalance minimums are environment-configured and applied per executable leg. Sandbox uses small minimums (e.g. 5 stablecoin units per leg) so you can test with small amounts; production minimums are typically higher. If idle cash cannot produce executable legs above the configured threshold, those funds remain idle and withdrawable instead of being swept into yield -- no error is returned.
If idle cash is split across multiple strategy targets, each resulting target leg must meet the active environment's configured minimum. For example, if the active minimum is 5 units per leg, an 8 USDT idle balance with a 50/50 USDT strategy would split into two 4 USDT target legs, so no rebalance legs are created.
### Balance definitions
* `totalUsd` answers: "How much value does the customer own?"
* `withdrawableUsd` answers: "How much gross wallet value can the customer withdraw now before plan-specific fees?"
* `reservedUsd` answers: "How much customer-owned value is currently reserved by active wallet work?"
* `earnedUsd` answers: "How much lifetime yield has this wallet earned?"
`totalUsd`, `withdrawableUsd`, and `reservedUsd` are intentionally not interchangeable. Value can remain customer-owned while still being unavailable to withdraw immediately, for example when a withdrawal or rebalance is settling.
Internally, Ground keeps that value represented through explicit transfer boundaries. When funds move from one representation to another, the transfer row stays active until the destination representation is durably written or observed. That is what keeps `totalUsd` from dipping during internal protocol steps.
### `reservedUsd` timing
`reservedUsd` reflects the amount reserved by active withdrawal and rebalance work. It is set when the system reserves liquidity for a wallet workflow.
The reservation is not always cleared immediately when a transaction broadcasts. It stays active until Ground's ledger and holdings refresh confirm the next representation of the funds. In practice:
* **Workflow initiated**: `reservedUsd` increases, `withdrawableUsd` decreases by the same amount, `totalUsd` stays the same.
* **Workflow completed**: `reservedUsd` may remain briefly while the next ledger or holdings refresh catches up.
* **Refresh catches up**: `reservedUsd` decreases back toward zero, and `totalUsd` reflects the settled holdings.
* **Workflow cancelled**: `reservedUsd` decreases, `withdrawableUsd` increases back to its pre-workflow level.
The invariant `0 <= withdrawableUsd <= totalUsd` always holds.
The invariant "customer-owned value is always represented somewhere" also always holds.
### Customer-facing valuation notes
* `syrup-usdc` is valued from live onchain share-to-assets conversion and can move continuously.
* Morpho ERC-4626 vaults exposed by the live catalog are valued from live onchain ERC-4626 conversion reads.
* Treat NAV-based positions as protocol valuations, not guaranteed 1:1 stablecoin marks at every instant.
For destination-specific initiation readiness and a sourcing plan, use `POST /v2/wallets/:id/withdrawal-preview` (see [Withdraw Funds](/docs/portfolio-wallets/withdraw-funds)).
## Yield metrics
You can monitor yield through the wallet response:
* `balance.earnedUsd` -- lifetime yield earned since wallet creation
* `balance.totalUsd` -- total value including accumulated yield
For a detailed breakdown including per-source APY, use `GET /v2/wallets/:id/yield` (see [Calculating Yield Accrual](/docs/portfolio-wallets/calculating-yield-accrual)).
### Earned and APY over time
`GET /v2/accounting/yield/earned-series` returns a time series for charting wallet performance, scoped to one wallet (`walletId`) or the whole organization (`scope=organization`). Each point carries two distinct metrics:
* `cumulativeEarnedUsd` -- lifetime cumulative earned value, mark-to-market. This is realized dollars: it moves both ways and can dip to show losses, unlike the monotonic `balance.earnedUsd`.
* `blendedApyBps` -- the wallet's blended APY at that point, in basis points. Each held yield source contributes its trailing \~30-day NAV growth (annualized), weighted by marked position value, with idle cash weighted at 0%. It describes the rate the allocation is positioned at, independent of wallet size -- not what was earned, which the cumulative fields carry.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET "$BASE_URL/v2/accounting/yield/earned-series?walletId=$WALLET_ID" \
-H "Authorization: Bearer $API_TOKEN"
```
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"scope": "wallet",
"interval": "day",
"status": "exact",
"series": [
{
"asOf": "2026-08-23T00:00:00.000Z",
"cumulativeEarnedUsd": "0.383780",
"blendedApyBps": 424,
"status": "ok"
}
]
}
```
Omit `start` to anchor the series at inception; the server picks day/week/month granularity for the span. A `null` point value is a data gap -- render a gap, never zero. `blendedApyBps` is `null` only when the wallet holds nothing at that point; a held source with under 7 days of price history contributes 0% (its rate is not yet knowable, so it dilutes the blend rather than inflating it).
## List wallets
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET "$BASE_URL/v2/wallets?limit=25" \
-H "Authorization: Bearer $API_TOKEN"
```
Supports cursor-based pagination. The response includes a `nextCursor` field for fetching subsequent pages. See [API Conventions](/docs/portfolio-wallets/api-conventions) for details.
## Look up a wallet
Look up a wallet by `requestId`, `id`, `label`, `search`, `status`, or `createdAtGte` using the list endpoint's filters. Filters can be combined when you want to narrow the result set:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET "$BASE_URL/v2/wallets?requestId=$REQUEST_ID" \
-H "Authorization: Bearer $API_TOKEN"
```
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET "$BASE_URL/v2/wallets?label=My%20Portfolio" \
-H "Authorization: Bearer $API_TOKEN"
```
The response is the standard cursor-paginated list shape (`{ "data": [...], "nextCursor": ... }`); a matching wallet appears in `data`.
## Balance and workflow webhooks
Subscribe to workflow lifecycle events and fetch the wallet when you need a fresh balance snapshot:
* `portfolio_wallet.deposit.status_changed` -- fires when an external deposit posts to the wallet
* `portfolio_wallet.rebalance.status_changed` -- fires as Ground deploys cash or adjusts strategy positions
* `portfolio_wallet.withdrawal.status_changed` -- fires when withdrawals change the wallet's available balance
See [Webhooks](/docs/portfolio-wallets/webhooks) for registration and payload details.
# Calculating Yield Accrual
Source: https://docs.groundtech.co/docs/portfolio-wallets/calculating-yield-accrual
Understand how yield accrues across different position types and how to display earnings to your users.
Portfolio Wallets earn yield from multiple sources with different accrual mechanics. This guide explains how yield is computed and how to use the yield endpoint for detailed reporting.
## How yield accrues
All yield sources are variable-rate. Rates change over time based on market conditions and protocol performance.
### NAV-based tokens (Syrup, Morpho vaults)
These positions appreciate in value over time. When Ground deploys USDC or USDT, the stablecoin is converted into a compatible yield token or vault shares at the current exchange rate. As the protocol earns yield, the NAV (net asset value) per share increases, making those shares worth more of the source's deposit token.
The wallet's `balance.totalUsd` reflects the current market value of all positions, so NAV appreciation is automatically captured in the balance.
## Wallet-level earnings
Yield is reported at the wallet level, not per position. This is because rebalances move funds between positions over time, making per-position attribution misleading.
The `earnedUsd` field represents lifetime yield earned across all positions since the wallet was created. It is computed internally using per-position yield accrual tracking rather than a simple deposit/withdrawal accounting formula, so it accurately reflects actual yield even through rebalances and partial withdrawals.
Yield does not move on the same cadence for every source:
* `syrup-usdc` updates continuously from live onchain share-to-assets conversion.
* Morpho ERC-4626 vaults exposed by the live catalog update continuously from live onchain ERC-4626 conversion reads.
For the simple case of one completed deposit and no returned principal, `balance.totalUsd = depositUsd + balance.earnedUsd` to the supported accounting precision.
## The yield endpoint
Use `GET /v2/wallets/:id/yield` for a detailed yield breakdown:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET "$BASE_URL/v2/wallets/$WALLET_ID/yield" \
-H "Authorization: Bearer $API_TOKEN"
```
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"walletId": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
"earnedUsd": "250.000000",
"annualizedUsd": "376.750000",
"currentBalanceUsd": "82500.000000",
"positions": [
{
"yieldSourceId": "syrup-usdc",
"name": "Syrup USDC",
"apyBps": 453,
"pct": 40,
"deployedValueUsd": "33000.000000"
},
{
"yieldSourceId": "morpho-gauntlet-usdc",
"name": "Morpho Gauntlet USDC Prime",
"apyBps": 472,
"pct": 30,
"deployedValueUsd": "24750.000000"
},
{
"yieldSourceId": "morpho-steakhouse-usdc",
"name": "Morpho Steakhouse USDC Prime",
"apyBps": 468,
"pct": 30,
"deployedValueUsd": "24750.000000"
}
]
}
```
### Response fields
| Field | Description |
| - | - |
| `earnedUsd` | Cumulative yield earned (same as `balance.earnedUsd` on the wallet) |
| `annualizedUsd` | Estimated annualized yield in USD at current position values and rates |
| `currentBalanceUsd` | Current total wallet balance |
| `positions` | Per-source APY and allocation breakdown |
### Yield source fields
| Field | Description |
| - | - |
| `yieldSourceId` | Identifier for the yield source |
| `name` | Display name |
| `apyBps` | Current annualized yield in basis points (100 bps = 1%). `null` if unavailable. |
| `pct` | Actual deployed value as a percentage of the total wallet balance, including cash |
| `deployedValueUsd` | Current USD value deployed to this yield source |
## Displaying yield to users
### Recommended approach
1. **Total earned** — use `balance.earnedUsd` from the wallet response or `earnedUsd` from the yield endpoint
2. **Current APY** — compute a blended APY from the yield endpoint's `positions` array:
```
blendedApyBps = sum(source.apyBps * source.pct / 100) for each source
```
Source rates use the same historical 30-day APY as the yield-source catalog. Cash earns 0% and is included in the total balance used for the weights. This works for manual and auto-rebalancing wallets. If a held source has unavailable APY, show the blend as unavailable rather than treating it as 0%.
3. **Yield breakdown** — show each yield source's `name`, `apyBps`, and `pct` from the yield endpoint
4. **Performance over time** — for charts, use `GET /v2/accounting/yield/earned-series`: `cumulativeEarnedUsd` per point for an earned-dollars line, and the precomputed `blendedApyBps` per point for an APY line (same weighted-blend idea as above, computed server-side from trailing NAV growth at each historical point). See [Balances and Yield](/docs/portfolio-wallets/balances-and-yield#earned-and-apy-over-time).
### Important notes
* Ground surfaces annualized rates in `apyBps`, but the methodology varies by source. Syrup and Morpho vaults do not use identical lookback windows.
* Balances may briefly lag deposits or withdrawals due to caching. See [Balances and Yield](/docs/portfolio-wallets/balances-and-yield) for valuation notes.
* `earnedUsd` is lifetime. It remains monotonic even when individual source valuations update on different cadences.
## JustLend valuation and rate display
JustLend V1 positions hold jUSDT receipts whose accrued exchange rate determines their USDT value. This value feeds balances and earned-yield reporting. The yield-source catalog’s `currentApy` is a current supply-rate preview, not the wallet’s realized earnings or historical 30-day return. Label that preview **Current APY** and stop displaying it at `validUntil`; keep historical `apyBps` separate.
# Create a Wallet
Source: https://docs.groundtech.co/docs/portfolio-wallets/create-wallet
Create a portfolio wallet and poll for activation.
Creating a wallet is asynchronous. `POST /v2/wallets` returns immediately with `status: "creating"`. Deposit addresses and on-chain state are not populated synchronously — poll `GET /v2/wallets/{id}` until `status` becomes `"idle"`.
## Create a portfolio wallet
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST "$BASE_URL/v2/wallets" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"requestId": "0b7e3b4d-88a5-4a75-8c0f-3b7b4f9d9f1d",
"label": "RWA Yield Portfolio",
"strategy": {
"allocations": {
"usdc": [
{ "yieldSourceId": "ground-ustb-vault", "pct": 50 },
{ "yieldSourceId": "ground-jaaa-usdc-vault", "pct": 50 }
],
"usdt": [
{ "yieldSourceId": "cash", "pct": 100 }
]
}
}
}'
```
| Field | Required | Description |
| - | - | - |
| `requestId` | Yes | UUID v4 idempotency key |
| `label` | No | Human-readable wallet name |
| `strategy` | Yes | Object containing `allocations` |
| `strategy.allocations` | Yes | An object keyed by `usdc`, `usdt` (Ethereum), and optionally `usdt:tron`. Each included array must sum to 100%. |
Place each yield source under its catalog `allocationGroup` from `GET /v2/wallets/yield-sources`. `depositToken` alone cannot distinguish Ethereum USDT from TRON USDT. Use `cash` to keep funds uninvested in that token. If you omit USDC or USDT when creating a wallet, that token defaults to 100% cash.
The request above uses RWA sources available in the production catalog. Both
production and sandbox expose `ground-jaaa-usdt-vault` under `usdt`. Sandbox
also exposes token-specific JTRSY rows when returned by its live catalog:
`ground-jtrsy-usdc-vault` belongs under
`usdc`, and `ground-jtrsy-usdt-vault` belongs under `usdt`. Always use the live
environment-specific catalog rather than assuming a row is active.
A first POST with a new `requestId` returns **`200 OK`** with a minimal creating envelope (no deposit addresses, zero balances). An idempotent replay with the same `requestId` **and an identical payload** returns **`200 OK`** with the wallet's current state. Reusing a `requestId` with a **divergent payload** (different label or allocations) returns **`409 request_id_conflict`** — use a fresh `requestId` for a genuinely new wallet. See [Idempotency](/docs/portfolio-wallets/api-conventions#idempotency) for the full comparison rules.
## Creating response
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"id": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
"label": "RWA Yield Portfolio",
"status": "creating",
"failureReason": null,
"createdAt": "2026-02-05T08:15:00Z",
"depositAddresses": {},
"balance": {
"totalUsd": "0.000000",
"withdrawableUsd": "0.000000",
"reservedUsd": "0.000000",
"earnedUsd": "0.000000"
},
"positions": [],
"strategyAllocations": {
"usdc": [
{ "yieldSourceId": "ground-ustb-vault", "targetWeightBps": 5000 },
{ "yieldSourceId": "ground-jaaa-usdc-vault", "targetWeightBps": 5000 }
],
"usdt": [
{ "yieldSourceId": "cash", "targetWeightBps": 10000, "token": "usdt", "chain": "ethereum" }
]
}
}
```
## Polling for activation
Poll `GET /v2/wallets/{id}` every 1-2 seconds until `status !== "creating"`:
```javascript theme={"theme":{"light":"github-light","dark":"github-dark"}}
async function waitUntilReady(walletId, apiToken) {
while (true) {
const res = await fetch(`${BASE_URL}/v2/wallets/${walletId}`, {
headers: { Authorization: `Bearer ${apiToken}` },
});
const wallet = await res.json();
if (wallet.status === "idle") return wallet;
if (wallet.status === "failed") {
throw new Error(`wallet ${walletId} failed: ${wallet.failureReason}`);
}
await new Promise((r) => setTimeout(r, 1500));
}
}
```
See [Polling](/docs/portfolio-wallets/polling) for more on the poll pattern.
## Ready response
Once `status === "idle"`, the response is ready for deposits:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"id": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
"label": "RWA Yield Portfolio",
"status": "idle",
"failureReason": null,
"createdAt": "2026-02-05T08:15:00Z",
"depositAddresses": {
"arbitrum": "0x21246509968c4d24611f414560971AEc2e3A079B",
"base": "0x21246509968c4d24611f414560971AEc2e3A079B",
"ethereum": "0x21246509968c4d24611f414560971AEc2e3A079B",
"polygon": "0x21246509968c4d24611f414560971AEc2e3A079B",
"solana": "7nYzKxM3bP4oEFbqkPmA5E2rYJ8HqKz8vFg9abc1"
},
"balance": {
"totalUsd": "0.000000",
"withdrawableUsd": "0.000000",
"reservedUsd": "0.000000",
"earnedUsd": "0.000000"
},
"positions": [
{
"id": "ground-ustb-vault",
"kind": "yield_source",
"label": "Invesco USTB tokenized by Superstate",
"valueUsd": "0.000000",
"yieldSourceId": "ground-ustb-vault",
"pct": 50
},
{
"id": "ground-jaaa-usdc-vault",
"kind": "yield_source",
"label": "Janus Henderson JAAA (USDC)",
"valueUsd": "0.000000",
"yieldSourceId": "ground-jaaa-usdc-vault",
"pct": 50
}
],
"strategyAllocations": {
"usdc": [
{ "yieldSourceId": "ground-ustb-vault", "targetWeightBps": 5000 },
{ "yieldSourceId": "ground-jaaa-usdc-vault", "targetWeightBps": 5000 }
],
"usdt": [
{ "yieldSourceId": "cash", "targetWeightBps": 10000, "token": "usdt", "chain": "ethereum" }
]
}
}
```
### Sandbox ready response
Sandbox returns the same wallet shape with explicit testnet keys for its two supported USDC chains:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"id": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
"status": "idle",
"depositAddresses": {
"solana_devnet": "DmaePN7kL6WjSecMaH4urdS8zN8FwNPTxbdTqFwAw6Fd",
"ethereum_sepolia": "0xFf71D07826d0aFcEcDF07Be9c499f9DDF3ee5cFa"
}
}
```
### Key fields
| Field | Description |
| - | - |
| `status` | `"creating"` during provisioning, `"idle"` when ready, `"withdrawal_active"` / `"rebalance_active"` / `"withdrawal_and_rebalance_active"` while work is running, `"failed"` if setup terminated |
| `failureReason` | Populated only when `status === "failed"` — short human-readable reason |
| `depositAddresses` | Ready per-chain deposit addresses. TRON is included only after its separate address setup is ready. |
| `addressProvisioning` | General deposit address setup state: `provisioning`, `ready`, or `failed`. Independent of wallet activity status; already available addresses remain usable. |
| `balance.totalUsd` | Total economically owned wallet value |
| `balance.withdrawableUsd` | Gross amount that can be withdrawn now, before plan-specific fees |
| `balance.reservedUsd` | Customer-owned value currently reserved by active withdrawals or rebalances |
| `balance.earnedUsd` | Lifetime yield earned (monotonic) |
| `positions` | Current cash and yield-source holdings with USD values. Yield-source positions include `yieldSourceId` and `pct`; configured cash allocations may also include `pct`. |
| `strategyAllocations` | Desired targets grouped by stablecoin. Target weights use basis points and each returned group totals 10,000. |
## Failed response
If wallet setup fails terminally, `GET /v2/wallets/{id}` returns:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"id": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
"label": "Core Yield Portfolio",
"status": "failed",
"failureReason": "Turnkey create_wallet activity ACTIVITY_STATUS_REJECTED: act_abc",
"createdAt": "2026-02-05T08:15:00Z",
"depositAddresses": {},
"balance": { "totalUsd": "0.000000", "withdrawableUsd": "0.000000", "reservedUsd": "0.000000", "earnedUsd": "0.000000" },
"positions": []
}
```
Failed wallets cannot be recovered by retrying the same `requestId`. Create a new wallet with a fresh `requestId` and contact support if the failure reason is unclear.
## Rebalancing
Portfolio wallets are best-effort targets. Over time, or after deposits and withdrawals, holdings drift from target allocations. The system rebalances to bring positions back toward the strategy targets. Deposits first appear as cash and are then deployed into the configured yield sources.
## Sandbox notes
In sandbox, USDC deposit addresses are keyed as `ethereum_sepolia` and `solana_devnet`. Sandbox USDT uses the supported test token for Ethereum Sepolia or TRON Shasta. Shasta JustLend is simulated. See [Supported Chains](/docs/portfolio-wallets/supported-chains) for details.
Every Portfolio Wallet automatically provisions its TRON USDT address, independently of allocation strategy. Auto-rebalancing wallets can configure `strategy.allocations["usdt:tron"]`; if omitted, the group starts at 100% cash. In the portal, automatic wallet setup includes a **Configure USDT (TRON) strategy** step. Manual-wallet deposits remain cash until allocated.
A wallet can be `idle` while its TRON address is provisioning. Wait for `addressProvisioning.status: "ready"` and the corresponding deposit address before sending TRON USDT. Ground funds activation and gas when an outgoing transaction needs them.
# Deposits
Source: https://docs.groundtech.co/docs/portfolio-wallets/deposits
Fund a portfolio wallet and track deposit lifecycle.
After creating a wallet, fund it by sending a supported stablecoin to the wallet's deposit addresses.
## Fund the wallet
The wallet response includes `depositAddresses` for ready supported networks. Send USDC on a supported chain or USDT on Ethereum. When TRON is enabled and ready for your wallet, `depositAddresses.tron` accepts native TRC-20 USDT. TRON USDT belongs to the separate `usdt:tron` allocation group. Ground pays transaction resource costs.
Wait until the API returns the address for the intended network before depositing. A TRON address is omitted while its wallet setup, signing policies, or deposit reconciliation are not ready. Do not derive it from another network's address. The EVM address can be the same for both supported tokens, so always validate the token and network before sending.
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
to: 0x21246509968c4d24611f414560971AEc2e3A079B
chain: ethereum
token: USDC
```
### Minimum amounts
There is no minimum deposit amount. However, small deposits may remain as undeployed [cash](/docs/portfolio-wallets/balances-and-yield#cash-positions) if deploying them into yield positions is not economically efficient (considering gas costs, minimum trade sizing, and protocol constraints). This minimum is environment-configured and applied per executable rebalance leg: sandbox uses small minimums (e.g. 5 units per leg) so you can test with small amounts, while production minimums are typically higher. See [Balances and Yield](/docs/portfolio-wallets/balances-and-yield#when-idle-stablecoins-move-into-yield).
### Sandbox
* Use the explicit sandbox key `depositAddresses.ethereum_sepolia`. Fund it with Sepolia USDC from the [Circle faucet](https://faucet.circle.com/).
* Use `depositAddresses.solana_devnet` for Solana Devnet USDC deposits.
* To receive Ground's mock USDT on Ethereum Sepolia, [request it from the sandbox faucet](/api-reference/request-sandbox-usdt) for `depositAddresses.ethereum_sepolia`, then [poll the request](/api-reference/get-sandbox-usdt-request) until it is confirmed.
* For TRON, wait for `depositAddresses.tron_shasta` and fund it with the supported Shasta test USDT. The Sepolia faucet above does not fund Shasta. Contact Ground for Shasta test funding.
* Sandbox USDT integrations accept only their configured test token. Do not send USDT from another test-token deployment or network.
## Deposit lifecycle
| Status | Terminal | Meaning |
| - | - | - |
| `processing` | No | Ground detected the inbound stablecoin transaction for a workflow-idle wallet or is processing the finalized deposit |
| `completed` | Yes | Deposit finalized and posted; wallet balances updated |
| `failed` | Yes | Deposit processing failed |
Processing deposits are informational until finality. They can appear in deposit and activity APIs shortly after the source wallet broadcasts the transfer when the destination wallet has no active Ground-managed workflow, but they are not included in `balance.totalUsd`, `balance.withdrawableUsd`, or positions until ledger posting completes. Wallets with active rebalances, withdrawals, or protocol workflows may wait until final attribution before a deposit appears.
As deposits settle, the wallet's `balance.totalUsd` and `positions` update.
Wallets with automatic rebalancing deploy cash toward percentage targets.
Otherwise deposits stay in cash until the partner calls `/allocate`.
## Deposit response
### Ethereum Sepolia
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"id": "797a5ca3-2a1c-443b-b190-ca951964aa35",
"walletId": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
"amount": "10.000000",
"token": "usdc",
"chain": "ethereum_sepolia",
"fromAddress": "0x76f8fc6667e239f83a547d4e16225d6a34f6fa22",
"txHash": "0xe2e8a534960a7cf19932b35759788a0de4fa131d46d3e36a96a4802b6b93e5e1",
"status": "completed",
"cashPositionId": "cash:ethereum_sepolia:usdc",
"createdAt": "2026-07-09T21:34:17.973Z",
"completedAt": "2026-07-09T21:34:17.973Z"
}
```
### Solana Devnet
Solana responses use base58 addresses and transaction signatures while preserving the same deposit shape:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"id": "797a5ca3-2a1c-443b-b190-ca951964aa36",
"walletId": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
"amount": "10.000000",
"token": "usdc",
"chain": "solana_devnet",
"fromAddress": "DmaePN7kL6WjSecMaH4urdS8zN8FwNPTxbdTqFwAw6Fd",
"txHash": "yTeUkZfSRTRZRdp9UcwRRMtqbeeViAFRWKiTLapi2qPrd7omYfatYsEt2Cak2rFoGDEuLeBPQJPjn7YF88yyWRZ",
"status": "completed",
"cashPositionId": "cash:solana_devnet:usdc",
"createdAt": "2026-07-09T21:34:17.973Z",
"completedAt": "2026-07-09T21:34:17.973Z"
}
```
### TRON
TRON deposits use the same response shape with `token: "usdt"`, `chain: "tron"` (or `"tron_shasta"` in sandbox), and a TRON base58 `fromAddress`. Transaction hashes are hexadecimal without an EVM `0x` prefix. Completed deposits identify cash as `cash:tron:usdt` or `cash:tron_shasta:usdt`; both belong to the `usdt:tron` group.
| Field | Description |
| - | - |
| `id` | Unique deposit identifier; when backed by an observed onchain event, the same ID is used from `processing` through `completed` |
| `walletId` | Portfolio wallet credited by the deposit |
| `amount` | Deposit amount as a fixed-precision decimal string |
| `chain` | Source chain |
| `token` | Deposited token (`usdc` or `usdt`) |
| `fromAddress` | Sender address (when available) |
| `txHash` | Onchain transaction hash |
| `status` | Current status (see lifecycle table above) |
| `cashPositionId` | Credited cash position ID, or `null` until the deposit completes |
| `createdAt` | When the deposit was first detected |
| `completedAt` | When the deposit finalized and posted (`null` while processing) |
## List deposits
```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl "$BASE_URL/v2/wallets/$WALLET_ID/deposits?limit=25" \
-H "Authorization: Bearer $GROUND_API_TOKEN"
```
```javascript Node.js theme={"theme":{"light":"github-light","dark":"github-dark"}}
const resp = await fetch(`${BASE_URL}/v2/wallets/${walletId}/deposits?limit=25`, {
headers: { Authorization: `Bearer ${GROUND_API_TOKEN}` },
});
const { data, nextCursor } = await resp.json();
```
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"data": [
{
"id": "797a5ca3-2a1c-443b-b190-ca951964aa35",
"walletId": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
"amount": "10.000000",
"token": "usdc",
"chain": "ethereum_sepolia",
"fromAddress": "0x76f8fc6667e239f83a547d4e16225d6a34f6fa22",
"txHash": "0xe2e8a534960a7cf19932b35759788a0de4fa131d46d3e36a96a4802b6b93e5e1",
"status": "completed",
"cashPositionId": "cash:ethereum_sepolia:usdc",
"createdAt": "2026-07-09T21:34:17.973Z",
"completedAt": "2026-07-09T21:34:17.973Z"
}
],
"nextCursor": null
}
```
Supports cursor-based pagination. See [API Conventions](/docs/portfolio-wallets/api-conventions#pagination).
## Fetch a deposit by ID
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl "$BASE_URL/v2/wallets/$WALLET_ID/deposits/$DEPOSIT_ID" \
-H "Authorization: Bearer $GROUND_API_TOKEN"
```
## Look up by transaction hash
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl "$BASE_URL/v2/wallets/$WALLET_ID/deposits/lookup?txHash=$TX_HASH" \
-H "Authorization: Bearer $GROUND_API_TOKEN"
```
Optionally scope to a specific chain:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl "$BASE_URL/v2/wallets/$WALLET_ID/deposits/lookup?txHash=$TX_HASH&chain=arbitrum" \
-H "Authorization: Bearer $GROUND_API_TOKEN"
```
## Deposit webhooks
Subscribe to `portfolio_wallet.deposit.status_changed` for real-time deposit tracking. See [Webhooks](/docs/portfolio-wallets/webhooks).
## Next steps
* [Balances and Yield](/docs/portfolio-wallets/balances-and-yield) — track how deposits are allocated and earning yield
* [Withdraw Funds](/docs/portfolio-wallets/withdraw-funds) — withdraw USDC or USDT from the wallet
* [Webhooks](/docs/portfolio-wallets/webhooks) — subscribe to deposit status events
## TRON deposit addresses
Every Portfolio Wallet receives a TRON USDT deposit address automatically. In the portal, open **Deposit funds** and select TRON in the blockchain dropdown once the address is ready.
Poll `GET /v2/wallets/{id}` until `addressProvisioning.status` is `ready` and use `depositAddresses.tron` in production or `depositAddresses.tron_shasta` in Sandbox. Address setup can continue while the wallet is `idle`; any addresses already returned remain usable. Contact Ground if setup returns `failed`.
For automatic wallets, each `usdt:tron` strategy applies only to TRON USDT; an unspecified TRON strategy starts in cash. Manual-wallet deposits remain cash until allocated. Ground funds activation and transaction resources when an outgoing transaction needs them; customers do not need to send TRX.
# Fee Wrappers (Beta)
Source: https://docs.groundtech.co/docs/portfolio-wallets/fee-wrappers
Collect fees while your end users' funds remain invested in supported portfolio-wallet yield sources.
Fee wrappers let you collect fees while your end users' funds remain invested.
Fees are paid in shares representing a portion of the investment. Your fee
recipient can redeem those shares without waiting for end users to withdraw.
Fee wrappers are in beta and available for selected yield sources. Contact Ground
to configure fees, recipients, and organization access.
```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
flowchart LR
Wallet[Portfolio Wallet] -->|Holds shares in| Wrapper[Fee Wrapper]
Wrapper -->|Holds a position in| Vault[Underlying Vault]
Wrapper -->|Issues fee shares to| Recipient[Fee Recipient]
```
## How fees accrue
Wrappers support two types of fees:
| Fee | How it works |
| - | - |
| Management | An annual percentage of the invested amount. |
| Performance | A percentage of eligible new gains |
A 0.1% annual management fee on a constant \$10,000 investment is approximately
\$10 per year. A 20% performance fee on \$100 of eligible gains is \$20.
## Getting started
Contact Ground with the yield source you want to use, your fee rates, and the
wallet addresses that should receive fees. Ground sets up the wrapper and makes
it available to your organization.
Your integration continues to use the same Portfolio Wallet APIs. For configured
yield sources, Ground returns the fee wrapper instead of the underlying vault,
with balances and yields reflecting its fees.
## Collecting fees
Your fee recipient can check and redeem fee shares directly through the wrapper.
`previewBalanceOf(address)` shows both existing shares and pending fee shares.
When you redeem, the contract first mints any pending fee shares. Redeeming fee
shares follows the same withdrawal process as the underlying vault.
# Onchain Architecture
Source: https://docs.groundtech.co/docs/portfolio-wallets/flow-of-funds
How deposits, direct protocol positions, and Ground RWA Vault positions move through Ground's contract stack.
This page describes how stablecoins enter a Portfolio Wallet and then move through either a direct protocol position or a Ground RWA Vault position. USDC, Ethereum USDT, and TRON USDT follow independent allocation groups.
## Funding a Portfolio Wallet
Funding and allocation are separate stages. An external transfer first becomes cash in the Portfolio Wallet. A later Ground-managed rebalance deploys that cash according to the wallet's strategy.
```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
sequenceDiagram
participant User as External Wallet
participant PW as Portfolio Wallet
participant API as Ground API
User->>PW: Transfer selected stablecoin
API->>API: Detect and confirm transfer
API->>API: Post wallet cash balance
API->>PW: Start eligible strategy allocation
```
The stablecoin remains in its original token lane. Ground does not convert between USDC and USDT during deposits, rebalances, or withdrawals. USDC may bridge within its supported CCTP domain; USDT stays on Ethereum or TRON according to its allocation group. Ethereum USDT cannot fund a TRON position or payout, and TRON USDT cannot fund an Ethereum position or payout.
## Signing Portfolio Wallet actions
Ground uses Turnkey to sign Portfolio Wallet transactions according to the wallet's configured policies. Ground then broadcasts the signed transaction, which calls the MasterRouter from the Portfolio Wallet address.
```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
sequenceDiagram
participant API as Ground API
participant TK as Turnkey
participant PW as Portfolio Wallet
participant MR as MasterRouter
API->>TK: Request Portfolio Wallet transaction signature
TK-->>API: Return approved signed transaction
API->>PW: Broadcast signed wallet transaction
PW->>MR: Execute authorized action
```
Some actions require customer approval before Turnkey releases the signature. See [Transaction Approvals](/docs/portfolio-wallets/transaction-approvals) for the approval API and verification flow.
## Direct protocol positions
DeFi sources such as Morpho use a direct protocol path. The Portfolio Wallet calls the MasterRouter, the router resolves an approved adapter through the AdapterRegistry, and the adapter interacts with the underlying protocol. The resulting protocol position token or vault share is held by the Portfolio Wallet.
```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
sequenceDiagram
participant PW as Portfolio Wallet
participant MR as MasterRouter
participant AR as AdapterRegistry
participant Adapter as Protocol Adapter
participant Protocol as Underlying Protocol
PW->>MR: Execute yield-source action
MR->>AR: Resolve approved adapter
AR-->>MR: Return active adapter
MR->>Adapter: Forward action and stablecoin
Adapter->>Protocol: Deposit into protocol
Protocol-->>PW: Issue protocol position
```
An exit follows the corresponding protocol's redeem path. Some direct protocols settle in one transaction; others require additional waiting or settlement steps before stablecoin is available for payout.
## Ground RWA Vault positions
Ground RWA Vault sources use dedicated vault contracts. The Portfolio Wallet interacts onchain with the MasterRouter and the applicable Ground vault, then holds a wallet-specific, non-transferable Ground receipt rather than an underlying provider instrument directly.
Depending on the product and available liquidity, a Ground RWA Vault deposit may settle immediately or use the following asynchronous deposit lifecycle:
```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
sequenceDiagram
participant PW as Portfolio Wallet
participant MR as MasterRouter
participant GV as Ground RWA Vault
participant Provider as RWA Provider
participant Processor as Vault Processor
PW->>MR: Submit vault deposit request
MR->>GV: Create wallet request
GV->>Provider: Start provider settlement
Provider-->>GV: Make underlying value claimable
Processor->>GV: Process available settlement
PW->>MR: Claim processed deposit
MR->>GV: Claim deposit for wallet
GV-->>PW: Issue non-transferable Ground receipt
```
Ground RWA Vault exits use the corresponding redemption lifecycle: the wallet requests redemption, provider settlement and vault processing complete, the wallet claims stablecoin, and Ground sends the payout to the withdrawal destination. Some vaults can settle through available onchain liquidity; otherwise the asynchronous path applies. Each withdrawal payout leg settles and pays out independently.
Processing policies in the [Yield Sources](/docs/portfolio-wallets/yield-sources) catalog describe operational estimates. Actual completion depends on external fulfillment and vault processing. Because payout legs may settle at different times, each requires separate approval when signing keys are configured in the [developer portal](https://portal.groundtech.co). See [Transaction Approvals](/docs/portfolio-wallets/transaction-approvals) for details.
## Contract addresses
See [Contract Addresses](/docs/resources/addresses) for the canonical production and sandbox router, registry, adapter, Ground vault, bridge, and Solana program deployments.
## JustLend V1 on TRON
Ground deploys the same Router, Adapter Registry, and adapter architecture on TRON. A JustLend allocation routes native USDT through the JustLend V1 adapter, supplies the lending market, and leaves jUSDT receipts in the Portfolio Wallet. Redemption converts those receipts back into USDT in the same wallet. A separate approved payout delivers USDT to the requested TRON destination.
This flow stays in the `usdt:tron` group and does not bridge to Ethereum. Ground pays resources for Ground-managed transactions. Shasta uses the same integration flow with test assets and a simulated lending market.
# Example: Neobank/Exchanges
Source: https://docs.groundtech.co/docs/portfolio-wallets/integration-guides/neobank-customer-portfolio-wallet
Set `autoRebalance: false` when creating the wallet so your application controls
exact dollar allocations and deposits remain in cash until you allocate them.
```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
sequenceDiagram
participant Customer
participant App as "Your application"
participant Ground
App->>Ground: Create wallet without auto-rebalance
Ground-->>App: Wallet and deposit addresses
Customer->>Ground: Send stablecoin
Ground-->>App: Deposit completed
App->>Ground: Preview allocation
App->>Ground: Create allocation
Ground-->>App: Rebalance completed
App->>Ground: Read customer positions and yield
Customer->>App: Request withdrawal
App->>Ground: Preview and create withdrawal
Ground-->>Customer: Stablecoin payout
```
Store your API key in `GROUND_API_TOKEN`. The examples below use the sandbox API:
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const baseUrl = "https://sandbox.groundtech.co";
const apiToken = process.env.GROUND_API_TOKEN!;
```
## 1. Create a wallet for each customer
Create a wallet without auto-rebalancing by setting `autoRebalance: false`. Save the Ground wallet
ID on your customer record.
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
type Wallet = {
id: string;
autoRebalance: boolean;
status: string;
depositAddresses: Record;
positions: Array<{
id: string;
kind: string;
label: string;
valueUsd: string;
}>;
};
async function createCustomerWallet(customerId: string) {
const response = await fetch(`${baseUrl}/v2/wallets`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
requestId: crypto.randomUUID(),
label: `Customer ${customerId}`,
autoRebalance: false,
}),
});
if (!response.ok) throw new Error(await response.text());
const wallet = (await response.json()) as Wallet;
await saveGroundWalletId(customerId, wallet.id);
return wallet;
}
const customerWallet = await createCustomerWallet("customer-1234");
```
Subscribe to `portfolio_wallet.status_changed` before creating the wallet. When
the matching `walletId` reaches `idle`, read `GET /v2/wallets/{id}` and show the
customer the appropriate deposit address. Handle `failed` as a provisioning
failure, and use the wallet read to reconcile a delayed or missed webhook.
## 2. Receive the completed deposit
Subscribe to `portfolio_wallet.deposit.status_changed` or poll the wallet's
deposits. Allocate funds only after the deposit becomes `completed`. Verify the
webhook signature before parsing it, and deduplicate deliveries with the
`Ground-Event-Id` header.
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
type CompletedDeposit = {
id: string;
walletId: string;
amount: string;
status: "completed";
cashPositionId: string;
};
type DepositWebhook = {
event: "portfolio_wallet.deposit.status_changed";
deposit: {
id: string;
walletId: string;
amount: string;
status: "processing" | "completed" | "failed";
cashPositionId: string | null;
};
};
async function handleDepositEvent(event: DepositWebhook) {
if (event.deposit.status !== "completed") return;
if (!event.deposit.cashPositionId) return;
await allocateDeposit(
event.deposit as CompletedDeposit,
"morpho-august-usdc-v2",
);
}
```
Use `cashPositionId` as the source when allocating this deposit. IDs are unique
to each environment, so do not reuse sandbox IDs in production.
See [Webhook signature verification](/docs/portfolio-wallets/webhook-signature-verification)
for the complete verification flow. Use the wallet read as a fallback when a
webhook is delayed or missed.
## 3. Allocate the cash
Fetch `GET /v2/wallets/yield-sources` and use a returned source `id` as the
destination. Send the same `allocations` array to preview and create. An
allocation can move cash into a yield source, move a yield position to cash, or
move between yield sources. Cash-to-cash allocations are not supported.
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
async function allocateDeposit(
deposit: CompletedDeposit,
yieldSourceId: string,
) {
const allocations = [
{
from: deposit.cashPositionId,
to: yieldSourceId,
amountUsd: deposit.amount,
},
];
const previewResponse = await fetch(
`${baseUrl}/v2/wallets/${deposit.walletId}/allocate-preview`,
{
method: "POST",
headers: {
Authorization: `Bearer ${apiToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ allocations }),
},
);
if (!previewResponse.ok) throw new Error(await previewResponse.text());
const preview = await previewResponse.json();
const allocationResponse = await fetch(
`${baseUrl}/v2/wallets/${deposit.walletId}/allocate`,
{
method: "POST",
headers: {
Authorization: `Bearer ${apiToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
requestId: crypto.randomUUID(),
allocations,
}),
},
);
if (!allocationResponse.ok) throw new Error(await allocationResponse.text());
const allocation = await allocationResponse.json();
return { preview, allocation };
}
```
The response includes the `rebalanceId`, `requestId`, `createdAt`, current
`status`, and accepted `allocations`. Returned dollar amounts use six decimal
places. Track `portfolio_wallet.rebalance.status_changed` or rebalance activity
until it finishes.
If Ground returns `workflow_conflict`, wait for the active withdrawal or
rebalance to finish, then retry.
## 4. Read or change an allocation
Read the wallet to get the customer's current cash and investments:
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const response = await fetch(`${baseUrl}/v2/wallets/${customerWallet.id}`, {
headers: { Authorization: `Bearer ${apiToken}` },
});
if (!response.ok) throw new Error(await response.text());
const wallet = (await response.json()) as Wallet;
console.log(wallet.positions);
```
To move an existing investment, use IDs from `wallet.positions`:
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const allocations = [
{
from: "morpho-august-usdc-v2",
to: "syrup-usdc",
amountUsd: "250.000000",
},
];
const previewResponse = await fetch(
`${baseUrl}/v2/wallets/${customerWallet.id}/allocate-preview`,
{
method: "POST",
headers: {
Authorization: `Bearer ${apiToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ allocations }),
},
);
if (!previewResponse.ok) throw new Error(await previewResponse.text());
const allocationResponse = await fetch(
`${baseUrl}/v2/wallets/${customerWallet.id}/allocate`,
{
method: "POST",
headers: {
Authorization: `Bearer ${apiToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
requestId: crypto.randomUUID(),
allocations,
}),
},
);
if (!allocationResponse.ok) {
throw new Error(await allocationResponse.text());
}
```
## 5. View yield
Read the customer's lifetime earnings, estimated annualized yield, and current
breakdown by yield source:
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
type WalletYield = {
walletId: string;
earnedUsd: string;
annualizedUsd: string;
currentBalanceUsd: string;
positions: Array<{
yieldSourceId: string;
name: string;
apyBps: number | null;
deployedValueUsd: string;
}>;
};
const response = await fetch(
`${baseUrl}/v2/wallets/${customerWallet.id}/yield`,
{ headers: { Authorization: `Bearer ${apiToken}` } },
);
if (!response.ok) throw new Error(await response.text());
const customerYield = (await response.json()) as WalletYield;
console.log({
earnedUsd: customerYield.earnedUsd,
annualizedUsd: customerYield.annualizedUsd,
positions: customerYield.positions,
});
```
This response can be shown directly in the customer-facing application. See
[Calculating Yield Accrual](/docs/portfolio-wallets/calculating-yield-accrual)
for display and calculation guidance.
## 6. Withdraw funds
Omit `sources` to let Ground choose the positions. To withdraw from specific
positions, include the amount from each source. Source amounts must add up to
`amountUsd`, and each source must contribute at least \$0.01.
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const withdrawalRequest = {
amountUsd: 1_000,
token: "usdc",
destinationChain: "ethereum_sepolia",
sources: [
{ id: "morpho-august-usdc-v2", amountUsd: 600 },
{ id: "syrup-usdc", amountUsd: 400 },
],
};
const previewResponse = await fetch(
`${baseUrl}/v2/wallets/${customerWallet.id}/withdrawal-preview`,
{
method: "POST",
headers: {
Authorization: `Bearer ${apiToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(withdrawalRequest),
},
);
if (!previewResponse.ok) throw new Error(await previewResponse.text());
const preview = await previewResponse.json();
const withdrawalResponse = await fetch(
`${baseUrl}/v2/wallets/${customerWallet.id}/withdrawals`,
{
method: "POST",
headers: {
Authorization: `Bearer ${apiToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
...withdrawalRequest,
requestId: crypto.randomUUID(),
destinationAddress: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
}),
},
);
if (!withdrawalResponse.ok) throw new Error(await withdrawalResponse.text());
const withdrawal = await withdrawalResponse.json();
console.log({ preview, withdrawal });
```
Send the same source amounts to preview and create. Track
`portfolio_wallet.withdrawal.status_changed` until the withdrawal finishes.
## Test the integration
In sandbox:
1. Create a wallet without automatic rebalancing and save its ID on a test customer.
2. Deposit test USDC and confirm the completed deposit includes `cashPositionId`.
3. Allocate part of the cash to two yield sources.
4. Confirm the rebalance finishes and the wallet returns the new positions.
5. Confirm `GET /v2/wallets/{id}/yield` returns the customer's yield summary.
6. Move funds between two existing positions.
7. Complete one automatic withdrawal and one selected-source withdrawal.
Save each `requestId` before sending a request. If the request times out, retry
with the same ID and body.
# Example: Treasury Portfolio
Source: https://docs.groundtech.co/docs/portfolio-wallets/integration-guides/treasury-portfolio-wallet
Enable automatic rebalancing when Ground should keep funds near percentage
targets. Ground deploys deposits and rebalances the wallet as balances change.
```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
sequenceDiagram
participant Treasury
participant App as "Your application"
participant Ground
App->>Ground: Create wallet with auto-rebalance
Ground-->>App: Wallet and deposit addresses
Treasury->>Ground: Send stablecoin
Ground-->>App: Deposit completed
Ground->>Ground: Deploy toward percentage targets
Ground-->>App: Rebalance completed
App->>Ground: Read balances, positions, and yield
App->>Ground: Create withdrawal
Ground-->>Treasury: Stablecoin payout
```
Store your API key in `GROUND_API_TOKEN`. The examples below use the sandbox API:
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const baseUrl = "https://sandbox.groundtech.co";
const apiToken = process.env.GROUND_API_TOKEN!;
```
## 1. Fetch yield sources
Fetch the available yield sources before creating the wallet. Use the returned
`id` and `depositToken` values when building the strategy.
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
type YieldSource = {
id: string;
name: string;
depositToken: "usdc" | "usdt";
apyBps: number | null;
};
const response = await fetch(`${baseUrl}/v2/wallets/yield-sources`, {
headers: { Authorization: `Bearer ${apiToken}` },
});
if (!response.ok) throw new Error(await response.text());
const catalog = (await response.json()) as { data: YieldSource[] };
```
## 2. Create the wallet
Allocations for each token must total 100%.
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
type Wallet = {
id: string;
autoRebalance: boolean;
status: string;
depositAddresses: Record;
positions: Array<{
id: string;
kind: string;
label: string;
valueUsd: string;
}>;
};
const response = await fetch(`${baseUrl}/v2/wallets`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
requestId: crypto.randomUUID(),
label: "Corporate Treasury",
autoRebalance: true,
strategy: {
allocations: {
usdc: [
{ yieldSourceId: "morpho-august-usdc-v2", pct: 60 },
{ yieldSourceId: "syrup-usdc", pct: 40 },
],
},
},
}),
});
if (!response.ok) throw new Error(await response.text());
const treasuryWallet = (await response.json()) as Wallet;
```
The response starts with `status: "creating"`. Subscribe to
`portfolio_wallet.status_changed` before creating the wallet. When the matching
`walletId` reaches `idle`, read `GET /v2/wallets/{id}` to get its deposit
addresses. Handle `failed` as a provisioning failure, and use the wallet read
to reconcile a delayed or missed webhook.
## 3. Deposit funds
Send a supported stablecoin to the matching deposit address. Ground credits the
deposit and deploys the funds toward the target percentages.
Subscribe to these events or poll the related resources:
| Event | Meaning |
| - | - |
| `portfolio_wallet.deposit.status_changed` | The deposit status changed |
| `portfolio_wallet.rebalance.status_changed` | Deployment or rebalancing progressed |
Wait for the deposit to become `completed` before using it in your application.
See [Deposits](/docs/portfolio-wallets/deposits).
## 4. Read or update the portfolio
Read the wallet to get its current cash, investments, and balances:
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const response = await fetch(
`${baseUrl}/v2/wallets/${treasuryWallet.id}`,
{ headers: { Authorization: `Bearer ${apiToken}` } },
);
if (!response.ok) throw new Error(await response.text());
const currentTreasury = (await response.json()) as Wallet;
console.log(currentTreasury.positions);
```
Ground continues to maintain the target percentages. To change them, call
`PATCH /v2/wallets/{id}/strategy`.
## 5. View yield
Read lifetime earnings, estimated annualized yield, and the current breakdown
by yield source:
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
type WalletYield = {
walletId: string;
earnedUsd: string;
annualizedUsd: string;
currentBalanceUsd: string;
positions: Array<{
yieldSourceId: string;
name: string;
apyBps: number | null;
deployedValueUsd: string;
}>;
};
const response = await fetch(
`${baseUrl}/v2/wallets/${treasuryWallet.id}/yield`,
{ headers: { Authorization: `Bearer ${apiToken}` } },
);
if (!response.ok) throw new Error(await response.text());
const treasuryYield = (await response.json()) as WalletYield;
console.log({
earnedUsd: treasuryYield.earnedUsd,
annualizedUsd: treasuryYield.annualizedUsd,
positions: treasuryYield.positions,
});
```
See [Calculating Yield Accrual](/docs/portfolio-wallets/calculating-yield-accrual)
for display and calculation guidance.
## 6. Withdraw funds
Do not specify funding sources while automatic rebalancing is enabled. Ground chooses the
positions used for the withdrawal.
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const withdrawalRequest = {
amountUsd: 1_000,
token: "usdc",
destinationChain: "ethereum_sepolia",
};
const previewResponse = await fetch(
`${baseUrl}/v2/wallets/${treasuryWallet.id}/withdrawal-preview`,
{
method: "POST",
headers: {
Authorization: `Bearer ${apiToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(withdrawalRequest),
},
);
if (!previewResponse.ok) throw new Error(await previewResponse.text());
const preview = await previewResponse.json();
const withdrawalResponse = await fetch(
`${baseUrl}/v2/wallets/${treasuryWallet.id}/withdrawals`,
{
method: "POST",
headers: {
Authorization: `Bearer ${apiToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
...withdrawalRequest,
requestId: crypto.randomUUID(),
destinationAddress: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
}),
},
);
if (!withdrawalResponse.ok) throw new Error(await withdrawalResponse.text());
const withdrawal = await withdrawalResponse.json();
console.log({ preview, withdrawal });
```
Track `portfolio_wallet.withdrawal.status_changed` until the withdrawal
finishes. If it requires customer approval, complete the configured
[transaction approval](/docs/portfolio-wallets/transaction-approvals).
## Test the integration
In sandbox:
1. Create a wallet with automatic rebalancing and wait for `idle`.
2. Deposit test USDC to `depositAddresses.ethereum_sepolia`.
3. Confirm the deposit completes and a rebalance deploys the funds.
4. Confirm `GET /v2/wallets/{id}` returns the new positions.
5. Confirm `GET /v2/wallets/{id}/yield` returns the yield summary.
6. Preview and complete a small withdrawal.
Switch the base URL and API key before using production chain names and
addresses.
# Portfolio Wallets Overview
Source: https://docs.groundtech.co/docs/portfolio-wallets/introduction
Multi-strategy yield wallets with blended rates, automated rebalancing, and liquidity profiles.
Portfolio Wallets let neobanks, exchanges, and treasuries put idle stablecoins to work. Allocate customer deposits across multiple yield sources with a single API integration — Ground handles allocation, rebalancing, withdrawals, and reporting.
## How it works
1. **Configure** — choose yield sources and target weights from the [yield source catalog](/docs/portfolio-wallets/yield-sources).
2. **Create** — create a wallet with your chosen strategy. Ground provisions per-chain deposit addresses.
3. **Deposit** — send USDC on a supported chain or USDT on Ethereum or an enabled TRON address.
4. **Earn** — Ground allocates deposits across yield sources and rebalances to maintain target weights.
5. **Withdraw** — request USDC or USDT. Ground unwinds positions and delivers the same stablecoin; USDC may bridge across supported chains, while USDT stays on its Ethereum or TRON network.
Once you create a wallet and receive deposit addresses, the lifecycle follows a simple pattern: deposit stablecoins, monitor balances and accrued yield via polling or webhooks, and withdraw when ready. Withdrawal payouts require transaction signing through Turnkey before Ground broadcasts and settles the funds to your destination address.
## Independent allocation groups
A portfolio wallet can hold three independent allocation groups: `strategy.allocations.usdc`, `strategy.allocations.usdt` for Ethereum USDT, and `strategy.allocations["usdt:tron"]` for TRON USDT. Each included group must independently total 100%. JustLend V1 USDT is the first supported TRON yield source. [Wait for the TRON address to be ready](/docs/portfolio-wallets/deposits#tron-deposit-addresses) before funding that network.
Ground does not swap between USDC and USDT or bridge USDT between Ethereum and TRON. Deposits follow their allocation group, and withdrawals source funds compatible with both the requested stablecoin and destination network. Sandbox uses Ethereum Sepolia, Solana Devnet, and TRON Shasta with supported test tokens.
```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
sequenceDiagram
participant YS as Your Server
participant GA as Ground API
participant TK as Turnkey API
note over YS,GA: Stablecoins sent to deposit address
GA-->>YS: Webhook: deposit confirmed
YS->>GA: GET /v2/wallets/:id
GA-->>YS: { balance: { totalUsd, withdrawableUsd } }
YS->>GA: POST /v2/wallets/:id/withdrawals
GA-->>YS: { id, status: "processing", payoutLegs }
GA-->>YS: Webhook: payout pending_customer_approval
YS->>TK: Sign and approve payout legs
GA-->>YS: Webhook: withdrawal completed
note over YS,GA: Stablecoins arrive at destination address
```
## Custody and security
* **Non-custodial key management** — each wallet's private keys are managed by [Turnkey](https://www.turnkey.com/). Ground never has access to signing keys.
* **Signing policies** — your organization controls which withdrawal transactions require approval. Strategy updates are API-authenticated target-allocation changes; later rebalances may execute asynchronously under configured policy.
* **Wallet-specific addresses and accounting** — each wallet gets dedicated deposit addresses, ledger accounts, and position balances. For a Ground RWA Vault allocation, the wallet holds its own non-transferable Ground receipt while the authorized investment and borrower obligations are associated with the vault structure. See [Yield Sources](/docs/portfolio-wallets/yield-sources#how-ground-rwa-vaults-work).
* **HTTPS only** — all API and webhook communication is encrypted in transit.
## Start here
Create a wallet and initiate a withdrawal in 5 minutes.
Authentication, pagination, rate limits, and error handling.
Available yield sources and strategy construction guidance.
# Polling
Source: https://docs.groundtech.co/docs/portfolio-wallets/polling
Examples for scheduled monitoring with list and get endpoints.
Polling is a good fit when you want a periodic reconciliation job (for example, every 1-5 minutes) and can tolerate some delay compared to webhooks.
## Poll until wallet is ready
After creating a wallet, poll `GET /v2/wallets/{id}` every 1-2 seconds until `status !== "creating"`:
```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
while true; do
STATUS=$(curl -s "$BASE_URL/v2/wallets/$WALLET_ID" \
-H "Authorization: Bearer $GROUND_API_KEY" | jq -r .status)
if [ "$STATUS" = "idle" ] || [ "$STATUS" = "failed" ]; then break; fi
sleep 1.5
done
```
```javascript Node theme={"theme":{"light":"github-light","dark":"github-dark"}}
async function waitUntilReady(walletId, apiToken) {
while (true) {
const res = await fetch(`${BASE_URL}/v2/wallets/${walletId}`, {
headers: { Authorization: `Bearer ${apiToken}` },
});
const wallet = await res.json();
if (wallet.status === "idle") return wallet;
if (wallet.status === "failed") {
throw new Error(`wallet ${walletId} failed: ${wallet.failureReason}`);
}
await new Promise((r) => setTimeout(r, 1500));
}
}
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import time, requests
def wait_until_ready(wallet_id, api_token):
while True:
r = requests.get(
f"{BASE_URL}/v2/wallets/{wallet_id}",
headers={"Authorization": f"Bearer {api_token}"},
)
wallet = r.json()
if wallet["status"] == "idle":
return wallet
if wallet["status"] == "failed":
raise RuntimeError(f"wallet {wallet_id} failed: {wallet['failureReason']}")
time.sleep(1.5)
```
## List wallets
```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET "$BASE_URL/v2/wallets?limit=25" \
-H "Authorization: Bearer $GROUND_API_KEY"
```
```javascript Node theme={"theme":{"light":"github-light","dark":"github-dark"}}
const res = await fetch(`${BASE_URL}/v2/wallets?limit=25`, {
headers: { Authorization: `Bearer ${GROUND_API_KEY}` },
});
console.log(await res.json());
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import requests
res = requests.get(
f"{BASE_URL}/v2/wallets?limit=25",
headers={"Authorization": f"Bearer {GROUND_API_KEY}"},
)
print(res.json())
```
Next page:
```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET "$BASE_URL/v2/wallets?limit=25&cursor=$NEXT_CURSOR" \
-H "Authorization: Bearer $GROUND_API_KEY"
```
```javascript Node theme={"theme":{"light":"github-light","dark":"github-dark"}}
const res = await fetch(`${BASE_URL}/v2/wallets?limit=25&cursor=${NEXT_CURSOR}`, {
headers: { Authorization: `Bearer ${GROUND_API_KEY}` },
});
console.log(await res.json());
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import requests
res = requests.get(
f"{BASE_URL}/v2/wallets?limit=25&cursor={NEXT_CURSOR}",
headers={"Authorization": f"Bearer {GROUND_API_KEY}"},
)
print(res.json())
```
## Get a single wallet
```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET "$BASE_URL/v2/wallets/$WALLET_ID" \
-H "Authorization: Bearer $GROUND_API_KEY"
```
```javascript Node theme={"theme":{"light":"github-light","dark":"github-dark"}}
const res = await fetch(`${BASE_URL}/v2/wallets/${WALLET_ID}`, {
headers: { Authorization: `Bearer ${GROUND_API_KEY}` },
});
console.log(await res.json());
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import requests
res = requests.get(
f"{BASE_URL}/v2/wallets/{WALLET_ID}",
headers={"Authorization": f"Bearer {GROUND_API_KEY}"},
)
print(res.json())
```
## List deposits
```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET "$BASE_URL/v2/wallets/$WALLET_ID/deposits?limit=50" \
-H "Authorization: Bearer $GROUND_API_KEY"
```
```javascript Node theme={"theme":{"light":"github-light","dark":"github-dark"}}
const res = await fetch(`${BASE_URL}/v2/wallets/${WALLET_ID}/deposits?limit=50`, {
headers: { Authorization: `Bearer ${GROUND_API_KEY}` },
});
console.log(await res.json());
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import requests
res = requests.get(
f"{BASE_URL}/v2/wallets/{WALLET_ID}/deposits?limit=50",
headers={"Authorization": f"Bearer {GROUND_API_KEY}"},
)
print(res.json())
```
## Activity feed
The activity endpoint provides a unified, reverse-chronological feed of customer-visible deposits, withdrawals, and rebalances. Use `scope=all` for organization-wide activity, or pass one or more `walletId` parameters to filter to specific wallets.
```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET "$BASE_URL/v2/activity?walletId=$WALLET_ID&limit=20" \
-H "Authorization: Bearer $GROUND_API_KEY"
```
```javascript Node theme={"theme":{"light":"github-light","dark":"github-dark"}}
const res = await fetch(`${BASE_URL}/v2/activity?walletId=${WALLET_ID}&limit=20`, {
headers: { Authorization: `Bearer ${GROUND_API_KEY}` },
});
console.log(await res.json());
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import requests
res = requests.get(
f"{BASE_URL}/v2/activity?walletId={WALLET_ID}&limit=20",
headers={"Authorization": f"Bearer {GROUND_API_KEY}"},
)
print(res.json())
```
Query parameters:
| Param | Description |
| - | - |
| `limit` | Max items per page. Runtime clamps invalid, missing, and oversized values into the supported 1-100 range. |
| `cursor` | Opaque cursor from `nextCursor` for pagination |
| `walletId` | Optional repeatable wallet ID filter. |
| `scope` | Optional `all` for organization-wide activity. |
| `type` | Optional type filter: `deposit`, `withdrawal`, or `rebalance` |
| `status` | Optional comma-separated status filter: `created`, `processing`, `completed`, `partially_completed`, `failed`, `cancelled`, or `all` |
| `startAt` | Optional inclusive ISO-8601 UTC lower bound for activity timestamps. |
| `endAt` | Optional exclusive ISO-8601 UTC upper bound for activity timestamps. |
To poll only active wallet work, use:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET "$BASE_URL/v2/activity?walletId=$WALLET_ID&status=created,processing&limit=20" \
-H "Authorization: Bearer $GROUND_API_KEY"
```
Response:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"data": [
{
"id": "8f9720c1-2679-4d56-afc3-9b18edb83ce2",
"type": "deposit",
"status": "completed",
"amountUsd": "5000.000000",
"timestamp": "2026-02-05T09:40:00.000Z",
"deposit": {
"id": "8f9720c1-2679-4d56-afc3-9b18edb83ce2",
"amount": "5000.000000",
"token": "usdc",
"chain": "ethereum",
"fromAddress": "0xfeed00000000000000000000000000000000beef",
"txHash": "0xabc123...",
"status": "completed",
"createdAt": "2026-02-05T09:40:00.000Z",
"completedAt": "2026-02-05T09:41:15.000Z"
}
},
{
"id": "11b17950-1f5c-4d36-8f0d-0f3d1d0c6a45",
"type": "withdrawal",
"status": "processing",
"amountUsd": "65000.000000",
"timestamp": "2026-02-05T11:00:00.000Z",
"withdrawal": {
"id": "11b17950-1f5c-4d36-8f0d-0f3d1d0c6a45",
"amountRequestedUsd": "65000.000000",
"amountPaidUsd": null,
"feeUsd": "0.000000",
"destinationChain": "ethereum",
"destinationAddress": "0x76F8fc6667E239f83a547d4e16225d6a34f6FA22",
"destinationToken": "usdc",
"status": "processing",
"legsCompleted": 0,
"legsTotal": 1,
"payoutLegs": [
{
"status": "pending_customer_approval",
"amountUsd": "65000.000000",
"from": { "kind": "yield_source", "id": "syrup-usdc", "label": "Syrup USDC" },
"to": { "kind": "external_payout", "id": "external_payout:ethereum:usdc", "label": "External payout (Ethereum)" },
"startedAt": "2026-02-05T11:00:00.000Z",
"completedAt": null,
"stepsCompleted": 1,
"stepsTotal": 2,
"steps": [
{
"name": "Redeeming from Syrup USDC",
"stepKind": "yield_source_redeem",
"sequenceRole": "yield_redeem",
"protocolType": "syrup",
"stepOrdinal": 0,
"chain": "ethereum",
"txKind": "yield_source_redeem",
"state": "completed",
"startedAt": "2026-02-05T11:01:00.000Z",
"completedAt": "2026-02-05T11:02:00.000Z",
"cancelledAt": null
},
{
"name": "Sending payout",
"stepKind": "external_payout_transfer",
"sequenceRole": "external_payout_transfer",
"protocolType": "external_payout",
"stepOrdinal": 1,
"chain": "ethereum",
"txKind": "external_payout_transfer",
"state": "pending_customer_approval",
"startedAt": "2026-02-05T11:03:00.000Z",
"completedAt": null,
"cancelledAt": null
}
]
}
],
"failureReason": null,
"createdAt": "2026-02-05T11:00:00.000Z",
"completedAt": null
}
},
{
"id": "449f9ed4-b924-4a2d-9b18-e372cc5c4d65",
"type": "rebalance",
"status": "created",
"amountUsd": "12000.000000",
"timestamp": "2026-02-05T12:00:00.000Z",
"rebalance": {
"id": "449f9ed4-b924-4a2d-9b18-e372cc5c4d65",
"status": "created",
"createdAt": "2026-02-05T12:00:00.000Z",
"updatedAt": "2026-02-05T12:00:00.000Z",
"startedAt": "2026-02-05T12:00:00.000Z",
"completedAt": null,
"legsCompleted": 0,
"legsTotal": 1,
"amountUsd": "12000.000000",
"rebalanceLegs": [
{
"status": "created",
"amountUsd": "12000.000000",
"from": { "kind": "cash", "id": "cash:base:usdc", "label": "Cash (Base)" },
"to": { "kind": "yield_source", "id": "morpho-gauntlet-usdc", "label": "Morpho Gauntlet USDC Prime" },
"startedAt": "2026-02-05T12:00:00.000Z",
"completedAt": null,
"stepsCompleted": 0,
"stepsTotal": 1,
"steps": [
{
"name": "Depositing into Morpho Gauntlet USDC Prime",
"stepKind": "yield_source_deposit",
"sequenceRole": "yield_deposit",
"protocolType": "morpho",
"stepOrdinal": 0,
"chain": "ethereum",
"txKind": "yield_source_deposit",
"state": "created",
"startedAt": null,
"completedAt": null,
"cancelledAt": null
}
]
}
],
"failureReason": null
}
}
],
"nextCursor": null
}
```
Pass a non-null `nextCursor` as `cursor` to fetch the next page. See [API
Conventions](/docs/portfolio-wallets/api-conventions) for the list envelope.
| Field | Description |
| - | - |
| `data` | Activity items for the current page. |
| `data[].id` | Unique ID for the activity parent |
| `data[].type` | `"deposit"`, `"withdrawal"`, or `"rebalance"` |
| `data[].status` | Public status: `created`, `processing`, `completed`, `failed`, or `cancelled` |
| `data[].amountUsd` | Amount as a fixed-precision decimal string (6 decimal places) |
| `data[].timestamp` | ISO-8601 timestamp of when the activity occurred |
| `data[].deposit` | Present for deposit rows |
| `data[].withdrawal` | Present for withdrawal rows |
| `data[].rebalance` | Present for rebalance rows |
| `nextCursor` | Opaque cursor for the next page, or `null` |
## List withdrawals
Filter by status to monitor in-flight withdrawals:
```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET "$BASE_URL/v2/wallets/$WALLET_ID/withdrawals?status=processing&limit=50" \
-H "Authorization: Bearer $GROUND_API_KEY"
```
```javascript Node theme={"theme":{"light":"github-light","dark":"github-dark"}}
const res = await fetch(
`${BASE_URL}/v2/wallets/${WALLET_ID}/withdrawals?status=processing&limit=50`,
{ headers: { Authorization: `Bearer ${GROUND_API_KEY}` } },
);
console.log(await res.json());
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import requests
res = requests.get(
f"{BASE_URL}/v2/wallets/{WALLET_ID}/withdrawals",
params={"status": ["processing"], "limit": 50},
headers={"Authorization": f"Bearer {GROUND_API_KEY}"},
)
print(res.json())
```
## Deposit address readiness
All supported deposit addresses are prepared automatically when a wallet is created. Read `GET /v2/wallets/{id}` until `addressProvisioning.status` is `ready` and `depositAddresses.tron` (production) or `depositAddresses.tron_shasta` (sandbox) is present. The wallet’s overall `status` can remain `idle` while TRON is provisioning. Show one general address-setup indicator while this state is `provisioning` and remove it when `ready`. Use only addresses returned in `depositAddresses`.
# Quickstart
Source: https://docs.groundtech.co/docs/portfolio-wallets/quickstart
Create a wallet, deposit funds, and withdraw — all in 5 minutes.
This quickstart walks through the full Portfolio Wallet lifecycle: reviewing yield sources, creating a wallet, funding it, and initiating a withdrawal.
## Prerequisites
Create your API key at [portal.groundtech.co](https://portal.groundtech.co), then export it below.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export BASE_URL="https://sandbox.groundtech.co"
export GROUND_API_TOKEN="your_api_token"
```
See [API Conventions](/docs/portfolio-wallets/api-conventions) for authentication, pagination, rate limits, and error handling.
## 1. Review the yield source catalog
Fetch the current yield source catalog, then decide the allocation you want to use for wallet creation. This sandbox example uses the deployed USTB, stablecoin-explicit JAAA, and token-specific JTRSY rows; omit any row that is not present in your live response.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET "$BASE_URL/v2/wallets/yield-sources" \
-H "Authorization: Bearer $GROUND_API_TOKEN"
```
## 2. Create a Portfolio Wallet
Choose the allocation strategy you'd like for your wallet:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST "$BASE_URL/v2/wallets" \
-H "Authorization: Bearer $GROUND_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"requestId": "0b7e3b4d-88a5-4a75-8c0f-3b7b4f9d9f1d",
"label": "RWA Yield Portfolio",
"strategy": {
"allocations": {
"usdc": [
{ "yieldSourceId": "ground-ustb-vault", "pct": 34 },
{ "yieldSourceId": "ground-jaaa-usdc-vault", "pct": 33 },
{ "yieldSourceId": "ground-jtrsy-usdc-vault", "pct": 33 }
],
"usdt": [
{ "yieldSourceId": "ground-jaaa-usdt-vault", "pct": 50 },
{ "yieldSourceId": "ground-jtrsy-usdt-vault", "pct": 50 }
]
}
}
}'
```
The create response returns immediately with `status: "creating"` and an empty `depositAddresses` object. Poll `GET /v2/wallets/{id}` until `status` is `"idle"`; the ready wallet response includes `depositAddresses` keyed by supported chain.
## 3. Deposit a stablecoin
Send USDC on a supported chain or USDT on Ethereum or TRON. For TRON, first [wait for address readiness](/docs/portfolio-wallets/deposits#tron-deposit-addresses). In sandbox, use the supported Sepolia or Shasta test USDT for the selected network; tokens from other deployments are not credited.
You can get Sepolia USDC from the [Circle faucet](https://faucet.circle.com/) or bridge testnet ETH via the [Sepolia faucet](https://sepoliafaucet.com/).
## 4. Check balance and yield
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl "$BASE_URL/v2/wallets/$WALLET_ID" \
-H "Authorization: Bearer $GROUND_API_TOKEN"
```
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"id": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
"label": "RWA Yield Portfolio",
"status": "idle",
"balance": {
"totalUsd": "100250.000000",
"earnedUsd": "250.000000",
"reservedUsd": "0.000000",
"withdrawableUsd": "100250.000000"
},
"createdAt": "2026-02-05T08:15:00.000Z",
"positions": [
{ "id": "cash:ethereum_sepolia:usdc", "kind": "cash", "label": "Cash (Ethereum Sepolia)", "valueUsd": "0.000000" },
{ "id": "ground-ustb-vault", "pct": 34, "kind": "yield_source", "label": "Invesco USTB tokenized by Superstate", "valueUsd": "34085.000000", "yieldSourceId": "ground-ustb-vault", "mode": "active", "apyBps": 330 },
{ "id": "ground-jaaa-usdc-vault", "pct": 33, "kind": "yield_source", "label": "Janus Henderson JAAA (USDC)", "valueUsd": "33082.500000", "yieldSourceId": "ground-jaaa-usdc-vault", "mode": "active", "apyBps": 687 },
{ "id": "ground-jtrsy-usdc-vault", "pct": 33, "kind": "yield_source", "label": "Janus Henderson JTRSY tokenized by Centrifuge", "valueUsd": "33082.500000", "yieldSourceId": "ground-jtrsy-usdc-vault", "mode": "active", "apyBps": 421 }
],
"strategyAllocations": {
"usdc": [
{ "yieldSourceId": "ground-ustb-vault", "targetWeightBps": 3400 },
{ "yieldSourceId": "ground-jaaa-usdc-vault", "targetWeightBps": 3300 },
{ "yieldSourceId": "ground-jtrsy-usdc-vault", "targetWeightBps": 3300 }
],
"usdt": [
{ "yieldSourceId": "ground-jaaa-usdt-vault", "targetWeightBps": 5000 },
{ "yieldSourceId": "ground-jtrsy-usdt-vault", "targetWeightBps": 5000 }
]
},
"failureReason": null,
"depositAddresses": {
"solana_devnet": "DmaePN7kL6WjSecMaH4urdS8zN8FwNPTxbdTqFwAw6Fd",
"ethereum_sepolia": "0xFf71D07826d0aFcEcDF07Be9c499f9DDF3ee5cFa"
}
}
```
Sandbox keys `depositAddresses` by explicit testnet chain (`ethereum_sepolia`, `solana_devnet`, and `tron_shasta` when enabled). Production uses canonical chain names (`ethereum`, `base`, `arbitrum`, `polygon`, `solana`, and `tron` when enabled). Use the [mock USDT faucet](/api-reference/request-sandbox-usdt) if you need test USDT on Ethereum Sepolia. See [Supported Chains](/docs/portfolio-wallets/supported-chains).
## 5. Preview a withdrawal
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST "$BASE_URL/v2/wallets/$WALLET_ID/withdrawal-preview" \
-H "Authorization: Bearer $GROUND_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"destinationChain": "ethereum_sepolia",
"amountUsd": 65000,
"token": "usdc"
}'
```
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"amountRequestedUsd": "65000.000000",
"feeUsd": "0.000000",
"partnerFeeUsd": "0.000000",
"amountToReceiveUsd": "65000.000000",
"withdrawableUsd": "100250.000000",
"totalUsdAfterWithdrawal": "35250.000000",
"processingEstimate": {
"basis": "banking_days",
"typicalCompletionDate": "2026-07-20",
"typicalWindowEndDate": "2026-07-21"
}
}
```
The estimate is calculated when you request the preview. Date values above are illustrative; use the response you receive when presenting withdrawal timing.
## 6. Initiate a withdrawal
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST "$BASE_URL/v2/wallets/$WALLET_ID/withdrawals" \
-H "Authorization: Bearer $GROUND_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"requestId": "df8b7be6-e110-4f6d-9b2d-7c44a5b1f0b0",
"destinationChain": "ethereum_sepolia",
"amountUsd": 65000,
"token": "usdc",
"destinationAddress": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e"
}'
```
## 7. Track the withdrawal
Poll the withdrawal or subscribe to webhooks:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl "$BASE_URL/v2/wallets/$WALLET_ID/withdrawals/$WITHDRAWAL_ID" \
-H "Authorization: Bearer $GROUND_API_TOKEN"
```
The withdrawal moves through `processing` and then a terminal status such as `completed`, `partially_completed`, `failed`, or `cancelled`.
If approval is required, a payout leg and its external payout step enter `pending_customer_approval`; complete the approval in Ground Portal or through the approval integration enabled for your organization. See [Transaction Approvals](/docs/portfolio-wallets/transaction-approvals).
## Next steps
* [API Conventions](/docs/portfolio-wallets/api-conventions) — authentication, pagination, rate limits, error codes
* [Create a Wallet](/docs/portfolio-wallets/create-wallet) — wallet lifecycle and response shape
* [Deposits](/docs/portfolio-wallets/deposits) — funding and deposit tracking
* [Withdraw Funds](/docs/portfolio-wallets/withdraw-funds) — full withdrawal lifecycle and payout legs
* [Webhooks](/docs/portfolio-wallets/webhooks) — real-time status notifications
# Supported Chains
Source: https://docs.groundtech.co/docs/portfolio-wallets/supported-chains
Portfolio wallet deposit and withdrawal coverage.
Portfolio wallet chain support depends on both the stablecoin and the operation.
## Production
| Stablecoin | Deposits | Withdrawals | Yield strategy |
| - | - | - | - |
| USDC | Arbitrum, Base, Ethereum, Polygon, Solana | Arbitrum, Base, Ethereum, Polygon, Solana | Sources returned with `depositToken: "usdc"` |
| USDT (Ethereum) | Ethereum | Ethereum | Eligible sources in the USDT group |
| USDT (TRON) | TRON, after address setup | TRON | JustLend V1 USDT (`justlend-v1-usdt`), subject to catalog eligibility |
USDT is not bridged between Ethereum and TRON. Each network has a separate allocation group. Ground does not convert between USDT and USDC. Use `token: "usdt"` with `destinationChain: "ethereum"` or `"tron"` for the corresponding withdrawal.
If a chain is unsupported for the action you’re attempting, the API returns a 400 error with details.
## Sandbox
Sandbox uses explicit testnet chain keys in both responses and request parameters. For example, use `ethereum_sepolia` instead of `ethereum`.
| Stablecoin | Sandbox deposits and withdrawals |
| - | - |
| USDC | `ethereum_sepolia`, `solana_devnet` |
| USDT (Ethereum) | `ethereum_sepolia` |
| USDT (TRON) | `tron_shasta`, after address setup |
Sandbox assets are valueless test tokens. Shasta is the TRON Sandbox network; use the testnet address returned by your wallet.
NOTE: USDC deposits and withdrawals on some chains may require additional time to bridge via [CCTP](https://www.circle.com/cross-chain-transfer-protocol). Withdrawal previews currently estimate this CCTP leg at about 20 minutes (`PT20M`), while live settlement can vary by chain and attestation conditions.
## TRON integration
Every Portfolio Wallet automatically receives a TRON USDT address. Poll the wallet until `addressProvisioning.status` is `ready` and the corresponding address is present in `depositAddresses`. This general address state covers all networks. Addresses already returned remain usable while setup continues.
| Environment | Chain key | Deposit-address key | Allocation group |
| - | - | - | - |
| Production | `tron` | `depositAddresses.tron` | `usdt:tron` |
| Sandbox | `tron_shasta` | `depositAddresses.tron_shasta` | `usdt:tron` |
Ground pays the transaction resource costs for Ground-managed TRON operations. Customers do not need to acquire or stake TRX to use these API flows. Ground’s Shasta experience uses a simulated JustLend market; production uses the mainnet JustLend V1 USDT market. Nile is not the Portfolio Wallet Sandbox network.
# Transaction Approvals
Source: https://docs.groundtech.co/docs/portfolio-wallets/transaction-approvals
How customer approvals work, when they are required, and what to verify before signing.
Portfolio wallet withdrawals can require cryptographic approval before execution. When that happens, Ground creates a signing activity and exposes it through the Ground Portal and the Turnkey approval API.
The customer signer must generate the Turnkey approval stamp outside Ground's servers. Ground receives the stamp and validates it against the pending activity before forwarding the vote to Turnkey.
## When approvals happen
* **Withdrawals** (`POST /v2/wallets/{id}/withdrawals`): each payout leg transitions to `pending_customer_approval` when it's ready for signing.
At most one customer approval can be outstanding per `walletId + chain`. If you create multiple withdrawals rapidly, later payouts queue behind the earlier approval automatically.
## What does not require approval
Strategy updates (`PATCH /v2/wallets/{id}/strategy`) do not require this customer approval flow. They are API-authenticated requests; later rebalances may still be executed by Ground under the wallet's configured policy.
## Detecting a pending approval
When a payout leg needs approval, `payoutLegs[].status` and the external payout step `state` become `pending_customer_approval`.
Poll `GET /v2/wallets/:id/withdrawals/:withdrawalId` or subscribe to the `portfolio_wallet.withdrawal.status_changed` webhook to detect when a payout enters `pending_customer_approval`. Then call `GET /v2/turnkey/activities/pending` to fetch the pending approval details and activity fingerprint for the authenticated organization.
## Approving an activity
1. Capture the pending payout leg/step from the withdrawal response or webhook.
2. Fetch the matching activity from `GET /v2/turnkey/activities/pending`.
3. **Verify the transaction details** (see [best practice below](#verify-before-you-approve)).
4. Request the Turnkey approval or rejection payload from `POST /v2/turnkey/activity-approval-request`.
5. Stamp `stampPayload` with the customer signer in customer-controlled infrastructure.
6. Submit the stamp with `POST /v2/turnkey/activities/{activityId}/vote`.
7. Processing continues automatically once approved.
Do not send customer private keys to Ground or any service outside the customer's control. Approval integrations should use the customer signer to produce a custody-provider approval stamp; raw signing credentials should never leave customer-controlled infrastructure.
Example API sequence:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET "$BASE_URL/v2/turnkey/activities/pending" \
-H "Authorization: Bearer $GROUND_API_KEY"
curl -X POST "$BASE_URL/v2/turnkey/activity-approval-request" \
-H "Authorization: Bearer $GROUND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"activityId": "018f4b7a-1111-7000-8000-000000000001",
"action": "approve"
}'
```
The approval-request response includes a `turnkeyRequest` object and a `stampPayload` string. Stamp `stampPayload` locally with the customer signer, then submit the resulting Turnkey stamp:
## Stamping the approval payload
Ground does not stamp the Turnkey approval for you. Your integration must create `customerApprovalStamp` by passing the exact `stampPayload` string to a Turnkey-compatible stamper owned by the customer.
For a customer-held Turnkey API key, stamp the payload from your own backend or signing service:
```javascript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { ApiKeyStamper } from "@turnkey/api-key-stamper";
const approvalRequest = await fetch(`${BASE_URL}/v2/turnkey/activity-approval-request`, {
method: "POST",
headers: {
Authorization: `Bearer ${GROUND_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
activityId: "018f4b7a-1111-7000-8000-000000000001",
action: "approve"
})
}).then((res) => res.json());
const stamper = new ApiKeyStamper({
apiPublicKey: process.env.TURNKEY_API_PUBLIC_KEY,
apiPrivateKey: process.env.TURNKEY_API_PRIVATE_KEY
});
const customerApprovalStamp = await stamper.stamp(approvalRequest.stampPayload);
await fetch(`${BASE_URL}/v2/turnkey/activities/${approvalRequest.activityId}/vote`, {
method: "POST",
headers: {
Authorization: `Bearer ${GROUND_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
action: "approve",
turnkeyRequest: approvalRequest.turnkeyRequest,
customerApprovalStamp
})
});
```
The stamp returned by Turnkey has this shape:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"stampHeaderName": "X-Stamp",
"stampHeaderValue": "..."
}
```
For a passkey signer, run the stamping step in the browser with Turnkey's WebAuthn stamper and submit the returned stamp to your backend before calling the vote endpoint:
```javascript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { WebauthnStamper } from "@turnkey/webauthn-stamper";
const stamper = new WebauthnStamper({
rpId: window.location.hostname,
userVerification: "preferred"
});
const customerApprovalStamp = await stamper.stamp(approvalRequest.stampPayload);
```
Never send `TURNKEY_API_PRIVATE_KEY`, raw passkey material, or any other customer signing secret to Ground. Ground only needs the `customerApprovalStamp` object, the original `turnkeyRequest`, and the intended `action`.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST "$BASE_URL/v2/turnkey/activities/018f4b7a-1111-7000-8000-000000000001/vote" \
-H "Authorization: Bearer $GROUND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"action": "approve",
"turnkeyRequest": {
"type": "ACTIVITY_TYPE_APPROVE_ACTIVITY",
"timestampMs": "1782302400000",
"organizationId": "turnkey-suborg-id",
"parameters": {
"fingerprint": "activity-fingerprint"
}
},
"customerApprovalStamp": {
"stampHeaderName": "X-Stamp",
"stampHeaderValue": "..."
}
}'
```
## Verify before you approve
Never blindly approve a Turnkey activity. Always verify the transaction details match what you expect before signing.
When your system receives a `pending_customer_approval` signal, it should verify the transaction before approving. This is critical because an approval is an irreversible cryptographic signature -- once signed and broadcast, the transaction cannot be reversed.
**What to verify:**
1. **Destination address** -- confirm the pending activity's `destinationAddress` matches the address your system originally submitted in the withdrawal request. Reject if it doesn't match.
2. **Amount** -- confirm the displayed amount is reasonable for the withdrawal and yield source. Do not assume it must exactly match the originally requested amount for asynchronous sources.
3. **Chain and token** -- confirm `destinationChain` and `destinationToken` match your expectations.
4. **Withdrawal correlation** -- match the pending approval back to a withdrawal your system actually initiated. Reject orphaned or unexpected approval requests.
**Example verification flow:**
```javascript theme={"theme":{"light":"github-light","dark":"github-dark"}}
async function verifyAndApprove(approvalActivity, originalWithdrawal, signedApproval) {
// 1. Correlate: did we actually request this withdrawal?
if (!originalWithdrawal) {
throw new Error("No matching withdrawal found -- rejecting approval");
}
// 2. Verify destination address
if (approvalActivity.destinationAddress.toLowerCase() !== originalWithdrawal.destinationAddress.toLowerCase()) {
throw new Error("Destination address mismatch -- rejecting approval");
}
// 3. Verify chain
if (approvalActivity.destinationChain !== originalWithdrawal.destinationChain) {
throw new Error("Chain mismatch -- rejecting approval");
}
// 4. Request POST /v2/turnkey/activity-approval-request.
// 5. Locally stamp response.stampPayload, then submit it to:
// POST /v2/turnkey/activities/{activityId}/vote
return true;
}
```
This pattern protects against compromised intermediaries, replay attacks, and bugs that could cause funds to be sent to unintended destinations. Treat every approval request as untrusted input until your system has independently verified it.
## Automation patterns
Two patterns work well for handling approvals:
1. **Webhook-driven** -- subscribe to `portfolio_wallet.withdrawal.status_changed`. When a payout leg or step enters `pending_customer_approval`, fetch `GET /v2/turnkey/activities/pending`, verify the pending approval details, request `POST /v2/turnkey/activity-approval-request`, stamp the returned `stampPayload` locally, and submit the vote.
2. **Polling-driven** -- poll `GET /v2/wallets/:id/withdrawals/:withdrawalId` and check `payoutLegs[].status` / `payoutLegs[].steps[].state`.
Recommended: use webhooks as the primary trigger, with polling as a backstop (webhooks are best-effort delivery).
See [Webhooks](/docs/portfolio-wallets/webhooks) for how to register and verify webhook deliveries.
## Status updates
Subscribe to these events to monitor progress:
* `portfolio_wallet.withdrawal.status_changed`
* `portfolio_wallet.withdrawal.payout.status_changed`
If approval-related execution fails, the withdrawal eventually moves to `failed` with a `failureReason`.
## TRON payouts
TRON withdrawals use this same customer approval workflow. Verify the requested network (`tron` or `tron_shasta`), the destination’s TRON base58 address, token, amount, and matching payout activity before approving. Approval authorizes signing; continue tracking the withdrawal until it reaches `completed`. Ground pays the transaction resource costs for Ground-managed operations.
# Update Strategy
Source: https://docs.groundtech.co/docs/portfolio-wallets/update-strategy
Change a wallet's yield allocation after creation.
After creating a wallet, you can update its allocation strategy when no incompatible withdrawal or rebalance workflow is active.
This applies to wallets with `autoRebalance: true`. See [How Auto Rebalancing Works](/docs/portfolio-wallets/auto-rebalancing) for wallet types, drift thresholds, and examples of when funds move.
## Update the strategy
```bash Both USDC and USDT theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X PATCH "$BASE_URL/v2/wallets/$WALLET_ID/strategy" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"requestId": "6d74dc1c-f1b5-4f2c-8f33-9f7b9e27cd4b",
"allocations": {
"usdc": [
{ "yieldSourceId": "cash", "pct": 20 },
{ "yieldSourceId": "syrup-usdc", "pct": 80 }
],
"usdt": [
{ "yieldSourceId": "cash", "pct": 50 },
{ "yieldSourceId": "syrup-usdt", "pct": 50 }
]
}
}'
```
```bash USDC only theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X PATCH "$BASE_URL/v2/wallets/$WALLET_ID/strategy" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"requestId": "de75d801-4c13-4a08-8995-79609bd31ca1",
"allocations": {
"usdc": [
{ "yieldSourceId": "cash", "pct": 100 }
]
}
}'
```
```bash USDT only theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X PATCH "$BASE_URL/v2/wallets/$WALLET_ID/strategy" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"requestId": "a6b5f57c-1460-4af0-aec1-e75015f38d77",
"allocations": {
"usdt": [
{ "yieldSourceId": "cash", "pct": 100 }
]
}
}'
```
| Field | Required | Description |
| - | - | - |
| `requestId` | Yes | UUID v4 idempotency key |
| `allocations` | Yes | An object keyed by the token groups to update. Each included group must sum to 100%. |
Each allocation must include `yieldSourceId` (from the yield source catalog) and `pct` (0-100, supporting 0.1% increments).
Each included token group is replaced in full. If a request includes only `usdc` or only `usdt`, the omitted group is not changed.
### Response
The strategy update returns only the canonical desired allocations. The response includes every token group, including groups preserved because they were omitted from the request:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"strategyAllocations": {
"usdc": [
{ "yieldSourceId": "cash", "targetWeightBps": 2000 },
{ "yieldSourceId": "syrup-usdc", "targetWeightBps": 8000 }
],
"usdt": [
{ "yieldSourceId": "cash", "targetWeightBps": 5000 },
{ "yieldSourceId": "syrup-usdt", "targetWeightBps": 5000 }
]
}
}
```
`strategyAllocations` reflects the desired token-keyed targets immediately. Fetch the wallet separately to read its current holdings and balances, which can remain different until a rebalance executes.
## What happens during a strategy update
Strategy updates are API-authenticated target-allocation changes. They do not create a Turnkey approval activity and do not immediately commit or enqueue a rebalance. When you update the strategy:
1. The included token groups are saved immediately and reflected in `strategyAllocations`.
2. A later rebalance may move holdings toward the new targets when the wallet has executable work and no incompatible workflow is active.
3. If a later rebalance is created, execution happens over time. For positions with longer unwind windows such as `syrup-usdc`, the full rebalance can take hours even if synchronous ERC-4626 legs such as the supported Morpho vaults move immediately.
If a rebalance runs, each position's `valueUsd` may move toward its `pct`. You can monitor progress by comparing these two fields.
## Strategy webhook
Strategy updates emit `portfolio_wallet.strategy.status_changed` events as the update is applied.
See [Webhooks](/docs/portfolio-wallets/webhooks) for registration and payload details.
## TRON USDT targets
Use `usdt:tron` for JustLend V1 and TRON cash. An update to this group does not change the wallet’s USDC or Ethereum USDT targets. Each included group still totals 100%:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X PATCH "$BASE_URL/v2/wallets/$WALLET_ID/strategy" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"requestId": "b6a80f48-e2d3-4db1-8ee2-586d924a4039",
"allocations": {
"usdt:tron": [
{ "yieldSourceId": "justlend-v1-usdt", "pct": 80 },
{ "yieldSourceId": "cash", "pct": 20 }
]
}
}'
```
Wait for the [TRON deposit address](/docs/portfolio-wallets/deposits#tron-deposit-addresses) to be ready before funding the group. Manual-allocation wallets use [Allocate](/api-reference/allocate) instead of strategy targets.
# Update a Wallet
Source: https://docs.groundtech.co/docs/portfolio-wallets/update-wallet
Rename a portfolio wallet.
Use [Update a wallet](/api-reference/update-wallet) (`PATCH /v2/wallets/{id}`) to change a wallet's label. Send `null` or an empty string to clear it.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X PATCH "$GROUND_API_BASE_URL/v2/wallets/$WALLET_ID" \
-H "Authorization: Bearer $GROUND_API_KEY" \
-H "Content-Type: application/json" \
-d '{"label":"Operating Treasury"}'
```
The response is HTTP 200 with the full updated wallet snapshot. Deposit addresses, balances, and allocations remain unchanged.
# Webhook payload examples
Source: https://docs.groundtech.co/docs/portfolio-wallets/webhook-payload-examples
Example JSON bodies for Portfolio Wallet webhook events.
Below are example JSON bodies for each event type. Headers like `Ground-Event-Id`, `Ground-Event-Type`, and `Ground-Signature` are sent separately.
## `portfolio_wallet.status_changed`
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"event": "portfolio_wallet.status_changed",
"organizationId": "43c78f4f-44a7-45ff-a524-8f010f7a4c72",
"walletId": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
"previousStatus": "idle",
"status": "rebalance_active",
"occurredAt": "2026-02-05T10:28:00.000Z"
}
```
## `portfolio_wallet.deposit.status_changed`
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"event": "portfolio_wallet.deposit.status_changed",
"walletId": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
"observedAt": "2026-02-05T09:45:00.000Z",
"deposit": {
"id": "2a3ad0af-11b3-41d5-96c5-2b9d8799f1e2",
"walletId": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
"amount": "50000.000000",
"token": "usdc",
"chain": "ethereum",
"fromAddress": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
"txHash": "0xabc123...",
"status": "completed",
"cashPositionId": "cash:ethereum:usdc",
"createdAt": "2026-02-05T09:40:00.000Z",
"completedAt": "2026-02-05T09:45:00.000Z"
}
}
```
## `portfolio_wallet.withdrawal.status_changed`
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"event": "portfolio_wallet.withdrawal.status_changed",
"walletId": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
"observedAt": "2026-02-05T11:05:00.000Z",
"withdrawal": {
"id": "11b17950-1f5c-4d36-8f0d-0f3d1d0c6a45",
"amountRequestedUsd": "65000.000000",
"amountPaidUsd": null,
"feeUsd": "0.000000",
"destinationChain": "ethereum",
"destinationAddress": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
"destinationToken": "usdc",
"status": "processing",
"legsCompleted": 0,
"legsTotal": 1,
"payoutLegs": [
{
"status": "pending_customer_approval",
"amountUsd": "65000.000000",
"from": { "kind": "yield_source", "id": "morpho-gauntlet-usdc", "label": "Morpho Gauntlet USDC Prime" },
"to": { "kind": "external_payout", "id": "external_payout:ethereum:usdc", "label": "External payout (Ethereum)" },
"startedAt": "2026-02-05T11:01:00.000Z",
"completedAt": null,
"stepsCompleted": 1,
"stepsTotal": 2,
"steps": [
{
"name": "Redeeming from Morpho Gauntlet USDC Prime",
"chain": "ethereum",
"state": "completed",
"txKind": "yield_source_redeem",
"stepKind": "yield_source_redeem",
"startedAt": "2026-02-05T11:01:00.000Z",
"completedAt": "2026-02-05T11:02:00.000Z",
"cancelledAt": null,
"stepOrdinal": 0,
"protocolType": "morpho",
"sequenceRole": "yield_redeem"
},
{
"name": "Sending payout",
"chain": "ethereum",
"state": "pending_customer_approval",
"txKind": "external_payout_transfer",
"stepKind": "external_payout_transfer",
"startedAt": "2026-02-05T11:03:00.000Z",
"completedAt": null,
"cancelledAt": null,
"stepOrdinal": 1,
"protocolType": "external_payout",
"sequenceRole": "external_payout_transfer"
}
]
}
],
"failureReason": null,
"createdAt": "2026-02-05T11:00:00.000Z",
"completedAt": null
}
}
```
## `portfolio_wallet.withdrawal.payout.status_changed`
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"event": "portfolio_wallet.withdrawal.payout.status_changed",
"walletId": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
"observedAt": "2026-02-05T12:40:00.000Z",
"payout": {
"id": "4f70f27a-760c-45f1-8ac9-01878d8d2d6c",
"status": "completed",
"turnkeyActivityId": null,
"approvalRequestedAt": null,
"approvalValidBefore": null,
"amountUsd": "35000.000000",
"feeUsd": "0.000000",
"txHash": "0xabc123...",
"failureReason": null,
"steps": [
{
"name": "Redeeming from Syrup USDC",
"stepKind": "syrup_request_redeem",
"sequenceRole": "yield_redeem",
"protocolType": "syrup",
"stepOrdinal": 0,
"chain": "ethereum",
"txKind": "yield_source_redeem",
"state": "completed",
"startedAt": "2026-02-05T12:35:00.000Z",
"completedAt": "2026-02-05T12:36:00.000Z",
"cancelledAt": null
},
{
"name": "Sending payout",
"stepKind": "external_payout_transfer",
"sequenceRole": "external_payout_transfer",
"protocolType": "external_payout",
"stepOrdinal": 1,
"chain": "ethereum",
"txKind": "external_payout_transfer",
"state": "completed",
"startedAt": "2026-02-05T12:38:00.000Z",
"completedAt": "2026-02-05T12:40:00.000Z",
"cancelledAt": null
}
],
"createdAt": "2026-02-05T12:34:00.000Z",
"completedAt": "2026-02-05T12:40:00.000Z"
}
}
```
## `portfolio_wallet.strategy.status_changed`
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"event": "portfolio_wallet.strategy.status_changed",
"walletId": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
"observedAt": "2026-02-05T09:30:00.000Z",
"strategy": {
"walletId": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
"requestId": "6d74dc1c-f1b5-4f2c-8f33-9f7b9e27cd4b",
"status": "completed",
"allocations": [
{ "yieldSourceId": "syrup-usdc", "pct": 40 },
{ "yieldSourceId": "morpho-gauntlet-usdc", "pct": 30 },
{ "yieldSourceId": "morpho-steakhouse-usdc", "pct": 30 }
],
"createdAt": "2026-02-05T09:29:00.000Z"
}
}
```
## `portfolio_wallet.rebalance.status_changed`
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"event": "portfolio_wallet.rebalance.status_changed",
"walletId": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
"observedAt": "2026-02-05T10:30:00.000Z",
"rebalance": {
"id": "b6c7d8e9-1234-4abc-8def-0123456789ab",
"status": "processing",
"reason": "deposit_deployment",
"amountUsd": "50000.000000",
"legsCompleted": 0,
"legsTotal": 2,
"rebalanceLegs": [
{
"status": "processing",
"amountUsd": "25000.000000",
"from": { "kind": "cash", "id": "cash:ethereum:usdc", "label": "Cash (Ethereum)" },
"to": { "kind": "yield_source", "id": "morpho-gauntlet-usdc", "label": "Morpho Gauntlet USDC Prime" },
"startedAt": "2026-02-05T10:29:00.000Z",
"completedAt": null,
"stepsCompleted": 0,
"stepsTotal": 1,
"steps": [
{
"name": "Depositing into Morpho Gauntlet USDC Prime",
"chain": "ethereum",
"state": "processing",
"txKind": "yield_source_deposit",
"stepKind": "yield_source_deposit",
"startedAt": "2026-02-05T10:29:00.000Z",
"completedAt": null,
"cancelledAt": null,
"stepOrdinal": 0,
"protocolType": "morpho",
"sequenceRole": "yield_deposit"
}
]
}
],
"failureReason": null,
"createdAt": "2026-02-05T10:28:00.000Z",
"startedAt": "2026-02-05T10:29:00.000Z",
"completedAt": null
}
}
```
Rebalance and withdrawal step objects expose workflow execution metadata. Broadcast transaction hashes are exposed on payout rows when available, not on every workflow step.
## `portfolio_wallet.risk.alert.triggered`
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"event": "portfolio_wallet.risk.alert.triggered",
"organizationId": "43c78f4f-44a7-45ff-a524-8f010f7a4c72",
"portfolioWalletId": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
"riskAlertId": "8f0d1a2b-3c4d-4e5f-8a9b-0c1d2e3f4a5b",
"riskRuleId": "1f0d1a2b-3c4d-4e5f-8a9b-0c1d2e3f4a5b",
"yieldSourceId": "morpho-gauntlet-usdc",
"metric": "apy_bps",
"ruleKind": "threshold",
"comparator": "lt",
"thresholdValue": 300,
"observedValue": 275,
"baselineValue": null,
"observedChangeBps": null,
"windowSeconds": null,
"status": "triggered",
"metricsObservationId": "5d0d1a2b-3c4d-4e5f-8a9b-0c1d2e3f4a5b",
"triggeredAt": "2026-02-05T10:30:00.000Z",
"explanation": {
"message": "APY fell below threshold"
}
}
```
## `portfolio_wallet.risk.alert.resolved`
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"event": "portfolio_wallet.risk.alert.resolved",
"organizationId": "43c78f4f-44a7-45ff-a524-8f010f7a4c72",
"portfolioWalletId": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
"riskAlertId": "8f0d1a2b-3c4d-4e5f-8a9b-0c1d2e3f4a5b",
"riskRuleId": "1f0d1a2b-3c4d-4e5f-8a9b-0c1d2e3f4a5b",
"yieldSourceId": "morpho-gauntlet-usdc",
"metric": "apy_bps",
"ruleKind": "threshold",
"comparator": "lt",
"thresholdValue": 300,
"observedValue": 325,
"baselineValue": null,
"observedChangeBps": null,
"windowSeconds": null,
"status": "resolved",
"metricsObservationId": "5d0d1a2b-3c4d-4e5f-8a9b-0c1d2e3f4a5b",
"triggeredAt": "2026-02-05T10:30:00.000Z",
"resolvedAt": "2026-02-05T11:30:00.000Z",
"resolutionSource": "metric_recovered",
"resolutionNote": "APY recovered above threshold",
"explanation": {}
}
```
## `portfolio_wallet.risk.action.status_changed`
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"event": "portfolio_wallet.risk.action.status_changed",
"organizationId": "43c78f4f-44a7-45ff-a524-8f010f7a4c72",
"portfolioWalletId": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
"riskActionId": "6f0d1a2b-3c4d-4e5f-8a9b-0c1d2e3f4a5b",
"riskAlertId": "8f0d1a2b-3c4d-4e5f-8a9b-0c1d2e3f4a5b",
"riskRuleId": "1f0d1a2b-3c4d-4e5f-8a9b-0c1d2e3f4a5b",
"yieldSourceId": "morpho-gauntlet-usdc",
"outcome": "exit_position",
"approvalMode": "automatic",
"previousStatus": "pending_auto_execution",
"status": "executing",
"attemptCount": 1,
"executionVersion": 1,
"rebalanceId": "7f0d1a2b-3c4d-4e5f-8a9b-0c1d2e3f4a5b",
"changedAt": "2026-02-05T10:31:00.000Z",
"executionError": null,
"explanation": {}
}
```
## `portfolio_wallet.risk.unwind.completed`
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"event": "portfolio_wallet.risk.unwind.completed",
"organizationId": "43c78f4f-44a7-45ff-a524-8f010f7a4c72",
"portfolioWalletId": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
"riskActionId": "6f0d1a2b-3c4d-4e5f-8a9b-0c1d2e3f4a5b",
"riskAlertId": "8f0d1a2b-3c4d-4e5f-8a9b-0c1d2e3f4a5b",
"riskRuleId": "1f0d1a2b-3c4d-4e5f-8a9b-0c1d2e3f4a5b",
"rebalanceId": "7f0d1a2b-3c4d-4e5f-8a9b-0c1d2e3f4a5b",
"yieldSourceId": "morpho-gauntlet-usdc",
"yieldSourceName": "Morpho Gauntlet USDC Prime",
"completedRebalanceLegId": "2b0d1a2b-3c4d-4e5f-8a9b-0c1d2e3f4a5b",
"completedSourceAccountId": "morpho-gauntlet-usdc",
"cashDestinationAccountId": "cash:ethereum:usdc",
"cashDestinationChain": "ethereum",
"cashDestinationToken": "usdc",
"completedAmountNativeUnits": "25000000000",
"completedAt": "2026-02-05T10:45:00.000Z",
"eventId": "3c0d1a2b-3c4d-4e5f-8a9b-0c1d2e3f4a5b",
"occurredAt": "2026-02-05T10:45:00.000Z",
"explanation": {
"message": "Exited Morpho Gauntlet USDC Prime to cash after APY alert"
}
}
```
# Webhook signature verification
Source: https://docs.groundtech.co/docs/portfolio-wallets/webhook-signature-verification
Verify Ground webhook request authenticity using Stripe-style HMAC.
## Delivery and headers
Ground sends `POST` requests to your registered `url` with:
* `Content-Type: application/json`
* `Ground-Event-Id`
* `Ground-Event-Type`
* `Ground-Signature`
Body is the JSON event payload (for example, a deposit object or withdrawal object).
## Signature format
Stripe-style HMAC:
* Header: `Ground-Signature: t=,v1=`
* HMAC: `v1 = HMAC_SHA256(key = secret, message = t + "." + rawBody)`
* `rawBody` is the exact request body string (verify before `JSON.parse`)
## Node / Express verification example
```javascript Node theme={"theme":{"light":"github-light","dark":"github-dark"}}
// Express example
const crypto = require("crypto");
const express = require("express");
function verifyGroundSignature(rawBody, signatureHeader, signingSecret, toleranceSeconds = 300) {
if (!signatureHeader) throw new Error("Missing Ground-Signature header");
const parts = signatureHeader.split(",");
const tPart = parts.find((p) => p.startsWith("t="));
const v1Part = parts.find((p) => p.startsWith("v1="));
if (!tPart || !v1Part) throw new Error("Invalid Ground-Signature header format");
const timestamp = Number(tPart.split("=")[1]);
const signature = v1Part.split("=")[1];
// Optional replay protection
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > toleranceSeconds) {
throw new Error("Ground-Signature timestamp outside allowed window");
}
const payloadToSign = `${timestamp}.${rawBody}`;
const computed = crypto.createHmac("sha256", signingSecret).update(payloadToSign).digest("hex");
const expected = Buffer.from(computed, "utf8");
const actual = Buffer.from(signature, "utf8");
if (expected.length !== actual.length || !crypto.timingSafeEqual(expected, actual)) {
throw new Error("Invalid Ground-Signature");
}
}
const app = express();
app.post("/ground/webhook", express.raw({ type: "application/json" }), (req, res) => {
const rawBody = req.body.toString("utf8");
const signatureHeader = req.get("Ground-Signature");
const signingSecret = process.env.GROUND_WEBHOOK_SECRET;
try {
verifyGroundSignature(rawBody, signatureHeader, signingSecret);
} catch (err) {
return res.status(400).json({ error: "Invalid signature" });
}
const event = JSON.parse(rawBody);
// Use event.event to route handling:
// - "portfolio_wallet.withdrawal.status_changed"
// - "portfolio_wallet.deposit.status_changed"
return res.status(200).json({ received: true });
});
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Flask example
import hmac
import hashlib
import time
from flask import Flask, request, jsonify
def verify_ground_signature(raw_body: bytes, signature_header: str, signing_secret: str, tolerance_seconds: int = 300):
if not signature_header:
raise ValueError("Missing Ground-Signature header")
parts = [p.strip() for p in signature_header.split(",")]
t_part = next((p for p in parts if p.startswith("t=")), None)
v1_part = next((p for p in parts if p.startswith("v1=")), None)
if not t_part or not v1_part:
raise ValueError("Invalid Ground-Signature header format")
timestamp = int(t_part.split("=", 1)[1])
signature = v1_part.split("=", 1)[1]
now = int(time.time())
if abs(now - timestamp) > tolerance_seconds:
raise ValueError("Ground-Signature timestamp outside allowed window")
payload_to_sign = f"{timestamp}.".encode("utf-8") + raw_body
computed = hmac.new(signing_secret.encode("utf-8"), payload_to_sign, hashlib.sha256).hexdigest()
if not hmac.compare_digest(computed, signature):
raise ValueError("Invalid Ground-Signature")
app = Flask(__name__)
@app.post("/ground/webhook")
def ground_webhook():
raw_body = request.get_data(cache=False) # raw bytes
signature_header = request.headers.get("Ground-Signature")
signing_secret = "YOUR_GROUND_WEBHOOK_SECRET"
try:
verify_ground_signature(raw_body, signature_header, signing_secret)
except Exception:
return jsonify({"error": "Invalid signature"}), 400
event = request.get_json()
# Use event["event"] to route handling:
# - "portfolio_wallet.withdrawal.status_changed"
# - "portfolio_wallet.deposit.status_changed"
return jsonify({"received": True}), 200
```
```go Go theme={"theme":{"light":"github-light","dark":"github-dark"}}
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
func verifyGroundSignature(rawBody []byte, signatureHeader string, signingSecret string, toleranceSeconds int64) error {
if signatureHeader == "" {
return errors.New("missing Ground-Signature header")
}
var tStr, v1 string
for _, part := range strings.Split(signatureHeader, ",") {
part = strings.TrimSpace(part)
if strings.HasPrefix(part, "t=") {
tStr = strings.TrimPrefix(part, "t=")
} else if strings.HasPrefix(part, "v1=") {
v1 = strings.TrimPrefix(part, "v1=")
}
}
if tStr == "" || v1 == "" {
return errors.New("invalid Ground-Signature header format")
}
timestamp, err := strconv.ParseInt(tStr, 10, 64)
if err != nil {
return errors.New("invalid Ground-Signature timestamp")
}
now := time.Now().Unix()
if now-timestamp > toleranceSeconds || timestamp-now > toleranceSeconds {
return errors.New("Ground-Signature timestamp outside allowed window")
}
sigBytes, err := hex.DecodeString(v1)
if err != nil {
return errors.New("invalid Ground-Signature v1 hex")
}
mac := hmac.New(sha256.New, []byte(signingSecret))
mac.Write([]byte(fmt.Sprintf("%d.", timestamp)))
mac.Write(rawBody)
expected := mac.Sum(nil)
if !hmac.Equal(sigBytes, expected) {
return errors.New("invalid signature")
}
return nil
}
func groundWebhook(w http.ResponseWriter, r *http.Request) {
rawBody, _ := io.ReadAll(r.Body)
signatureHeader := r.Header.Get("Ground-Signature")
signingSecret := os.Getenv("GROUND_WEBHOOK_SECRET")
if err := verifyGroundSignature(rawBody, signatureHeader, signingSecret, 300); err != nil {
w.WriteHeader(http.StatusBadRequest)
_ = json.NewEncoder(w).Encode(map[string]string{"error": "Invalid signature"})
return
}
var event map[string]any
_ = json.Unmarshal(rawBody, &event)
_ = json.NewEncoder(w).Encode(map[string]bool{"received": true})
}
func main() {
// net/http example
http.HandleFunc("/ground/webhook", groundWebhook)
_ = http.ListenAndServe(":3000", nil)
}
```
# Webhooks
Source: https://docs.groundtech.co/docs/portfolio-wallets/webhooks
Register webhooks and receive real-time Portfolio Wallet events.
Subscribe to webhooks to receive real-time notifications about wallet workflow, deposit, rebalance, withdrawal, payout, and strategy lifecycle changes.
## Register a webhook
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST "$BASE_URL/v2/webhooks" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/ground/webhook",
"events": [
"portfolio_wallet.status_changed",
"portfolio_wallet.deposit.status_changed",
"portfolio_wallet.withdrawal.status_changed",
"portfolio_wallet.withdrawal.payout.status_changed",
"portfolio_wallet.strategy.status_changed",
"portfolio_wallet.rebalance.status_changed",
"portfolio_wallet.risk.alert.triggered",
"portfolio_wallet.risk.alert.resolved",
"portfolio_wallet.risk.action.status_changed",
"portfolio_wallet.risk.unwind.completed"
],
"description": "Production webhook endpoint"
}'
```
The create response includes `id`, `url`, `events`, `createdAt`, and `secret`. Store `secret` on the server only (env var / secret manager). The `secret` field is **only returned on create** -- it is not included when listing endpoints.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"id": "a1b2c3d4-0000-4000-8000-000000000001",
"url": "https://example.com/ground/webhook",
"events": [
"portfolio_wallet.deposit.status_changed",
"portfolio_wallet.rebalance.status_changed"
],
"createdAt": "2026-02-05T08:00:00.000Z",
"secret": "abcdef1234567890..."
}
```
Only `portfolio_wallet.*` event types are accepted. Requests that include event types outside the `portfolio_wallet.*` namespace will be rejected with a validation error.
## List webhooks
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET "$BASE_URL/v2/webhooks?limit=25" \
-H "Authorization: Bearer $API_TOKEN"
```
The list is returned under `data`. Pass a non-null `nextCursor` as `cursor` to fetch the next page. Note that `secret` is **not** included in list responses. See [API Conventions](/docs/portfolio-wallets/api-conventions) for the list envelope.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"data": [
{
"id": "a1b2c3d4-0000-4000-8000-000000000001",
"url": "https://example.com/ground/webhook",
"events": [
"portfolio_wallet.deposit.status_changed",
"portfolio_wallet.rebalance.status_changed"
],
"createdAt": "2026-02-05T08:00:00.000Z"
}
],
"nextCursor": null
}
```
## Delete a webhook
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X DELETE "$BASE_URL/v2/webhooks/$ENDPOINT_ID" \
-H "Authorization: Bearer $API_TOKEN"
```
Returns `{ "id": "...", "deleted": true }` on success.
## Portfolio Wallet events
| Event | Fires when |
| - | - |
| `portfolio_wallet.status_changed` | The wallet's lifecycle status changes (e.g. `creating` -> `idle`, `rebalance_active`, `withdrawal_active`, or `failed`) |
| `portfolio_wallet.deposit.status_changed` | A deposit posts to the wallet after observed finality and ledger accounting |
| `portfolio_wallet.withdrawal.status_changed` | A withdrawal workflow changes status or step progress |
| `portfolio_wallet.withdrawal.payout.status_changed` | An individual withdrawal payout leg changes status or step progress |
| `portfolio_wallet.strategy.status_changed` | A strategy update is applied |
| `portfolio_wallet.rebalance.status_changed` | A rebalance workflow transitions status |
| `portfolio_wallet.risk.alert.triggered` | A portal-configured risk rule triggers an alert |
| `portfolio_wallet.risk.alert.resolved` | A risk alert is resolved |
| `portfolio_wallet.risk.action.status_changed` | A risk action changes status |
| `portfolio_wallet.risk.unwind.completed` | A risk-triggered unwind finishes moving value to cash |
## Event payloads
### `portfolio_wallet.status_changed`
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"event": "portfolio_wallet.status_changed",
"organizationId": "43c78f4f-44a7-45ff-a524-8f010f7a4c72",
"walletId": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
"previousStatus": "idle",
"status": "rebalance_active",
"occurredAt": "2026-02-05T10:28:00.000Z"
}
```
### `portfolio_wallet.deposit.status_changed`
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"event": "portfolio_wallet.deposit.status_changed",
"walletId": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
"observedAt": "2026-02-05T09:45:00.000Z",
"deposit": {
"id": "2a3ad0af-11b3-41d5-96c5-2b9d8799f1e2",
"walletId": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
"amount": "50000.000000",
"token": "usdc",
"chain": "ethereum",
"fromAddress": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
"txHash": "0xabc123...",
"status": "completed",
"cashPositionId": "cash:ethereum:usdc",
"createdAt": "2026-02-05T09:40:00.000Z",
"completedAt": "2026-02-05T09:45:00.000Z"
}
}
```
### `portfolio_wallet.withdrawal.status_changed`
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"event": "portfolio_wallet.withdrawal.status_changed",
"walletId": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
"observedAt": "2026-02-05T11:05:00.000Z",
"withdrawal": {
"id": "11b17950-1f5c-4d36-8f0d-0f3d1d0c6a45",
"amountRequestedUsd": "65000.000000",
"amountPaidUsd": null,
"feeUsd": "0.000000",
"destinationChain": "ethereum",
"destinationAddress": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
"destinationToken": "usdc",
"status": "processing",
"legsCompleted": 0,
"legsTotal": 1,
"payoutLegs": [
{
"status": "pending_customer_approval",
"amountUsd": "65000.000000",
"from": { "kind": "cash", "id": "cash:ethereum:usdc", "label": "Cash (Ethereum)" },
"to": { "kind": "external_payout", "id": "external_payout:ethereum:usdc", "label": "External payout (Ethereum)" },
"startedAt": "2026-02-05T11:00:00.000Z",
"completedAt": null,
"stepsCompleted": 0,
"stepsTotal": 1,
"steps": [
{
"name": "Sending payout",
"chain": "ethereum",
"state": "pending_customer_approval",
"txKind": "external_payout_transfer",
"stepKind": "external_payout_transfer",
"startedAt": "2026-02-05T11:00:00.000Z",
"completedAt": null,
"cancelledAt": null,
"stepOrdinal": 0,
"protocolType": "external_payout",
"sequenceRole": "external_payout_transfer"
}
]
}
],
"failureReason": null,
"createdAt": "2026-02-05T11:00:00.000Z",
"completedAt": null
}
}
```
The `withdrawal` object uses the same projection as `GET /v2/wallets/:id/withdrawals/:withdrawalId`, including `feeUsd`. It also includes payout legs and their current execution steps. See [Withdraw Funds](/docs/portfolio-wallets/withdraw-funds) for the full `payoutLegs` structure.
### `portfolio_wallet.withdrawal.payout.status_changed`
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"event": "portfolio_wallet.withdrawal.payout.status_changed",
"walletId": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
"observedAt": "2026-02-05T12:40:00.000Z",
"payout": {
"id": "4f70f27a-760c-45f1-8ac9-01878d8d2d6c",
"status": "completed",
"turnkeyActivityId": null,
"approvalRequestedAt": null,
"approvalValidBefore": null,
"amountUsd": "35000.000000",
"feeUsd": "0.000000",
"txHash": "0xabc123...",
"failureReason": null,
"steps": [
{
"name": "Redeeming from Syrup USDC",
"stepKind": "syrup_request_redeem",
"sequenceRole": "yield_redeem",
"protocolType": "syrup",
"stepOrdinal": 0,
"chain": "ethereum",
"txKind": "yield_source_redeem",
"state": "completed",
"startedAt": "2026-02-05T12:35:00.000Z",
"completedAt": "2026-02-05T12:36:00.000Z",
"cancelledAt": null
},
{
"name": "Sending payout",
"stepKind": "external_payout_transfer",
"sequenceRole": "external_payout_transfer",
"protocolType": "external_payout",
"stepOrdinal": 1,
"chain": "ethereum",
"txKind": "external_payout_transfer",
"state": "completed",
"startedAt": "2026-02-05T12:38:00.000Z",
"completedAt": "2026-02-05T12:40:00.000Z",
"cancelledAt": null
}
],
"createdAt": "2026-02-05T12:34:00.000Z",
"completedAt": "2026-02-05T12:40:00.000Z"
}
}
```
### `portfolio_wallet.strategy.status_changed`
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"event": "portfolio_wallet.strategy.status_changed",
"walletId": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
"observedAt": "2026-02-05T09:30:00.000Z",
"strategy": {
"walletId": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
"requestId": "6d74dc1c-f1b5-4f2c-8f33-9f7b9e27cd4b",
"status": "completed",
"allocations": [
{ "yieldSourceId": "syrup-usdc", "pct": 40 },
{ "yieldSourceId": "morpho-gauntlet-usdc", "pct": 30 },
{ "yieldSourceId": "morpho-steakhouse-usdc", "pct": 30 }
],
"createdAt": "2026-02-05T09:29:00.000Z"
}
}
```
### `portfolio_wallet.rebalance.status_changed`
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"event": "portfolio_wallet.rebalance.status_changed",
"walletId": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
"observedAt": "2026-02-05T10:30:00.000Z",
"rebalance": {
"id": "b6c7d8e9-1234-4abc-8def-0123456789ab",
"status": "processing",
"reason": "deposit_deployment",
"amountUsd": "50000.000000",
"legsCompleted": 0,
"legsTotal": 2,
"rebalanceLegs": [
{
"status": "processing",
"amountUsd": "25000.000000",
"from": { "kind": "cash", "id": "cash:ethereum:usdc", "label": "Cash (Ethereum)" },
"to": { "kind": "yield_source", "id": "morpho-gauntlet-usdc", "label": "Morpho Gauntlet USDC Prime" },
"startedAt": "2026-02-05T10:29:00.000Z",
"completedAt": null,
"stepsCompleted": 0,
"stepsTotal": 1,
"steps": [
{
"name": "Depositing into Morpho Gauntlet USDC Prime",
"chain": "ethereum",
"state": "processing",
"txKind": "yield_source_deposit",
"stepKind": "yield_source_deposit",
"startedAt": "2026-02-05T10:29:00.000Z",
"completedAt": null,
"cancelledAt": null,
"stepOrdinal": 0,
"protocolType": "morpho",
"sequenceRole": "yield_deposit"
}
]
}
],
"failureReason": null,
"createdAt": "2026-02-05T10:28:00.000Z",
"startedAt": "2026-02-05T10:29:00.000Z",
"completedAt": null
}
}
```
Rebalance and withdrawal step objects expose workflow execution metadata. Broadcast transaction hashes are exposed on payout rows when available, not on every workflow step.
## Error responses
Webhook registration and management endpoints return errors in a consistent `{error, code}` structure:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"error": "Unsupported event types: portfolio_wallet.legacy_event",
"code": "validation_error"
}
```
## Delivery and headers
Ground POSTs to your registered `url` with these headers:
| Header | Description |
| - | - |
| `Content-Type` | `application/json` |
| `Ground-Event-Id` | Unique event identifier |
| `Ground-Event-Type` | Event type (e.g. `portfolio_wallet.rebalance.status_changed`) |
| `Ground-Signature` | HMAC signature for verification |
## Signature verification
Ground uses a Stripe-style HMAC scheme:
* Header format: `Ground-Signature: t=,v1=`
* HMAC: `v1 = HMAC_SHA256(key = secret, message = t + "." + rawBody)`
* `rawBody` is the exact request body string (before `JSON.parse`).
### Node / Express verification example
```javascript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const crypto = require("crypto");
const express = require("express");
function verifyGroundSignature(
rawBody,
signatureHeader,
signingSecret,
toleranceSeconds = 300,
) {
if (!signatureHeader) throw new Error("Missing Ground-Signature header");
const parts = signatureHeader.split(",");
const tPart = parts.find((p) => p.startsWith("t="));
const v1Part = parts.find((p) => p.startsWith("v1="));
if (!tPart || !v1Part)
throw new Error("Invalid Ground-Signature header format");
const timestamp = Number(tPart.split("=")[1]);
const signature = v1Part.split("=")[1];
// Optional replay protection
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > toleranceSeconds) {
throw new Error("Ground-Signature timestamp outside allowed window");
}
const payloadToSign = `${timestamp}.${rawBody}`;
const computed = crypto
.createHmac("sha256", signingSecret)
.update(payloadToSign)
.digest("hex");
const expected = Buffer.from(computed, "utf8");
const actual = Buffer.from(signature, "utf8");
if (
expected.length !== actual.length ||
!crypto.timingSafeEqual(expected, actual)
) {
throw new Error("Invalid Ground-Signature");
}
}
const app = express();
// Use raw body so you can verify before parsing
app.post(
"/ground/webhook",
express.raw({ type: "application/json" }),
(req, res) => {
const rawBody = req.body.toString("utf8");
const signatureHeader = req.get("Ground-Signature");
const signingSecret = process.env.GROUND_WEBHOOK_SECRET;
try {
verifyGroundSignature(rawBody, signatureHeader, signingSecret);
} catch (err) {
console.error("Webhook signature verification failed:", err.message);
return res.status(400).json({ error: "Invalid signature" });
}
const event = JSON.parse(rawBody);
if (event.event === "portfolio_wallet.withdrawal.status_changed") {
// handle withdrawal status change
}
res.status(200).json({ received: true });
},
);
```
## Quick checklist
* **Register** a webhook and persist `secret`.
* **Verify** every request with `t + "." + rawBody` and constant-time compare.
* **Enforce** a timestamp window and use HTTPS.
* **Log** `Ground-Event-Id` and `Ground-Event-Type` for debugging.
* **Respond** with `200` quickly -- do heavy processing asynchronously.
# Withdraw Funds
Source: https://docs.groundtech.co/docs/portfolio-wallets/withdraw-funds
Preview, initiate, and track portfolio wallet withdrawals.
Portfolio wallet withdrawals are asynchronous. A withdrawal may involve unwinding yield positions, bridging across chains, and waiting for customer approval -- all orchestrated automatically.
Creating a withdrawal reserves funds but does not guarantee the final payout. Morpho exits are normally synchronous, while Syrup USDC, the U.S. Treasury Bill Vault, and JTRSY may settle asynchronously. JTRSY USDC redemptions are instant through Basin when available; otherwise JTRSY typically takes 1-2 banking days and up to 3.
Investing through a [fee wrapper](/docs/portfolio-wallets/fee-wrappers) accrues fees in wrapper shares. This is separate from the optional partner fee deducted during a withdrawal payout.
## Withdrawal lifecycle
The typical flow is: **preview -> initiate -> track -> complete**.
1. **Preview** -- call `withdrawal-preview` to see what's available and get a sourcing plan.
2. **Initiate** -- call `withdrawals` to create the withdrawal. The system reserves liquidity and begins sourcing.
3. **Track** -- poll the withdrawal or subscribe to webhooks. The withdrawal moves through statuses as positions unwind, funds bridge, and payouts execute.
4. **Complete** -- the withdrawal reaches `completed` when all funds have been delivered.
## Preview a withdrawal
Before initiating, preview what balance is available for a given destination.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST "$BASE_URL/v2/wallets/$WALLET_ID/withdrawal-preview" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"destinationChain": "polygon",
"amountUsd": 65000,
"token": "usdc",
"partnerFeeAmountUsd": 25,
"partnerFeeRecipientAddress": "0x1111111111111111111111111111111111111111"
}'
```
| Field | Required | Description |
| - | - | - |
| `destinationChain` | Yes | Destination chain for the withdrawal |
| `amountUsd` | No | Gross wallet value to withdraw before fees (max 6 decimal places). If omitted, previews the maximum gross available balance. |
| `token` | Yes | Token to deliver: `usdc` or `usdt`. USDT withdrawals use Ethereum or TRON in production, and Ethereum Sepolia or Shasta in Sandbox. Funds must be available in the corresponding allocation group. |
| `sources` | No | Exact source positions when automatic rebalancing is disabled. Their gross amounts must sum to `amountUsd`. Omit this field for automatic source selection. |
| `partnerFeeAmountUsd` | No | Partner-calculated fee for an EVM USDC or USDT withdrawal. Must be supplied with `partnerFeeRecipientAddress`. |
| `partnerFeeRecipientAddress` | No | EVM address that receives the partner fee. Must be supplied with `partnerFeeAmountUsd`. |
With an explicit `amountUsd`, `amountRequestedUsd` echoes the requested amount:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"amountRequestedUsd": "65000.000000",
"feeUsd": "0.000000",
"partnerFeeUsd": "25.000000",
"amountToReceiveUsd": "64975.000000",
"withdrawableUsd": "82500.000000",
"totalUsdAfterWithdrawal": "35000.000000",
"estimatedCompletionTime": "PT2H",
"maxCompletionTime": "PT4H"
}
```
When `amountUsd` is omitted, the preview resolves the maximum available balance and returns that amount in `amountRequestedUsd`:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"amountRequestedUsd": "82500.000000",
"feeUsd": "0.000000",
"partnerFeeUsd": "0.000000",
"amountToReceiveUsd": "82500.000000",
"withdrawableUsd": "82500.000000",
"totalUsdAfterWithdrawal": "17500.000000",
"estimatedCompletionTime": "PT2H",
"maxCompletionTime": "PT4H"
}
```
| Field | Description |
| - | - |
| `amountRequestedUsd` | Gross wallet value previewed (equals the requested amount, or the resolved gross maximum when `amountUsd` was omitted) |
| `feeUsd` | Estimated execution fee deducted from the gross amount, including yield source protocol exit fees |
| `partnerFeeUsd` | Partner fee deducted from the gross amount |
| `amountToReceiveUsd` | Estimated net amount to be sent to the destination after execution and partner fees |
| `withdrawableUsd` | Maximum gross wallet value that can safely start a withdrawal for this destination right now |
| `totalUsdAfterWithdrawal` | Estimated wallet total after the gross requested amount is removed |
| `estimatedCompletionTime` | Estimated end-to-end completion time as an ISO 8601 duration |
| `maxCompletionTime` | Worst-case (SLA upper bound) end-to-end completion time as an ISO 8601 duration |
`withdrawableUsd` on the wallet object and in preview are related but different:
* Wallet `withdrawableUsd` is the wallet-level conservative gross amount that can be withdrawn now before plan-specific fees.
* Preview `withdrawableUsd` is the destination-specific gross amount that can safely start right now.
* Preview `withdrawableUsd` can be lower than the wallet object's `withdrawableUsd`.
Ground keeps wallet-level ownership, wallet-level readiness, and destination-specific readiness separate on purpose. Value can remain customer-owned while still being unavailable to start a fresh withdrawal to a specific destination, for example when a prior unwind, bridge, or payout boundary is still settling and the destination representation has not yet been observed.
## Initiate a withdrawal
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST "$BASE_URL/v2/wallets/$WALLET_ID/withdrawals" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"requestId": "df8b7be6-e110-4f6d-9b2d-7c44a5b1f0b0",
"destinationChain": "ethereum",
"amountUsd": 65000,
"token": "usdc",
"destinationAddress": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
"partnerFeeAmountUsd": 25,
"partnerFeeRecipientAddress": "0x1111111111111111111111111111111111111111"
}'
```
| Field | Required | Description |
| - | - | - |
| `requestId` | Yes | UUID v4 idempotency key |
| `destinationChain` | Yes | Destination chain |
| `amountUsd` | Yes | Gross wallet value to withdraw before execution fees (max 6 decimal places) |
| `token` | Yes | Token to deliver: `usdc` or `usdt`. |
| `destinationAddress` | Yes | Recipient address on the destination chain |
| `sources` | No | Exact source positions when automatic rebalancing is disabled. Their gross amounts must sum to `amountUsd`. |
| `partnerFeeAmountUsd` | No | Partner-calculated fee for an EVM USDC or USDT withdrawal. Must be positive, smaller than `amountUsd`, have at most 6 decimal places, and be supplied with `partnerFeeRecipientAddress`. |
| `partnerFeeRecipientAddress` | No | EVM address that receives the partner fee. Must be supplied with `partnerFeeAmountUsd` and differ from the customer destination. |
To withdraw exact amounts from selected positions, disable automatic rebalancing and send the same
`sources` array to preview and create:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"amountUsd": 1000,
"sources": [
{ "id": "morpho-august-usdc-v2", "amountUsd": 600 },
{ "id": "syrup-usdc", "amountUsd": 400 }
]
}
```
Use position IDs returned by `GET /v2/wallets/{id}`. Ground does not fall back
to another position when a selected source cannot cover its requested amount.
Wallets with automatic rebalancing do not support explicit sources in V1.
When `withdraw` succeeds, Ground has accepted the gross debit and reserved liquidity against the wallet. The destination receives the gross `amountUsd` minus execution fees and any partner fee. Ground snapshots the partner fee and recipient for that withdrawal and sends the customer payout and partner fee atomically. The payout legs remain the source of truth for what was actually delivered, especially for asynchronous exits.
Sandbox notes:
* Sandbox USDC destinations are `ethereum_sepolia` and `solana_devnet`.
* Sandbox USDT withdrawals require `token: "usdt"` with `destinationChain: "ethereum_sepolia"` or `"tron_shasta"`, matching the funded allocation group.
* `destinationAddress` must match the selected chain format: EVM hex for EVM chains, base58 for `solana` and `solana_devnet`, and checksummed TRON base58 for `tron` and `tron_shasta`.
### Solana Devnet withdrawal
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST "$BASE_URL/v2/wallets/$WALLET_ID/withdrawals" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"requestId": "e1f2a3b4-0000-4000-8000-000000000001",
"destinationChain": "solana_devnet",
"amountUsd": 5,
"token": "usdc",
"destinationAddress": "DmaePN7kL6WjSecMaH4urdS8zN8FwNPTxbdTqFwAw6Fd"
}'
```
## List withdrawals
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET "$BASE_URL/v2/wallets/$WALLET_ID/withdrawals?limit=25" \
-H "Authorization: Bearer $API_TOKEN"
```
Query parameters:
| Param | Description |
| - | - |
| `limit` | Max items (default 50, max 200) |
| `cursor` | Opaque cursor from `nextCursor` |
| `sort` | Sort field (default `createdAt`) |
| `order` | `desc` (default) or `asc` |
| `createdAtGte` | ISO-8601 timestamp filter |
| `status` | Repeatable status filter (e.g. `status=processing&status=completed`) |
## Fetch a withdrawal
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET "$BASE_URL/v2/wallets/$WALLET_ID/withdrawals/$WITHDRAWAL_ID" \
-H "Authorization: Bearer $API_TOKEN"
```
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"id": "0ad2fd60-f906-46fd-a395-f50b21dccd18",
"amountRequestedUsd": "5.000000",
"amountPaidUsd": "5.000000",
"amountPaidToPartnerUsd": "0.000000",
"feeUsd": "0.000000",
"destinationChain": "ethereum_sepolia",
"destinationAddress": "0x76F8fc6667E239f83a547d4e16225d6a34f6FA22",
"destinationToken": "usdc",
"status": "completed",
"legsCompleted": 2,
"legsTotal": 2,
"payoutLegs": [
{
"status": "completed",
"amountUsd": "2.999999",
"from": { "kind": "yield_source", "id": "morpho-steakhouse-usdc", "label": "Morpho Steakhouse USDC Prime" },
"to": { "kind": "external_payout", "id": "external_payout:ethereum_sepolia:usdc", "label": "External payout (Ethereum Sepolia)" },
"startedAt": "2026-07-09T21:37:22.227Z",
"completedAt": "2026-07-09T21:40:43.578Z",
"stepsCompleted": 2,
"stepsTotal": 2,
"steps": [
{
"name": "Redeeming from Morpho Steakhouse USDC Prime",
"stepKind": "yield_source_redeem",
"sequenceRole": "yield_redeem",
"protocolType": "morpho",
"stepOrdinal": 0,
"chain": "ethereum_sepolia",
"txKind": "yield_source_redeem",
"state": "completed",
"startedAt": "2026-07-09T21:38:29.885Z",
"completedAt": "2026-07-09T21:38:29.885Z",
"cancelledAt": null
},
{
"name": "Sending payout",
"stepKind": "external_payout_transfer",
"sequenceRole": "external_payout_transfer",
"protocolType": "external_payout",
"stepOrdinal": 1,
"chain": "ethereum_sepolia",
"txKind": "external_payout_transfer",
"state": "completed",
"startedAt": "2026-07-09T21:38:31.697Z",
"completedAt": "2026-07-09T21:40:43.578Z",
"cancelledAt": null
}
]
},
{
"status": "completed",
"amountUsd": "2.000001",
"from": { "kind": "yield_source", "id": "syrup-usdc", "label": "Syrup USDC" },
"to": { "kind": "external_payout", "id": "external_payout:ethereum_sepolia:usdc", "label": "External payout (Ethereum Sepolia)" },
"startedAt": "2026-07-09T21:37:22.227Z",
"completedAt": "2026-07-09T21:45:10.274Z",
"stepsCompleted": 2,
"stepsTotal": 2,
"steps": [
{
"name": "Redeeming from Syrup USDC",
"stepKind": "syrup_request_redeem",
"sequenceRole": "yield_redeem",
"protocolType": "syrup",
"stepOrdinal": 0,
"chain": "ethereum_sepolia",
"txKind": "yield_source_redeem",
"state": "completed",
"startedAt": "2026-07-09T21:39:05.596Z",
"completedAt": "2026-07-09T21:39:05.596Z",
"cancelledAt": null
},
{
"name": "Sending payout",
"stepKind": "external_payout_transfer",
"sequenceRole": "external_payout_transfer",
"protocolType": "external_payout",
"stepOrdinal": 1,
"chain": "ethereum_sepolia",
"txKind": "external_payout_transfer",
"state": "completed",
"startedAt": "2026-07-09T21:44:25.926Z",
"completedAt": "2026-07-09T21:45:10.274Z",
"cancelledAt": null
}
]
}
],
"failureReason": null,
"createdAt": "2026-07-09T21:37:22.227Z",
"completedAt": "2026-07-09T21:44:37.964Z"
}
```
Top-level fields:
| Field | Description |
| - | - |
| `amountRequestedUsd` | The gross wallet value requested for withdrawal. |
| `amountPaidUsd` | Sum of completed payout legs; `null` until at least one leg completes. |
| `amountPaidToPartnerUsd` | Confirmed amount paid to the partner; `null` until at least one partner payout completes. |
| `feeUsd` | Total execution and protocol fees across legs. |
| `destinationChain` / `destinationAddress` | Where funds are delivered. |
| `destinationToken` | Stablecoin delivered by the withdrawal: `usdc` or `usdt`. Older records without token metadata can return `null`. |
| `status` | Withdrawal status (see [Withdrawal statuses](#withdrawal-statuses)). |
| `legsCompleted` / `legsTotal` | Progress across payout legs. |
| `payoutLegs[]` | The individual payout legs of the withdrawal (see below). |
| `failureReason` | Populated when `status` is `failed` or `partially_completed`. |
The `payoutLegs` array contains the individual legs of the withdrawal. Each leg moves funds from one representation (`from`) to another (`to`) through one or more onchain `steps`:
| Field | Description |
| - | - |
| `status` | Leg status (see complete list below) |
| `amountUsd` | Amount routed through this leg, as a fixed-precision USD string |
| `from` / `to` | `{ kind, id, label }` source and destination of the leg (e.g. `yield_source` → `external_payout`) |
| `startedAt` / `completedAt` | Leg timing |
| `stepsCompleted` / `stepsTotal` | Progress across the leg's onchain steps |
| `steps[]` | Per-step detail: `{ name, stepKind, sequenceRole, protocolType, stepOrdinal, chain, txKind, state, startedAt, completedAt, cancelledAt }`. |
Use `portfolio_wallet.withdrawal.payout.status_changed` for per-leg progress, including the redeem and payout-transfer steps. When a step enters `pending_customer_approval`, fetch the pending Turnkey activity and verify it before approving — see [Transaction Approvals](/docs/portfolio-wallets/transaction-approvals).
### Payout statuses
Withdrawal `payoutLegs[].status` and `payoutLegs[].steps[].state` use the workflow status set:
| Status | Terminal | Meaning |
| - | - | - |
| `created` | No | Payout leg or step has been planned but has not started. |
| `processing` | No | Payout is actively being processed. |
| `pending_customer_approval` | No | Awaiting customer signature. Use Ground Portal or your enabled approval integration to review and approve or reject the activity. |
| `completed` | Yes | Payout delivered successfully. |
| `failed` | Yes | Payout failed; see `failureReason` |
| `cancelled` | Yes | Payout was cancelled (e.g. parent withdrawal cancelled) |
## Withdrawal statuses
Withdrawals move through five public statuses:
| Status | Meaning |
| - | - |
| `processing` | Accepted and in progress: unwinding positions, bridging, or delivering funds |
| `partially_completed` | Terminal: some payout legs completed while others did not |
| `completed` | Terminal: funds delivered to destination address |
| `failed` | Terminal: withdrawal failed |
| `cancelled` | Terminal: withdrawal was cancelled |
If realized redemption proceeds are not enough to pay the committed partner fee while leaving a positive customer payout, the withdrawal fails before payout with `failureReason: "insufficient_proceeds_after_partner_fee"`. No customer payout or partner fee is sent, and the realized proceeds remain available in the portfolio wallet. Submit a new withdrawal with a lower or zero partner fee.
## Timing expectations
Withdrawal time depends on which yield positions need to unwind and whether cross-chain bridging is required.
| Position | Typical timing | Developer note | Customer-facing copy |
| - | - | - | - |
| Cash (USDC) | Instant | No unwind needed. | This withdrawal is expected to complete right away. |
| Cash (USDT) | Instant | No unwind needed. Ethereum or TRON, matching the funded allocation group. | This withdrawal is expected to complete right away. |
| `morpho-gauntlet-usdc` | Instant (`PT0H`) | Synchronous ERC-4626 redeem on Ethereum. There is no normal queue-based delay. | This withdrawal is expected to complete quickly and is not normally queued. |
| `morpho-steakhouse-usdc` | Instant (`PT0H`) | Synchronous ERC-4626 redeem on Ethereum. There is no normal queue-based delay. | This withdrawal is expected to complete quickly and is not normally queued. |
| `syrup-usdc` | Up to 24h (`PT24H`) | Queue-based withdrawal. Low-liquidity periods can take longer. The payout can be processed in parts, and the final amount is based on Maple's withdrawal value when the queue processes the request. | This withdrawal is queued rather than instant. Most withdrawals are processed within 24 hours, but it can take longer in low-liquidity periods. The payout can be processed in parts, and the final amount can be lower if Maple's withdrawal value falls before processing. |
Cross-chain delivery (CCTP) adds time on top of the unwind. Withdrawal previews currently estimate this CCTP leg at about 20 minutes (`PT20M`); live settlement can vary with chain and Circle attestation conditions.
For precise per-position estimates to a specific destination, use the withdrawal preview endpoint.
## Withdrawal webhooks
Subscribe to `portfolio_wallet.withdrawal.status_changed` for real-time withdrawal tracking.
See [Webhooks](/docs/portfolio-wallets/webhooks) for registration and payload details.
***
## Bridge domains reference
A bridge domain is a hard boundary for where liquidity can come from. The withdrawal engine never crosses bridge domain boundaries.
### USDC unified (CCTP)
USDC on CCTP-supported chains forms a single `usdc:unified` domain:
* Arbitrum, Base, Ethereum, Polygon, Solana
A withdrawal to any of these chains can source USDC from any other chain in the domain (via CCTP bridging).
### USDT Ethereum
USDT withdrawals on Ethereum use `destinationChain: "ethereum"` in production or `"ethereum_sepolia"` in sandbox. They source only Ethereum USDT cash and compatible yield positions.
### USDT TRON
Use `destinationChain: "tron"` in production or `"tron_shasta"` in sandbox with `token: "usdt"` and the recipient’s TRON base58 address. The withdrawal sources TRON USDT cash and JustLend V1 positions in `usdt:tron`; Ethereum USDT is not available to this plan. Ground pays transaction resource costs. JustLend redemption depends on market liquidity and confirmation, and payouts follow the same customer approval flow.
### How cash fits in
Within a bridge domain, **cash is always the first source of liquidity**. Only if cash is insufficient will the system unwind yield positions in the same domain.
Bridge domain IDs are always lowercase and intended to be stable, but use the withdrawal preview as the canonical source of truth for what's available.
### TRON withdrawal preview
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST "$BASE_URL/v2/wallets/$WALLET_ID/withdrawal-preview" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "destinationChain": "tron", "token": "usdt", "amountUsd": 10 }'
```
For sandbox, substitute `tron_shasta`. To initiate the payout, use the [withdrawal endpoint](/api-reference/withdraw) with the same token, chain, and amount plus a fresh `requestId` and your recipient’s `destinationAddress`.
# Yield Sources
Source: https://docs.groundtech.co/docs/portfolio-wallets/yield-sources
Available RWA and DeFi yield sources, rate semantics, and liquidity expectations.
Yield sources are the underlying strategies a portfolio wallet can allocate into. A wallet strategy is a weighted allocation across one or more yield sources.
Each source has a stable public `id`, display metadata, chain support, a `depositToken`, current rate information, action-specific processing policies, and optional market context such as TVL, protocol addresses, provider links, and underlying allocations.
Use the catalog’s `allocationGroup` to choose the strategy group: `usdc`, `usdt` for Ethereum USDT, or `usdt:tron` for native TRON USDT. `depositToken` identifies the stablecoin but does not establish network compatibility. Put `justlend-v1-usdt` under `strategy.allocations["usdt:tron"]`; Ethereum USDT cannot fund it.
For supported vaults, [fee wrappers](/docs/portfolio-wallets/fee-wrappers) add management or performance fees to the position held by your portfolio wallet.
## How Ground RWA Vaults work
Ground RWA Vault sources use a receipt-vault path rather than a direct protocol position.
* The Portfolio Wallet routes a compatible stablecoin through the MasterRouter to the applicable Ground vault.
* The vault creates a wallet-specific request and coordinates any required external provider fulfillment.
* A vault processor advances fulfilled requests, after which the Portfolio Wallet claims the result.
* The Portfolio Wallet holds a wallet-specific, non-transferable Ground receipt that tracks the position rather than holding the underlying provider instrument directly.
Some actions settle immediately, while others require a request, external fulfillment, vault processing, and wallet claim before the receipt or stablecoin is available. Catalog `processingPolicies` and withdrawal previews are operational processing estimates; actual completion depends on provider fulfillment and vault processing.
The [Terms and Conditions of Lending](https://groundtech.co/lendingterms) govern use of Ground RWA Vaults.
## Available yield sources
Fetch the live, environment-specific catalog before creating or updating a strategy:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET "$BASE_URL/v2/wallets/yield-sources" \
-H "Authorization: Bearer $API_TOKEN"
```
The catalog is returned in the canonical `data` list envelope. `nextCursor` is always `null` because the catalog is unpaginated.
The following response is illustrative and shows one source, not the complete
catalog. `apyBps: null` means the historical rate is unavailable; it is not zero yield. JustLend can separately expose a fresh `currentApy` preview.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"data": [
{
"id": "figure-prime-usdc",
"name": "Figure PRIME",
"chain": "solana",
"mode": "active",
"protocol": "figure",
"depositToken": "usdc",
"apyBps": null,
"navUpdateMode": "discrete",
"maxAllocationUsd": null,
"processingPolicies": {
"deposit": {
"processingTimeBasis": "elapsed_seconds",
"typicalMinUnits": 0,
"typicalMaxUnits": 0
},
"redeem": {
"processingTimeBasis": "provider_managed",
"typicalMinUnits": null,
"typicalMaxUnits": null,
"processingEstimate": null,
"maximum": null
}
}
}
],
"nextCursor": null
}
```
The inventory below was checked against production and sandbox on October 1,
2026\. Fetch the live endpoint for current membership, organization eligibility,
limits, and processing policies. Production and sandbox can support different
sources and chains. A catalog entry or timing estimate does not guarantee that
a withdrawal can settle immediately.
## RWA
| Yield source ID | Stablecoins | Availability | Liquidity | Description |
| - | - | - | - | - |
| `ground-ustb-vault` | USDC | Production and sandbox | Typically within 2 hours | Short-duration U.S. Treasury bill exposure through Invesco Short Duration US Government Securities Fund tokenized by Superstate. |
| `ground-uscc-vault` | USDC | Production and sandbox | 1–2 banking days typical | Crypto cash-and-carry, staking, and U.S. Treasury exposure through the Bitwise Crypto Carry Fund tokenized by Superstate. |
| `ground-solana-ustb-vault` | USDC | Sandbox only | 1–2 banking days typical | USTB through a Ground vault on Solana devnet; sandbox only. |
| `ground-jaaa-usdc-vault` | USDC | Production and sandbox | 1–2 banking days typical | AAA-rated collateralized loan obligation exposure through Janus Henderson JAAA tokenized by Centrifuge. |
| `ground-jaaa-usdt-vault` | USDT | Production and sandbox | 1–2 banking days typical | The USDT-funded lane for the same JAAA strategy, available in production and sandbox. |
| `ground-jtrsy-usdc-vault` | USDC | Production and sandbox | Instant through Basin when available; otherwise 1–2 banking days typical | Short-duration U.S. Treasury exposure through Janus Henderson JTRSY tokenized by Centrifuge. |
| `ground-jtrsy-usdt-vault` | USDT | Production and sandbox | 1–2 banking days typical | The USDT-funded source for the same JTRSY strategy. |
| `figure-prime-usdc` | USDC | Production and sandbox | Provider-managed USDC settlement | Solana shares backed by pooled home-equity lines of credit (HELOCs) and cash equivalents. |
| `figure-auto-usdc` | USDC | Production and sandbox | Provider-managed USDC settlement | Solana shares backed by pooled auto loans and cash equivalents. |
## DeFi
| Yield source ID | Stablecoins | Availability | Liquidity | Description |
| - | - | - | - | - |
| `syrup-usdc` | USDC | Production and sandbox | Typically within 3 hours | Overcollateralized lending via [Maple Finance's Syrup](https://syrup.fi). |
| `syrup-usdt` | USDT | Production and sandbox | Typically within 3 hours | Ethereum USDT lending via Maple Finance. |
| `morpho-gauntlet-usdc` | USDC | Production and sandbox | Instant | Morpho Gauntlet USDC Prime ERC-4626 vault. |
| `morpho-steakhouse-usdc` | USDC | Production and sandbox | Instant | Morpho Steakhouse USDC Prime ERC-4626 vault. |
| `morpho-august-usdc-v2` | USDC | Production and sandbox | Instant | Morpho August USDC V2 ERC-4626 vault. |
| `morpho-smokehouse-usdc` | USDC | Production and sandbox | Instant, subject to vault liquidity | Morpho Smokehouse USDC on Ethereum. |
| `morpho-gauntlet-usdc-base` | USDC | Production only | Instant, subject to vault liquidity | Morpho Gauntlet USDC Prime on Base; production only. |
| `morpho-steakhouse-usdc-base` | USDC | Production only | Instant, subject to vault liquidity | Morpho Steakhouse Prime USDC on Base; production only. |
| `kamino-allez-usdc` | USDC | Production and sandbox | Instant | Solana USDC kvault curated by Allez. |
| `kamino-gauntlet-frontier-usdc` | USDC | Production and sandbox | Instant | Solana USDC kvault curated by Gauntlet. |
| `kamino-rockawayx-rwa-usdc` | USDC | Production and sandbox | Instant | Solana USDC kvault curated by RockawayX. |
| `kamino-steakhouse-usdc` | USDC | Production and sandbox | Instant | Solana USDC kvault curated by Steakhouse. |
| `kamino-superstate-usdc` | USDC | Production and sandbox | Instant | Kamino klend reserve backed by Superstate collateral. |
| `aave-v3-usdc` | USDC | Production and sandbox | Instant, subject to pool liquidity | Ethereum USDC lending through Aave V3 Core. |
| `justlend-v1-usdt` | USDT (TRON) | Production and sandbox | Onchain, subject to market liquidity | TRON USDT lending through JustLend V1; simulated market on Shasta in sandbox. |
JustLend V1 availability was checked in production and sandbox on October 4, 2026. Use the live catalog for your organization’s current eligibility.
Use `id` as the `yieldSourceId` when creating a wallet or updating a strategy.
Catalog rows may also include optional market context:
| Field | Description |
| - | - |
| `tvlUsd` | Current source TVL in USD when provider data is available. |
| `depositToken` | Stablecoin accepted and returned by the source. |
| `allocationGroup` | Independent liquidity group: `usdc`, `usdt`, or `usdt:tron`. |
| `currentApy` | Optional JustLend current supply-rate preview: `apyBps`, `asOf`, and `validUntil`. Separate from historical `apyBps`. |
| `utilizationPct` | Vault-level deployed-asset utilization when applicable. RWA funds return `null`. |
| `addresses[]` | Protocol addresses relevant to the source. |
| `links[]` | Provider and contract links. |
| `allocations[]` | Underlying allocation or target-exposure rows. |
| `maxAllocationUsd` | Optional per-wallet allocation limit as a fixed-precision USD string; `null` means no configured catalog cap. |
| `processingPolicies` | Deposit and redeem timing semantics, with an optional estimate computed at request time. |
Minimum deposit requirements can vary by source. The USCC lane has an effective
$1 USDC minimum. JAAA and JTRSY lanes have a configured 0.01-unit stablecoin
minimum. Sandbox JAAA-USDT currently has a $1,000 per-wallet catalog cap;
production JAAA-USDT does not. Validate allocation sizes against the live
catalog and request validation for your organization.
## Yield source details
## RWA
### Invesco Short Duration US Government Securities Fund tokenized by Superstate
| | |
| - | - |
| **Yield source IDs** | `ground-ustb-vault` |
| **Type** | Tokenized U.S. Treasury exposure |
| **Stablecoins** | USDC |
| **Chain** | Ethereum in production; Ethereum Sepolia in sandbox |
| **Liquidity** | Typically within 2 hours; timing depends on available Superstate liquidity |
| **Provider page** | [Superstate USTB](https://superstate.com/assets/ustb) |
#### Ground vaults
| Environment | Stablecoin | Yield source ID | Ground vault |
| - | - | - | - |
| Production | USDC | `ground-ustb-vault` | [`0x5a68cfE0DCCcfA4991e5a92d2c382b15B41C4119`](https://etherscan.io/address/0x5a68cfE0DCCcfA4991e5a92d2c382b15B41C4119) |
| Sandbox | USDC | `ground-ustb-vault` | [`0x7DB59Ed654aE4b3756eA81803Ff32528eA907DCa`](https://sepolia.etherscan.io/address/0x7DB59Ed654aE4b3756eA81803Ff32528eA907DCa) |
The Ground USTB vault issues a non-transferable Ground receipt to the Portfolio Wallet rather than transferring USTB to the wallet directly.
#### Superstate redemption facility
Superstate's onchain USDC redemption facility (`RedemptionIdle`) exchanges USTB
for USDC at the provider's redemption price, subject to the USDC available in
the facility. This is a liquidity pool for redeeming fund shares, not a market
sale of the wallet's Ground receipt. [Superstate describes onchain liquidity](https://superstate.com/platform)
as available for instant redemption where available.
The Ground vault checks the facility's redeemable amount before exchanging
USTB. Its contract supports synchronous redemption when sufficient facility
liquidity is available. Portfolio Wallet withdrawals use an asynchronous
request, process, and claim workflow: the requested position is reserved,
processing redeems the underlying USTB, and the wallet claims USDC. Facility
liquidity enables the underlying exchange; it does not make every API
withdrawal complete instantly.
The catalog's typical two-hour estimate is an operational estimate, not a
completion deadline. If liquidity is insufficient, settlement must wait for
available liquidity and processing. Use withdrawal previews and poll the
withdrawal or consume status-change webhooks until it completes.
### Bitwise Crypto Carry Fund tokenized by Superstate
| | |
| - | - |
| **Yield source IDs** | `ground-uscc-vault` |
| **Type** | Tokenized crypto carry fund |
| **Stablecoins** | USDC |
| **Chain** | Ethereum in production; Ethereum Sepolia in sandbox |
| **Liquidity** | Typically 1–2 banking days for deposits and redemptions; no configured maximum |
| **Provider page** | [Superstate USCC](https://superstate.com/assets/uscc) |
The Ground gUSCC vault provides exposure to crypto cash-and-carry, staking, and
U.S. Treasury strategies. The Portfolio Wallet holds a non-transferable Ground
receipt. Deposits and redemptions use an asynchronous request, process, and
claim workflow; submitted requests remain pending until provider fulfillment
and wallet settlement complete. Fund value can increase or decrease.
| Environment | Stablecoin | Yield source ID | Ground vault |
| - | - | - | - |
| Production | USDC | `ground-uscc-vault` | [`0x6fd0f5224d571250A830ADb09bC8cD21A0d64BEf`](https://etherscan.io/address/0x6fd0f5224d571250A830ADb09bC8cD21A0d64BEf) |
| Sandbox | USDC | `ground-uscc-vault` | [`0x70B2458EB62EC019b53494E3C214730549E5B558`](https://sepolia.etherscan.io/address/0x70B2458EB62EC019b53494E3C214730549E5B558) |
Put this source under `strategy.allocations.usdc`. The effective minimum deposit
is \$1 USDC. There is currently no configured catalog allocation cap in either
environment; check `maxAllocationUsd` before allocating.
### Superstate USTB on Solana
| | |
| - | - |
| **Yield source IDs** | `ground-solana-ustb-vault` |
| **Type** | Tokenized U.S. Treasury exposure |
| **Stablecoins** | USDC |
| **Chain** | Solana devnet in sandbox; unavailable in production |
| **Liquidity** | Typically 1–2 banking days for deposits and redemptions; no configured maximum |
| **Provider page** | [Superstate USTB](https://superstate.com/assets/ustb) |
This sandbox source uses an asynchronous Ground vault on Solana devnet. Select
`ground-solana-ustb-vault` under `strategy.allocations.usdc`; the Ethereum source
`ground-ustb-vault` is a separate catalog entry. The Solana lane follows
provider-operated settlement and does not use Ethereum's onchain redemption
facility. Wait for the deposit or withdrawal workflow to complete before
using the settled position or USDC.
### Janus Henderson JAAA tokenized by Centrifuge
| | |
| - | - |
| **Yield source IDs** | `ground-jaaa-usdc-vault`, `ground-jaaa-usdt-vault` |
| **Type** | Tokenized AAA-rated collateralized loan obligation fund |
| **Stablecoins** | USDC, USDT |
| **Chain** | Ethereum in production; Ethereum Sepolia in sandbox |
| **Liquidity** | Typically 1–2 joint U.S./BVI banking days |
| **Provider page** | [Centrifuge JAAA](https://app.centrifuge.io/pool/281474976710663) |
#### Ground vaults
| Environment | Stablecoin | Yield source ID | Ground vault |
| - | - | - | - |
| Production | USDC | `ground-jaaa-usdc-vault` | [`0xA0FC15E1c879acd4036d912425997811fA49A195`](https://etherscan.io/address/0xA0FC15E1c879acd4036d912425997811fA49A195) |
| Production | USDT | `ground-jaaa-usdt-vault` | [`0x25b7EB555864504a413E573647b90F69Bf29937D`](https://etherscan.io/address/0x25b7EB555864504a413E573647b90F69Bf29937D) |
| Sandbox | USDC | `ground-jaaa-usdc-vault` | [`0xf13E207d7F0CFAc068Bd34cA13c062c66772541d`](https://sepolia.etherscan.io/address/0xf13E207d7F0CFAc068Bd34cA13c062c66772541d) |
| Sandbox | USDT | `ground-jaaa-usdt-vault` | [`0x0251E101bf9811F7c5907Bf55c36AB094F88Fa54`](https://sepolia.etherscan.io/address/0x0251E101bf9811F7c5907Bf55c36AB094F88Fa54) |
The Ground gJAAA vault issues a non-transferable Ground receipt to the Portfolio Wallet rather than transferring JAAA to the wallet directly. Deposits and redemptions use an asynchronous request, process, and claim workflow, and NAV updates discretely when the fund publishes a new value. Put `ground-jaaa-usdc-vault` under `strategy.allocations.usdc` and `ground-jaaa-usdt-vault` under `strategy.allocations.usdt`. Both lanes are available in production and sandbox; fetch the live environment catalog before constructing a strategy.
### Janus Henderson JTRSY tokenized by Centrifuge
| | |
| - | - |
| **Yield source IDs** | `ground-jtrsy-usdc-vault`, `ground-jtrsy-usdt-vault` |
| **Type** | Tokenized short-duration U.S. Treasury fund |
| **Stablecoins** | USDC, USDT |
| **Chain** | Ethereum in production; Ethereum Sepolia in sandbox |
| **Liquidity** | Instant through Basin when available for USDC; otherwise typically 1–2 banking days and up to 3 banking days |
| **Maximum allocation** | No configured catalog cap in production or sandbox; check `maxAllocationUsd` |
| **Provider page** | [Centrifuge JTRSY](https://app.centrifuge.io/pool/281474976710662) |
#### Ground vaults
| Environment | Stablecoin | Yield source ID | Ground vault |
| - | - | - | - |
| Production | USDC | `ground-jtrsy-usdc-vault` | [`0x668c8B417314cC231bB18D948F3835e79c85Fe43`](https://etherscan.io/address/0x668c8B417314cC231bB18D948F3835e79c85Fe43) |
| Production | USDT | `ground-jtrsy-usdt-vault` | [`0x6E48246dC3eFC8A5F7d342F90AC7833791C782c7`](https://etherscan.io/address/0x6E48246dC3eFC8A5F7d342F90AC7833791C782c7) |
| Sandbox | USDC | `ground-jtrsy-usdc-vault` | [`0xfF5155c2389f8552795C0fA12fa3005260F417ec`](https://sepolia.etherscan.io/address/0xfF5155c2389f8552795C0fA12fa3005260F417ec) |
| Sandbox | USDT | `ground-jtrsy-usdt-vault` | [`0xCAa3b4C38Ee8EA43aAa3a44b50dACB4B3032C1CA`](https://sepolia.etherscan.io/address/0xCAa3b4C38Ee8EA43aAa3a44b50dACB4B3032C1CA) |
The Ground gJTRSY vaults issue non-transferable Ground receipts to the Portfolio Wallet rather than transferring JTRSY to the wallet directly. JTRSY provides separate USDC and USDT yield sources. USDC redemptions use Basin liquidity when available; otherwise both sources use asynchronous Centrifuge processing. Neither lane currently has a configured catalog cap in production or sandbox. Check `maxAllocationUsd` before constructing a strategy. Fetch the live catalog before constructing a strategy.
### Figure PRIME and Figure AUTO
| | Figure PRIME | Figure AUTO |
| - | - | - |
| **Yield source ID** | `figure-prime-usdc` | `figure-auto-usdc` |
| **Type** | Pooled HELOC-backed assets and cash equivalents | Pooled auto-loan-backed assets and cash equivalents |
| **Stablecoins** | USDC | USDC |
| **Chain** | Solana in production; Solana devnet in sandbox | Solana in production; Solana devnet in sandbox |
| **Wallet asset** | Direct PRIME SPL shares | Direct AUTO SPL shares |
| **NAV** | Discrete; may increase or decrease | Discrete; may increase or decrease |
| **Liquidity** | Provider-managed USDC settlement | Provider-managed USDC settlement |
| **Provider page** | [Hastra PRIME](https://hastra.io/prime) | [Hastra AUTO](https://hastra.io/auto) |
PRIME provides exposure to a pool of home-equity lines of credit; AUTO provides
exposure to a pool of auto loans. Both can also hold cash equivalents. The
Portfolio Wallet holds the selected product's SPL shares directly. These are
credit exposures: share value and returns depend on the underlying pool, and
NAV can increase or decrease.
#### Allocation and settlement
Put either source under `strategy.allocations.usdc`. Entry atomically converts
USDC through wYLDS into PRIME or AUTO shares. Share redemption converts the
selected shares into wYLDS, followed by a separate provider-managed redemption
from wYLDS to USDC. See Hastra's [Solana integration guide](https://help.hastra.io/35f2339356548002a00ef2e113e44090)
for the provider redemption lifecycle.
Pending redemption value remains in the wallet total and the originating
product's performance, but is unavailable for another withdrawal or allocation.
Receiving wYLDS does not mean the USDC withdrawal is complete. Poll the
withdrawal or consume status-change webhooks until it completes.
For USDC settlement, `processingPolicies.redeem.processingTimeBasis` is
`"provider_managed"`. The API returns
`processingEstimate` and `maximum` as `null` for this basis. Present this as an unavailable
estimate; neither the initial share redemption nor a missing estimate is a
promise of immediate USDC liquidity or a fixed completion deadline.
#### Rate and availability
The catalog returns a signed 30-day realized APY when enough fresh price
history exists; otherwise `apyBps` is `null`. A negative rate indicates a decline
over that observation period. This historical return is not a promised rate
and can differ from the provider's displayed rate when the observation windows
differ. `utilizationPct` is `null`.
Fetch the live environment catalog before constructing a strategy. Dormant or
halted sources are not selectable for new allocations; existing positions
remain visible and can enter the redemption workflow, subject to provider
liquidity and processing.
## DeFi
### Maple Syrup USDC
| | |
| - | - |
| **Yield source ID** | `syrup-usdc` |
| **Type** | Overcollateralized lending |
| **Stablecoins** | USDC |
| **Chain** | Ethereum |
| **Liquidity** | Typically within 3 hours |
Syrup USDC is an overcollateralized lending product from [Maple Finance](https://maple.finance/). Withdrawals can be queue-based depending on protocol liquidity.
### Maple Syrup USDT
| | |
| - | - |
| **Yield source ID** | `syrup-usdt` |
| **Type** | Overcollateralized lending |
| **Stablecoins** | USDT |
| **Chain** | Ethereum |
| **Liquidity** | Typically within 3 hours |
Syrup USDT currently has no configured catalog cap in production or sandbox. Check `maxAllocationUsd` in the live catalog for your organization.
### Morpho Gauntlet USDC Prime
| | |
| - | - |
| **Yield source ID** | `morpho-gauntlet-usdc` |
| **Type** | Overcollateralized lending |
| **Stablecoins** | USDC |
| **Chain** | Ethereum |
| **Liquidity** | Instant |
This source allocates into Morpho's Gauntlet USDC Prime ERC-4626 vault. Ground values the position from onchain `convertToAssets()` reads.
### Morpho Steakhouse USDC Prime
| | |
| - | - |
| **Yield source ID** | `morpho-steakhouse-usdc` |
| **Type** | Overcollateralized lending |
| **Stablecoins** | USDC |
| **Chain** | Ethereum |
| **Liquidity** | Instant |
This source allocates into Morpho's Steakhouse USDC Prime ERC-4626 vault.
### Morpho August USDC V2
| | |
| - | - |
| **Yield source ID** | `morpho-august-usdc-v2` |
| **Type** | Overcollateralized lending |
| **Stablecoins** | USDC |
| **Chain** | Ethereum |
| **Liquidity** | Instant |
This source allocates into Morpho's August USDC V2 ERC-4626 vault.
### Morpho Smokehouse USDC
| | |
| - | - |
| **Yield source IDs** | `morpho-smokehouse-usdc` |
| **Stablecoins** | USDC |
| **Chain** | Ethereum in production; Ethereum Sepolia in sandbox |
| **Availability** | Production and sandbox |
| **Liquidity** | Instant, subject to vault liquidity |
This source holds an ERC-4626 vault position. Put `morpho-smokehouse-usdc` under
`strategy.allocations.usdc`. Valuation uses the vault's onchain asset-per-share
conversion; withdrawal liquidity depends on the vault's available assets.
### Morpho Gauntlet USDC Prime (Base)
| | |
| - | - |
| **Yield source IDs** | `morpho-gauntlet-usdc-base` |
| **Stablecoins** | USDC |
| **Chain** | Base in production |
| **Availability** | Production only; halted in sandbox |
| **Liquidity** | Instant, subject to vault liquidity |
This source holds an ERC-4626 vault position. Put `morpho-gauntlet-usdc-base` under
`strategy.allocations.usdc`. Valuation uses the vault's onchain asset-per-share
conversion; withdrawal liquidity depends on the vault's available assets.
### Morpho Steakhouse Prime USDC (Base)
| | |
| - | - |
| **Yield source IDs** | `morpho-steakhouse-usdc-base` |
| **Stablecoins** | USDC |
| **Chain** | Base in production |
| **Availability** | Production only; halted in sandbox |
| **Liquidity** | Instant, subject to vault liquidity |
This source holds an ERC-4626 vault position. Put `morpho-steakhouse-usdc-base` under
`strategy.allocations.usdc`. Valuation uses the vault's onchain asset-per-share
conversion; withdrawal liquidity depends on the vault's available assets.
### Kamino Allez USDC
| | |
| - | - |
| **Yield source ID** | `kamino-allez-usdc` |
| **Type** | Curated kvault |
| **Stablecoins** | USDC |
| **Chain** | Solana |
| **Liquidity** | Instant |
Kamino Allez USDC is a Solana-native USDC vault curated by Allez.
### Kamino Gauntlet USDC Frontier
| | |
| - | - |
| **Yield source ID** | `kamino-gauntlet-frontier-usdc` |
| **Type** | Curated kvault |
| **Stablecoins** | USDC |
| **Chain** | Solana |
| **Liquidity** | Instant |
This Kamino kvault is curated under Gauntlet's Frontier mandate.
### Kamino RockawayX RWA USDC
| | |
| - | - |
| **Yield source ID** | `kamino-rockawayx-rwa-usdc` |
| **Type** | Curated kvault with an RWA-tilted strategy mix |
| **Stablecoins** | USDC |
| **Chain** | Solana |
| **Liquidity** | Instant |
This source remains in the DeFi section because the customer position is a Kamino lending vault, even though its curator targets an RWA-tilted mix.
### Kamino Steakhouse USDC
| | |
| - | - |
| **Yield source ID** | `kamino-steakhouse-usdc` |
| **Type** | Curated kvault |
| **Stablecoins** | USDC |
| **Chain** | Solana |
| **Liquidity** | Instant |
This Kamino kvault is curated by Steakhouse Financial.
### Kamino Superstate USDC
| | |
| - | - |
| **Yield source ID** | `kamino-superstate-usdc` |
| **Type** | Kamino klend reserve |
| **Stablecoins** | USDC |
| **Chain** | Solana |
| **Liquidity** | Instant |
This source remains in the DeFi section because the wallet supplies a Kamino lending reserve, even though the reserve is backed by Superstate collateral.
### Aave V3 Core USDC
| | |
| - | - |
| **Yield source ID** | `aave-v3-usdc` |
| **Type** | Overcollateralized lending |
| **Stablecoins** | USDC |
| **Chain** | Ethereum |
| **Liquidity** | Instant, subject to Aave pool liquidity |
| **Provider page** | [Aave USDC reserve](https://app.aave.com/reserve-overview/?underlyingAsset=0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48\&marketName=proto_mainnet_v3) |
Ground represents Aave positions as scaled aUSDC units and values them from Aave V3 reserve normalized income.
### JustLend V1 USDT
| | |
| - | - |
| **Yield source ID** | `justlend-v1-usdt` |
| **Type** | Collateralized lending market |
| **Stablecoins** | Native TRC-20 USDT |
| **Chain** | TRON (`tron`) in production; Shasta (`tron_shasta`) in sandbox |
| **Allocation group** | `usdt:tron` |
| **Liquidity** | Onchain redemption, subject to available market cash and protocol controls |
| **Provider page** | [JustLend V1 USDT market](https://app.justlend.org/marketDetailNew?jtokenAddress=TXJgMdjVX5dKiQaUi9QobwNxtSQaFqccvd\&lang=en-US) |
Ground supplies USDT to JustLend V1 and the Portfolio Wallet holds jUSDT receipt tokens. The position is valued using JustLend’s accrued share-to-USDT exchange rate. The catalog shows 100% exposure to the JustLend V1 USDT market; it is not a breakdown of individual borrowers or their collateral.
JustLend’s lending rate is variable, not a fixed promised return. `currentApy` follows the published supply-rate convention, excluding reward-token incentives: 365-day annualization at the target three-second block interval, without an extra compounding step, truncated to whole basis points. Display it as **Current APY** only until `validUntil`. Historical `apyBps` remains separate and may be `null` until sufficient fresh 30-day history exists. Sampling times can cause brief differences from the provider page.
Deposits and redemptions settle onchain without a provider-managed redemption queue. Completion still depends on transaction confirmation, available market liquidity, and protocol controls. Use withdrawal previews and final workflow status rather than treating an “instant” estimate as guaranteed settlement.
Sandbox uses valueless Shasta test USDT and a simulated JustLend market. It exercises the same API flow but does not reproduce mainnet interest rates, liquidity, or market risk. See [Supported Chains](/docs/portfolio-wallets/supported-chains) and [Deposits](/docs/portfolio-wallets/deposits#tron-deposit-addresses).
## Rate semantics
* `apyBps` is the persisted annualized 30-day yield in basis points.
* Ground stores independent seven-day and 30-day rates for each yield source.
* In production, Ground-calculated and official-provider rates are stored independently. `apyBps` prefers a fresh Ground-calculated 30-day rate and falls back to a fresh official 30-day rate.
* USTB and USCC use trailing rates derived from discrete provider NAV publications.
* JAAA and JTRSY use Centrifuge's annualized seven-day and compounded 30-day yield snapshots.
* Figure PRIME and AUTO use signed annualized 30-day realized returns from discrete share-price history.
* Syrup uses Maple's rate feed.
* Morpho vaults use trailing rates derived from ERC-4626 NAV/share history.
* Aave V3 uses reserve normalized income for valuation and trailing rates.
* JustLend V1 uses its accrued jUSDT exchange rate for valuation and historical rates; `currentApy` is a separate, expiring current supply-rate preview. Do not substitute it for a wallet’s historical blended performance.
* Kamino sources use trailing rates derived from onchain share prices or collateral exchange rates.
* All rates are variable and may change over time.
## NAV freshness
`navUpdateMode` is `"discrete"` for sources such as USTB, USCC, JAAA, JTRSY, PRIME, and AUTO whose value changes when a new NAV is published. Continuously accruing onchain sources return `"continuous"`. Wallet yield positions expose `priceAsOf` so clients can show when the reference price was observed.
## `protocol`
| Value | Protocol |
| - | - |
| `gustb` | Ground gUSTB vault backed by Invesco Short Duration US Government Securities Fund tokenized by Superstate |
| `centrifuge` | Centrifuge |
| `figure` | Figure PRIME and AUTO through Hastra on Solana |
| `guscc` | Ground gUSCC vault backed by the Bitwise Crypto Carry Fund tokenized by Superstate |
| `morpho` | Morpho Labs |
| `maple` | Maple Finance |
| `aave_v3` | Aave V3 |
| `justlend_v1` | JustLend V1 |
| `kamino` | Kamino Finance |
## `processingPolicies`
Each catalog row exposes action-specific `deposit` and `redeem` policies:
* `processingTimeBasis: "elapsed_seconds"` describes ordinary elapsed time.
* `processingTimeBasis: "banking_days"` describes calendar-aware settlement, such as JAAA and JTRSY.
* `processingTimeBasis: "provider_managed"` describes Figure USDC settlement; `processingEstimate` and `maximum` are `null`, and completion depends on provider processing and available liquidity.
* `typicalMinUnits` and `typicalMaxUnits` describe the typical range in that basis.
* `processingEstimate`, when present, resolves the policy into durations or dates at request time.
Use `POST /v2/wallets/:id/withdrawal-preview` for wallet-specific withdrawal timing rather than treating catalog policies as a wallet completion promise.
## Coming Soon
These are yield sources Ground has not yet integrated but is tracking for future support.
### Aave USDT
| | |
| - | - |
| **Type** | Overcollateralized lending |
| **Stablecoins** | USDT |
| **Chain** | Ethereum |
| **Liquidity** | Variable; withdrawals depend on pool liquidity |
| **Provider page** | [Aave USDT reserve](https://app.aave.com/reserve-overview/?underlyingAsset=0xdac17f958d2ee523a2206206994597c13d831ec7\&marketName=proto_mainnet_v3) |
Aave USDT is the USDT version of Aave's Ethereum lending market, offering variable yield with withdrawals limited by pool liquidity and collateral constraints.
### OpenTrade XHYC
| | |
| - | - |
| **Type** | ETF-backed corporate credit |
| **Stablecoins** | Provider-specific |
| **Chain** | Avalanche, Plume |
| **Liquidity** | Repaid T+1 |
| **Provider page** | [OpenTrade High Yield Corporate Bond Vault](https://docs.opentrade.io/stablecoin-yield/stablecoin-yield-vaults/high-yield-corporate-bond-vault) |
XHYC is backed by a short-duration high-yield corporate bond ETF and targets higher credit yield without a fixed lockup.
### OpenTrade XRV+
| | |
| - | - |
| **Type** | Structured rates and private credit |
| **Stablecoins** | Provider-specific |
| **Chain** | Not yet publicly disclosed |
| **Liquidity** | Not yet publicly disclosed |
| **Provider page** | [OpenTrade product suite](https://www.opentrade.io/product) |
XRV+ is OpenTrade's higher-yield rates product family focused on assets such as commercial paper and commodities trade finance.
### Ondo OUSG
| | |
| - | - |
| **Type** | Tokenized U.S. Treasuries |
| **Stablecoins** | Provider-specific |
| **Chain** | Ethereum, Solana, Polygon, XRPL |
| **Liquidity** | 24/7 instant mint and redemption |
| **Provider page** | [Ondo Nexus](https://blog.ondo.finance/introducing-ondo-nexus) |
OUSG is Ondo's tokenized U.S. Treasury product for institutional cash management with around-the-clock mint and redemption rails.
### Etherfuse Bonds
| | |
| - | - |
| **Type** | Tokenized sovereign bonds |
| **Stablecoins** | Provider-specific |
| **Chain** | Solana, Polygon, Stellar |
| **Liquidity** | Varies by bond series and venue |
| **Provider page** | [Etherfuse Bonds](https://app.etherfuse.com/) |
Etherfuse turns government bond exposure into onchain tokens spanning U.S. and local-market bond series.
### JitoSOL
| | |
| - | - |
| **Type** | Liquid staking |
| **Stablecoins** | Not applicable |
| **Chain** | Solana |
| **Liquidity** | Liquid token with secondary-market access |
| **Provider page** | [JitoSOL overview](https://www.jito.network/docs/jitosol/get-started/using-jitosol-flow/using-overview/) |
JitoSOL keeps Solana staking exposure liquid and DeFi-usable while earning staking and MEV-linked rewards.
### dSOL
| | |
| - | - |
| **Type** | Liquid staking |
| **Stablecoins** | Not applicable |
| **Chain** | Solana |
| **Liquidity** | Liquid token within Drift |
| **Provider page** | [Drift v3 overview](https://www.drift.trade/updates/introducing-drift-v3-built-to-outperform) |
dSOL is Drift's liquid staking token for use across Drift's trading and borrow-lend product surface.
### stETH
| | |
| - | - |
| **Type** | Liquid staking |
| **Stablecoins** | Not applicable |
| **Chain** | Ethereum |
| **Liquidity** | Liquid token with broad DeFi liquidity |
| **Provider page** | [What is stETH?](https://help.lido.fi/en/articles/5230610-what-is-steth) |
stETH is Lido's liquid version of staked ETH, preserving staking exposure while remaining usable across DeFi.
### Katana vbUSDC yVault
| | |
| - | - |
| **Type** | Yield-bearing vault |
| **Stablecoins** | USDC |
| **Chain** | Katana |
| **Liquidity** | Onchain vault liquidity |
| **Provider page** | [Katana vbUSDC vault](https://app.katana.network/earn/0x80c34BD3A3569E126e7055831036aa7b212cB159) |
Katana's vbUSDC vault makes bridged USDC productive through the network's Vault Bridge model.
### Katana vbUSDT yVault
| | |
| - | - |
| **Type** | Yield-bearing vault |
| **Stablecoins** | USDT |
| **Chain** | Katana |
| **Liquidity** | Onchain vault liquidity |
| **Provider page** | [Katana vbUSDT vault](https://app.katana.network/earn/0x9A6bd7B6Fd5C4F87eb66356441502fc7dCdd185B) |
Katana's vbUSDT vault is the USDT counterpart to vbUSDC, packaging bridged stablecoin liquidity into a yield-bearing vault.
# Contract Addresses
Source: https://docs.groundtech.co/docs/resources/addresses
Current production and sandbox contract and program deployments.
This page lists Ground's current contract and program deployments for supported production and sandbox chains.
These addresses are deployment references for production integrations. Always use the Portfolio Wallet API response for wallet-specific deposit addresses.
## Ethereum
| Component | Address | Explorer | Notes |
| - | - | - | - |
| Master Router | `0xD743D5b09eceA67880c4bc326a18A874bafE246E` | [Etherscan](https://etherscan.io/address/0xD743D5b09eceA67880c4bc326a18A874bafE246E) | Portfolio wallet routing |
| Adapter Registry | `0x6Da1180A84C0E38a0d292f1e51853ce4C75fc150` | [Etherscan](https://etherscan.io/address/0x6Da1180A84C0E38a0d292f1e51853ce4C75fc150) | Adapter allowlist and type registry |
| Wallet Binding Registry | `0x36E76eE20168300a1005e3d607499f6f0C62318b` | [Etherscan](https://etherscan.io/address/0x36E76eE20168300a1005e3d607499f6f0C62318b) | Wallet binding registry |
| CCTP Adapter | `0x49bD4C78cf3B4dDcF9c3857269BFa08C69f5E044` | [Etherscan](https://etherscan.io/address/0x49bD4C78cf3B4dDcF9c3857269BFa08C69f5E044) | USDC bridging adapter |
| Syrup USDC Adapter | `0x746253ea7ecd9c356C4E42A977238779fa1F85BA` | [Etherscan](https://etherscan.io/address/0x746253ea7ecd9c356C4E42A977238779fa1F85BA) | Maple syrupUSDC |
| Morpho Smokehouse USDC Adapter | `0x71abc48b627B31a4D1530F85C61057484edf0F8e` | [Etherscan](https://etherscan.io/address/0x71abc48b627B31a4D1530F85C61057484edf0F8e) | ERC-4626 adapter |
| Morpho Gauntlet USDC Adapter | `0xb36Ee685D1AdE0af03995E0e9E6D7D050736E4bE` | [Etherscan](https://etherscan.io/address/0xb36Ee685D1AdE0af03995E0e9E6D7D050736E4bE) | ERC-4626 adapter |
| Morpho Steakhouse USDC Adapter | `0xe196b91BfF3F0EC4279e8494Ab9b9727B62FFDEb` | [Etherscan](https://etherscan.io/address/0xe196b91BfF3F0EC4279e8494Ab9b9727B62FFDEb) | ERC-4626 adapter |
| Morpho August USDC V2 Adapter | `0x6268d5c875acAB235d411563A4A03163aDa08F90` | [Etherscan](https://etherscan.io/address/0x6268d5c875acAB235d411563A4A03163aDa08F90) | ERC-4626 adapter |
| Morpho Rewards Adapter | `0xde6466A4224616b17956fc34750daf613CbD3523` | [Etherscan](https://etherscan.io/address/0xde6466A4224616b17956fc34750daf613CbD3523) | Rewards conversion |
| Aave V3 USDC Adapter | `0x6D1d9F09e99cd954Ba6A7a52fDaa60b9419437dc` | [Etherscan](https://etherscan.io/address/0x6D1d9F09e99cd954Ba6A7a52fDaa60b9419437dc) | Aave V3 USDC |
| USTB Vault | `0x5a68cfE0DCCcfA4991e5a92d2c382b15B41C4119` | [Etherscan](https://etherscan.io/address/0x5a68cfE0DCCcfA4991e5a92d2c382b15B41C4119) | Ground USTB receipt vault funded with USDC |
| JAAA USDC Vault | `0xA0FC15E1c879acd4036d912425997811fA49A195` | [Etherscan](https://etherscan.io/address/0xA0FC15E1c879acd4036d912425997811fA49A195) | Ground JAAA receipt vault funded with USDC |
| JAAA USDT Vault | `0x25b7EB555864504a413E573647b90F69Bf29937D` | [Etherscan](https://etherscan.io/address/0x25b7EB555864504a413E573647b90F69Bf29937D) | Ground JAAA receipt vault funded with USDT |
| USCC Vault | `0x6fd0f5224d571250A830ADb09bC8cD21A0d64BEf` | [Etherscan](https://etherscan.io/address/0x6fd0f5224d571250A830ADb09bC8cD21A0d64BEf) | Ground USCC receipt vault funded with USDC |
| JTRSY USDC Vault | `0x668c8B417314cC231bB18D948F3835e79c85Fe43` | [Etherscan](https://etherscan.io/address/0x668c8B417314cC231bB18D948F3835e79c85Fe43) | Ground JTRSY receipt vault funded with USDC |
| JTRSY USDT Vault | `0x6E48246dC3eFC8A5F7d342F90AC7833791C782c7` | [Etherscan](https://etherscan.io/address/0x6E48246dC3eFC8A5F7d342F90AC7833791C782c7) | Ground JTRSY receipt vault funded with USDT |
## Base
| Component | Address | Explorer | Notes |
| - | - | - | - |
| Master Router | `0xD743D5b09eceA67880c4bc326a18A874bafE246E` | [Basescan](https://basescan.org/address/0xD743D5b09eceA67880c4bc326a18A874bafE246E) | Portfolio wallet routing |
| Adapter Registry | `0x6Da1180A84C0E38a0d292f1e51853ce4C75fc150` | [Basescan](https://basescan.org/address/0x6Da1180A84C0E38a0d292f1e51853ce4C75fc150) | Adapter allowlist and type registry |
| Wallet Binding Registry | `0xc8bC5a9F6F2e5C0a78B1dDC4088A8dCE9B5316c8` | [Basescan](https://basescan.org/address/0xc8bC5a9F6F2e5C0a78B1dDC4088A8dCE9B5316c8) | Wallet binding registry |
| CCTP Adapter | `0x49bD4C78cf3B4dDcF9c3857269BFa08C69f5E044` | [Basescan](https://basescan.org/address/0x49bD4C78cf3B4dDcF9c3857269BFa08C69f5E044) | USDC bridging adapter |
| Morpho Gauntlet USDC Adapter | `0x7e5b3186b9c3f0d1bf6dFfb8172b563328845Bd1` | [Basescan](https://basescan.org/address/0x7e5b3186b9c3f0d1bf6dFfb8172b563328845Bd1) | ERC-4626 adapter |
| Morpho Steakhouse USDC Adapter | `0x96BF9b650b61F76cca1f1754a3D7beDdfC85a792` | [Basescan](https://basescan.org/address/0x96BF9b650b61F76cca1f1754a3D7beDdfC85a792) | ERC-4626 adapter |
| Morpho Rewards Adapter | `0x1F5441f97B408231e74AFf05d4972334cb35f0a8` | [Basescan](https://basescan.org/address/0x1F5441f97B408231e74AFf05d4972334cb35f0a8) | Rewards conversion |
## Arbitrum
| Component | Address | Explorer | Notes |
| - | - | - | - |
| Master Router | `0xD743D5b09eceA67880c4bc326a18A874bafE246E` | [Arbiscan](https://arbiscan.io/address/0xD743D5b09eceA67880c4bc326a18A874bafE246E) | Portfolio wallet routing |
| Adapter Registry | `0x6Da1180A84C0E38a0d292f1e51853ce4C75fc150` | [Arbiscan](https://arbiscan.io/address/0x6Da1180A84C0E38a0d292f1e51853ce4C75fc150) | Adapter allowlist and type registry |
| Wallet Binding Registry | `0xd66582Ae72f13C699FfB369F9265A17cc09B4D93` | [Arbiscan](https://arbiscan.io/address/0xd66582Ae72f13C699FfB369F9265A17cc09B4D93) | Current wallet binding registry |
| CCTP Adapter | `0x49bD4C78cf3B4dDcF9c3857269BFa08C69f5E044` | [Arbiscan](https://arbiscan.io/address/0x49bD4C78cf3B4dDcF9c3857269BFa08C69f5E044) | USDC bridging adapter |
## Polygon
| Component | Address | Explorer | Notes |
| - | - | - | - |
| Master Router | `0xD743D5b09eceA67880c4bc326a18A874bafE246E` | [Polygonscan](https://polygonscan.com/address/0xD743D5b09eceA67880c4bc326a18A874bafE246E) | Portfolio wallet routing |
| Adapter Registry | `0x6Da1180A84C0E38a0d292f1e51853ce4C75fc150` | [Polygonscan](https://polygonscan.com/address/0x6Da1180A84C0E38a0d292f1e51853ce4C75fc150) | Adapter allowlist and type registry |
| Wallet Binding Registry | `0xc8bC5a9F6F2e5C0a78B1dDC4088A8dCE9B5316c8` | [Polygonscan](https://polygonscan.com/address/0xc8bC5a9F6F2e5C0a78B1dDC4088A8dCE9B5316c8) | Wallet binding registry |
| CCTP Adapter | `0x49bD4C78cf3B4dDcF9c3857269BFa08C69f5E044` | [Polygonscan](https://polygonscan.com/address/0x49bD4C78cf3B4dDcF9c3857269BFa08C69f5E044) | USDC bridging adapter |
## TRON
The Ground contracts below are deployed on TRON mainnet for Portfolio Wallet routing and the JustLend V1 USDT integration. Fetch the yield-source catalog for current availability.
| Component | Address | Explorer | Notes |
| - | - | - | - |
| Master Router | `TFjwkhjxt3U6uxCfSoLwAtrB9Sj8ni8B5x` | [TRONSCAN](https://tronscan.org/#/contract/TFjwkhjxt3U6uxCfSoLwAtrB9Sj8ni8B5x) | Portfolio wallet routing |
| Adapter Registry | `TVFE7w46Jnwm8vQ3r4EKEYEHLJC33pb7oH` | [TRONSCAN](https://tronscan.org/#/contract/TVFE7w46Jnwm8vQ3r4EKEYEHLJC33pb7oH) | Adapter registry |
| JustLend V1 Adapter | `TNK8uBoTbSwR3JcdQE1jKxXyn9opcBjksL` | [TRONSCAN](https://tronscan.org/#/contract/TNK8uBoTbSwR3JcdQE1jKxXyn9opcBjksL) | JustLend V1 USDT integration |
## Solana
| Component | Address / Program ID | Explorer | Notes |
| - | - | - | - |
| Ground Solana Router | `2RCMWFGKJRRWNHCiz4sEinRk1gEwq7SRVW8aLGj8oRdA` | [Solscan](https://solscan.io/account/2RCMWFGKJRRWNHCiz4sEinRk1gEwq7SRVW8aLGj8oRdA) | Canonical production portfolio wallet router |
| Historical production router | `7s66SoJ9nEf8JssXbCJbrZAbNs32ZzWhjmbCPVtaJZg5` | [Solscan](https://solscan.io/account/7s66SoJ9nEf8JssXbCJbrZAbNs32ZzWhjmbCPVtaJZg5) | Retired; never target for new production policy or upgrades |
| Circle CCTP Message Transmitter V2 | `CCTPV2Sm4AdWt5296sk4P66VBZ7bEhcARwFaaS9YPbeC` | [Solscan](https://solscan.io/account/CCTPV2Sm4AdWt5296sk4P66VBZ7bEhcARwFaaS9YPbeC) | CCTP receive |
| Circle CCTP Token Messenger Minter V2 | `CCTPV2vPZJS2u2BBsUoscuikbYjnpFmbFsvVuJdgUMQe` | [Solscan](https://solscan.io/account/CCTPV2vPZJS2u2BBsUoscuikbYjnpFmbFsvVuJdgUMQe) | CCTP mint/burn |
| Kamino kVault Program | `KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd` | [Solscan](https://solscan.io/account/KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd) | kVault deposits and withdrawals |
| Kamino kLend Program | `KLend2g3cP87fffoy8q1mQqGKjrxjC8boSyAYavgmjD` | [Solscan](https://solscan.io/account/KLend2g3cP87fffoy8q1mQqGKjrxjC8boSyAYavgmjD) | Superstate USDC reserve |
## Sandbox Deployments
These sandbox addresses are used by Ground's staging/sandbox environment. They are separate from production deployments and should not be used for production integrations.
### Ethereum Sepolia
| Component | Address | Explorer | Notes |
| - | - | - | - |
| Master Router | `0xBB6cD0a42cb96eb7bA9234a0B5738545D53958d3` | [Etherscan](https://sepolia.etherscan.io/address/0xBB6cD0a42cb96eb7bA9234a0B5738545D53958d3) | Sandbox portfolio wallet routing |
| Adapter Registry | `0x4d18Ee079fc36573d62B52B02eB9c6D8F0fc38E2` | [Etherscan](https://sepolia.etherscan.io/address/0x4d18Ee079fc36573d62B52B02eB9c6D8F0fc38E2) | Sandbox adapter registry |
| Wallet Binding Registry | `0xDd48ddD16B3E6906F8d64dB51c03a39EFE0C3EA4` | [Etherscan](https://sepolia.etherscan.io/address/0xDd48ddD16B3E6906F8d64dB51c03a39EFE0C3EA4) | Sandbox wallet binding registry |
| CCTP Adapter | `0x826abf3b74c59B6Aec8a0d4aF07fe5fC81A0Fe0F` | [Etherscan](https://sepolia.etherscan.io/address/0x826abf3b74c59B6Aec8a0d4aF07fe5fC81A0Fe0F) | Sandbox USDC bridging adapter |
| Syrup USDC Adapter | `0xdce284c107c34aD5C9f005BCeC3889fabD87097B` | [Etherscan](https://sepolia.etherscan.io/address/0xdce284c107c34aD5C9f005BCeC3889fabD87097B) | Sandbox Syrup adapter |
| Morpho Smokehouse USDC Adapter | `0x9A6dF9504fD29061C96A94204E0c5c4A7be9be9B` | [Etherscan](https://sepolia.etherscan.io/address/0x9A6dF9504fD29061C96A94204E0c5c4A7be9be9B) | Sandbox ERC-4626 adapter |
| Morpho Gauntlet USDC Adapter | `0x1A767Cbe12939A66F2013c38d3880F231DfB4Ad2` | [Etherscan](https://sepolia.etherscan.io/address/0x1A767Cbe12939A66F2013c38d3880F231DfB4Ad2) | Sandbox ERC-4626 adapter |
| Morpho Steakhouse USDC Adapter | `0xc8f2D6f08D8b5A51877C2Eb7BF1279998AF2f755` | [Etherscan](https://sepolia.etherscan.io/address/0xc8f2D6f08D8b5A51877C2Eb7BF1279998AF2f755) | Sandbox ERC-4626 adapter |
| Morpho August USDC V2 Adapter | `0xFa86eF30a830A27e2d00b4D8eC9cd853690222B7` | [Etherscan](https://sepolia.etherscan.io/address/0xFa86eF30a830A27e2d00b4D8eC9cd853690222B7) | Sandbox ERC-4626 adapter |
| U.S. Treasury Bill Vault | `0x7DB59Ed654aE4b3756eA81803Ff32528eA907DCa` | [Etherscan](https://sepolia.etherscan.io/address/0x7DB59Ed654aE4b3756eA81803Ff32528eA907DCa) | Sandbox U.S. Treasury Bill Vault |
| Aave V3 USDC Adapter | `0x09cb9a191cC38aDbb72e5628aE840e7803169154` | [Etherscan](https://sepolia.etherscan.io/address/0x09cb9a191cC38aDbb72e5628aE840e7803169154) | Sandbox Aave V3 USDC adapter |
| JTRSY USDC Vault | `0xfF5155c2389f8552795C0fA12fa3005260F417ec` | [Etherscan](https://sepolia.etherscan.io/address/0xfF5155c2389f8552795C0fA12fa3005260F417ec) | Sandbox Ground JTRSY receipt vault funded with USDC |
| JTRSY USDT Vault | `0xCAa3b4C38Ee8EA43aAa3a44b50dACB4B3032C1CA` | [Etherscan](https://sepolia.etherscan.io/address/0xCAa3b4C38Ee8EA43aAa3a44b50dACB4B3032C1CA) | Sandbox Ground JTRSY receipt vault funded with USDT |
### TRON Shasta
These Ground contracts are deployed and verified on Shasta and enabled for Sandbox customer flows after the October 4, 2026 hosted roundtrip. Shasta uses valueless test USDT and a simulated JustLend market.
| Component | Address | Explorer | Notes |
| - | - | - | - |
| Master Router | `T9zXBit114cLsKoEw8cyNuhNadUEy7WWcS` | [TRONSCAN](https://shasta.tronscan.org/contract/T9zXBit114cLsKoEw8cyNuhNadUEy7WWcS/code) | Active portfolio wallet router |
| Adapter Registry | `TWY2dmytYxE4XWG9vx4cH8FwK5znsL2ZqZ` | [TRONSCAN](https://shasta.tronscan.org/contract/TWY2dmytYxE4XWG9vx4cH8FwK5znsL2ZqZ/code) | Adapter registry |
| JustLend V1 Adapter | `TBysFQMjkeFk9b7cZbm27hp9aV9q9M6MnH` | [TRONSCAN](https://shasta.tronscan.org/contract/TBysFQMjkeFk9b7cZbm27hp9aV9q9M6MnH/code) | Registered and enabled for Sandbox |
### Solana Devnet
| Component | Address / Program ID | Explorer | Notes |
| - | - | - | - |
| Ground Solana Router | `2RCMWFGKJRRWNHCiz4sEinRk1gEwq7SRVW8aLGj8oRdA` | [Solscan](https://solscan.io/account/2RCMWFGKJRRWNHCiz4sEinRk1gEwq7SRVW8aLGj8oRdA?cluster=devnet) | Sandbox portfolio wallet routing, bindings, and CCTP burn |
| Circle CCTP Message Transmitter V2 | `CCTPV2Sm4AdWt5296sk4P66VBZ7bEhcARwFaaS9YPbeC` | [Solscan](https://solscan.io/account/CCTPV2Sm4AdWt5296sk4P66VBZ7bEhcARwFaaS9YPbeC?cluster=devnet) | Configured CCTP receive program |
| Circle CCTP Token Messenger Minter V2 | `CCTPV2vPZJS2u2BBsUoscuikbYjnpFmbFsvVuJdgUMQe` | [Solscan](https://solscan.io/account/CCTPV2vPZJS2u2BBsUoscuikbYjnpFmbFsvVuJdgUMQe?cluster=devnet) | Configured CCTP mint/burn program |
| Kamino kVault Program | `devkRngFnfp4gBc5a3LsadgbQKdPo8MSZ4prFiNSVmY` | [Solscan](https://solscan.io/account/devkRngFnfp4gBc5a3LsadgbQKdPo8MSZ4prFiNSVmY?cluster=devnet) | Sandbox kVault program |
| Kamino kLend Program | `KLend2g3cP87fffoy8q1mQqGKjrxjC8boSyAYavgmjD` | [Solscan](https://solscan.io/account/KLend2g3cP87fffoy8q1mQqGKjrxjC8boSyAYavgmjD?cluster=devnet) | Sandbox kLend program |
## Source of Truth
The TRON mainnet addresses were verified on October 4, 2026; the TRON Shasta Ground contract addresses were verified on October 3, 2026. Other addresses reflect their respective production and sandbox chain configuration and contract release records.
# Security
Source: https://docs.groundtech.co/docs/security/custody-model
Security reviews, custody model, wallet controls, and deployment references.
This page describes how control is distributed between you and Ground when using Portfolio Wallets.
## Independent security review
Ground has completed three independent Quantstamp reviews covering its EVM and Solana contracts, USTB vault upgrade, and Centrifuge JAAA vault and Aave adapter. Ground publishes the original EVM and Solana report and sanitized, Ground-authored public summaries of the two later reviews.
Review period: Apr 6, 2026 - Apr 17, 2026. Final report: Jun 23, 2026.
Ground-authored summary. Review period: Jun 18, 2026 - Jun 19, 2026. Findings: 4 fixed, 1 acknowledged.
Ground-authored summary. Review date: Jul 3, 2026. Findings: 2 fixed, 1 mitigated.
Security reviews are point-in-time assessments. The two public summaries intentionally omit sensitive implementation and operational details. They are prepared and published by Ground, are not original Quantstamp reports, and apply only to the scope and review dates stated in each summary.
See [Contract Addresses](/docs/resources/addresses) for current production deployment references.
## Signing-key custody
* Deposit addresses are provisioned and managed by Ground.
* Withdrawals may require customer approval, depending on the organization's approval policy.
* Strategy updates are API-authenticated requests. They save target allocations immediately; later rebalance execution is Ground-signed under policy.
* Each customer organization has a dedicated custody sub-organization used for wallet signing policy and approvals.
Signing-key custody describes who can authorize transactions from a Portfolio Wallet. It does not by itself describe the legal ownership, settlement, or counterparty structure of every yield source.
## Wallet type per chain
Ground provides chain-native deposit addresses per supported chain. Treat them as standard addresses for funding and withdrawals. The exact address format follows the chain: EVM hex addresses, Solana base58 public keys, or checksummed TRON base58 addresses. TRON deposit addresses are available after the wallet opts in and address setup completes. Always use the network-specific address returned by Ground.
## Address uniqueness and reuse
* Each wallet receives **dedicated deposit addresses** per chain.
* Addresses are **not reused across customers**.
* If you see the same EVM address across multiple EVM chains for a single wallet, that is expected and still scoped to that wallet.
## What this means for your integration
* Always send funds to the deposit address returned for the specific wallet and chain.
* Never reuse deposit addresses across wallets.
* Rely on the API or webhooks to reconcile balances and allocations.