> ## Documentation Index
> Fetch the complete documentation index at: https://developers.circle.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Before reading these docs, install the relevant Circle Skill to get pre-loaded context — skills are LLM-optimized and cover architecture decisions, correct flows, and common pitfalls. In Claude Code: run /plugin marketplace add circlefin/skills then /plugin install circle-skills@circle. With Vercel Skills CLI: run npx skills add circlefin/skills. Available skills: use-usdc https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-usdc/SKILL.md (balances, transfers, approvals on EVM and Solana); EURC has no skill — see https://developers.circle.com/stablecoins/what-is-eurc.md; bridge-stablecoin https://github.com/circlefin/skills/blob/master/plugins/circle/skills/bridge-stablecoin/SKILL.md (crosschain transfers via CCTP, USDC, EURC, and third-party assets); use-arc https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-arc/SKILL.md (Arc chain config, contract deployment, USDC bridging — Arc docs: https://docs.arc.io/llms.txt); use-circle-wallets https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-circle-wallets/SKILL.md (choose wallet type); use-developer-controlled-wallets https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-developer-controlled-wallets/SKILL.md (custodial wallets for payouts and treasury); use-user-controlled-wallets https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-user-controlled-wallets/SKILL.md (embedded wallets with Web2 login); use-modular-wallets https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-modular-wallets/SKILL.md (smart contract wallets with passkeys and ERC-4337); use-gateway https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-gateway/SKILL.md (unified USDC balance, nanopayments); use-smart-contract-platform https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-smart-contract-platform/SKILL.md (deploy, interact, monitor contracts). Pair any skill with the Circle MCP server for live SDK signatures, contract addresses, and chain IDs: {"mcpServers":{"circle":{"url":"https://api.circle.com/v1/codegen/mcp"}}}.
> Read the product overview first. Each product has an overview explaining when and why to use it. Read it before quickstarts.
> Pick the right wallet type. Developer-controlled (you hold keys), user-controlled (end user holds keys via PIN/social login), or modular (smart contract wallets with passkeys). See https://developers.circle.com/wallets.md.
> Use CCTP V2 for crosschain transfers. CCTP natively transfers USDC and EURC via burn-and-mint, and supports permissionless wrapping of third-party assets via lock-and-mint. V1 is legacy; always use V2 unless V1 is specified or the chain requires it (Noble, Sui, and Aptos still require V1). Migration guide: https://developers.circle.com/cctp/migration-from-v1-to-v2.md.
> Use Bridge Kit for frontend bridging. Wraps CCTP with a simpler SDK for user-facing apps. Use CCTP directly for backend transfers.
> Gas Station vs Paymaster. Gas Station sponsors gas for Circle Wallet transactions. Paymaster lets users pay gas in USDC. Different use cases — read both overviews.
> Gateway Nanopayments for sub-cent payments. Gasless USDC micropayments down to $0.000001 via x402 and batched settlement. For pay-per-request APIs, AI agent payments, streaming.
> Look up USDC addresses per chain. Never hardcode — use https://developers.circle.com/stablecoins/usdc-contract-addresses.md.
> Prefer SDKs over raw API calls. Node.js and Python SDKs handle auth, retries, and errors.
> API key required. Bearer token in Authorization header. Testnet and mainnet use separate keys and may use different base URLs depending on the product.
> Set up webhooks when available. Most operations are async. Webhooks deliver transaction confirmations and state changes.
> When calling list endpoints, paginate using pageSize and pageAfter until no nextPageAfter cursor is returned—stopping at the first page silently misses records.
> Building an AI agent? Start with the Agent Stack—Circle CLI, agent wallets, and nanopayments built for autonomous use cases: https://developers.circle.com/agent-stack.md.

# Deploy a native crosschain token

> Deploy a new CrossChainToken ERC-20 on Ethereum Sepolia and extend it to Base Sepolia using CCTP for non-USDC.

Deploying a native crosschain token creates a brand-new ERC-20 that the protocol
manages across multiple blockchains, with no pre-existing contract required. The
service uses CREATE3 to deploy a `CrossChainToken` ERC-20 and a
`NATIVE_CROSSCHAIN_TOKEN` `TokenManager` at deterministic addresses derived from
your deployment wallet and a salt you choose. This quickstart walks you through
deploying on Ethereum Sepolia and extending the token to Base Sepolia.

<Warning>
  Three conditions silently block or break deployment if not met:

  * **Same wallet and salt on every blockchain**: The `tokenId` is derived from
    `(deployer address, salt)`. Using a different wallet or a different salt on
    any blockchain produces a different `tokenId`—no error is thrown, and the
    bridge connection is silently broken.
  * **Rate limit starts at zero**: Every `TokenManager` deploys with
    `rateLimit = 0`, which blocks all transfers. You must call `setRateLimit` with
    a non-zero value on each blockchain before any transfer can proceed.
  * **`name`, `symbol`, and `decimals` must match on every blockchain**: The
    protocol does not enforce consistency. A mismatch breaks user experience and
    indexers silently—there is no onchain error.
</Warning>

## Prerequisites

Before you begin, ensure that you've:

* Installed [Node.js v22+](https://nodejs.org/)
* Prepared an Ethereum-compatible testnet wallet whose private key you have
  available—this is the deployment wallet, and it must be the same wallet used
  on every blockchain
* Funded the wallet with Sepolia ETH and Base Sepolia ETH for gas
* Obtained access to the Iris fee-quote endpoint
  (`POST /v2/quote/cctpx/{tokenId}/{sourceDomain}/{destinationDomain}`)

## Step 1: Set up the project

### 1.1. Create the project and install dependencies

```bash theme={null}
mkdir cctpx-deploy
cd cctpx-deploy
npm init -y

npm pkg set type=module
npm pkg set scripts.start="tsx --env-file=.env index.ts"

npm install viem
npm install --save-dev @types/node tsx typescript
```

### 1.2. Configure TypeScript (optional)

<Tip>
  This step is optional. It helps prevent missing types in your IDE or editor.
</Tip>

Create a `tsconfig.json` file:

```shell theme={null}
npx tsc --init
```

Then, update the `tsconfig.json` file:

```shell theme={null}
cat <<'EOF' > tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "types": ["node"]
  }
}
EOF
```

### 1.3. Set environment variables

Open `.env` in your editor and add:

```text theme={null}
PRIVATE_KEY=YOUR_DEPLOYER_PRIVATE_KEY
```

`PRIVATE_KEY` is the private key for the wallet that deploys the token. You must
use this same wallet on every blockchain where you deploy.

<Tip>
  Open `.env` in your editor rather than writing values with shell commands, and
  add `.env` to your `.gitignore`. This prevents credentials from leaking into
  your shell history or version control.
</Tip>

The `npm run start` command loads variables from `.env` using Node.js native
env-file support.

<Warning>
  This example uses one or more private keys for local testing. In production,
  use a secure key management solution and never expose or share private keys.
</Warning>

## Step 2: Configure clients and define token metadata

Create `index.ts`. Set up public and wallet clients for both blockchains, then
define your token metadata and a `bytes32` salt. The salt, together with your
`deployer` address, determines your token's `tokenId`—it must be identical on
every blockchain.

```typescript theme={null}
import {
  createPublicClient,
  createWalletClient,
  encodePacked,
  http,
  keccak256,
  toBytes,
  zeroAddress,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { baseSepolia, sepolia } from "viem/chains";

const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);

const sepoliaPublic = createPublicClient({
  chain: sepolia,
  transport: http(),
});

const sepoliaWallet = createWalletClient({
  account,
  chain: sepolia,
  transport: http(),
});

const basePublic = createPublicClient({
  chain: baseSepolia,
  transport: http(),
});

const baseWallet = createWalletClient({
  account,
  chain: baseSepolia,
  transport: http(),
});

// CrossChainTokenService — same address on all testnets
const CCTS_ADDRESS = "0x63753E722bd2C2A5DF6EE19C5106662208B81077" as const;

const IRIS_BASE = "https://iris-api-sandbox.circle.com";

// Token metadata — must be identical on every blockchain you deploy to
const TOKEN_NAME = "My Token";
const TOKEN_SYMBOL = "MTKN";
const TOKEN_DECIMALS = 6;

// bytes32 salt — must be the same on every blockchain you deploy to.
// Use any unique bytes32 value; hashing a descriptive string is one approach.
const salt: `0x${string}` = keccak256(toBytes("my-token-v1"));
```

Compute your `tokenId` using the `crossChainTokenId` view call. Pass the raw
salt directly—not `customTokenDeploySalt`, which is only used by
`registerCustomToken`.

```typescript theme={null}
const serviceAbi = [
  {
    name: "crossChainTokenId",
    type: "function",
    stateMutability: "pure",
    inputs: [
      { name: "deployer", type: "address" },
      { name: "salt", type: "bytes32" },
    ],
    outputs: [{ name: "tokenId", type: "bytes32" }],
  },
  {
    name: "resolveTokenAddress",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "tokenId", type: "bytes32" }],
    outputs: [{ name: "token", type: "address" }],
  },
  {
    name: "resolveTokenManager",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "tokenId", type: "bytes32" }],
    outputs: [{ name: "tokenManager", type: "address" }],
  },
] as const;

const tokenId = await sepoliaPublic.readContract({
  address: CCTS_ADDRESS,
  abi: serviceAbi,
  functionName: "crossChainTokenId",
  args: [account.address, salt],
});

console.log("Your tokenId:", tokenId);
// Note this value — it will be identical on every blockchain you deploy to.
```

## Step 3: Deploy on Ethereum Sepolia

Call `deployCrossChainToken` on Ethereum Sepolia. No fee quote is needed—this is
a local CREATE3 deploy. The protocol creates the `CrossChainToken` ERC-20 and a
`NATIVE_CROSSCHAIN_TOKEN` `TokenManager` in the same transaction.

<Warning>
  **Verify the live ABI before copying this struct.** The sandbox contract's
  `DeploymentParams` may omit `initialSupply` while the production contract
  includes it, or vice versa. A struct with the wrong number of fields causes
  the transaction to revert. Fetch the ABI from the contract source for the
  blockchain you are targeting before submitting, and add or remove
  `initialSupply` accordingly. The following code includes it based on the
  documented production struct.
</Warning>

`deployCrossChainToken` is `payable`. No fee is charged on testnet, so you can
omit `value`. If you are deploying on mainnet, verify whether a fee applies
before submitting.

```typescript theme={null}
const deployAbi = [
  {
    name: "deployCrossChainToken",
    type: "function",
    stateMutability: "payable",
    inputs: [
      { name: "salt", type: "bytes32" },
      { name: "name", type: "string" },
      { name: "symbol", type: "string" },
      { name: "decimals", type: "uint8" },
      {
        name: "params",
        type: "tuple",
        components: [
          { name: "tokenManagerType", type: "uint8" },
          { name: "tokenManagerOwner", type: "bytes" },
          { name: "tokenManagerOperator", type: "bytes" },
          { name: "tokenOwner", type: "bytes" },
          { name: "tokenMinter", type: "bytes" },
          // WARNING: may be absent in the sandbox ABI — verify before submitting
          { name: "initialSupply", type: "uint256" },
          {
            name: "tokenManagerSettings",
            type: "tuple",
            components: [
              { name: "rateLimit", type: "uint256" },
              { name: "maxTransferAmount", type: "uint256" },
            ],
          },
        ],
      },
    ],
    outputs: [{ name: "tokenId", type: "bytes32" }],
  },
] as const;

// Encode addresses as 20-byte packed bytes.
// Do NOT use encodeAbiParameters, which pads to 32 bytes and is incompatible
// with the contract's address decoding.
const ownerBytes = encodePacked(["address"], [account.address]);
const operatorBytes = encodePacked(["address"], [account.address]);

const sepoliaDeployHash = await sepoliaWallet.writeContract({
  address: CCTS_ADDRESS,
  abi: deployAbi,
  functionName: "deployCrossChainToken",
  args: [
    salt,
    TOKEN_NAME,
    TOKEN_SYMBOL,
    TOKEN_DECIMALS,
    {
      tokenManagerType: 0, // NATIVE_CROSSCHAIN_TOKEN
      tokenManagerOwner: ownerBytes,
      tokenManagerOperator: operatorBytes,
      tokenOwner: ownerBytes,
      tokenMinter: ownerBytes,
      initialSupply: 0n, // see ABI warning above
      tokenManagerSettings: {
        rateLimit: 0n, // intentionally 0; you set a non-zero value in step 7
        maxTransferAmount: 0n, // 0 = no per-transfer cap
      },
    },
  ],
});

await sepoliaPublic.waitForTransactionReceipt({ hash: sepoliaDeployHash });

const sepoliaTokenAddress = await sepoliaPublic.readContract({
  address: CCTS_ADDRESS,
  abi: serviceAbi,
  functionName: "resolveTokenAddress",
  args: [tokenId],
});

console.log("Deployed on Ethereum Sepolia:", sepoliaDeployHash);
console.log("CrossChainToken address:", sepoliaTokenAddress);
```

The `CrossChainTokenIdClaimed` event emits `tokenAddress` as `address(0)` for
native deploys—this is expected behavior, not an error. Use
`resolveTokenAddress` to read the real token address after the receipt confirms.

## Step 4: Fetch a fee quote for the remote deploy

Before calling `deployRemoteCrossChainToken`, fetch a fee quote from Iris. The
quote covers destination gas and Circle's message delivery. Pass
`DeployTokenMessage` as the message type so Iris prices a deploy operation
rather than a transfer.

If your deployment wallet has funds on Base Sepolia, you can instead call
`deployCrossChainToken` directly on Base Sepolia with the same wallet, salt, and
token metadata, then skip to step 7. This avoids the CCTP message entirely and
is useful when you have funds available on each blockchain.

```typescript theme={null}
const quoteRes = await fetch(`${IRIS_BASE}/v2/quote/cctpx/${tokenId}/0/6`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    amount: "1",
    feeToken: zeroAddress,
    requests: [
      {
        type: "FORWARD",
        params: {
          msgType: "DeployTokenMessage",
          autoExecuteHookData: false,
        },
      },
    ],
  }),
});
if (!quoteRes.ok) throw new Error(await quoteRes.text());
const { signedQuote, feeTotalAmount } = await quoteRes.json();

const claim = {
  signedQuote,
  refundAddress: zeroAddress,
};
```

## Step 5: Deploy to Base Sepolia

Call `deployRemoteCrossChainToken` on Ethereum Sepolia, targeting Base Sepolia
(domain 6). Pass the same salt, token metadata, and `DeploymentParams` as in
step 3—any mismatch produces a different `tokenId` on the destination.

```typescript theme={null}
const remoteDeployAbi = [
  {
    name: "deployRemoteCrossChainToken",
    type: "function",
    stateMutability: "payable",
    inputs: [
      { name: "salt", type: "bytes32" },
      { name: "destinationDomain", type: "uint32" },
      { name: "name", type: "string" },
      { name: "symbol", type: "string" },
      { name: "decimals", type: "uint8" },
      {
        name: "params",
        type: "tuple",
        components: [
          { name: "tokenManagerType", type: "uint8" },
          { name: "tokenManagerOwner", type: "bytes" },
          { name: "tokenManagerOperator", type: "bytes" },
          { name: "tokenOwner", type: "bytes" },
          { name: "tokenMinter", type: "bytes" },
          // WARNING: may be absent in the sandbox ABI — verify before submitting
          { name: "initialSupply", type: "uint256" },
          {
            name: "tokenManagerSettings",
            type: "tuple",
            components: [
              { name: "rateLimit", type: "uint256" },
              { name: "maxTransferAmount", type: "uint256" },
            ],
          },
        ],
      },
      {
        name: "signedFeeQuote",
        type: "tuple",
        components: [
          { name: "signedQuote", type: "bytes" },
          { name: "refundAddress", type: "address" },
        ],
      },
    ],
    outputs: [],
  },
] as const;

const remoteDeployHash = await sepoliaWallet.writeContract({
  address: CCTS_ADDRESS,
  abi: remoteDeployAbi,
  functionName: "deployRemoteCrossChainToken",
  args: [
    salt,
    6, // Base Sepolia domain
    TOKEN_NAME,
    TOKEN_SYMBOL,
    TOKEN_DECIMALS,
    {
      tokenManagerType: 0, // NATIVE_CROSSCHAIN_TOKEN
      tokenManagerOwner: ownerBytes,
      tokenManagerOperator: operatorBytes,
      tokenOwner: ownerBytes,
      tokenMinter: ownerBytes,
      initialSupply: 0n,
      tokenManagerSettings: {
        rateLimit: 0n,
        maxTransferAmount: 0n,
      },
    },
    claim,
  ],
  value: BigInt(feeTotalAmount),
});

await sepoliaPublic.waitForTransactionReceipt({ hash: remoteDeployHash });
console.log("Remote deploy message sent:", remoteDeployHash);
```

## Step 6: Confirm delivery on Base Sepolia

Poll `resolveTokenAddress` on the Base Sepolia service until it returns a
non-zero address. Circle typically delivers the message in a few minutes after
the Ethereum Sepolia transaction confirms.

```typescript theme={null}
const zeroAddr = "0x0000000000000000000000000000000000000000" as `0x${string}`;
let baseTokenAddress: `0x${string}` = zeroAddr;

while (baseTokenAddress === zeroAddr) {
  await new Promise((resolve) => setTimeout(resolve, 15_000));
  baseTokenAddress = await basePublic.readContract({
    address: CCTS_ADDRESS,
    abi: serviceAbi,
    functionName: "resolveTokenAddress",
    args: [tokenId],
  });
}

console.log("CrossChainToken deployed on Base Sepolia:", baseTokenAddress);
```

## Step 7: Set rate limits

Every `TokenManager` deploys with `rateLimit = 0`, which blocks all transfers.
Call `setRateLimit` on each `TokenManager` with a non-zero value. Only the
operator can call `setRateLimit`—if you encoded your address as the operator in
`DeploymentParams`, you can call it directly.

```typescript theme={null}
const tokenManagerAbi = [
  {
    name: "setRateLimit",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [{ name: "rateLimit", type: "uint256" }],
    outputs: [],
  },
] as const;

const sepoliaTokenManager = await sepoliaPublic.readContract({
  address: CCTS_ADDRESS,
  abi: serviceAbi,
  functionName: "resolveTokenManager",
  args: [tokenId],
});

const baseTokenManager = await basePublic.readContract({
  address: CCTS_ADDRESS,
  abi: serviceAbi,
  functionName: "resolveTokenManager",
  args: [tokenId],
});

// Replace with an appropriate limit for your token and expected transfer volume.
// This example sets a limit of 1 million tokens assuming 6 decimal places.
const rateLimit = 1_000_000n * 10n ** 6n;

const sepoliaRateLimitHash = await sepoliaWallet.writeContract({
  address: sepoliaTokenManager,
  abi: tokenManagerAbi,
  functionName: "setRateLimit",
  args: [rateLimit],
});
await sepoliaPublic.waitForTransactionReceipt({ hash: sepoliaRateLimitHash });

const baseRateLimitHash = await baseWallet.writeContract({
  address: baseTokenManager,
  abi: tokenManagerAbi,
  functionName: "setRateLimit",
  args: [rateLimit],
});
await basePublic.waitForTransactionReceipt({ hash: baseRateLimitHash });

console.log("Rate limits set on both blockchains");
```

## Step 8: Verify

Call `resolveTokenAddress` and `resolveTokenManager` on both blockchains to
confirm the `tokenId` resolves correctly.

```typescript theme={null}
const sepoliaResolved = await sepoliaPublic.readContract({
  address: CCTS_ADDRESS,
  abi: serviceAbi,
  functionName: "resolveTokenAddress",
  args: [tokenId],
});

const baseResolved = await basePublic.readContract({
  address: CCTS_ADDRESS,
  abi: serviceAbi,
  functionName: "resolveTokenAddress",
  args: [tokenId],
});

console.log("Sepolia CrossChainToken:", sepoliaResolved);
console.log("Base Sepolia CrossChainToken:", baseResolved);
console.log("Sepolia TokenManager:", sepoliaTokenManager);
console.log("Base Sepolia TokenManager:", baseTokenManager);
```

Your token is now deployed on both blockchains and ready for crosschain
transfers. Before enabling fast transfers, note that fast transfers require
additional Circle coordination—FX pricing and backend indexing must be
configured by Circle before fast transfers work for newly deployed tokens.

Once the token is deployed and rate limits are set, transferring it follows the
same pattern as
[Transfer EURC from Ethereum to Arc](/cctp/expanded-assets/quickstarts/transfer-eurc-ethereum-to-arc)—swap
in your new `tokenId`.
