Skip to main content
Custodial Wallets require a business account. This recipe assumes your platform is already onboarded with Custodial Wallets enabled. If you’re interested or want to check whether this fits your use case, get in touch and our team will follow up.
Use case: Your customers want to settle corporate balances in stablecoins, but your entity does not want to hold stablecoins on the balance sheet. What you’ll build: Incoming customer USDC is automatically converted to USD and collected in your treasury account, with no stablecoin held on your balance sheet. These steps run in Sandbox, Coinbase’s test environment. To go live, point at production (https://api.cdp.coinbase.com/platform for the SDK, or cdp env production for the CLI) and use a production API key.

Flow of funds

Flow of funds: a customer's USDC wallet sends USDC on-chain to a liquidation deposit address on their account, which auto-converts it to USD in the customer account, which is then transferred to the entity-owned treasury account.

Prerequisites

  • Your entity is onboarded (KYB’d) with Custodial Wallets enabled. See the Custodial Wallets overview.
  • Your customer is onboarded and KYC’d, with custody and transfer capabilities for crypto, fiat, and stablecoin. New to onboarding? See the Customers / KYC quickstart.
  • The CDP CLI or TypeScript SDK installed and pointed at Sandbox. Both require Node.js 22 or later.

Account structure

Install and configure

Pick one path, the CDP CLI or the TypeScript SDK, and follow that tab in every step. Each CLI command runs on its own. The SDK snippets build up a single main.ts file, so the complete runnable script is at the end under Run the SDK flow as scripts.
Copying these commands: anything shown as a placeholder — values ending in ... (like customer_...) or wrapped in <ANGLE_BRACKETS> — must be replaced with your real value before running. Use each code block’s copy button; if you paste through a rich-text editor (Google Docs, Notes), straight quotes " can turn into curly " and the shell will hang on a dquote> prompt.

1. Create your treasury account

Your entity-owned account that collects the USD. Omitting an owner makes the account entity-owned.
Confirm it is entity-owned. The owner should start with entity_:
If the owner comes back as customer_... instead, the CLI reused a previous owner from its saved history. Run cdp history clear, then recreate the treasury with a different name and check the owner again.
Account names must be unique within your entity. A duplicate name returns 409 already_exists.

2. Create the customer’s account

You already have your customer’s ID from onboarding them (the customerId the Customers API returned). Pass it as the owner. Customer-owned accounts also require the end-user’s IP on compliance.requesterIpAddress.
The owner field on createAccount, and compliance on createDepositDestination/createTransfer, are honored by the API but are not yet in the published @coinbase/cdp-sdk types, so strict TypeScript rejects them (TS2353). Until the SDK types are updated, cast these requests with as any (shown in the SDK tabs below). This does not affect the CLI.
A customer’s ID is returned when you onboard them through the Customers API: the create-customer call returns a customerId, which you store in your own system. In production your app already has it, so the CUSTOMER_ID below is set by hand only for testing. New to onboarding, or need a test customer in Sandbox (using the magic SSN 000-00-0000)? See the Customers / KYC quickstart.

3. Create a liquidation deposit address

Create a crypto deposit destination on the customer’s account, with target.accountId set to that same account and target.asset = usd. Any USDC sent to it converts to USD and lands in the customer’s account.
target.accountId must equal accountId: the converted USD lands in the same account, which is why Step 6 moves it to your treasury. metadata.reference must be a UUID or integer string. Leave target off and deposited USDC stays USDC; the target is what converts it to cash.

4. Customer deposits USDC

No API call here, this step is the customer’s action. Share the deposit address with them; they send USDC to it, and it converts to USD in their account. In production the customer sends USDC on-chain from their own wallet, and your app reacts to the deposit webhook. In Sandbox there is no blockchain, so simulate the deposit in the Portal:
1

Open the customer account

In Sandbox mode in the CDP Portal, open Accounts and select the customer account.
2

Open Deposit addresses

Open the Deposit addresses tab and find the address matching your DEPOSIT_ADDRESS.
3

Simulate the deposit

Click Deposit, enter a USDC amount, and click Deposit now.
CDP Portal deposit modal on the customer account's Deposit addresses tab.

5. Confirm the customer’s USD balance

The customer deposited USDC, but the balance shows USD. That is the liquidation address doing its job.
CDP Portal showing the customer account balance as USD after the USDC deposit converted.
The USDC line reads 0 because it was converted to USD.

6. Move the USD to your treasury

Transfer the USD from the customer’s account into your treasury. Because the customer’s account is the source, include compliance.requesterIpAddress. This example transfers $50 of the $100 in the customer’s account. Transfer any amount up to what’s available.
The USD is now collected in your treasury, with no stablecoin held on your balance sheet. Wiring the treasury balance out to a bank (via payment methods) is covered separately.
CDP Portal showing the entity-owned treasury account holding the transferred USD.
The SDK snippets above build on one client and belong in a single file. Because the customer’s deposit (Step 4) happens in the middle, run it as two scripts: set up, do the deposit, then finish. Save each as a .ts file and run it with npx tsx <file>.ts (Node 22 or later; tsx runs TypeScript directly, no build step).Part 1 — set up (Steps 1 to 3). Prints the account IDs and the deposit address.
main.ts
Part 2 — after the deposit (Steps 5 to 6). Paste the two account IDs Part 1 printed.
main.ts

Good to know

  • Liquidation credits the same account the deposit address belongs to. That is why the USD lands in the customer’s account first, and Step 6 moves it to your treasury. A single deposit address cannot sweep directly into a different account.
  • Reconciliation: attach metadata (a reference or customer ID) to transfers, and query history with cdp transfers list or the Transfers API.
Transfer returns “Invalid source and target pair”? Your treasury is probably customer-owned instead of entity-owned, so the transfer is really between two accounts owned by the same customer. Check the owner with cdp accounts get $TREASURY_ACCOUNT_ID -e sandbox --jq=.owner (it should start with entity_). If it shows customer_, run cdp history clear, recreate the treasury, and confirm the owner is entity_ before retrying.

Customers / KYC

Onboard the customers whose funds this flow moves

Transfers

All transfer types, rails, and lifecycle

Crypto Deposit Destinations

Liquidation addresses and inbound deposits

Webhooks

React to deposits and transfer status in production