> ## Documentation Index
> Fetch the complete documentation index at: https://seilabs-docs-evm-cookbook.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# USDC on Sei

> Guide to integrating USDC stablecoin on Sei using Viem and Node.js for transfers and balance checks.

## Addresses and decimals

* Sei Testnet USDC: [`0x4fCF1784B31630811181f670Aea7A7bEF803eaED`](https://testnet.seiscan.io/address/0x4fCF1784B31630811181f670Aea7A7bEF803eaED)
* Sei Mainnet USDC: [`0xe15fC38F6D8c56aF07bbCBe3BAf5708A2Bf42392`](https://seiscan.io/address/0xe15fC38F6D8c56aF07bbCBe3BAf5708A2Bf42392)
* Decimals: `6`

## Overview

USDC is a digital dollar, also known as a stablecoin, issued by [Circle](http://developers.circle.com). It runs on many of the world's leading blockchains. USDC is designed to represent US dollars on the internet. It is backed 100% by highly liquid cash and cash-equivalent assets, so it is always redeemable 1:1 for USD. It is commonly used for payments, trading, and on-ramps and off-ramps in dApps.

On Sei, you can transfer USDC like any standard ERC-20 token.

This guide shows how to build a standalone `index.js` script with viem and Node.js. The script checks your USDC balance and sends a test transfer to another address. The sample is a minimal, generic ERC-20 flow that uses USDC as the example token.

### Prerequisites

* Node.js v18 or later, with `"type": "module"` in `package.json`
* viem and dotenv installed
* A Sei wallet that holds USDC and SEI (for gas) on your selected network. The default network is Sei Testnet.
* To get USDC on Sei Testnet, use the [Circle Faucet](https://faucet.circle.com). You can also use the [Circle CCTP v2](https://github.com/circlefin/circle-cctp-crosschain-transfer) sample application to transfer USDC cross-chain to your Sei wallet.
* A private key and a recipient address, stored in a `.env` file
* Optional: set `SEI_NETWORK=testnet|mainnet` (defaults to `testnet`)

## Project setup

Follow these steps to set up your project and environment:

* To initialize a Node.js project and install the dependencies, create a project folder. Then run these commands in it:

```shell theme={null}
npm init -y
npm install viem dotenv
```

* **Prepare environment variables**: In the project root, create a file named `.env`. Add your private key and the recipient address to it:

```
PRIVATE_KEY=<YOUR_PRIVATE_KEY>       # 0x-prefixed 64-hex-character key
RECIPIENT_ADDRESS=0x<RECIPIENT_ADDRESS>
# Optional (defaults to testnet): testnet | mainnet
SEI_NETWORK=testnet
```

* **Create the script file**: Create an `index.js` file in the project directory. You build this script step by step in the next section. Make sure that your Node.js environment can handle ES module imports, because the code uses `import` syntax.

## Script breakdown

Open `index.js` in your editor. Then add these script sections. An explanation follows each one:

1\. Import modules and define chain and token constants: First, import the required functions from viem. Then set up constants for the Sei networks (Sei Testnet and Sei Mainnet) and the USDC token contract.

The constants include the chain ID, RPC URL, token address, decimals, and a minimal ABI for the `balanceOf` and `transfer` functions of the USDC contract. The ABI has only the function signatures that the script needs:

```ts theme={null}
import 'dotenv/config';
import { createPublicClient, createWalletClient, http, formatUnits, parseUnits } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';

// --- Chain and Contract Config ---
const seiTestnet = {
	id: 1328,
	name: 'Sei Testnet',
	network: 'sei-atlantic-2',
	nativeCurrency: { name: 'Sei', symbol: 'SEI', decimals: 18 },
	rpcUrls: { default: { http: ['https://evm-rpc-testnet.sei-apis.com'] } },
	blockExplorers: { default: { url: 'https://testnet.seiscan.io' } },
	testnet: true
};

const seiMainnet = {
	id: 1329,
	name: 'Sei Mainnet',
	network: 'sei-pacific-1',
	nativeCurrency: { name: 'Sei', symbol: 'SEI', decimals: 18 },
	rpcUrls: { default: { http: ['https://evm-rpc.sei-apis.com'] } },
	blockExplorers: { default: { url: 'https://seiscan.io' } },
	testnet: false
};

// --- Network selection (default: testnet) ---
const NETWORK = (process.env.SEI_NETWORK || 'testnet').toLowerCase();
const chain = NETWORK === 'mainnet' ? seiMainnet : seiTestnet;

// --- USDC Addresses and ABI ---
const USDC_ADDRESSES = {
	testnet: '0x4fCF1784B31630811181f670Aea7A7bEF803eaED',
	mainnet: '0xe15fC38F6D8c56aF07bbCBe3BAf5708A2Bf42392'
};
const USDC_ADDRESS = NETWORK === 'mainnet' ? USDC_ADDRESSES.mainnet : USDC_ADDRESSES.testnet;
const USDC_DECIMALS = 6;
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' }]
	}
];
```

2\. **Load environment variables and validate input:**

Next, load and validate the private key and the recipient address from the environment. The script checks that the variables exist and that the recipient address looks valid. Then it normalizes the private key string:

```ts theme={null}
// --- Environment Variables ---
const PRIVATE_KEY_RAW = process.env.PRIVATE_KEY;
const RECIPIENT = process.env.RECIPIENT_ADDRESS || process.env.RECIPIENT;

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 (or RECIPIENT) in your .env file');
	process.exit(1);
}
if (!/^0x[a-fA-F0-9]{40}$/.test(RECIPIENT)) {
	console.error('Error: Recipient address is not a valid Ethereum address');
	process.exit(1);
}

// --- Private Key Normalization ---
const PRIVATE_KEY = PRIVATE_KEY_RAW.startsWith('0x') ? PRIVATE_KEY_RAW : '0x' + PRIVATE_KEY_RAW;
```

This code makes sure that the script has the necessary inputs. If the private key or the recipient address is missing or malformed, the script logs an error and exits. If the private key does not start with "0x", the script adds the prefix, because viem expects a 0x-prefixed key.

After this step, `PRIVATE_KEY` is a clean hex string, and `RECIPIENT` is a validated address.

3\. Initialize viem clients:

Use the chain config and the credentials to create two clients. The public client reads blockchain data. The wallet client writes to the chain by signing transactions. Also derive an account object from the private key:

```ts theme={null}
// --- Client Setup ---
const account = privateKeyToAccount(PRIVATE_KEY);
const publicClient = createPublicClient({ chain, transport: http() });
const walletClient = createWalletClient({ account, chain, transport: http() });
```

* `privateKeyToAccount` converts the hex private key into an account object. The object includes the corresponding address and other account properties.
* `createPublicClient` connects to the RPC endpoint of the selected Sei network for read-only calls. It does not need a private key.
* `createWalletClient` uses your account and the RPC endpoint to send transactions.

The script can now interact with the selected Sei network. `publicClient` calls `eth_call` for contract reads, and `walletClient` signs and sends transactions from your account.

4\. Main transfer logic:

Finally, write an asynchronous function that performs the USDC transfer. The function checks the sender's USDC balance and makes sure that it is sufficient. Then it calls the `transfer` function of the USDC contract.

The function also logs the transfer details and handles errors:

```ts theme={null}
// --- Main Transfer Logic ---
(async () => {
	try {
		// Check sender's USDC balance
		const balance = await publicClient.readContract({
			address: USDC_ADDRESS,
			abi: USDC_ABI,
			functionName: 'balanceOf',
			args: [account.address]
		});
		const balanceFormatted = Number(formatUnits(balance, USDC_DECIMALS));
		const amount = 10; // amount of USDC to send (in whole units)
		console.log('Sender:', account.address);
		console.log('Recipient:', RECIPIENT);
		console.log('USDC balance:', balanceFormatted);
		if (amount > balanceFormatted) {
			console.error('Error: Insufficient USDC balance');
			process.exit(1);
		}

		// Convert amount to token decimals and send transfer
		const amountInDecimals = parseUnits(amount.toString(), USDC_DECIMALS);
		const hash = await walletClient.writeContract({
			address: USDC_ADDRESS,
			abi: USDC_ABI,
			functionName: 'transfer',
			args: [RECIPIENT, amountInDecimals]
		});
		console.log('Transfer successful!');
		console.log('Tx hash:', hash);
		const explorerBase =
			chain.blockExplorers && chain.blockExplorers.default && chain.blockExplorers.default.url ? chain.blockExplorers.default.url : 'https://seiscan.io';
		console.log('Explorer:', `${explorerBase}/tx/${hash}`);
	} catch (err) {
		console.error('Transfer failed:', err.message || err);
		process.exit(1);
	}
	process.exit(0);
})();
```

* The script uses `publicClient.readContract` to call `balanceOf(address)` on the USDC contract and get the sender's token balance.

* `formatUnits` converts the balance from the smallest units (6 decimals for USDC) into a human-readable number. This example sets the amount to 10 USDC. You can change this value. The script compares the amount to the current balance. If the balance is not sufficient, the script logs an error and exits.

* If the balance is sufficient, the script uses `parseUnits` to convert 10 USDC into the raw token amount (10 \* 10^6, because USDC has 6 decimals).\
  Then `walletClient.writeContract` calls the `transfer(to, amount)` function of the USDC contract. This sends a transaction from your account to transfer the tokens. On success, it returns a transaction hash. The script logs the hash and a URL where you can view the transaction on the Sei block explorer.

* If an error occurs, such as an RPC issue or a transaction failure, the `try`/`catch` block logs "Transfer failed" with the error message. The script ends with `process.exit(0)`, which stops the process after the async function completes.

## Run the script

When `index.js` is complete, you can run the script from your terminal:

```shell theme={null}
# Default: testnet
node index.js

# Mainnet
SEI_NETWORK=mainnet node index.js
```

If the transfer succeeds, the output looks like this, with your own addresses and values:

```
Sender: 0x1A2b...7890      # your sender address
Recipient: 0x9F8f...1234   # recipient address
USDC balance: 250.0        # current USDC balance of sender
Transfer successful!
Tx hash: 0xabc123...def456 # transaction hash of the transfer
Explorer: https://testnet.seiscan.io/tx/0xabc123...def456
```

You should see "Transfer successful!" and a transaction hash. To view the transaction details on Seiscan, you can copy the explorer URL into a browser.

For more information, see the [Circle Developer Docs](https://developers.circle.com/).

### Important notes

* Testnet only: Sei Testnet USDC has no real value. Do not use Sei Mainnet keys, and do not expect real funds.
* Security: Store private keys in `.env`. Never commit secrets. Follow best practices for key management.
* Gas: You need a small amount of SEI on Sei Testnet to pay for gas.
* Lightweight ABI: The script uses only `balanceOf` and `transfer`. These functions are enough for simple transfers.
* viem behavior: `readContract` handles reads, and `writeContract` handles writes. The script adds the `0x` prefix to the private key automatically.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.