Custodial wallets support transferring assets that are supported by the custodian. To transfer these assets, applications can use the same interface as non-custodial wallets through Privy’s server-side SDKs or REST API .
All transactions from custodial wallets are executed server-side.
Send a transaction
For custodial wallets, only assets that are supported by the custodian can be transferred. Below are the assets supported:
Asset Chain Custodians that support Contract/mint address USDC Base Bridge 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913USDB Base Bridge 0x100Faa513aC917181EB29f73B64Bf7a434A206feEURC Base Bridge 0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42USDC Solana Bridge EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1vUSDB Solana Bridge ENL66PGy8d8j5KNqLtCcg4uidDUac5ibt45wbjH9REzBEURC Solana Bridge HzwqbKZw8HxMN6bF2yFZNrht3c2iXXzpKcFu7uBEDKtr
Transactions from custodial wallets are gasless by default, customers do not need to enable gas
sponsorship or set sponsor: true for gas to be sponsored.
Like non-custodial wallets, custodial wallets with an owner or additional_signers require an
authorization signature for transaction
requests.
Use the sendTransaction method to send ERC20 token transfers on Base. Usage import { encodeFunctionData , erc20Abi } from 'viem' ;
const recipientAddress = '0x...' ;
const amountToSend = 10 ; // Sender wants to send 1 USDC
const decimals = 6 ; // USDC has 6 decimals
const encodedData = encodeFunctionData ({
abi: erc20Abi ,
functionName: 'transfer' ,
args: [ recipientAddress , BigInt ( amountToSend * 10 ** decimals )]
});
const { hash , caip2 } = await privy . wallets (). ethereum (). sendTransaction ( 'insert-wallet-id' , {
caip2: 'eip155:8453' ,
params: {
transaction: {
to: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' , // USDC contract on Base
data: encodedData ,
},
},
});
Parameters The ID of the custodial wallet to send the transaction from.
The CAIP-2 chain ID. For ethereum type custodial wallets on Base, this is eip155:8453.
The transaction details. The recipient address. For ERC20 transfers, this is the token contract address (e.g., USDC on Base: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913).
Encoded transaction data. For ERC20 transfers, this contains the encoded transfer(address,uint256) function call.
Returns This will be an empty string since the transaction must go through custodian screening first before being broadcasted.
The CAIP-2 chain ID confirming the transaction was sent on Base.
The transaction ID for the transaction.
Check out the API reference for more details. For Solana custodial wallets, the transaction must be a versioned transaction containing:
A createAssociatedTokenAccountIdempotent instruction for the destination wallet
A TransferChecked instruction for the SPL token transfer
Both instructions are required. The ATA creation instruction ensures the destination token account exists and provides the recipient wallet address for the custodian to process the transfer. The transaction must include a createAssociatedTokenAccountIdempotent instruction.
Transactions with only a TransferChecked instruction will be rejected.
Usage import {
createAssociatedTokenAccountIdempotentInstruction ,
createTransferCheckedInstruction ,
getAssociatedTokenAddressSync ,
TOKEN_PROGRAM_ID ,
} from '@solana/spl-token' ;
import { PublicKey , TransactionMessage , VersionedTransaction } from '@solana/web3.js' ;
const walletAddress = new PublicKey ( 'insert-custodial-wallet-address' );
const recipientWallet = new PublicKey ( 'insert-recipient-wallet-address' );
const usdcMint = new PublicKey ( 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' );
const amount = 10000 n ; // 0.01 USDC (6 decimals)
const decimals = 6 ;
// Derive the associated token accounts
const sourceAta = getAssociatedTokenAddressSync ( usdcMint , walletAddress , false , TOKEN_PROGRAM_ID );
const destinationAta = getAssociatedTokenAddressSync ( usdcMint , recipientWallet , true , TOKEN_PROGRAM_ID );
// Build the instructions
const ataInstruction = createAssociatedTokenAccountIdempotentInstruction (
walletAddress , // payer
destinationAta , // associated token account
recipientWallet , // owner of the destination token account
usdcMint , // token mint
TOKEN_PROGRAM_ID ,
);
const transferInstruction = createTransferCheckedInstruction (
sourceAta , // source token account
usdcMint , // token mint
destinationAta , // destination token account
walletAddress , // owner (signer)
amount ,
decimals ,
[],
TOKEN_PROGRAM_ID ,
);
// Build and serialize the versioned transaction
const message = new TransactionMessage ({
payerKey: walletAddress ,
recentBlockhash: '11111111111111111111111111111111' , // Privy replaces this before signing
instructions: [ ataInstruction , transferInstruction ],
}). compileToV0Message ();
const transaction = new VersionedTransaction ( message );
const encodedTransaction = Buffer . from ( transaction . serialize ()). toString ( 'base64' );
// Send via Privy
const response = await privy . wallets (). solana (). signAndSendTransaction ( 'insert-wallet-id' , {
caip2: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp' ,
params: {
transaction: encodedTransaction ,
encoding: 'base64' ,
},
});
Parameters The ID of the custodial wallet to send the transaction from.
The CAIP-2 chain ID. For Solana mainnet, this is solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp.
The base64-encoded serialized versioned transaction containing the createAssociatedTokenAccountIdempotent and TransferChecked instructions.
The encoding format for the transaction. Must be base64.
Returns This will be an empty string since the transaction must go through custodian screening first before being broadcasted.
The CAIP-2 chain ID confirming the transaction was sent on Solana.
The transaction ID for the transaction.
Next steps
Transaction lifecycle Learn about the transaction lifecycle for custodial wallets
Sending USDC recipe Learn how to format and encode ERC-20 token transfers
Authorization controls Configure policies and multi-party approvals for custodial wallets
Transaction webhooks Monitor transaction status and lifecycle events