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

# Lend

CDP Wallets lets your end users earn yield on their USDC through a native DeFi Lend API. You create a vault on top of an approved vault, and your end users deposit USDC into it from their non-custodial wallets. You earn a performance fee on the yield that their deposits generate.

> [card] Create a vault
>
> Set up a vault and your fee in CDP Portal

> [card] Deposit and withdraw
>
> Users deposit USDC and withdraw it with accrued yield

> [card] Earn fees
>
> Earn a share of the yield generated by your users' deposits

> [!NOTE]
>
> Currently, **Base Mainnet** is the only supported network, **USDC** is the only supported asset, and **Morpho** is the only supported protocol.

Lend supports both **EOAs** and **smart accounts**. Deposits and withdrawals from a smart account are sent as user operations, so they support [gas sponsorship](https://docs.cdp.coinbase.com/wallets/using-wallets/smart-accounts.md#gas-sponsorship). Deposits and withdrawals from an EOA are sent as a single transaction, and the EOA pays the gas.

# Overview

### How vaults work

A **CDP Vault** is a vault that wraps an approved, curated vault, such as Steakhouse USDC on Base. [Curators](https://docs.morpho.org/learn/concepts/curator/) allocate the vault's deposits across lending markets, and the vault earns interest from borrowers in those markets.

When you create a **CDP Vault** in CDP Portal, CDP deploys a vault onchain that wraps the curator vault. Your end users deposit USDC into your vault and receive vault shares. The value of each share grows as the underlying curator vault earns yield. When an end user withdraws, their shares are redeemed for USDC at the current share price.

For example, if an end user deposits 100 USDC into a vault earning 5% APY, their position is worth about 105 USDC after one year, before your fees.

### Fees

The yield a vault earns is subject to three layers of fees:

| Fee | Set by | Description |
| - | - | - |
| Curator fee | The curator vault | Taken by the underlying vault. The APY shown for each CDP Vault is net of this fee. |
| Your performance fee | You | A percentage of your CDP Vault's yield, paid to your fee recipient address. |
| CDP fee | CDP | 10% of your performance fee, charged on top of it. |

Your performance fee and the CDP fee are both taken from the yield, never from your end users' principal.

For example, if you set a performance fee of 20% (`2000` bps) on a vault earning 4.4% APY:

* The CDP fee is 10% of your fee, or 2% of the yield.
* Your APY is 4.4% × 20% = 0.88%, and CDP's is 4.4% × 2% = 0.088%.
* Your users earn 4.4% − 0.88% − 0.088% = 3.432% APY.

$\text{User APY} = \text{Vault APY} - \text{Your APY} - \text{CDP APY}$

$\text{User APY} = 4.4\% - 0.88\% - 0.088\% = 3.432\%$

### Fee distribution

CDP distributes accrued fees once a day. Your share is sent to your **fee recipient** as vault shares, which you [redeem for USDC](#redeem-your-fees) whenever you choose.

Your fee recipient must be an EVM account or EVM Smart Account in the same CDP project as the vault. If it's a smart account, then its owner must be a CDP EVM account. Addresses outside your project are rejected. To use a wallet you already have, such as one from MetaMask or Phantom, first [import its EVM private key](https://docs.cdp.coinbase.com/wallets/using-wallets/import-and-export.md#import-api-key-wallets) into your project. The imported account keeps the same address.

> [!TIP]
>
> Use a dedicated account as your fee recipient. Redeeming fees redeems the account's entire share balance, including any shares from the account's own deposits.

# Create a vault

Create your vault in CDP Portal.

**1. Open CDP Portal**

Navigate to the **DeFi** tab in your project's [Embedded Wallets settings](https://portal.cdp.coinbase.com/wallets/non-custodial/defi), and find the **Lend integration** section.

**2. Configure the integration**

Click **Configure**, then choose a **Vault provider** from the dropdown.

**3. Set the recipient address**

Enter the address of the EVM account or EVM Smart Account in your project that will receive your fees.

**4. Set your yield cut**

Choose your performance fee, from 0% to 45%. Portal shows the vault APY, your estimated APY, and your users' estimated APY as you adjust it.

**5. Create the vault**

Click **Create vault**. CDP deploys the vault on Base, which usually takes under a minute.

**6. Copy your vault ID**

Once the vault is active, copy its vault ID (for example, `vault_019f5f10-096a-7a23-86df-f479650d813f`). Use this ID when depositing or withdrawing. You can also pass it in `vaultIds` to list positions in a specific vault.

After the vault is deployed onchain, you can copy its vault ID and use it in your app for end users to deposit, withdraw, and view their positions. If a deployment fails, you can retry with the same or different settings.

# Integration

In React apps, use the hooks from `@coinbase/cdp-hooks` to let your end users deposit, view their positions, and withdraw. In other JavaScript apps, use the matching actions from `@coinbase/cdp-core`.

The hooks track each operation until it completes onchain. The actions return the `transactionHash` or `userOpHash` as soon as the operation is submitted, so your app tracks it from there. For smart accounts, call `getUserOperation` to check the status.

Amounts are specified in **human-readable decimal units** (for example, `"25.50"` for 25.50 USDC), not atomic units. CDP converts them to atomic units using the asset's decimals.

### Deposit into a vault

Deposit USDC from an end user's account into your vault. The end user receives vault shares in return.

> [tab] Smart account
>
> The token approval and deposit are batched into a single user operation. `useDepositToVaultFromEvmSmartAccount` submits the user operation and monitors it onchain. `status` stays `"pending"` until the operation completes.
>
> ```tsx React theme={null}
> import {
>   useCurrentUser,
>   useDepositToVaultFromEvmSmartAccount,
> } from "@coinbase/cdp-hooks";
>
> function Deposit() {
>   const { currentUser } = useCurrentUser();
>   const smartAccount = currentUser?.evmSmartAccounts?.[0];
>   const { depositToVaultFromEvmSmartAccount, data, status, error } =
>     useDepositToVaultFromEvmSmartAccount();
>
>   const handleDeposit = async () => {
>     if (!smartAccount) return;
>     await depositToVaultFromEvmSmartAccount({
>       evmSmartAccount: smartAccount,
>       vaultId: "vault_019f5f10-096a-7a23-86df-f479650d813f",
>       amount: "25.50", // 25.50 USDC
>       useCdpPaymaster: true,
>     });
>   };
>
>   return (
>     <div>
>       <button onClick={handleDeposit} disabled={status === "pending"}>
>         {status === "pending" ? "Depositing..." : "Deposit"}
>       </button>
>       {status === "success" && data?.userOpHash && (
>         <p>Confirmed: {data.userOpHash}</p>
>       )}
>       {error && <p>Error: {error.message}</p>}
>     </div>
>   );
> }
> ```
>
> ```typescript JavaScript theme={null}
> import {
>   depositToVaultFromEvmSmartAccount,
>   getCurrentUser,
> } from "@coinbase/cdp-core";
>
> const user = await getCurrentUser();
> const smartAccount = user?.evmSmartAccounts?.[0];
> if (!smartAccount) throw new Error("No smart account found");
>
> const { userOpHash } = await depositToVaultFromEvmSmartAccount({
>   evmSmartAccount: smartAccount,
>   vaultId: "vault_019f5f10-096a-7a23-86df-f479650d813f",
>   amount: "25.50", // 25.50 USDC
>   useCdpPaymaster: true,
> });
> ```

> [tab] EOA
>
> The end user's account signs an [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612) permit, so the token approval and deposit run in a single transaction. `useDepositToVaultFromEvmEoa` broadcasts the transaction and waits for it to be confirmed onchain. The EOA must hold enough ETH on Base to pay for gas.
>
> ```tsx React theme={null}
> import {
>   useCurrentUser,
>   useDepositToVaultFromEvmEoa,
> } from "@coinbase/cdp-hooks";
>
> function Deposit() {
>   const { currentUser } = useCurrentUser();
>   const evmAccount = currentUser?.evmAccounts?.[0];
>   const { depositToVaultFromEvmEoa, data, status, error } =
>     useDepositToVaultFromEvmEoa();
>
>   const handleDeposit = async () => {
>     if (!evmAccount) return;
>     await depositToVaultFromEvmEoa({
>       evmAccount,
>       vaultId: "vault_019f5f10-096a-7a23-86df-f479650d813f",
>       amount: "25.50", // 25.50 USDC
>     });
>   };
>
>   return (
>     <div>
>       <button onClick={handleDeposit} disabled={status === "pending"}>
>         {status === "pending" ? "Depositing..." : "Deposit"}
>       </button>
>       {status === "success" && data?.transactionHash && (
>         <p>Confirmed: {data.transactionHash}</p>
>       )}
>       {error && <p>Error: {error.message}</p>}
>     </div>
>   );
> }
> ```
>
> ```typescript JavaScript theme={null}
> import { depositToVaultFromEvmEoa, getCurrentUser } from "@coinbase/cdp-core";
>
> const user = await getCurrentUser();
> const evmAccount = user?.evmAccounts?.[0];
> if (!evmAccount) throw new Error("No EVM account found");
>
> const { transactionHash } = await depositToVaultFromEvmEoa({
>   evmAccount,
>   vaultId: "vault_019f5f10-096a-7a23-86df-f479650d813f",
>   amount: "25.50", // 25.50 USDC
> });
> ```

By default, the vault shares go to the depositing account. To send them to a different address, pass `receiver`.

### List lend positions

View an end user's positions in your project's vaults. Each position includes the account's vault share balance and its current value in USDC. Only non-zero positions are returned.

`useListLendPositions` automatically fetches when the user is signed in and an `evmAddress` is provided.

> [tab] Smart account
>
> ```tsx React theme={null}
> import { useCurrentUser, useListLendPositions } from "@coinbase/cdp-hooks";
>
> function LendPositions() {
>   const { currentUser } = useCurrentUser();
>   const smartAccount = currentUser?.evmSmartAccounts?.[0];
>
>   const { data, status, error } = useListLendPositions({
>     evmAddress: smartAccount,
>     enabled: !!smartAccount,
>   });
>
>   if (status === "pending") return <p>Loading...</p>;
>   if (status === "error") return <p>Error: {error?.message}</p>;
>   if (!data?.lendPositions.length) return <p>No positions found.</p>;
>
>   return (
>     <table>
>       <thead>
>         <tr>
>           <th>Vault</th>
>           <th>Shares</th>
>           <th>Balance</th>
>         </tr>
>       </thead>
>       <tbody>
>         {data.lendPositions.map((pos) => (
>           <tr key={pos.vaultId}>
>             <td>{pos.vaultId}</td>
>             <td>{pos.vaultShares}</td>
>             <td>
>               {pos.assetBalance} {pos.underlyingAsset.toUpperCase()}
>             </td>
>           </tr>
>         ))}
>       </tbody>
>     </table>
>   );
> }
> ```
>
> ```typescript JavaScript theme={null}
> import { getCurrentUser, listLendPositions } from "@coinbase/cdp-core";
>
> const user = await getCurrentUser();
> const smartAccount = user?.evmSmartAccounts?.[0];
> if (!smartAccount) throw new Error("No smart account found");
>
> const { lendPositions } = await listLendPositions({ evmAddress: smartAccount });
>
> for (const pos of lendPositions) {
>   console.log(pos.vaultId, pos.vaultShares, pos.assetBalance, pos.underlyingAsset);
> }
> ```

> [tab] EOA
>
> ```tsx React theme={null}
> import { useCurrentUser, useListLendPositions } from "@coinbase/cdp-hooks";
>
> function LendPositions() {
>   const { currentUser } = useCurrentUser();
>   const evmAccount = currentUser?.evmAccounts?.[0];
>
>   const { data, status, error } = useListLendPositions({
>     evmAddress: evmAccount,
>     enabled: !!evmAccount,
>   });
>
>   if (status === "pending") return <p>Loading...</p>;
>   if (status === "error") return <p>Error: {error?.message}</p>;
>   if (!data?.lendPositions.length) return <p>No positions found.</p>;
>
>   return (
>     <table>
>       <thead>
>         <tr>
>           <th>Vault</th>
>           <th>Shares</th>
>           <th>Balance</th>
>         </tr>
>       </thead>
>       <tbody>
>         {data.lendPositions.map((pos) => (
>           <tr key={pos.vaultId}>
>             <td>{pos.vaultId}</td>
>             <td>{pos.vaultShares}</td>
>             <td>
>               {pos.assetBalance} {pos.underlyingAsset.toUpperCase()}
>             </td>
>           </tr>
>         ))}
>       </tbody>
>     </table>
>   );
> }
> ```
>
> ```typescript JavaScript theme={null}
> import { getCurrentUser, listLendPositions } from "@coinbase/cdp-core";
>
> const user = await getCurrentUser();
> const evmAccount = user?.evmAccounts?.[0];
> if (!evmAccount) throw new Error("No EVM account found");
>
> const { lendPositions } = await listLendPositions({ evmAddress: evmAccount });
>
> for (const pos of lendPositions) {
>   console.log(pos.vaultId, pos.vaultShares, pos.assetBalance, pos.underlyingAsset);
> }
> ```

> [!NOTE]
>
> When you omit `vaultIds`, positions are found from the vaults the account has deposited into through CDP. If an account received vault shares another way, such as a direct onchain deposit or a share transfer, pass the vault ID in `vaultIds` to include that position.

### Withdraw from a vault

Withdraw USDC from an end user's position. Pass `amount` to withdraw part of the position, or `withdrawAll: true` to redeem every share the account holds, including all accrued yield. Pass exactly one of the two.

> [tab] Smart account
>
> ```tsx React theme={null}
> import {
>   useCurrentUser,
>   useWithdrawFromVaultWithEvmSmartAccount,
> } from "@coinbase/cdp-hooks";
>
> function Withdraw() {
>   const { currentUser } = useCurrentUser();
>   const smartAccount = currentUser?.evmSmartAccounts?.[0];
>   const { withdrawFromVaultWithEvmSmartAccount, data, status, error } =
>     useWithdrawFromVaultWithEvmSmartAccount();
>
>   const handleWithdrawAll = async () => {
>     if (!smartAccount) return;
>     await withdrawFromVaultWithEvmSmartAccount({
>       evmSmartAccount: smartAccount,
>       vaultId: "vault_019f5f10-096a-7a23-86df-f479650d813f",
>       withdrawAll: true,
>       useCdpPaymaster: true,
>     });
>   };
>
>   return (
>     <div>
>       <button onClick={handleWithdrawAll} disabled={status === "pending"}>
>         {status === "pending" ? "Withdrawing..." : "Withdraw all"}
>       </button>
>       {status === "success" && data?.userOpHash && (
>         <p>Confirmed: {data.userOpHash}</p>
>       )}
>       {error && <p>Error: {error.message}</p>}
>     </div>
>   );
> }
> ```
>
> ```typescript JavaScript theme={null}
> import {
>   getCurrentUser,
>   withdrawFromVaultWithEvmSmartAccount,
> } from "@coinbase/cdp-core";
>
> const user = await getCurrentUser();
> const smartAccount = user?.evmSmartAccounts?.[0];
> if (!smartAccount) throw new Error("No smart account found");
>
> const { userOpHash } = await withdrawFromVaultWithEvmSmartAccount({
>   evmSmartAccount: smartAccount,
>   vaultId: "vault_019f5f10-096a-7a23-86df-f479650d813f",
>   withdrawAll: true,
>   useCdpPaymaster: true,
> });
> ```

> [tab] EOA
>
> ```tsx React theme={null}
> import {
>   useCurrentUser,
>   useWithdrawFromVaultWithEvmEoa,
> } from "@coinbase/cdp-hooks";
>
> function Withdraw() {
>   const { currentUser } = useCurrentUser();
>   const evmAccount = currentUser?.evmAccounts?.[0];
>   const { withdrawFromVaultWithEvmEoa, data, status, error } =
>     useWithdrawFromVaultWithEvmEoa();
>
>   const handleWithdraw = async () => {
>     if (!evmAccount) return;
>     await withdrawFromVaultWithEvmEoa({
>       evmAccount,
>       vaultId: "vault_019f5f10-096a-7a23-86df-f479650d813f",
>       amount: "10", // 10 USDC
>     });
>   };
>
>   return (
>     <div>
>       <button onClick={handleWithdraw} disabled={status === "pending"}>
>         {status === "pending" ? "Withdrawing..." : "Withdraw"}
>       </button>
>       {status === "success" && data?.transactionHash && (
>         <p>Confirmed: {data.transactionHash}</p>
>       )}
>       {error && <p>Error: {error.message}</p>}
>     </div>
>   );
> }
> ```
>
> ```typescript JavaScript theme={null}
> import { getCurrentUser, withdrawFromVaultWithEvmEoa } from "@coinbase/cdp-core";
>
> const user = await getCurrentUser();
> const evmAccount = user?.evmAccounts?.[0];
> if (!evmAccount) throw new Error("No EVM account found");
>
> const { transactionHash } = await withdrawFromVaultWithEvmEoa({
>   evmAccount,
>   vaultId: "vault_019f5f10-096a-7a23-86df-f479650d813f",
>   amount: "10", // 10 USDC
> });
> ```

By default, the USDC goes to the withdrawing account. To send it to a different address, pass `receiver`.

### One operation at a time

An account can run one Lend operation at a time. If you start a deposit or withdrawal while another is still in progress for the same account, the request returns a `409`. Wait for the first operation to complete before starting another. The account stays locked for 30 seconds after the request starts. If you still get a `409`, wait a few seconds and try again.

# Redeem your fees

CDP sends your share of the fees to your fee recipient as vault shares once a day. To convert them to USDC, call `redeemVaultFees` on your fee recipient account with the [CDP SDK](https://docs.cdp.coinbase.com/wallets/quickstart/api-key-auth.md), or run `lend redeem-fees` with the CDP CLI. This redeems the account's full share balance in the vault. If the account has no shares to redeem, the request returns a `400`.

> [tab] Smart account
>
> The approval and redemption are batched into a single user operation, so you can sponsor the gas with `useCdpPaymaster` or `paymasterUrl`.
>
> ```ts TypeScript theme={null}
> import { CdpClient } from "@coinbase/cdp-sdk";
>
> const cdp = new CdpClient();
> const owner = await cdp.evm.getAccount({ address: "0xOwner" });
> const smartAccount = await cdp.evm.getSmartAccount({
>   address: "0xYourFeeRecipient",
>   owner,
> });
>
> const { userOpHash } = await smartAccount.redeemVaultFees({
>   vaultId: "vault_019f5f10-096a-7a23-86df-f479650d813f",
>   useCdpPaymaster: true,
> });
>
> const result = await smartAccount.waitForUserOperation({ userOpHash });
> ```
>
> ```python Python theme={null}
> from cdp import CdpClient
>
> async with CdpClient() as cdp:
>     owner = await cdp.evm.get_account(address="0xOwner")
>     smart_account = await cdp.evm.get_smart_account(
>         address="0xYourFeeRecipient",
>         owner=owner,
>     )
>
>     user_op_hash = await smart_account.redeem_vault_fees(
>         vault_id="vault_019f5f10-096a-7a23-86df-f479650d813f",
>         use_cdp_paymaster=True,
>     )
>
>     result = await smart_account.wait_for_user_operation(user_op_hash=user_op_hash)
> ```
>
> ```bash CDP CLI theme={null}
> cdp evm smart-accounts lend redeem-fees 0xYourFeeRecipient \
>   vaultId=vault_019f5f10-096a-7a23-86df-f479650d813f \
>   useCdpPaymaster:=true
> ```

> [tab] EOA
>
> The account signs a permit, so the approval and redemption run in a single transaction. The account pays the gas.
>
> ```ts TypeScript theme={null}
> import { CdpClient } from "@coinbase/cdp-sdk";
> import { createPublicClient, http } from "viem";
> import { base } from "viem/chains";
>
> const cdp = new CdpClient();
> const publicClient = createPublicClient({ chain: base, transport: http() });
> const account = await cdp.evm.getAccount({ address: "0xYourFeeRecipient" });
>
> const { transactionHash } = await account.redeemVaultFees({
>   vaultId: "vault_019f5f10-096a-7a23-86df-f479650d813f",
> });
>
> const receipt = await publicClient.waitForTransactionReceipt({ hash: transactionHash });
> ```
>
> ```python Python theme={null}
> from cdp import CdpClient
> from web3 import Web3
>
> async with CdpClient() as cdp:
>     account = await cdp.evm.get_account(address="0xYourFeeRecipient")
>
>     transaction_hash = await account.redeem_vault_fees(
>         vault_id="vault_019f5f10-096a-7a23-86df-f479650d813f",
>     )
>
>     w3 = Web3(Web3.HTTPProvider("https://mainnet.base.org"))
>     receipt = w3.eth.wait_for_transaction_receipt(transaction_hash)
> ```
>
> ```bash CDP CLI theme={null}
> cdp evm accounts lend redeem-fees 0xYourFeeRecipient \
>   vaultId=vault_019f5f10-096a-7a23-86df-f479650d813f
> ```

The CLI uses the same API key and wallet secret as the SDK, and needs the wallet secret to sign the redemption. If you haven't added it yet, run `cdp env live --wallet-secret-file ./cdp_wallet_secret.txt`. See the [Server Wallet quickstart](https://docs.cdp.coinbase.com/wallets/quickstart/api-key-auth.md) for CLI setup.

# What to read next

* [Smart accounts](https://docs.cdp.coinbase.com/wallets/using-wallets/smart-accounts.md): Sponsor gas for your end users' deposits and withdrawals.
* [Borrow](https://docs.cdp.coinbase.com/wallets/using-wallets/defi-borrow.md): Let your end users borrow against their crypto.
* [EVM policies](https://docs.cdp.coinbase.com/wallets/security-and-policies/policy-engine/evm-policies.md): Control which transactions your project's accounts can send.
