Your private key must be a 0x
-prefixed 64-character hex string.
This quickstart guide helps you write a standalone index.js
script that checks
your USDC balance and sends a test transfer on the Sei Atlantic-2 Testnet. It's
designed for developers looking to quickly get hands-on with
USDC on Sei.
Before you begin, make sure you have:
npm
, which comes with
Node.js
..env
file.Use the following contract address for USDC on the Sei Atlantic-2 Testnet:
0x4fCF1784B31630811181f670Aea7A7bEF803eaED
Follow these steps to install dependencies and set up your script environment.
Create a new directory and initialize it with npm
:
mkdir usdc-sei-script
cd usdc-sei-script
npm init -y
npm pkg set type=module
Install viem
and dotenv
:
npm install viem dotenv
.env
fileIn the project root, create a .env
file and add the following values:
# Your private key (64 hex characters with 0x prefix)
PRIVATE_KEY=<YOUR_PRIVATE_KEY>
# The address you want to send USDC to
RECIPIENT_ADDRESS=0x<RECIPIENT_ADDRESS>
Your private key must be a 0x
-prefixed 64-character hex string.
Create a file named index.js
. You'll build your script step-by-step in the
following sections.
In index.js
, start by importing dependencies and setting up the chain and
token configuration:
// Load environment variables from .env
import "dotenv/config";
// Import Viem utilities for blockchain interaction
import {
createPublicClient,
createWalletClient,
http,
formatUnits,
parseUnits,
} from "viem";
// Convert a raw private key string into an account object
import { privateKeyToAccount } from "viem/accounts";
// Define Sei Atlantic-2 testnet chain configuration
const seiTestnet = {
id: 1328,
name: "Sei Atlantic-2 Testnet",
network: "sei-atlantic-2",
nativeCurrency: { name: "Sei", symbol: "SEI", decimals: 18 },
rpcUrls: { default: { http: ["https://evm-rpc.atlantic-2.seinetwork.io"] } },
blockExplorers: {
default: { url: "https://seitrace.com/?chain=atlantic-2" },
},
testnet: true,
};
// Define USDC token address and its decimals on Sei testnet
const USDC_ADDRESS = "0x4fCF1784B31630811181f670Aea7A7bEF803eaED";
const USDC_DECIMALS = 6;
// Define a minimal ABI with only the required functions
const USDC_ABI = [
{
name: "balanceOf",
type: "function",
stateMutability: "view",
inputs: [{ name: "account", type: "address" }],
outputs: [{ name: "", type: "uint256" }],
},
{
name: "transfer",
type: "function",
stateMutability: "nonpayable",
inputs: [
{ name: "to", type: "address" },
{ name: "amount", type: "uint256" },
],
outputs: [{ name: "", type: "bool" }],
},
];
This ABI includes only the two methods needed for this script:
balanceOf(address)
to check the token balance.transfer(to, amount)
to send USDC.This makes the ABI compact and easier to understand. Using a minimal ABI:
Next, load your credentials from .env
and validate them:
// Load variables from .env file
const PRIVATE_KEY_RAW = process.env.PRIVATE_KEY;
const RECIPIENT = process.env.RECIPIENT_ADDRESS || process.env.RECIPIENT;
// Validate .env inputs to avoid runtime errors
if (!PRIVATE_KEY_RAW) {
console.error("Error: Set PRIVATE_KEY in your .env file");
process.exit(1);
}
if (!RECIPIENT) {
console.error("Error: Set RECIPIENT_ADDRESS in your .env file");
process.exit(1);
}
if (!/^0x[a-fA-F0-9]{40}$/.test(RECIPIENT)) {
console.error("Error: Invalid recipient address");
process.exit(1);
}
// Ensure the private key has 0x prefix for compatibility
const PRIVATE_KEY = PRIVATE_KEY_RAW.startsWith("0x")
? PRIVATE_KEY_RAW
: "0x" + PRIVATE_KEY_RAW;
Failing fast means catching mistakes early in the execution cycle. This script validates inputs to avoid:
0x
.These checks:
Set up the Viem clients and your account object:
// Convert private key to account object
const account = privateKeyToAccount(PRIVATE_KEY);
// Create a client for reading chain state (for example, balanceOf)
const publicClient = createPublicClient({
chain: seiTestnet,
transport: http(),
});
// Create a wallet client for signing and sending transactions
const walletClient = createWalletClient({
account,
chain: seiTestnet,
transport: http(),
});
const walletClient = createWalletClient({
account,
chain: seiTestnet,
transport: http(),
});
publicClient
vs. walletClient
publicClient
calls eth_call
to read blockchain data. It doesn't need your
private key.
balanceOf
, symbol
, or decimals
.walletClient
sends transactions using eth_sendTransaction
.
Both clients use the same RPC connection but serve different roles.
Now add the logic to check your balance and send tokens:
(async () => {
try {
// Read the sender's USDC balance using the balanceOf function
const balance = await publicClient.readContract({
address: USDC_ADDRESS,
abi: USDC_ABI,
functionName: "balanceOf",
args: [account.address],
});
// Convert raw balance (in smallest units) into readable format (e.g., 250.0 USDC)
const balanceFormatted = Number(formatUnits(balance, USDC_DECIMALS));
// Set the amount of USDC to transfer (can be customized)
const amount = 10;
// Log the sender address, recipient, and current balance
console.log("Sender:", account.address);
console.log("Recipient:", RECIPIENT);
console.log("USDC balance:", balanceFormatted);
// Exit if the wallet balance is insufficient
if (amount > balanceFormatted) {
console.error("Error: Insufficient USDC balance");
process.exit(1);
}
// Convert the amount to raw format (10 USDC = 10 * 10^6 units)
const amountInDecimals = parseUnits(amount.toString(), USDC_DECIMALS);
// Send the USDC by calling the transfer function of the token contract
const hash = await walletClient.writeContract({
address: USDC_ADDRESS,
abi: USDC_ABI,
functionName: "transfer",
args: [RECIPIENT, amountInDecimals],
});
// Log the transaction hash and a link to view it on the Sei block explorer
console.log("Transfer successful!");
console.log("Tx hash:", hash);
console.log(
"Explorer:",
`https://seitrace.com/?chain=atlantic-2&tx=${hash}`,
);
} catch (err) {
// Log any errors that occur (e.g., RPC errors, contract reverts)
console.error("Transfer failed:", err.message || err);
process.exit(1);
}
// Exit cleanly after the transfer completes
process.exit(0);
})();
formatUnits
and parseUnits
Token contracts use the smallest indivisible unit of a token. For USDC, that’s 6 decimal places.
formatUnits(value, 6)
converts raw blockchain balances into readable amounts
like 250.0
.parseUnits('10', 6)
converts user inputs into raw values like 10000000
(10 * 10^6).These utilities:
To run your script, use the following command:
node index.js
If the script runs successfully, your terminal will print a transaction summary like this:
Sender: 0x1A2b...7890
Recipient: 0x9F8f...1234
USDC balance: 250.0
Transfer successful!
Tx hash: 0xabc123...def456
Explorer: https://seitrace.com/?chain=atlantic-2&tx=0xabc123...def456
You can open the explorer link in your browser to view the transaction on SeiTrace.
.env
files to version
control.0x
prefix from
private keys. This script adds it back if needed to avoid errors.