In this guide, you build a script that:
Sends USDC from an
EOA
dev-controlled wallet to a recipient address.
Requires the source wallet to pay gas fees for the transfer transaction.
Verifies the updated token balance and recent outbound transactions.
Prerequisites
Before you begin, ensure you have:
Step 1. Set up your project
Skip this step if you completed the Create a Dev-Controlled
Wallet quickstart. You can
reuse the dev-controlled-projects project folder and its .env file.
Create a .env file in your project directory and add the following variables:
CIRCLE_API_KEY=YOUR_API_KEY
CIRCLE_ENTITY_SECRET=YOUR_ENTITY_SECRET
CIRCLE_WALLET_ADDRESS=YOUR_WALLET_ADDRESS
CIRCLE_WALLET_BLOCKCHAIN=YOUR_WALLET_BLOCKCHAIN
Where:
YOUR_API_KEY: Your Circle Developer API key
YOUR_ENTITY_SECRET: Your Circle Developer
Entity Secret
YOUR_WALLET_ADDRESS: Wallet source address (used for the transfer)
YOUR_WALLET_BLOCKCHAIN: Wallet blockchain (for example, ARC-TESTNET). The
script derives the wallet ID from this and the wallet address using the API.
Step 2. Create the script
Create a file named send-assets.ts in your project folder. Each subsection
below adds a new functionality. If you prefer, you can skip ahead and
copy the full script .
2.1. Send the transfer
Add the imports and the code that transfers USDC to a recipient address, then
polls until the transaction completes:
// send-assets.ts
import {
initiateDeveloperControlledWalletsClient ,
type TokenBlockchain ,
type EvmBlockchain ,
} from "@circle-fin/developer-controlled-wallets" ;
// Hardcoded recipient address; replace with any valid Arc Testnet address.
const DESTINATION_ADDRESS = "0xb505c4ad888c05bc8c6f2bf237f57f2b1a11a0d2" ;
const TRANSFER_AMOUNT_USDC = "0.5" ;
const ARC_TESTNET_USDC = "0x3600000000000000000000000000000000000000" ;
function sleep ( ms : number ) {
return new Promise (( resolve ) => setTimeout ( resolve , ms ));
}
async function main () {
const apiKey = process . env . CIRCLE_API_KEY ;
const entitySecret = process . env . CIRCLE_ENTITY_SECRET ;
const walletAddress = process . env . CIRCLE_WALLET_ADDRESS ;
const blockchain = process . env . CIRCLE_WALLET_BLOCKCHAIN ;
if ( ! apiKey || ! entitySecret || ! walletAddress || ! blockchain ) {
throw new Error (
"CIRCLE_API_KEY, CIRCLE_ENTITY_SECRET, CIRCLE_WALLET_ADDRESS, and CIRCLE_WALLET_BLOCKCHAIN are required in .env"
);
}
const client = initiateDeveloperControlledWalletsClient ({
apiKey ,
entitySecret ,
});
const deriveResponse = await client . deriveWalletByAddress ({
sourceBlockchain: blockchain as EvmBlockchain ,
walletAddress ,
targetBlockchain: blockchain as EvmBlockchain ,
});
const walletId = deriveResponse . data ?. wallet ?. id ;
if ( ! walletId ) {
throw new Error ( "deriveWalletByAddress: no wallet id in response" );
}
console . log ( " \n Sender address:" , walletAddress );
console . log ( "Recipient address:" , DESTINATION_ADDRESS );
console . log ( ` \n Sending ${ TRANSFER_AMOUNT_USDC } USDC to recipient...` );
const txResponse = await client . createTransaction ({
blockchain: blockchain as TokenBlockchain ,
walletAddress ,
destinationAddress: DESTINATION_ADDRESS ,
amount: [ TRANSFER_AMOUNT_USDC ],
tokenAddress: ARC_TESTNET_USDC ,
fee: { type: "level" , config: { feeLevel: "MEDIUM" } },
});
const txId = txResponse . data ?. id ;
const txState = txResponse . data ?. state ;
if ( ! txId ) {
throw new Error ( "Transaction creation failed: no ID returned" );
}
console . log ( "Transaction ID:" , txId );
console . log ( "Initial State:" , txState );
// Poll until transaction is COMPLETE (or terminal)
const terminalStates = new Set ([ "COMPLETE" , "FAILED" , "CANCELLED" , "DENIED" ]);
let currentState = txState ;
while ( ! terminalStates . has ( currentState )) {
await sleep ( 3000 );
const pollResponse = await client . getTransaction ({ id: txId });
const tx = pollResponse . data ?. transaction ;
currentState = tx ?. state ?? "" ;
console . log ( "State:" , currentState );
if ( currentState === "COMPLETE" && tx ?. txHash ) {
console . log ( `Explorer: https://testnet.arcscan.app/tx/ ${ tx . txHash } ` );
}
}
if ( currentState !== "COMPLETE" ) {
throw new Error ( `Transaction ended in state: ${ currentState } ` );
}
See all 82 lines
2.2. Verify the balance
Add the code that fetches the sender’s token balance and lists recent outbound
transactions:
// Verify sender's token balance
console . log ( " \n Token Balances:" );
const balanceResponse = await client . getWalletTokenBalance ({ id: walletId });
const tokenBalances = balanceResponse . data ?. tokenBalances ;
if ( ! tokenBalances || tokenBalances . length === 0 ) {
console . log ( " No token balances found." );
} else {
for ( const balance of tokenBalances ) {
console . log ( ` ${ balance . token ?. symbol ?? "Unknown" } : ${ balance . amount } ` );
}
}
// List recent outbound transactions
console . log ( " \n Outbound Transactions:" );
const txListResponse = await client . listTransactions ({
walletIds: [ walletId ],
txType: "OUTBOUND" ,
});
const transactions = txListResponse . data ?. transactions ;
if ( ! transactions || transactions . length === 0 ) {
console . log ( " No outbound transactions found." );
} else {
for ( const tx of transactions ) {
console . log ( ` TX: https://testnet.arcscan.app/tx/ ${ tx . txHash } ` );
console . log ( ` To: ${ tx . destinationAddress } ` );
console . log ( ` Amount: ${ tx . amounts ?. join ( ", " ) ?? "—" } ` );
console . log ( ` State: ${ tx . state } ` );
console . log ( ` Date: ${ tx . createDate } ` );
console . log ();
}
}
}
main (). catch (( err ) => {
console . error ( "Error:" , err . message || err );
process . exit ( 1 );
});
See all 39 lines
2.3. Copy the full script
Here is the complete send-assets.ts:
/**
* send-assets.ts: Send a Transaction to Transfer Assets
*
* 1. Transfer USDC to a recipient address
* 2. Verify the sender's token balance
* 3. List recent outbound transactions
*
* Required .env (or environment):
* CIRCLE_API_KEY, CIRCLE_ENTITY_SECRET,
* CIRCLE_WALLET_ADDRESS, CIRCLE_WALLET_BLOCKCHAIN
*
* Usage:
* node --env-file=.env --import=tsx send-assets.ts
*/
import {
initiateDeveloperControlledWalletsClient ,
type TokenBlockchain ,
type EvmBlockchain ,
} from "@circle-fin/developer-controlled-wallets" ;
// Replace with any valid Arc Testnet address.
const DESTINATION_ADDRESS = "0xb505c4ad888c05bc8c6f2bf237f57f2b1a11a0d2" ;
const TRANSFER_AMOUNT_USDC = "0.5" ;
const ARC_TESTNET_USDC = "0x3600000000000000000000000000000000000000" ;
async function main () {
const apiKey = process . env . CIRCLE_API_KEY ;
const entitySecret = process . env . CIRCLE_ENTITY_SECRET ;
const walletAddress = process . env . CIRCLE_WALLET_ADDRESS ;
const blockchain = process . env . CIRCLE_WALLET_BLOCKCHAIN ;
if ( ! apiKey || ! entitySecret || ! walletAddress || ! blockchain ) {
throw new Error (
"CIRCLE_API_KEY, CIRCLE_ENTITY_SECRET, CIRCLE_WALLET_ADDRESS, and CIRCLE_WALLET_BLOCKCHAIN are required in .env" ,
);
}
const client = initiateDeveloperControlledWalletsClient ({
apiKey ,
entitySecret ,
});
const deriveResponse = await client . deriveWalletByAddress ({
sourceBlockchain: blockchain as EvmBlockchain ,
walletAddress ,
targetBlockchain: blockchain as EvmBlockchain ,
});
const walletId = deriveResponse . data ?. wallet ?. id ;
if ( ! walletId ) {
throw new Error ( "deriveWalletByAddress: no wallet id in response" );
}
// Transfer USDC
console . log ( "Sender address:" , walletAddress );
console . log ( "Recipient address:" , DESTINATION_ADDRESS );
console . log ( ` \n Sending ${ TRANSFER_AMOUNT_USDC } USDC to recipient...` );
const txResponse = await client . createTransaction ({
blockchain: blockchain as TokenBlockchain ,
walletAddress ,
destinationAddress: DESTINATION_ADDRESS ,
amount: [ TRANSFER_AMOUNT_USDC ],
tokenAddress: ARC_TESTNET_USDC ,
fee: { type: "level" , config: { feeLevel: "MEDIUM" } },
});
const txId = txResponse . data ?. id ;
if ( ! txId ) throw new Error ( "Transaction creation failed: no ID returned" );
console . log ( "Transaction ID:" , txId );
// Poll until transaction reaches a terminal state
let currentState : string | undefined = txResponse . data ?. state ;
while (
! currentState ||
! [ "COMPLETE" , "FAILED" , "CANCELLED" , "DENIED" ]. includes ( currentState )
) {
await new Promise (( resolve ) => setTimeout ( resolve , 3000 ));
const poll = await client . getTransaction ({ id: txId });
const tx = poll . data ?. transaction ;
currentState = tx ?. state ;
console . log ( "Transaction state:" , currentState );
if ( currentState === "COMPLETE" && tx ?. txHash ) {
console . log ( `Explorer: https://testnet.arcscan.app/tx/ ${ tx . txHash } ` );
}
}
if ( currentState !== "COMPLETE" ) {
throw new Error ( `Transaction ended in state: ${ currentState } ` );
}
// Verify sender's token balance
console . log ( " \n Token Balances:" );
const balanceResponse = await client . getWalletTokenBalance ({ id: walletId });
const tokenBalances = balanceResponse . data ?. tokenBalances ;
if ( ! tokenBalances || tokenBalances . length === 0 ) {
console . log ( " No token balances found." );
} else {
for ( const balance of tokenBalances ) {
console . log ( ` ${ balance . token ?. symbol ?? "Unknown" } : ${ balance . amount } ` );
}
}
// List recent outbound transactions
console . log ( " \n Outbound Transactions:" );
const txListResponse = await client . listTransactions ({
walletIds: [ walletId ],
txType: "OUTBOUND" ,
});
const transactions = txListResponse . data ?. transactions ;
if ( ! transactions || transactions . length === 0 ) {
console . log ( " No outbound transactions found." );
} else {
for ( const tx of transactions ) {
console . log ( ` TX: https://testnet.arcscan.app/tx/ ${ tx . txHash } ` );
console . log ( ` To: ${ tx . destinationAddress } ` );
console . log ( ` Amount: ${ tx . amounts ?. join ( ", " ) ?? "—" } ` );
console . log ( ` State: ${ tx . state } ` );
console . log ( ` Date: ${ tx . createDate } ` );
console . log ();
}
}
}
main (). catch (( err ) => {
console . error ( "Error:" , err . message || err );
process . exit ( 1 );
});
See all 125 lines
Step 3. Run the script
From your project folder (the same directory as send-assets.ts and .env),
run:
node --env-file=.env --import=tsx send-assets.ts
You should see output similar to:
Sending 0.5 USDC to recipient...
Transaction ID: ...
Transaction state: COMPLETE
Explorer: https://testnet.arcscan.app/tx/0x...
Token Balances:
USDC: ...
Outbound Transactions:
TX: https://testnet.arcscan.app/tx/0x...
To: ...
Amount: ...
State: COMPLETE
Date: ...
The balance may be slightly lower than before the transfer because of gas
fees.