On Stellar, USDC precision and address encoding differ from other CCTP-supported
blockchains. Before you integrate beyond these examples, read
CCTP on Stellar.
- Stellar to Arc
- Arc to Stellar
This quickstart demonstrates how to transfer USDC from Stellar Testnet to Arc
Testnet using CCTP. You use the
@stellar/stellar-sdk library to
interact with Stellar Soroban contracts, and Create a Then, update the The When the transfer finishes, the console logs a completion message and the
relevant transaction hashes. Successful output looks similar to the following:Attestation polling can take several minutes depending on network conditions and
the finality threshold you chose. The script retries every 5 seconds with no
timeout, so if it appears to hang at
viem to mint
USDC on Arc Testnet. When you finish, you will have executed a full
burn-attest-mint flow.You should be comfortable using a terminal and Node.js. Familiarity with Stellar
Soroban transactions and basic EVM usage helps you follow and adapt the script.
Examples use Arc Testnet as the destination, but you can use any
supported blockchain.Prerequisites
Before you begin this tutorial, ensure you have:- Installed Node.js v22+
- Prepared an EVM wallet with the private key available for Arc Testnet
- Added the Arc Testnet network to your wallet (network details)
- Funded your wallet with Arc Testnet USDC (for gas fees) from the Circle Faucet
- Prepared a Stellar Testnet wallet with the secret key (
S...) available- Funded your Stellar wallet with testnet XLM from the Stellar Friendbot (for Soroban fees on Stellar Testnet)
- Established a USDC trustline on your Stellar account so you can hold the testnet USDC you burn
- Funded your Stellar wallet with Stellar Testnet USDC from the Circle Faucet
You can use Stellar Lab on Stellar Testnet to fund
accounts and establish USDC trustlines.
Step 1: Set up the project
This step shows you how to prepare your project and environment.1.1. Set up your development environment
Create a new directory and install the required dependencies:Shell
# Set up your directory and initialize a Node.js project
mkdir cctp-stellar-to-arc
cd cctp-stellar-to-arc
npm init -y
# Set up module type and start command
npm pkg set type=module
npm pkg set scripts.start="npx tsx --env-file=.env index.ts"
# Install runtime dependencies
npm install @stellar/stellar-sdk viem tsx
# Install dev dependencies
npm install --save-dev typescript @types/node
1.2. Initialize and configure the project
This step is optional. It helps prevent missing types in your IDE or editor.
tsconfig.json file:npx tsc --init
tsconfig.json file:cat <<'EOF' > tsconfig.json
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"types": ["node"]
}
}
EOF
1.3. Configure environment variables
Add your wallet keys
Open
.env in your IDE or editor and add the following. Editing the file
directly (instead of using shell redirection) helps keep credentials out of
shell history..env
STELLAR_SECRET_KEY=YOUR_STELLAR_SECRET_KEY
EVM_PRIVATE_KEY=YOUR_EVM_PRIVATE_KEY
STELLAR_SECRET_KEYis the Stellar secret key (S...) used to sign Soroban transactions on Stellar Testnet.EVM_PRIVATE_KEYis the private key for the EVM wallet you use on Arc Testnet.
This use of a private key is simplified for demonstration purposes. In
production, store and access your private key securely and never share it.
Step 2: Configure the script
Define contract addresses, amounts, and clients for Stellar Testnet and Arc Testnet.2.1. Define configuration constants
The script predefines the contract addresses, transfer amount, and maximum fee.TypeScript
import {
Address,
Contract,
Keypair,
nativeToScVal,
rpc,
TransactionBuilder,
xdr,
} from "@stellar/stellar-sdk";
import {
createWalletClient,
createPublicClient,
http,
encodeFunctionData,
defineChain,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
// Stellar Testnet Configuration
const STELLAR_RPC_URL = "https://soroban-testnet.stellar.org";
const STELLAR_NETWORK_PASSPHRASE = "Test SDF Network ; September 2015";
const stellarKeypair = Keypair.fromSecret(process.env.STELLAR_SECRET_KEY!);
const STELLAR_TOKEN_MESSENGER_MINTER =
"CDNG7HXAPBWICI2E3AUBP3YZWZELJLYSB6F5CC7WLDTLTHVM74SLRTHP";
const STELLAR_USDC = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
// Arc Testnet Configuration
const evmAccount = privateKeyToAccount(
process.env.EVM_PRIVATE_KEY! as `0x${string}`,
);
const ARC_MESSAGE_TRANSMITTER = "0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275";
const ARC_RPC_URL = "https://rpc.testnet.arc.network";
// Transfer Parameters
const AMOUNT = 10_000_000n; // 1 USDC (Stellar has 7 decimals)
const MAX_FEE = 100_000n; // 0.01 USDC in Stellar subunits (7 decimals)
// Blockchain-specific Parameters
const STELLAR_DOMAIN = 27; // Source domain ID for Stellar Testnet
const ARC_TESTNET_DOMAIN = 26; // Destination domain ID for Arc Testnet
2.2. Set up wallet clients
The wallet client configures the appropriate network settings usingviem. In
this example, the script connects to Arc Testnet.TypeScript
const arcChain = defineChain({
id: 5042002,
name: "Arc Testnet",
nativeCurrency: { name: "USDC", symbol: "USDC", decimals: 18 },
rpcUrls: { default: { http: [ARC_RPC_URL] } },
});
const arcWalletClient = createWalletClient({
chain: arcChain,
transport: http(ARC_RPC_URL),
account: evmAccount,
});
const arcPublicClient = createPublicClient({
chain: arcChain,
transport: http(ARC_RPC_URL),
});
Step 3: Implement the transfer logic
This step implements the core transfer logic: approve and burn on Stellar, poll for an attestation, then mint on Arc. A successful run prints transaction hashes and a completion message in the console.3.1. Approve and burn USDC on Stellar
Approve theTokenMessengerMinterV2 contract to spend your USDC, then call
deposit_for_burn to burn USDC on Stellar. You specify the following
parameters:- Burn amount: The amount of USDC to burn (in Stellar subunits, 7 decimals)
- Destination domain: The target blockchain for minting USDC (see supported blockchains and domains)
- Mint recipient: The wallet address that receives the minted USDC on Arc
- Burn token: The contract address of the USDC token on Stellar
- Destination caller: The address on the target blockchain that may call
receiveMessage - Max fee: The maximum fee allowed for the transfer (in Stellar subunits, 7 decimals)
- Finality threshold: Determines whether it’s a Fast Transfer (1000 or less) or a Standard Transfer (2000 or more)
TypeScript
async function burnUSDCOnStellar() {
console.log("Burning USDC on Stellar...");
const server = new rpc.Server(STELLAR_RPC_URL);
// Step 1: Approve the TokenMessengerMinter to spend USDC
const latestLedger = await server.getLatestLedger();
const expirationLedger = latestLedger.sequence + 100_000;
const approveHash = await submitSorobanTx(server, STELLAR_USDC, "approve", [
new Address(stellarKeypair.publicKey()).toScVal(),
new Address(STELLAR_TOKEN_MESSENGER_MINTER).toScVal(),
nativeToScVal(AMOUNT, { type: "i128" }),
nativeToScVal(expirationLedger, { type: "u32" }),
]);
console.log(` Approved: ${approveHash}`);
// Step 2: Build the mint recipient (EVM address → zero-padded 32-byte ScVal)
const cleaned = evmAccount.address.slice(2);
const buf = Buffer.alloc(32);
Buffer.from(cleaned, "hex").copy(buf, 12);
const mintRecipient = xdr.ScVal.scvBytes(buf);
// Step 3: Call deposit_for_burn on the TokenMessengerMinter
const txHash = await submitSorobanTx(
server,
STELLAR_TOKEN_MESSENGER_MINTER,
"deposit_for_burn",
[
new Address(stellarKeypair.publicKey()).toScVal(), // caller
nativeToScVal(AMOUNT, { type: "i128" }), // amount
nativeToScVal(ARC_TESTNET_DOMAIN, { type: "u32" }), // destination_domain
mintRecipient, // mint_recipient
new Address(STELLAR_USDC).toScVal(), // burn_token
xdr.ScVal.scvBytes(Buffer.alloc(32)), // destination_caller (empty = any relayer)
nativeToScVal(MAX_FEE, { type: "i128" }), // max_fee
nativeToScVal(1000, { type: "u32" }), // min_finality_threshold (1000 = Fast Transfer)
],
);
console.log(` Burn Tx: ${txHash}`);
return txHash;
}
submitSorobanTx helper assembles, simulates, signs, and submits a Soroban
transaction:TypeScript
async function submitSorobanTx(
server: rpc.Server,
contractId: string,
method: string,
args: xdr.ScVal[],
): Promise<string> {
const account = await server.getAccount(stellarKeypair.publicKey());
const contract = new Contract(contractId);
const tx = new TransactionBuilder(account, {
fee: "10000000",
networkPassphrase: STELLAR_NETWORK_PASSPHRASE,
})
.addOperation(contract.call(method, ...args))
.setTimeout(120)
.build();
const simulated = await server.simulateTransaction(tx);
if (rpc.Api.isSimulationError(simulated)) {
throw new Error(`Simulation failed: ${JSON.stringify(simulated)}`);
}
const prepared = rpc.assembleTransaction(tx, simulated).build();
prepared.sign(stellarKeypair);
const sendResult = await server.sendTransaction(prepared);
if (sendResult.status === "ERROR") {
throw new Error(`Send failed: ${JSON.stringify(sendResult)}`);
}
let getResult = await server.getTransaction(sendResult.hash);
while (getResult.status === "NOT_FOUND") {
await new Promise((r) => setTimeout(r, 2000));
getResult = await server.getTransaction(sendResult.hash);
}
if (getResult.status !== "SUCCESS") {
throw new Error(`Transaction failed: ${JSON.stringify(getResult)}`);
}
return sendResult.hash;
}
3.2. Retrieve attestation
Retrieve the attestation required to complete the CCTP transfer by calling Circle’s attestation API.- Call Circle’s
GET /v2/messagesAPI endpoint to retrieve the attestation. - Pass the
srcDomainargument from the CCTP domain for Stellar (27). - Pass
transactionHashfrom the value returned bysubmitSorobanTxwithin theburnUSDCOnStellarfunction above.
TypeScript
interface AttestationMessage {
message: string;
attestation: string;
status: string;
}
interface AttestationResponse {
messages: AttestationMessage[];
}
async function retrieveAttestation(transactionHash: string) {
console.log("Retrieving attestation...");
const url = `https://iris-api-sandbox.circle.com/v2/messages/${STELLAR_DOMAIN}?transactionHash=${transactionHash}`;
while (true) {
try {
const response = await fetch(url, { method: "GET" });
if (!response.ok) {
if (response.status !== 404) {
const text = await response.text().catch(() => "");
console.error(
"Error fetching attestation:",
`${response.status} ${response.statusText}${
text ? ` - ${text}` : ""
}`,
);
}
await new Promise((resolve) => setTimeout(resolve, 5000));
continue;
}
const data = (await response.json()) as AttestationResponse;
if (data?.messages?.[0]?.status === "complete") {
console.log("Attestation retrieved successfully!");
return data.messages[0];
}
console.log("Waiting for attestation...");
await new Promise((resolve) => setTimeout(resolve, 5000));
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error("Error fetching attestation:", message);
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}
}
3.3. Mint USDC on Arc
Call thereceiveMessage function
from the MessageTransmitterV2 contract
deployed on Arc Testnet to mint USDC on the destination blockchain.- Pass the signed attestation and the message bytes as parameters.
- The contract verifies the attestation and mints USDC to the recipient encoded in the CCTP message.
TypeScript
async function mintUSDCOnArc(attestation: AttestationMessage) {
console.log("Minting USDC on Arc Testnet...");
const hash = await arcWalletClient.sendTransaction({
to: ARC_MESSAGE_TRANSMITTER as `0x${string}`,
data: encodeFunctionData({
abi: [
{
type: "function",
name: "receiveMessage",
stateMutability: "nonpayable",
inputs: [
{ name: "message", type: "bytes" },
{ name: "attestation", type: "bytes" },
],
outputs: [],
},
],
functionName: "receiveMessage",
args: [
attestation.message as `0x${string}`,
attestation.attestation as `0x${string}`,
],
}),
});
const receipt = await arcPublicClient.waitForTransactionReceipt({ hash });
if (receipt.status !== "success") {
throw new Error(`receiveMessage failed: ${hash}`);
}
console.log(` Mint Tx: ${hash}`);
}
Step 4: Full script
Create anindex.ts file in your project directory and paste the full script
below so you can run the flow from one file.index.ts
import {
Address,
Contract,
Keypair,
nativeToScVal,
rpc,
TransactionBuilder,
xdr,
} from "@stellar/stellar-sdk";
import {
createWalletClient,
createPublicClient,
http,
encodeFunctionData,
defineChain,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
// Stellar Testnet Configuration
const STELLAR_RPC_URL = "https://soroban-testnet.stellar.org";
const STELLAR_NETWORK_PASSPHRASE = "Test SDF Network ; September 2015";
const stellarKeypair = Keypair.fromSecret(process.env.STELLAR_SECRET_KEY!);
const STELLAR_TOKEN_MESSENGER_MINTER =
"CDNG7HXAPBWICI2E3AUBP3YZWZELJLYSB6F5CC7WLDTLTHVM74SLRTHP";
const STELLAR_USDC = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
// Arc Testnet Configuration
const evmAccount = privateKeyToAccount(
process.env.EVM_PRIVATE_KEY! as `0x${string}`,
);
const ARC_MESSAGE_TRANSMITTER = "0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275";
const ARC_RPC_URL = "https://rpc.testnet.arc.network";
// Transfer Parameters
const AMOUNT = 10_000_000n; // 1 USDC (Stellar has 7 decimals)
const MAX_FEE = 100_000n; // 0.01 USDC in Stellar subunits (7 decimals)
// Blockchain-specific Parameters
const STELLAR_DOMAIN = 27; // Source domain ID for Stellar Testnet
const ARC_TESTNET_DOMAIN = 26; // Destination domain ID for Arc Testnet
const arcChain = defineChain({
id: 5042002,
name: "Arc Testnet",
nativeCurrency: { name: "USDC", symbol: "USDC", decimals: 18 },
rpcUrls: { default: { http: [ARC_RPC_URL] } },
});
const arcWalletClient = createWalletClient({
chain: arcChain,
transport: http(ARC_RPC_URL),
account: evmAccount,
});
const arcPublicClient = createPublicClient({
chain: arcChain,
transport: http(ARC_RPC_URL),
});
async function submitSorobanTx(
server: rpc.Server,
contractId: string,
method: string,
args: xdr.ScVal[],
): Promise<string> {
const account = await server.getAccount(stellarKeypair.publicKey());
const contract = new Contract(contractId);
const tx = new TransactionBuilder(account, {
fee: "10000000",
networkPassphrase: STELLAR_NETWORK_PASSPHRASE,
})
.addOperation(contract.call(method, ...args))
.setTimeout(120)
.build();
const simulated = await server.simulateTransaction(tx);
if (rpc.Api.isSimulationError(simulated)) {
throw new Error(`Simulation failed: ${JSON.stringify(simulated)}`);
}
const prepared = rpc.assembleTransaction(tx, simulated).build();
prepared.sign(stellarKeypair);
const sendResult = await server.sendTransaction(prepared);
if (sendResult.status === "ERROR") {
throw new Error(`Send failed: ${JSON.stringify(sendResult)}`);
}
let getResult = await server.getTransaction(sendResult.hash);
while (getResult.status === "NOT_FOUND") {
await new Promise((r) => setTimeout(r, 2000));
getResult = await server.getTransaction(sendResult.hash);
}
if (getResult.status !== "SUCCESS") {
throw new Error(`Transaction failed: ${JSON.stringify(getResult)}`);
}
return sendResult.hash;
}
async function burnUSDCOnStellar() {
console.log("Burning USDC on Stellar...");
const server = new rpc.Server(STELLAR_RPC_URL);
// Step 1: Approve the TokenMessengerMinter to spend USDC
const latestLedger = await server.getLatestLedger();
const expirationLedger = latestLedger.sequence + 100_000;
const approveHash = await submitSorobanTx(server, STELLAR_USDC, "approve", [
new Address(stellarKeypair.publicKey()).toScVal(),
new Address(STELLAR_TOKEN_MESSENGER_MINTER).toScVal(),
nativeToScVal(AMOUNT, { type: "i128" }),
nativeToScVal(expirationLedger, { type: "u32" }),
]);
console.log(` Approved: ${approveHash}`);
// Step 2: Build the mint recipient (EVM address → zero-padded 32-byte ScVal)
const cleaned = evmAccount.address.slice(2);
const buf = Buffer.alloc(32);
Buffer.from(cleaned, "hex").copy(buf, 12);
const mintRecipient = xdr.ScVal.scvBytes(buf);
// Step 3: Call deposit_for_burn on the TokenMessengerMinter
const txHash = await submitSorobanTx(
server,
STELLAR_TOKEN_MESSENGER_MINTER,
"deposit_for_burn",
[
new Address(stellarKeypair.publicKey()).toScVal(), // caller
nativeToScVal(AMOUNT, { type: "i128" }), // amount
nativeToScVal(ARC_TESTNET_DOMAIN, { type: "u32" }), // destination_domain
mintRecipient, // mint_recipient
new Address(STELLAR_USDC).toScVal(), // burn_token
xdr.ScVal.scvBytes(Buffer.alloc(32)), // destination_caller (empty = any relayer)
nativeToScVal(MAX_FEE, { type: "i128" }), // max_fee
nativeToScVal(1000, { type: "u32" }), // min_finality_threshold (1000 = Fast Transfer)
],
);
console.log(` Burn Tx: ${txHash}`);
return txHash;
}
interface AttestationMessage {
message: string;
attestation: string;
status: string;
}
interface AttestationResponse {
messages: AttestationMessage[];
}
async function retrieveAttestation(transactionHash: string) {
console.log("Retrieving attestation...");
const url = `https://iris-api-sandbox.circle.com/v2/messages/${STELLAR_DOMAIN}?transactionHash=${transactionHash}`;
while (true) {
try {
const response = await fetch(url, { method: "GET" });
if (!response.ok) {
if (response.status !== 404) {
const text = await response.text().catch(() => "");
console.error(
"Error fetching attestation:",
`${response.status} ${response.statusText}${
text ? ` - ${text}` : ""
}`,
);
}
await new Promise((resolve) => setTimeout(resolve, 5000));
continue;
}
const data = (await response.json()) as AttestationResponse;
if (data?.messages?.[0]?.status === "complete") {
console.log("Attestation retrieved successfully!");
return data.messages[0];
}
console.log("Waiting for attestation...");
await new Promise((resolve) => setTimeout(resolve, 5000));
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error("Error fetching attestation:", message);
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}
}
async function mintUSDCOnArc(attestation: AttestationMessage) {
console.log("Minting USDC on Arc Testnet...");
const hash = await arcWalletClient.sendTransaction({
to: ARC_MESSAGE_TRANSMITTER as `0x${string}`,
data: encodeFunctionData({
abi: [
{
type: "function",
name: "receiveMessage",
stateMutability: "nonpayable",
inputs: [
{ name: "message", type: "bytes" },
{ name: "attestation", type: "bytes" },
],
outputs: [],
},
],
functionName: "receiveMessage",
args: [
attestation.message as `0x${string}`,
attestation.attestation as `0x${string}`,
],
}),
});
const receipt = await arcPublicClient.waitForTransactionReceipt({ hash });
if (receipt.status !== "success") {
throw new Error(`receiveMessage failed: ${hash}`);
}
console.log(` Mint Tx: ${hash}`);
}
async function main() {
const burnTxHash = await burnUSDCOnStellar();
const attestation = await retrieveAttestation(burnTxHash);
await mintUSDCOnArc(attestation);
console.log("USDC transfer from Stellar Testnet to Arc Testnet completed!");
}
main().catch(console.error);
Step 5: Test the script
Run the following command to execute the script:Shell
npm run start
Shell
Burning USDC on Stellar...
Approved: <stellar-transaction-hash>
Burn Tx: <stellar-transaction-hash>
Retrieving attestation...
Attestation retrieved successfully!
Minting USDC on Arc Testnet...
Mint Tx: 0x...
USDC transfer from Stellar Testnet to Arc Testnet completed!
Waiting for attestation..., allow at
least five minutes before investigating.Rate limit: The attestation service rate limit is 35 requests per second. If
you exceed this limit, the service blocks all API requests for the next 5
minutes and returns an HTTP 429 (Too Many Requests) response.
This quickstart demonstrates how to transfer USDC from Arc Testnet to Stellar
Testnet using CCTP. You use the viem library to burn USDC on
Arc, and Create a Then, update the Example: For forward recipient “GABC…XYZ” (56 chars):
The When the transfer finishes, the console logs a completion message and the
relevant transaction hashes. Successful output looks similar to the following:Attestation polling can take several minutes depending on network conditions and
the finality threshold you chose. The script retries every 5 seconds with no
timeout, so if it appears to hang at
@stellar/stellar-sdk to
mint and forward tokens on the Stellar CCTP Forwarder. When you finish, you will
have executed a full burn-attest-mint flow.You should be comfortable using a terminal and Node.js. Familiarity with basic
EVM usage and Stellar Soroban transactions helps you follow and adapt the
script. Examples use Arc Testnet as the source, but you can use any
supported blockchain.Always
use
CctpForwarder
when routing CCTP USDC to a Stellar address. CCTP treats mintRecipient as a
contract, so skipping the forwarder and pointing that field at a user account or
muxed address will not send funds to the wallet you intended. USDC can end up
stranded.Prerequisites
Before you begin this tutorial, ensure you have:- Installed Node.js v22+
- Prepared an EVM wallet with the private key available for Arc Testnet
- Added the Arc Testnet network to your wallet (network details)
- Funded your wallet with Arc Testnet USDC (for gas fees and the transfer amount) from the Circle Faucet
- Prepared a Stellar Testnet wallet with the secret key (
S...) available- Funded your Stellar wallet with testnet XLM from the Stellar Friendbot (for Soroban fees on Stellar Testnet)
- Identified the forward recipient Stellar
strkey(G...,C..., orM...) that will receive the USDC after minting - Established a
USDC trustline
on the forward recipient account when the forward recipient is a
GorMstrkey(required before receiving minted USDC)
You can use Stellar Lab on Stellar Testnet to fund
accounts and establish USDC trustlines.
Step 1: Set up the project
This step shows you how to prepare your project and environment.1.1. Set up your development environment
Create a new directory and install the required dependencies:Shell
# Set up your directory and initialize a Node.js project
mkdir cctp-arc-to-stellar
cd cctp-arc-to-stellar
npm init -y
# Set up module type and start command
npm pkg set type=module
npm pkg set scripts.start="npx tsx --env-file=.env index.ts"
# Install runtime dependencies
npm install @stellar/stellar-sdk viem tsx
# Install dev dependencies
npm install --save-dev typescript @types/node
1.2. Initialize and configure the project
This step is optional. It helps prevent missing types in your IDE or editor.
tsconfig.json file:npx tsc --init
tsconfig.json file:cat <<'EOF' > tsconfig.json
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"types": ["node"]
}
}
EOF
1.3. Configure environment variables
Add your wallet keys
Open
.env in your IDE or editor and add the following. Editing the file
directly (instead of using shell redirection) helps keep credentials out of
shell history..env
EVM_PRIVATE_KEY=YOUR_EVM_PRIVATE_KEY
STELLAR_SECRET_KEY=YOUR_STELLAR_SECRET_KEY
EVM_PRIVATE_KEYis the private key for the EVM wallet you use on Arc Testnet.STELLAR_SECRET_KEYis the Stellar secret key (S...) used to sign Soroban transactions on Stellar Testnet.
This use of a private key is simplified for demonstration purposes. In
production, store and access your private key securely and never share it.
Step 2: Configure the script
This section covers the necessary setup for the transfer script, including defining keys and addresses, and configuring the wallet clients for interacting with Arc and Stellar.2.1. Define configuration constants
The script predefines the contract addresses, transfer amount, and other parameters.TypeScript
import {
StrKey,
Contract,
Keypair,
rpc,
TransactionBuilder,
xdr,
} from "@stellar/stellar-sdk";
import {
createWalletClient,
createPublicClient,
http,
encodeFunctionData,
defineChain,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
// Arc Testnet Configuration
const evmAccount = privateKeyToAccount(
process.env.EVM_PRIVATE_KEY! as `0x${string}`,
);
const ARC_USDC = "0x3600000000000000000000000000000000000000";
const ARC_TOKEN_MESSENGER = "0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA";
const ARC_RPC_URL = "https://rpc.testnet.arc.network";
// Stellar Testnet Configuration
const STELLAR_RPC_URL = "https://soroban-testnet.stellar.org";
const STELLAR_NETWORK_PASSPHRASE = "Test SDF Network ; September 2015";
const stellarKeypair = Keypair.fromSecret(process.env.STELLAR_SECRET_KEY!);
const STELLAR_CCTP_FORWARDER =
"CA66Q2WFBND6V4UEB7RD4SAXSVIWMD6RA4X3U32ELVFGXV5PJK4T4VSZ";
// Transfer Parameters
const AMOUNT = 1_000_000n; // 1 USDC (Arc has 6 decimals)
const MAX_FEE = 500n; // 0.0005 USDC
// Blockchain-specific Parameters
const ARC_TESTNET_DOMAIN = 26; // Source domain ID for Arc Testnet
const STELLAR_DOMAIN = 27; // Destination domain ID for Stellar Testnet
// The Stellar forward recipient address (G..., C..., or M... strkey)
const STELLAR_FORWARD_RECIPIENT =
process.env.STELLAR_FORWARD_RECIPIENT ?? stellarKeypair.publicKey();
2.2. Set up wallet clients
The wallet client configures the appropriate network settings usingviem. In
this example, the script connects to Arc Testnet.TypeScript
const arcChain = defineChain({
id: 5042002,
name: "Arc Testnet",
nativeCurrency: { name: "USDC", symbol: "USDC", decimals: 18 },
rpcUrls: { default: { http: [ARC_RPC_URL] } },
});
const arcWalletClient = createWalletClient({
chain: arcChain,
transport: http(ARC_RPC_URL),
account: evmAccount,
});
const arcPublicClient = createPublicClient({
chain: arcChain,
transport: http(ARC_RPC_URL),
});
2.3. Encode the CCTP Forwarder hook data
When transferring to Stellar, the Arc burn encodes the forward recipient in thehookData field. The Stellar CCTP Forwarder contract is set as both the
mintRecipient and destinationCaller.The hook data encodes the forward recipient in a specific binary format:Byte layout
[32 bytes total]
├─ Bytes 0-23: Zero padding (0x000...000)
├─ Bytes 24-27: Hook version (uint32, currently 0)
├─ Bytes 28-31: Forward recipient strkey length (uint32, byte length)
└─ Bytes 32+: Forward recipient strkey as UTF-8 bytes
- Zero padding: 0x000000000000000000000000000000000000000000000000
- Hook version: 0x00000000
- Length: 0x00000038 (56 in hex)
- Forward recipient: “GABC…XYZ” encoded as UTF-8
G and M strkey forward recipients need an established
USDC trustline
before receiving funds. Transfers without a trustline fail.How the CCTP Forwarder flow works
How the CCTP Forwarder flow works
This path follows the
CCTP Forwarder
pattern:
- Burn with hook:
depositForBurnWithHookon the ArcTokenMessengerV2contract.mintRecipientis the Stellar CCTP Forwarder, andhookDatacarries the forward recipient strkey. - Attest: Circle’s attestation service signs the burn event.
- Mint and forward:
mint_and_forwardon the Stellar CCTP Forwarder relays the message to the MessageTransmitter, mints tokens, and forwards them per the hook data.
The CCTP Forwarder flow is non-custodial. In one atomic Soroban transaction,
mint_and_forward mints to CctpForwarder and pays forwardRecipient onchain.
Circle does not take custody of the minted balance in between.TypeScript
// Convert a Stellar contract address (C...) to 0x-prefixed bytes32
function contractStrkeyToBytes32(strkey: string): `0x${string}` {
if (!StrKey.isValidContract(strkey)) {
throw new Error(`Invalid contract strkey: ${strkey}`);
}
return `0x${Buffer.from(StrKey.decodeContract(strkey)).toString("hex")}`;
}
// Build hook data encoding the forward recipient
function buildCctpForwarderHookData(
forwardRecipientStrkey: string,
): `0x${string}` {
const isValid =
StrKey.isValidEd25519PublicKey(forwardRecipientStrkey) ||
StrKey.isValidContract(forwardRecipientStrkey) ||
StrKey.isValidMed25519PublicKey(forwardRecipientStrkey);
if (!isValid) {
throw new Error(
`Invalid forward recipient: ${forwardRecipientStrkey} (expected G..., C..., or M... address)`,
);
}
const recipientBytes = Buffer.from(forwardRecipientStrkey, "utf8");
const hookData = Buffer.alloc(32 + recipientBytes.length);
hookData.writeUInt32BE(0, 24); // hook version = 0
hookData.writeUInt32BE(recipientBytes.length, 28); // recipient byte length
recipientBytes.copy(hookData, 32); // recipient strkey as UTF-8
return `0x${hookData.toString("hex")}`;
}
// Prepare all depositForBurnWithHook arguments for a Stellar destination
interface DepositForBurnWithHookParams {
amount: bigint;
destinationDomain: number;
mintRecipient: `0x${string}`;
burnToken: `0x${string}`;
destinationCaller: `0x${string}`;
maxFee: bigint;
minFinalityThreshold: number;
hookData: `0x${string}`;
}
function prepareEvmDepositForBurnWithHookToStellar(
amount: bigint,
cctpForwarderStrkey: string,
burnToken: `0x${string}`,
maxFee: bigint,
minFinalityThreshold: number,
forwardRecipientStrkey: string,
): DepositForBurnWithHookParams {
const cctpForwarderHex = contractStrkeyToBytes32(cctpForwarderStrkey);
const hookData = buildCctpForwarderHookData(forwardRecipientStrkey);
return {
amount,
destinationDomain: STELLAR_DOMAIN,
mintRecipient: cctpForwarderHex,
burnToken,
destinationCaller: cctpForwarderHex,
maxFee,
minFinalityThreshold,
hookData,
};
}
// Prepare EVM burn parameters
const burnParams = prepareEvmDepositForBurnWithHookToStellar(
AMOUNT,
STELLAR_CCTP_FORWARDER,
ARC_USDC as `0x${string}`,
MAX_FEE,
2000, // minFinalityThreshold (2000 = Standard Transfer)
STELLAR_FORWARD_RECIPIENT,
);
Step 3: Implement the transfer logic
This step implements the core transfer logic: approve and burn on Arc, poll for an attestation, then mint and forward on Stellar. A successful run prints transaction hashes and a completion message in the console.3.1. Approve and burn USDC on Arc
Grant approval for theTokenMessengerV2 contract to withdraw
USDC from your wallet, then call depositForBurnWithHook to burn USDC with the
CCTP Forwarder hook data. You specify the following parameters:- Burn amount: The amount of USDC to burn (in Arc subunits, 6 decimals)
- Destination domain: The target blockchain for minting USDC (see supported blockchains and domains)
- Mint recipient: The CCTP Forwarder contract address on Stellar (encoded as
bytes32) - Burn token: The contract address of the USDC token being burned on Arc
- Destination caller: The CCTP Forwarder contract address on Stellar (restricts who can relay)
- Max fee: The maximum fee allowed for the transfer
- Finality threshold: Determines whether it’s a Fast Transfer (1000 or less) or a Standard Transfer (2000 or more)
- Hook data: Encodes the final Stellar recipient address for the CCTP Forwarder
TypeScript
async function burnUSDCOnArc() {
// Approve
console.log("Approving USDC spend on Arc...");
const approveHash = await arcWalletClient.sendTransaction({
to: ARC_USDC as `0x${string}`,
data: encodeFunctionData({
abi: [
{
type: "function",
name: "approve",
stateMutability: "nonpayable",
inputs: [
{ name: "spender", type: "address" },
{ name: "amount", type: "uint256" },
],
outputs: [{ name: "", type: "bool" }],
},
],
functionName: "approve",
args: [ARC_TOKEN_MESSENGER as `0x${string}`, AMOUNT],
}),
});
await arcPublicClient.waitForTransactionReceipt({ hash: approveHash });
console.log(` Approved: ${approveHash}`);
// Burn with hook
console.log("Burning USDC on Arc (with hook)...");
const burnHash = await arcWalletClient.sendTransaction({
to: ARC_TOKEN_MESSENGER as `0x${string}`,
data: encodeFunctionData({
abi: [
{
type: "function",
name: "depositForBurnWithHook",
stateMutability: "nonpayable",
inputs: [
{ name: "amount", type: "uint256" },
{ name: "destinationDomain", type: "uint32" },
{ name: "mintRecipient", type: "bytes32" },
{ name: "burnToken", type: "address" },
{ name: "destinationCaller", type: "bytes32" },
{ name: "maxFee", type: "uint256" },
{ name: "minFinalityThreshold", type: "uint32" },
{ name: "hookData", type: "bytes" },
],
outputs: [],
},
],
functionName: "depositForBurnWithHook",
args: [
burnParams.amount,
burnParams.destinationDomain,
burnParams.mintRecipient,
burnParams.burnToken,
burnParams.destinationCaller,
burnParams.maxFee,
burnParams.minFinalityThreshold,
burnParams.hookData,
],
}),
});
const receipt = await arcPublicClient.waitForTransactionReceipt({
hash: burnHash,
});
if (receipt.status !== "success") {
throw new Error(`depositForBurnWithHook failed: ${burnHash}`);
}
console.log(` Burn Tx: ${burnHash}`);
return burnHash;
}
3.2. Retrieve attestation
Retrieve the attestation required to complete the CCTP transfer by calling Circle’s attestation API.- Call Circle’s
GET /v2/messagesAPI endpoint to retrieve the attestation. - Pass the
srcDomainargument from the CCTP domain for Arc Testnet (26). - Pass
transactionHashfrom the value returned byburnUSDCOnArcabove.
TypeScript
interface AttestationMessage {
message: string;
attestation: string;
status: string;
}
interface AttestationResponse {
messages: AttestationMessage[];
}
async function retrieveAttestation(transactionHash: string) {
console.log("Retrieving attestation...");
const url = `https://iris-api-sandbox.circle.com/v2/messages/${ARC_TESTNET_DOMAIN}?transactionHash=${transactionHash}`;
while (true) {
try {
const response = await fetch(url, { method: "GET" });
if (!response.ok) {
if (response.status !== 404) {
const text = await response.text().catch(() => "");
console.error(
"Error fetching attestation:",
`${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`,
);
}
await new Promise((resolve) => setTimeout(resolve, 5000));
continue;
}
const data = (await response.json()) as AttestationResponse;
if (data?.messages?.[0]?.status === "complete") {
console.log("Attestation retrieved successfully!");
return data.messages[0];
}
console.log("Waiting for attestation...");
await new Promise((resolve) => setTimeout(resolve, 5000));
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error("Error fetching attestation:", message);
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}
}
3.3. Mint and forward USDC on Stellar
Callmint_and_forward on the Stellar CCTP Forwarder contract. The forwarder
verifies the CCTP message, mints tokens via the TokenMessengerMinter, and
forwards them to the recipient encoded in the hook data.TypeScript
async function mintAndForwardOnStellar(attestation: AttestationMessage) {
console.log("Minting and forwarding USDC on Stellar...");
const server = new rpc.Server(STELLAR_RPC_URL);
const messageBytes = Buffer.from(
attestation.message.replace("0x", ""),
"hex",
);
const attestationBytes = Buffer.from(
attestation.attestation.replace("0x", ""),
"hex",
);
const txHash = await submitSorobanTx(
server,
STELLAR_CCTP_FORWARDER,
"mint_and_forward",
[xdr.ScVal.scvBytes(messageBytes), xdr.ScVal.scvBytes(attestationBytes)],
);
console.log(` mint_and_forward Tx: ${txHash}`);
}
submitSorobanTx helper assembles, simulates, signs, and submits a Soroban
transaction:TypeScript
async function submitSorobanTx(
server: rpc.Server,
contractId: string,
method: string,
args: xdr.ScVal[],
): Promise<string> {
const account = await server.getAccount(stellarKeypair.publicKey());
const contract = new Contract(contractId);
const tx = new TransactionBuilder(account, {
fee: "10000000",
networkPassphrase: STELLAR_NETWORK_PASSPHRASE,
})
.addOperation(contract.call(method, ...args))
.setTimeout(120)
.build();
const simulated = await server.simulateTransaction(tx);
if (rpc.Api.isSimulationError(simulated)) {
throw new Error(`Simulation failed: ${JSON.stringify(simulated)}`);
}
const prepared = rpc.assembleTransaction(tx, simulated).build();
prepared.sign(stellarKeypair);
const sendResult = await server.sendTransaction(prepared);
if (sendResult.status === "ERROR") {
throw new Error(`Send failed: ${JSON.stringify(sendResult)}`);
}
let getResult = await server.getTransaction(sendResult.hash);
while (getResult.status === "NOT_FOUND") {
await new Promise((r) => setTimeout(r, 2000));
getResult = await server.getTransaction(sendResult.hash);
}
if (getResult.status !== "SUCCESS") {
throw new Error(`Transaction failed: ${JSON.stringify(getResult)}`);
}
return sendResult.hash;
}
Step 4: Full script
Create anindex.ts file in your project directory and paste the full script
below so you can run the flow from one file.index.ts
import {
StrKey,
Contract,
Keypair,
rpc,
TransactionBuilder,
xdr,
} from "@stellar/stellar-sdk";
import {
createWalletClient,
createPublicClient,
http,
encodeFunctionData,
defineChain,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
// Arc Testnet Configuration
const evmAccount = privateKeyToAccount(
process.env.EVM_PRIVATE_KEY! as `0x${string}`,
);
const ARC_USDC = "0x3600000000000000000000000000000000000000";
const ARC_TOKEN_MESSENGER = "0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA";
const ARC_RPC_URL = "https://rpc.testnet.arc.network";
// Stellar Testnet Configuration
const STELLAR_RPC_URL = "https://soroban-testnet.stellar.org";
const STELLAR_NETWORK_PASSPHRASE = "Test SDF Network ; September 2015";
const stellarKeypair = Keypair.fromSecret(process.env.STELLAR_SECRET_KEY!);
const STELLAR_CCTP_FORWARDER =
"CA66Q2WFBND6V4UEB7RD4SAXSVIWMD6RA4X3U32ELVFGXV5PJK4T4VSZ";
// Transfer Parameters
const AMOUNT = 1_000_000n; // 1 USDC (Arc has 6 decimals)
const MAX_FEE = 500n; // 0.0005 USDC
// Blockchain-specific Parameters
const ARC_TESTNET_DOMAIN = 26; // Source domain ID for Arc Testnet
const STELLAR_DOMAIN = 27; // Destination domain ID for Stellar Testnet
// The Stellar forward recipient address (G..., C..., or M... strkey)
const STELLAR_FORWARD_RECIPIENT =
process.env.STELLAR_FORWARD_RECIPIENT ?? stellarKeypair.publicKey();
const arcChain = defineChain({
id: 5042002,
name: "Arc Testnet",
nativeCurrency: { name: "USDC", symbol: "USDC", decimals: 18 },
rpcUrls: { default: { http: [ARC_RPC_URL] } },
});
const arcWalletClient = createWalletClient({
chain: arcChain,
transport: http(ARC_RPC_URL),
account: evmAccount,
});
const arcPublicClient = createPublicClient({
chain: arcChain,
transport: http(ARC_RPC_URL),
});
function contractStrkeyToBytes32(strkey: string): `0x${string}` {
if (!StrKey.isValidContract(strkey)) {
throw new Error(`Invalid contract strkey: ${strkey}`);
}
return `0x${Buffer.from(StrKey.decodeContract(strkey)).toString("hex")}`;
}
function buildCctpForwarderHookData(
forwardRecipientStrkey: string,
): `0x${string}` {
const isValid =
StrKey.isValidEd25519PublicKey(forwardRecipientStrkey) ||
StrKey.isValidContract(forwardRecipientStrkey) ||
StrKey.isValidMed25519PublicKey(forwardRecipientStrkey);
if (!isValid) {
throw new Error(
`Invalid forward recipient: ${forwardRecipientStrkey} (expected G..., C..., or M... address)`,
);
}
const recipientBytes = Buffer.from(forwardRecipientStrkey, "utf8");
const hookData = Buffer.alloc(32 + recipientBytes.length);
hookData.writeUInt32BE(0, 24); // hook version = 0
hookData.writeUInt32BE(recipientBytes.length, 28); // recipient byte length
recipientBytes.copy(hookData, 32); // recipient strkey as UTF-8
return `0x${hookData.toString("hex")}`;
}
interface DepositForBurnWithHookParams {
amount: bigint;
destinationDomain: number;
mintRecipient: `0x${string}`;
burnToken: `0x${string}`;
destinationCaller: `0x${string}`;
maxFee: bigint;
minFinalityThreshold: number;
hookData: `0x${string}`;
}
function prepareEvmDepositForBurnWithHookToStellar(
amount: bigint,
cctpForwarderStrkey: string,
burnToken: `0x${string}`,
maxFee: bigint,
minFinalityThreshold: number,
forwardRecipientStrkey: string,
): DepositForBurnWithHookParams {
const cctpForwarderHex = contractStrkeyToBytes32(cctpForwarderStrkey);
const hookData = buildCctpForwarderHookData(forwardRecipientStrkey);
return {
amount,
destinationDomain: STELLAR_DOMAIN,
mintRecipient: cctpForwarderHex,
burnToken,
destinationCaller: cctpForwarderHex,
maxFee,
minFinalityThreshold,
hookData,
};
}
const burnParams = prepareEvmDepositForBurnWithHookToStellar(
AMOUNT,
STELLAR_CCTP_FORWARDER,
ARC_USDC as `0x${string}`,
MAX_FEE,
2000, // minFinalityThreshold (2000 = Standard Transfer)
STELLAR_FORWARD_RECIPIENT,
);
interface AttestationMessage {
message: string;
attestation: string;
status: string;
}
interface AttestationResponse {
messages: AttestationMessage[];
}
async function burnUSDCOnArc() {
console.log("Approving USDC spend on Arc...");
const approveHash = await arcWalletClient.sendTransaction({
to: ARC_USDC as `0x${string}`,
data: encodeFunctionData({
abi: [
{
type: "function",
name: "approve",
stateMutability: "nonpayable",
inputs: [
{ name: "spender", type: "address" },
{ name: "amount", type: "uint256" },
],
outputs: [{ name: "", type: "bool" }],
},
],
functionName: "approve",
args: [ARC_TOKEN_MESSENGER as `0x${string}`, AMOUNT],
}),
});
await arcPublicClient.waitForTransactionReceipt({ hash: approveHash });
console.log(` Approved: ${approveHash}`);
console.log("Burning USDC on Arc (with hook)...");
const burnHash = await arcWalletClient.sendTransaction({
to: ARC_TOKEN_MESSENGER as `0x${string}`,
data: encodeFunctionData({
abi: [
{
type: "function",
name: "depositForBurnWithHook",
stateMutability: "nonpayable",
inputs: [
{ name: "amount", type: "uint256" },
{ name: "destinationDomain", type: "uint32" },
{ name: "mintRecipient", type: "bytes32" },
{ name: "burnToken", type: "address" },
{ name: "destinationCaller", type: "bytes32" },
{ name: "maxFee", type: "uint256" },
{ name: "minFinalityThreshold", type: "uint32" },
{ name: "hookData", type: "bytes" },
],
outputs: [],
},
],
functionName: "depositForBurnWithHook",
args: [
burnParams.amount,
burnParams.destinationDomain,
burnParams.mintRecipient,
burnParams.burnToken,
burnParams.destinationCaller,
burnParams.maxFee,
burnParams.minFinalityThreshold,
burnParams.hookData,
],
}),
});
const receipt = await arcPublicClient.waitForTransactionReceipt({
hash: burnHash,
});
if (receipt.status !== "success") {
throw new Error(`depositForBurnWithHook failed: ${burnHash}`);
}
console.log(` Burn Tx: ${burnHash}`);
return burnHash;
}
async function retrieveAttestation(transactionHash: string) {
console.log("Retrieving attestation...");
const url = `https://iris-api-sandbox.circle.com/v2/messages/${ARC_TESTNET_DOMAIN}?transactionHash=${transactionHash}`;
while (true) {
try {
const response = await fetch(url, { method: "GET" });
if (!response.ok) {
if (response.status !== 404) {
const text = await response.text().catch(() => "");
console.error(
"Error fetching attestation:",
`${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`,
);
}
await new Promise((resolve) => setTimeout(resolve, 5000));
continue;
}
const data = (await response.json()) as AttestationResponse;
if (data?.messages?.[0]?.status === "complete") {
console.log("Attestation retrieved successfully!");
return data.messages[0];
}
console.log("Waiting for attestation...");
await new Promise((resolve) => setTimeout(resolve, 5000));
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error("Error fetching attestation:", message);
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}
}
async function submitSorobanTx(
server: rpc.Server,
contractId: string,
method: string,
args: xdr.ScVal[],
): Promise<string> {
const account = await server.getAccount(stellarKeypair.publicKey());
const contract = new Contract(contractId);
const tx = new TransactionBuilder(account, {
fee: "10000000",
networkPassphrase: STELLAR_NETWORK_PASSPHRASE,
})
.addOperation(contract.call(method, ...args))
.setTimeout(120)
.build();
const simulated = await server.simulateTransaction(tx);
if (rpc.Api.isSimulationError(simulated)) {
throw new Error(`Simulation failed: ${JSON.stringify(simulated)}`);
}
const prepared = rpc.assembleTransaction(tx, simulated).build();
prepared.sign(stellarKeypair);
const sendResult = await server.sendTransaction(prepared);
if (sendResult.status === "ERROR") {
throw new Error(`Send failed: ${JSON.stringify(sendResult)}`);
}
let getResult = await server.getTransaction(sendResult.hash);
while (getResult.status === "NOT_FOUND") {
await new Promise((r) => setTimeout(r, 2000));
getResult = await server.getTransaction(sendResult.hash);
}
if (getResult.status !== "SUCCESS") {
throw new Error(`Transaction failed: ${JSON.stringify(getResult)}`);
}
return sendResult.hash;
}
async function mintAndForwardOnStellar(attestation: AttestationMessage) {
console.log("Minting and forwarding USDC on Stellar...");
const server = new rpc.Server(STELLAR_RPC_URL);
const messageBytes = Buffer.from(
attestation.message.replace("0x", ""),
"hex",
);
const attestationBytes = Buffer.from(
attestation.attestation.replace("0x", ""),
"hex",
);
const txHash = await submitSorobanTx(
server,
STELLAR_CCTP_FORWARDER,
"mint_and_forward",
[xdr.ScVal.scvBytes(messageBytes), xdr.ScVal.scvBytes(attestationBytes)],
);
console.log(` mint_and_forward Tx: ${txHash}`);
}
async function main() {
const burnTxHash = await burnUSDCOnArc();
const attestation = await retrieveAttestation(burnTxHash);
await mintAndForwardOnStellar(attestation);
console.log("USDC transfer from Arc to Stellar completed!");
}
main().catch(console.error);
Step 5: Test the script
Run the following command to execute the script:Shell
npm run start
Shell
Approving USDC spend on Arc...
Approved: 0x...
Burning USDC on Arc (with hook)...
Burn Tx: 0x...
Retrieving attestation...
Attestation retrieved successfully!
Minting and forwarding USDC on Stellar...
mint_and_forward Tx: <stellar-transaction-hash>
USDC transfer from Arc to Stellar completed!
Waiting for attestation..., allow at
least five minutes before investigating.Rate limit: The attestation service rate limit is 35 requests per second. If
you exceed this limit, the service blocks all API requests for the next 5
minutes and returns an HTTP 429 (Too Many Requests) response.