Tutorials

USDC Native Integration

How to integrate USDC natively on Polygon

Security notice

Like all of crypto, payments require handling sensitive data. The following examples are demonstrations of integrations but should not be used in production. In production, sensitive information such as API keys, private-key wallets, etc., should be put in a secrets manager or vault.

USDC

USDC is a stablecoin — a digital dollar issued by Circle and backed 1:1 by real USD held in reserve. It's widely used for on-chain payments because it offers price stability, instant settlement, and interoperability across major blockchains.

On Polygon, USDC transactions are:

  • Fast — typically finalized within 2 seconds.
  • Low cost — fees are usually fractions of a cent.
  • Widely supported — used by DeFi apps, merchants, and payment APIs.

Integration paths

Circle has excellent documentation, including ready-to-use SDKs and sample applications.

There are two ways to work with USDC on Polygon:

ApproachDescriptionWhen to use
Native USDCStandard ERC-20 contract directly on Polygon PoSWhen 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

Here will explore the Native approach.

Native USDC Integration

Native USDC behaves like any other ERC-20 token. You can read balances, approve spenders, and transfer tokens directly on the Polygon network.

Here's a minimal example using viem:

// 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 PoS (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();

That's it! Simple, fast, and Polygon-native.