Skip to main content
The following examples are demonstrations of integrations and should not be used in production. In production, store sensitive information such as API keys and private keys in a secrets manager or vault.
This guide covers native USDC on Polygon Chain. Native USDC behaves like any other ERC-20 token: you can read balances, approve spenders, and transfer tokens directly onchain. For cross-chain USDC transfers using Circle’s CCTP, see USDC Gateway Integration.
ApproachDescriptionWhen to use
Native USDCStandard ERC-20 contract directly on Polygon ChainWhen you only need payments within Polygon
Gateway USDCCircle’s cross-chain system that moves USDC between blockchains (CCTP)When your users need to send or receive USDC across chains

Example: Read Balance and Transfer

The following example uses viem to check the USDC balance and send 1 USDC on Polygon.
// pnpm add viem
import { createPublicClient, createWalletClient, http, parseUnits } from "viem";
import { polygon } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";

// Native USDC contract on Polygon Chain (not bridged USDC.e)
const USDC = "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359";

const erc20 = [
  { type: "function", name: "decimals", stateMutability: "view", inputs: [], outputs: [{ type: "uint8" }] },
  { type: "function", name: "balanceOf", stateMutability: "view", inputs: [{ type: "address" }], outputs: [{ type: "uint256" }] },
  { type: "function", name: "transfer", stateMutability: "nonpayable", inputs: [{ type: "address" }, { type: "uint256" }], outputs: [{ type: "bool" }] },
];

const account = privateKeyToAccount(process.env.PRIV_KEY as `0x${string}`);
const rpc = http(process.env.POLYGON_RPC_URL);
const pub = createPublicClient({ chain: polygon, transport: rpc });
const wallet = createWalletClient({ chain: polygon, transport: rpc, account });

async function main() {
  const me = account.address;
  const decimals = await pub.readContract({ address: USDC, abi: erc20, functionName: "decimals" });
  const bal = await pub.readContract({ address: USDC, abi: erc20, functionName: "balanceOf", args: [me] });
  console.log("Balance:", Number(bal) / 10 ** Number(decimals), "USDC");

  // Send 1 USDC
  const amount = parseUnits("1", Number(decimals));
  const hash = await wallet.writeContract({ address: USDC, abi: erc20, functionName: "transfer", args: ["0xRecipient...", amount] });
  console.log("tx:", hash);
}

main();
The example above reads PRIV_KEY from an environment variable, which is the minimum required for a production deployment. In production, go further: store private keys in a dedicated secrets manager (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault) and never let them touch application memory directly. A more durable pattern is to delegate signing to a backend service or use a managed key management service (KMS) that signs transactions server-side, so application code never holds the raw private key. The account object in viem is replaceable with any signer that implements the LocalAccount interface, including hardware wallets and KMS-backed signers.