Skip to main content
Payments require handling sensitive data. The code examples below demonstrate the integration pattern and should not be used in production as-is. In production, store sensitive information such as API keys and private keys in a secrets manager or vault.
USDC is one of the most-used stablecoins on Polygon PoS. Low transaction costs on Polygon make it a practical choice for USDC payments, including micro-payments and high-volume payment flows. Circle provides full USDC documentation, including SDKs and sample applications. Two integration paths are available: native USDC transfer using ERC-20, and cross-chain transfer using Circle’s Gateway.

Native USDC transfer

Native USDC on Polygon PoS is an ERC-20 contract that uses the standard approve/transfer pattern.
// pnpm add viem
import { createPublicClient, createWalletClient, http, parseUnits } from "viem";
import { polygon } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";

// Native USDC on Polygon PoS (NOT the old bridged USDC.e)
// Source: Circle "USDC Contract Addresses"
const USDC = "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359";

// Testnet USDC on Amoy
// const USDC = "0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582";

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: "approve", stateMutability: "nonpayable", inputs: [{ type: "address" }, { type: "uint256" }], outputs: [{ type: "bool" }] },
  { 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); // e.g. https://polygon-rpc.com
const pub = createPublicClient({ chain: polygon, transport: rpc });
const wallet = createWalletClient({ chain: polygon, transport: rpc, account });

async function main() {
  const me = account.address as `0x${string}`;
  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("USDC balance:", Number(bal) / 10 ** Number(decimals));

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

Gateway integration (cross-chain)

Circle Gateway aggregates USDC balances across any supported chain. Users deposit USDC into the Gateway Wallet (not an ERC-20 address). Once deposited, they can send USDC to any supported chain using the Gateway. For more details on setting up a Gateway balance, see Circle’s documentation.

Prerequisites


import { createPublicClient, createWalletClient, http, parseUnits } from "viem";
import { polygon } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
import { fetch } from "undici";

const USDC = "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359";
const GATEWAY_WALLET = "0x77777777Dcc4d5A8B6E418Fd04D8997ef11000eE";
const GATEWAY_MINTER = "0x2222222d7164433c4C09B0b0D809a9b52C04C205";

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

const gatewayWalletAbi = [
  { type: "function", name: "deposit", stateMutability: "nonpayable", inputs: [{ type: "address" }, { type: "uint256" }], outputs: [] },
];

const gatewayMinterAbi = [
  { type: "function", name: "gatewayMint", stateMutability: "nonpayable", inputs: [{ type: "bytes" }, { type: "bytes" }], outputs: [] },
];

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 gatewayTransfer() {
  // 1) Approve + deposit into GatewayWallet on Polygon
  const decimals = await pub.readContract({ address: USDC, abi: erc20, functionName: "decimals" });
  const amount = parseUnits("25", Number(decimals)); // example

  await wallet.writeContract({ address: USDC, abi: erc20, functionName: "approve", args: [GATEWAY_WALLET, amount] });
  await wallet.writeContract({ address: GATEWAY_WALLET, abi: gatewayWalletAbi, functionName: "deposit", args: [USDC, amount] });

  // 2) Request attestation from Circle Gateway API
  // Build and sign a BurnIntent (EIP-712 typed data) for source=Polygon, destination=Polygon (same-chain "withdraw") or another chain.
  // For brevity, assume you already produced `burnIntent` and EIP-712 signature with your EOA.
  const res = await fetch("https://gateway-api.circle.com/v1/transfer", {
    method: "POST",
    headers: { "content-type": "application/json", authorization: `Bearer ${process.env.CIRCLE_API_KEY}` },
    body: JSON.stringify({
      burnIntent: {
        maxBlockHeight: "999999999999",
        maxFee: "0",
        spec: {
          version: 1,
          sourceDomain: 7,          // Polygon PoS domain id (per Circle docs)
          destinationDomain: 7,     // or another supported domain id
          sourceContract: GATEWAY_WALLET,
          destinationContract: GATEWAY_MINTER,
          sourceToken: USDC,
          destinationToken: USDC,
          sourceDepositor: account.address,
          destinationRecipient: "0xRecipient...", // who receives on destination
          sourceSigner: account.address,
          destinationCaller: "0x0000000000000000000000000000000000000000",
          value: amount.toString(),
        },
      },
      // include your EIP-712 signature of BurnIntent here if required by your flow
    }),
  });
  const { attestationPayload, signature } = await res.json();

  // 3) Submit attestation to GatewayMinter on destination chain
  const tx = await wallet.writeContract({
    address: GATEWAY_MINTER,
    abi: gatewayMinterAbi,
    functionName: "gatewayMint",
    args: [attestationPayload as `0x${string}`, signature as `0x${string}`],
  });
  console.log("gatewayMint tx:", tx);
}

gatewayTransfer();