An open-source, dependency-light reference implementation for two things builders repeatedly need on Arc — Circle's public, USDC-native Layer 1:
- Native USDC payments on Arc Mainnet. Arc uses USDC as its gas token, so a
payment is a plain ERC-20 transfer. This stack adds request encoding
(
arc-usdc:pay?...URIs for QR codes / links) and on-chain payment verification so a merchant never has to trust a client-reported "it worked." - CCTP V2 transfers into Arc. Calldata builders for
depositForBurnon the source chain andreceiveMessageon Arc, plus a client for Circle's Iris attestation service — the same lifecycle used by the Arc Mainnet Hub.
This is calldata-building and verification logic, not a wallet or a custodial
service. It never holds a private key on your behalf; every example script
reads PRIVATE_KEY from the environment and signs locally.
Most CCTP examples are either a single hardcoded script or buried inside a larger dapp. This package isolates the reusable parts — config, calldata encoding, attestation polling, payment verification — behind a small, fully-tested API so you can import just what you need into your own backend, CLI, or frontend.
npm install arc-cctp-payment-referenceOr clone this repo directly and use it as a starting point:
git clone https://github.com/osr21/arc-cctp-payment-reference
cd arc-cctp-payment-reference
npm install
npm testimport { encodePaymentRequest, buildUsdcTransferCalldata, verifyPayment } from "arc-cctp-payment-reference";
import { createPublicClient, http } from "viem";
import { ARC_MAINNET } from "arc-cctp-payment-reference";
// 1. Create a payment request (share this as a link or QR code)
const uri = encodePaymentRequest({
recipient: "0xYourMerchantAddress",
amount: "25.00",
reference: "invoice-1042",
});
// 2. The payer's wallet builds the transfer from the request
const { to, data } = buildUsdcTransferCalldata("0xYourMerchantAddress", "25.00");
// ...wallet_sendTransaction({ to, data }) happens client-side...
// 3. You verify the resulting transaction against the chain, not the client
const publicClient = createPublicClient({ transport: http(ARC_MAINNET.rpcUrl) });
const receipt = await verifyPayment(publicClient, txHash, { recipient: "0xYourMerchantAddress", amount: "25.00" });
if (receipt.matchesRequest) {
// fulfil the order
}import { getSourceChain, buildApproveCalldata, buildDepositForBurnCalldata, waitForAttestation, buildReceiveMessageCalldata } from "arc-cctp-payment-reference";
const source = getSourceChain("base");
// 1. Approve TokenMessengerV2 to pull USDC
const approve = buildApproveCalldata(source, "50");
// ...sign & send `approve` on the source chain...
// 2. Burn USDC on the source chain, targeting Arc (CCTP domain 26)
const burn = buildDepositForBurnCalldata(source, {
amount: "50",
destinationRecipient: "0xYourArcAddress",
});
// ...sign & send `burn`, keep the resulting txHash...
// 3. Wait for Circle's Iris service to attest the burn
const attestation = await waitForAttestation(source.cctpDomain, burnTxHash);
// 4. Submit the mint on Arc
const mint = buildReceiveMessageCalldata(attestation.message, attestation.attestation!);
// ...sign & send `mint` on Arc Mainnet...Runnable, end-to-end versions of both flows live in examples/:
PRIVATE_KEY=0x... npm run example:send -- 0xRecipient 10
PRIVATE_KEY=0x... npm run example:bridge -- base 25 0xArcRecipient
npm run example:poll -- base 0xBurnTxHash| Module | Purpose |
|---|---|
config.ts |
Chain configs (Arc Mainnet + Ethereum/Base/Arbitrum), CCTP domains, contract addresses |
cctp.ts |
depositForBurn / receiveMessage calldata builders, Iris attestation polling |
payment.ts |
USDC transfer calldata, payment-request encode/decode, on-chain payment verification |
abi.ts |
Minimal ABIs for ERC-20, TokenMessengerV2, MessageTransmitterV2 |
All functions that build a transaction return { to, data } (or { to, data, value })
and never send it — you control signing and submission with your own client
(viem, ethers, a wallet SDK, etc).
- Standard vs. Fast Transfer.
minFinalityThreshold: 1000(default) waits for hard finality;2000requests Circle's Fast Transfer for a fee. Fast Transfers still require you to setmaxFeeabove zero or Circle will not fulfil them. destinationCallerdefaults to0x0, meaning anyone can relayreceiveMessageon Arc once the attestation is ready. Restrict it if you need the mint to only be submittable by a specific relayer.- Payment verification reads the chain, not the caller.
verifyPaymentfetches the transaction and receipt directly from an RPC node and checks the USDCTransferlog — a client claiming success is never sufficient proof of payment. - Addresses are the standard CCTP V2 CREATE2 deployment (same
TokenMessengerV2/MessageTransmitterV2address across supported EVM chains). Always cross-check against Circle's contract address registry before moving real funds — this file is a reference, not a substitute for the live registry.
npm testAll 20+ tests run against mocked fetch/RPC clients — no network access or
funded wallet required to validate the encoding and verification logic.
Issues and PRs are welcome. If you add support for another CCTP-connected source chain, please also add config test coverage confirming its CCTP domain is unique.
MIT — see LICENSE.