Skip to main content
This guide walks you through transferring USDC on EVM testnets using Viem and Node.js. You’ll build a simple script that checks your balance and sends test transfers.

Prerequisites

Before you begin, ensure that you’ve:
  • Installed Node.js v22+
  • Prepared a testnet wallet on the selected chain funded with:
    • Testnet USDC for the transfer
    • Testnet native tokens for gas fees
You can get testnet USDC from Circle’s faucet.

Step 1. Set up the project

This step shows you how to prepare your project and environment.

1.1. Set up your development environment

Create a new directory and install the required dependencies:
Shell
# Set up your directory and initialize a Node.js project
mkdir transfer-usdc-evm
cd transfer-usdc-evm
npm init -y

# Set up module type and start command
npm pkg set type=module
npm pkg set scripts.start="npx tsx --env-file=.env index.ts"

# Install runtime dependencies
npm install viem tsx

# Install dev dependencies
npm install --save-dev typescript @types/node

1.2. Initialize and configure the project

This command creates a tsconfig.json file:
Shell
npx tsc --init
Then, edit the tsconfig.json file:
Shell
# Replace the contents of the generated file
cat <<'EOF' > tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "types": ["node"]
  }
}
EOF

1.3. Configure environment variables

Create a .env file in the project directory and add your wallet private key, replacing {YOUR_PRIVATE_KEY} with the private key from your EVM wallet and {YOUR_RECIPIENT_ADDRESS} with the address of the recipient.
Shell
echo "PRIVATE_KEY={YOUR_PRIVATE_KEY}
RECIPIENT_ADDRESS={YOUR_RECIPIENT_ADDRESS}" > .env
Warning: This is strictly for testing purposes. Never share your private key.

Step 2: Create the transfer script

In this step, you’ll build a script in TypeScript that transfers USDC on the same EVM testnet. You’ll configure the chain and contract, load environment variables, initialize Viem clients, and implement the transfer logic.

2.1. Create the script file

touch index.ts

2.2. Configure chain and contract

In index.ts, import required modules and define constants for your selected EVM chain and USDC contract:
  • Imports required modules from Viem and your .env file. - Loads the selected EVM testnet configuration. - Defines the USDC contract address, decimals and minimal ABI. - Only includes the balanceOf and transfer functions, since they’re all that’s needed for this guide.

2.3. Load environment variables

Next, load your private key and recipient address from .env:
Typescript
const { PRIVATE_KEY, RECIPIENT_ADDRESS } = process.env;

if (!PRIVATE_KEY || !isHex(PRIVATE_KEY) || PRIVATE_KEY.length !== 66) {
  throw new Error(
    "PRIVATE_KEY must be a 0x-prefixed 32-byte hex string (66 chars)",
  );
}

if (!RECIPIENT_ADDRESS || !isAddress(RECIPIENT_ADDRESS)) {
  throw new Error("RECIPIENT_ADDRESS must be a valid EVM address");
}
  • Loads values from the .env file so you don’t hardcode sensitive information. - Checks that both the private key and recipient address are set.
  • Validates the recipient address format with a regex. - Ensures the private key is properly prefixed with 0x so Viem can use it.

2.4. Initialize Viem clients

Create a public client for reading blockchain data and a wallet client for sending transactions:
TypeScript
const account = privateKeyToAccount(PRIVATE_KEY);

const publicClient = createPublicClient({
  chain,
  transport: http(),
});

const walletClient = createWalletClient({
  account,
  chain,
  transport: http(),
});
  • privateKeyToAccount creates an account object from your private key. - createPublicClient is used for reading data from the blockchain (no private key needed). - createWalletClient uses your account to sign and send transactions. - Together, these clients give you read and write access to the selected EVM testnet.

2.5. Implement the transfer logic

Now, write the main logic to check your balance and send a transfer:
TypeScript
async function main() {
  try {
    // Check balance
    const balance = await publicClient.readContract({
      address: USDC_ADDRESS,
      abi: USDC_ABI,
      functionName: "balanceOf",
      args: [account.address],
    });

    const balanceFormatted = Number(
      formatUnits(balance as bigint, USDC_DECIMALS),
    );
    const amount = 1; // send 1 USDC

    console.log("Sender:", account.address);
    console.log("Recipient:", RECIPIENT_ADDRESS);
    console.log("Balance:", balanceFormatted, "USDC");

    if (amount > balanceFormatted) {
      throw new Error("Insufficient balance");
    }

    const amountInDecimals = parseUnits(amount.toString(), USDC_DECIMALS);

    // Transfer
    const hash = await walletClient.writeContract({
      address: USDC_ADDRESS,
      abi: USDC_ABI,
      functionName: "transfer",
      args: [RECIPIENT_ADDRESS, amountInDecimals],
    });

    console.log("Transfer successful!");
    console.log("Tx hash:", hash);
    console.log("Explorer:", `${chain.blockExplorers.default.url}/tx/${hash}`);
  } catch (err) {
    console.error("Transfer failed:", err instanceof Error ? err.message : err);
    process.exit(1);
  }
}

main();
  • Reads your USDC balance using the balanceOf function. - Converts the balance into a human-readable format with formatUnits. - Sets the transfer amount (1 USDC in this example). - Validates that your balance is sufficient.
  • Converts the transfer amount back into smallest units with parseUnits. - Calls the transfer function on the USDC contract to send tokens. - Logs the transaction hash and a block explorer link so you can verify the transfer. balance into a human-readable format with formatUnits. - Sets the transfer amount (1 USDC in this example).

Step 3: Run the script

Run the script using the following command:
Shell
npm start
You’ll see output similar to the following:
Sender: 0x1A2b...7890
Recipient: 0x9F8f...1234
Balance: 250.0 USDC
Transfer successful!
Tx hash: 0xabc123...def456
Explorer: https://explorer.example.com/tx/0xabc123...def456
To verify the transfer, copy the transaction hash URL from the Explorer: line and open it in your browser. This will take you to the testnet’s block explorer, where you can view full transaction details.
Tip: If your script doesn’t output a full explorer URL, you can manually paste the transaction hash into the testnet’s block explorer.

Summary

In this quickstart, you learned how to check balances and transfer USDC on EVM testnets using Viem and Node.js. Here are the key points to remember:
  • Testnet only. Testnet USDC has no real value.
  • Gas fees. You need a small amount of the testnet’s native token for gas.
  • Security. Keep your private key in .env. Never commit secrets.
  • Minimal ABI. The script only uses balanceOf and transfer for simplicity.