> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cdp.coinbase.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Settle customer stablecoins as USD

> Accept customer USDC, auto-convert it to USD, and collect it in your treasury, with the CDP CLI or SDK.

<Note>
  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](https://www.coinbase.com/developer-platform/developer-interest) and our team will follow up.
</Note>

**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**](/get-started/sandbox/quickstart), 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

<Frame>
  <img src="https://mintcdn.com/coinbase-prod/c0Ae2_X0GFrHcyYT/images/recipes/settle-stablecoins-flow.png?fit=max&auto=format&n=c0Ae2_X0GFrHcyYT&q=85&s=2a5820774084b67b998dee8fc1fc7d55" alt="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." width="1999" height="422" data-path="images/recipes/settle-stablecoins-flow.png" />
</Frame>

## Prerequisites

* Your entity is onboarded (KYB'd) with **Custodial Wallets enabled**. See the [Custodial Wallets overview](/wallets/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](/customers-kyc/quickstart).
* The CDP CLI or TypeScript SDK installed and pointed at Sandbox. Both require [Node.js](https://nodejs.org/) 22 or later.

## Account structure

| Role                             | Account                         | Purpose                              |
| -------------------------------- | ------------------------------- | ------------------------------------ |
| Your platform (entity, KYB'd)    | Treasury account (entity-owned) | Collects the converted USD           |
| Your customer (onboarded, KYC'd) | Customer-owned account          | Receives USDC and converts it to USD |

## Install and configure

<Tabs>
  <Tab title="CDP CLI">
    ```bash theme={null}
    npm install -g @coinbase/cdp-cli
    cdp env sandbox --key-file ~/Downloads/cdp_api_key.json
    ```
  </Tab>

  <Tab title="TypeScript SDK">
    ```bash theme={null}
    npm install @coinbase/cdp-sdk
    export CDP_API_KEY_ID="YOUR_API_KEY_ID"
    export CDP_API_KEY_SECRET="YOUR_API_KEY_SECRET"
    ```

    ```typescript main.ts theme={null}
    import { CdpClient } from "@coinbase/cdp-sdk";
    import { randomUUID } from "node:crypto";

    const cdp = new CdpClient({
      basePath: "https://sandbox.cdp.coinbase.com/platform",
    });

    const CUSTOMER_ID = "customer_..."; // your onboarded customer's ID
    ```

    <Note>
      The CDP SDK is TypeScript-only. For other languages, use the CLI or call the REST API directly. `CDP_API_KEY_ID` and `CDP_API_KEY_SECRET` are the `id` and `privateKey` from your downloaded key file. Each write passes an `idempotencyKey` (a random UUID) so a retried request returns the same result instead of creating a duplicate account or transfer.
    </Note>
  </Tab>
</Tabs>

<Note>
  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**.
</Note>

<Note>
  **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.
</Note>

## 1. Create your treasury account

Your entity-owned account that collects the USD. Omitting an owner makes the account entity-owned.

<Tabs>
  <Tab title="CDP CLI">
    ```bash theme={null}
    TREASURY_ACCOUNT_ID=$(cdp accounts create name="Treasury account" -e sandbox --jq=.accountId)
    echo "TREASURY_ACCOUNT_ID=$TREASURY_ACCOUNT_ID"
    ```

    Confirm it is entity-owned. The owner should start with `entity_`:

    ```bash theme={null}
    cdp accounts get $TREASURY_ACCOUNT_ID -e sandbox --jq=.owner
    ```

    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.
  </Tab>

  <Tab title="TypeScript SDK">
    ```typescript main.ts theme={null}
    const treasury = await cdp.accounts.createAccount({
      idempotencyKey: randomUUID(),
      name: "Treasury account",
    });
    ```
  </Tab>
</Tabs>

<Info>
  Account names must be unique within your entity. A duplicate name returns `409 already_exists`.
</Info>

## 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`.

<Warning>
  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.
</Warning>

<Accordion title="How do I get a customer ID?">
  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](/customers-kyc/quickstart).
</Accordion>

<Tabs>
  <Tab title="CDP CLI">
    ```bash theme={null}
    export CUSTOMER_ID=customer_...   # your onboarded customer's ID

    CUSTOMER_ACCOUNT_ID=$(cdp accounts create \
      owner=$CUSTOMER_ID name="Customer account" \
      compliance.requesterIpAddress=8.8.8.8 \
      -e sandbox --jq=.accountId)
    echo "CUSTOMER_ACCOUNT_ID=$CUSTOMER_ACCOUNT_ID"
    ```
  </Tab>

  <Tab title="TypeScript SDK">
    ```typescript main.ts theme={null}
    const customerAccount = await cdp.accounts.createAccount({
      idempotencyKey: randomUUID(),
      owner: CUSTOMER_ID,
      name: "Customer account",
      compliance: { requesterIpAddress: "8.8.8.8" },
    } as any); // owner + compliance work at runtime but aren't in the SDK types yet
    ```
  </Tab>
</Tabs>

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

<Tabs>
  <Tab title="CDP CLI">
    ```bash theme={null}
    DEPOSIT_ADDRESS=$(cdp deposit-destinations create \
      accountId=$CUSTOMER_ACCOUNT_ID \
      type=crypto \
      crypto.network=base \
      target.accountId=$CUSTOMER_ACCOUNT_ID \
      target.asset=usd \
      compliance.requesterIpAddress=8.8.8.8 \
      metadata.reference=77b50ed5-7e8b-41a5-a6ac-ff3cc5a08981 \
      -e sandbox --jq=.crypto.address)
    echo "DEPOSIT_ADDRESS=$DEPOSIT_ADDRESS"
    ```
  </Tab>

  <Tab title="TypeScript SDK">
    ```typescript main.ts theme={null}
    const destination = await cdp.depositDestinations.createDepositDestination({
      idempotencyKey: randomUUID(),
      type: "crypto",
      accountId: customerAccount.accountId,
      crypto: { network: "base" },
      target: { accountId: customerAccount.accountId, asset: "usd" },
      compliance: { requesterIpAddress: "8.8.8.8" },
      metadata: { reference: randomUUID() },
    } as any); // compliance works at runtime but isn't in the SDK types yet
    console.log("Deposit address:", destination.crypto?.address);
    ```
  </Tab>
</Tabs>

<Info>
  `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.
</Info>

## 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:

<Steps>
  <Step title="Open the customer account">
    In Sandbox mode in the [CDP Portal](https://portal.cdp.coinbase.com), open **Accounts** and select the customer account.
  </Step>

  <Step title="Open Deposit addresses">
    Open the **Deposit addresses** tab and find the address matching your `DEPOSIT_ADDRESS`.
  </Step>

  <Step title="Simulate the deposit">
    Click **Deposit**, enter a USDC amount, and click **Deposit now**.
  </Step>
</Steps>

<Frame>
  <img src="https://mintcdn.com/coinbase-prod/c0Ae2_X0GFrHcyYT/images/recipes/settle-stablecoins-portal-deposit.png?fit=max&auto=format&n=c0Ae2_X0GFrHcyYT&q=85&s=a53d5d8d7cf8c8eb687c4b38e9958efe" alt="CDP Portal deposit modal on the customer account's Deposit addresses tab." width="476" height="572" data-path="images/recipes/settle-stablecoins-portal-deposit.png" />
</Frame>

## 5. Confirm the customer's USD balance

The customer deposited USDC, but the balance shows USD. That is the liquidation address doing its job.

<Tabs>
  <Tab title="CDP CLI">
    ```bash theme={null}
    cdp accounts balances $CUSTOMER_ACCOUNT_ID -e sandbox
    ```
  </Tab>

  <Tab title="TypeScript SDK">
    ```typescript main.ts theme={null}
    const { balances } = await cdp.accounts.listBalances({
      accountId: customerAccount.accountId,
    });
    console.log(balances);
    ```
  </Tab>
</Tabs>

<Frame>
  <img src="https://mintcdn.com/coinbase-prod/c0Ae2_X0GFrHcyYT/images/recipes/settle-stablecoins-portal-balance.png?fit=max&auto=format&n=c0Ae2_X0GFrHcyYT&q=85&s=35184df09752982f7c646dae7df441e4" alt="CDP Portal showing the customer account balance as USD after the USDC deposit converted." width="1458" height="757" data-path="images/recipes/settle-stablecoins-portal-balance.png" />
</Frame>

<Accordion title="Example response">
  ```json theme={null}
  {
    "balances": [
      {
        "amount": { "USD": { "available": "100", "total": "100" } },
        "asset": { "symbol": "USD", "name": "United States Dollar", "type": "fiat", "decimals": 2 }
      },
      {
        "amount": { "USDC": { "available": "0", "total": "0" } },
        "asset": { "symbol": "USDC", "name": "USDC", "type": "crypto", "decimals": 6 }
      }
    ]
  }
  ```
</Accordion>

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.

<Tabs>
  <Tab title="CDP CLI">
    ```bash theme={null}
    cdp transfers create -e sandbox \
      source.accountId=$CUSTOMER_ACCOUNT_ID source.asset=usd \
      target.accountId=$TREASURY_ACCOUNT_ID target.asset=usd \
      amount=50.00 asset=usd \
      'execute:=true' \
      compliance.requesterIpAddress=8.8.8.8
    ```
  </Tab>

  <Tab title="TypeScript SDK">
    ```typescript main.ts theme={null}
    const transfer = await cdp.transfers.createTransfer({
      idempotencyKey: randomUUID(),
      source: { accountId: customerAccount.accountId, asset: "usd" },
      target: { accountId: treasury.accountId, asset: "usd" },
      amount: "50.00",
      asset: "usd",
      execute: true,
      compliance: { requesterIpAddress: "8.8.8.8" },
    } as any); // compliance works at runtime but isn't in the SDK types yet
    console.log("Transfer:", transfer.status);
    ```
  </Tab>
</Tabs>

<Accordion title="Example response">
  ```json theme={null}
  {
    "transferId": "transfer_...",
    "status": "completed",
    "source": { "accountId": "...", "asset": "usd" },
    "sourceAmount": "50",
    "sourceAsset": "usd",
    "target": { "accountId": "...", "asset": "usd" },
    "targetAmount": "50",
    "targetAsset": "usd",
    "createdAt": "...",
    "executedAt": "...",
    "completedAt": "...",
    "updatedAt": "..."
  }
  ```
</Accordion>

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](/payments/payment-methods/quickstart)) is covered separately.

<Frame>
  <img src="https://mintcdn.com/coinbase-prod/c0Ae2_X0GFrHcyYT/images/recipes/settle-stablecoins-treasury-balance.png?fit=max&auto=format&n=c0Ae2_X0GFrHcyYT&q=85&s=b5b6d72aa4e271ee81fd74dd3073721c" alt="CDP Portal showing the entity-owned treasury account holding the transferred USD." width="1455" height="779" data-path="images/recipes/settle-stablecoins-treasury-balance.png" />
</Frame>

<Accordion title="Run the SDK flow as scripts">
  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.

  ```typescript main.ts theme={null}
  import { CdpClient } from "@coinbase/cdp-sdk";
  import { randomUUID } from "node:crypto";

  const cdp = new CdpClient({ basePath: "https://sandbox.cdp.coinbase.com/platform" });
  const CUSTOMER_ID = "customer_..."; // your onboarded customer's ID

  async function main() {
    const treasury = await cdp.accounts.createAccount({
      idempotencyKey: randomUUID(),
      name: "Treasury account",
    });
    console.log("TREASURY_ACCOUNT_ID =", treasury.accountId);

    const customerAccount = await cdp.accounts.createAccount({
      idempotencyKey: randomUUID(),
      owner: CUSTOMER_ID,
      name: "Customer account",
      compliance: { requesterIpAddress: "8.8.8.8" },
    } as any); // owner + compliance work at runtime but aren't in the SDK types yet
    console.log("CUSTOMER_ACCOUNT_ID =", customerAccount.accountId);

    const destination = await cdp.depositDestinations.createDepositDestination({
      idempotencyKey: randomUUID(),
      type: "crypto",
      accountId: customerAccount.accountId,
      crypto: { network: "base" },
      target: { accountId: customerAccount.accountId, asset: "usd" },
      compliance: { requesterIpAddress: "8.8.8.8" },
      metadata: { reference: randomUUID() },
    } as any); // compliance works at runtime but isn't in the SDK types yet
    console.log("DEPOSIT_ADDRESS =", destination.crypto?.address);
  }

  main().catch(console.error);
  ```

  **Part 2 — after the deposit (Steps 5 to 6).** Paste the two account IDs Part 1 printed.

  ```typescript main.ts theme={null}
  import { CdpClient } from "@coinbase/cdp-sdk";
  import { randomUUID } from "node:crypto";

  const cdp = new CdpClient({ basePath: "https://sandbox.cdp.coinbase.com/platform" });

  const CUSTOMER_ACCOUNT_ID = "account_..."; // from Part 1
  const TREASURY_ACCOUNT_ID = "account_...";  // from Part 1

  async function main() {
    const { balances } = await cdp.accounts.listBalances({
      accountId: CUSTOMER_ACCOUNT_ID,
    });
    console.log(balances);

    const transfer = await cdp.transfers.createTransfer({
      idempotencyKey: randomUUID(),
      source: { accountId: CUSTOMER_ACCOUNT_ID, asset: "usd" },
      target: { accountId: TREASURY_ACCOUNT_ID, asset: "usd" },
      amount: "50.00",
      asset: "usd",
      execute: true,
      compliance: { requesterIpAddress: "8.8.8.8" },
    } as any); // compliance works at runtime but isn't in the SDK types yet
    console.log("Transfer:", transfer.status);
  }

  main().catch(console.error);
  ```
</Accordion>

## 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](/api-reference/v2/rest-api/transfers/transfers).

<Warning>
  **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.
</Warning>

## What to read next

<CardGroup cols={2}>
  <Card title="Customers / KYC" icon="user-check" href="/customers-kyc/quickstart">
    Onboard the customers whose funds this flow moves
  </Card>

  <Card title="Transfers" icon="arrow-right-arrow-left" href="/payments/transfers/quickstart">
    All transfer types, rails, and lifecycle
  </Card>

  <Card title="Crypto Deposit Destinations" icon="arrow-down-to-line" href="/payments/crypto-deposit-destinations/quickstart">
    Liquidation addresses and inbound deposits
  </Card>

  <Card title="Webhooks" icon="webhook" href="/webhooks/transfers/overview">
    React to deposits and transfer status in production
  </Card>
</CardGroup>
