# Arc MCP server Source: https://docs.arc.io/ai/mcp Connect your AI coding tools to Arc documentation using the Model Context Protocol (MCP) server for search and full-page retrieval. The Arc Model Context Protocol (MCP) server gives AI tools direct access to Arc documentation so they can search for relevant content and retrieve full pages during conversations. It is hosted at `https://docs.arc.io/mcp` and requires no authentication. The server exposes two tools: * **Search**: finds relevant documentation snippets based on a query. * **Get page**: retrieves the full content of a specific documentation page. For a machine-readable index of all documentation pages, see the [`llms.txt` file](https://docs.arc.io/llms.txt). ## Claude Code Run the following command to add the Arc MCP server: ```bash theme={null} claude mcp add --transport http arc-docs https://docs.arc.io/mcp ``` Claude Code automatically discovers the server's tools on the next conversation. ## Claude Desktop 1. Open **Settings** and navigate to **Connectors**. 2. Select **Add custom connector**. 3. Enter `Arc Docs` as the name and `https://docs.arc.io/mcp` as the URL. 4. During a chat, use the attachments button to select the Arc Docs connector. ## Cursor Add the following to your `mcp.json` file (accessible via **Cursor Settings > MCP**): ```json theme={null} { "mcpServers": { "arc-docs": { "url": "https://docs.arc.io/mcp" } } } ``` ## VS Code (Copilot) Create or update `.vscode/mcp.json` in your project root: ```json theme={null} { "servers": { "arc-docs": { "type": "http", "url": "https://docs.arc.io/mcp" } } } ``` ## Windsurf Add the following to your Windsurf MCP configuration: ```json theme={null} { "mcpServers": { "arc-docs": { "serverUrl": "https://docs.arc.io/mcp" } } } ``` ## Other MCP clients Any MCP-compatible client can connect using the HTTP transport at `https://docs.arc.io/mcp`. Most clients require only the server URL and transport type (`http`). Refer to your client's documentation for the exact configuration format. ## Verify the connection After adding the server, confirm the connection by asking your AI tool a question about Arc, such as "What smart contract standards does Arc support?" The tool should return content sourced from Arc documentation. If it does not: * **Check the URL**: confirm it is exactly `https://docs.arc.io/mcp` with no trailing path. * **Check network access**: the server must be reachable over HTTPS from your machine. * **Restart the client**: some tools only detect new MCP servers after a restart or new session. # AI skills for building on Arc Source: https://docs.arc.io/ai/skills Use Circle's open source AI skills to accelerate development on Arc with AI-assisted IDEs. Skills give AI coding tools specialized knowledge for building on Arc and with Circle's products, including USDC, agent wallets, crosschain transfers, and smart contracts. Skills are available in the [circlefin/skills](https://github.com/circlefin/skills) repository. ## Installation Install Circle Skills with the command-line. ```shell Claude Code icon="https://mintcdn.com/arcio-docs/DcVxxFlqh75sTiVQ/images/claude-logo.png?fit=max&auto=format&n=DcVxxFlqh75sTiVQ&q=85&s=b0262a5319054feab00173c330431e02" theme={null} /plugin marketplace add circlefin/skills /plugin install circle-skills@circle ``` ```shell Vercel Skills CLI icon="https://mintcdn.com/arcio-docs/DcVxxFlqh75sTiVQ/images/vercel_logo.png?fit=max&auto=format&n=DcVxxFlqh75sTiVQ&q=85&s=9a66052e003d6b18378e0d8a1e0f35f5" theme={null} npx skills add circlefin/skills ``` ## Available skills | Skill | What it covers | | :---------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------- | | [`use-arc`](https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-arc/SKILL.md) | Chain config, contract deployment, viem/wagmi integration, bridging via CCTP | | [`use-circle-cli`](https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-circle-cli/SKILL.md) | Unified CLI for agent wallets, x402 payments, and crosschain transfers | | [`use-agent-wallet`](https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-agent-wallet/SKILL.md) | Email/OTP login, wallet creation, status checks, balance inspection | | [`fund-agent-wallet`](https://github.com/circlefin/skills/blob/master/plugins/circle/skills/fund-agent-wallet/SKILL.md) | Fund with USDC via fiat on-ramp, crypto transfer, or Gateway deposit | | [`agent-wallet-policy`](https://github.com/circlefin/skills/blob/master/plugins/circle/skills/agent-wallet-policy/SKILL.md) | View and reset per-tx, daily, weekly, and monthly USDC spending caps | | [`pay-via-agent-wallet`](https://github.com/circlefin/skills/blob/master/plugins/circle/skills/pay-via-agent-wallet/SKILL.md) | Pay paid APIs in USDC via x402 (search, market data, weather, news, sports) | | [`use-usdc`](https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-usdc/SKILL.md) | Balances, transfers, approvals, verification across EVM and Solana | | [`bridge-stablecoin`](https://github.com/circlefin/skills/blob/master/plugins/circle/skills/bridge-stablecoin/SKILL.md) | CCTP bridging with App Kit, progress tracking, EVM and Solana | | [`swap-tokens`](https://github.com/circlefin/skills/blob/master/plugins/circle/skills/swap-tokens/SKILL.md) | Same-chain swaps via App Kit or Swap Kit; combine with bridge for crosschain | | [`use-gateway`](https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-gateway/SKILL.md) | Gateway unified balance for sub-500ms crosschain transfers | | [`unify-balance`](https://github.com/circlefin/skills/blob/master/plugins/circle/skills/unify-balance/SKILL.md) | Crosschain USDC balance via App Kit or Unified Balance Kit | | [`use-circle-wallets`](https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-circle-wallets/SKILL.md) | Choose between developer-controlled, user-controlled, and modular wallets | | [`use-developer-controlled-wallets`](https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-developer-controlled-wallets/SKILL.md) | Custodial wallets for payouts, treasury, subscriptions, automation | | [`use-user-controlled-wallets`](https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-user-controlled-wallets/SKILL.md) | Embedded wallets with social login or OTP/PIN, no seed phrases | | [`use-modular-wallets`](https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-modular-wallets/SKILL.md) | Passkey auth and gasless transactions; ERC-4337 and ERC-6900 | | [`use-smart-contract-platform`](https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-smart-contract-platform/SKILL.md) | Deploy/import/call/monitor contracts; ERC-20/721/1155 templates | # App Kits Source: https://docs.arc.io/app-kit Build payment and liquidity workflows across blockchains with App Kits App Kits is a suite of SDKs for composing multichain payment and liquidity flows without orchestrating separate, low-level protocol integrations for each blockchain or use case. The App Kit SDK is the all-in-one core package. Install it to access every capability—Send, Bridge, Swap, and Unified Balance—behind one type-safe interface. Or, you can install Bridge Kit, Swap Kit, or Unified Balance Kit separately. ## Quick install To get started quickly, install the App Kit SDK and the Viem adapter: ```bash npm theme={null} npm install @circle-fin/app-kit @circle-fin/adapter-viem-v2 viem ``` ```bash yarn theme={null} yarn add @circle-fin/app-kit @circle-fin/adapter-viem-v2 viem ``` Need a different adapter or individual kits? See the full [installation](/app-kit/tutorials/installation) guide. ## Core capabilities Combine and use any of the App Kit SDK's built-in core capabilities in your app. Bridge, Swap, and Unified Balance are also available as individual kits. Transfer USDC across blockchains. Exchange one token for another on the same blockchain or across blockchains. Transfer tokens between wallets on the same blockchain. Create a chain-abstracted balance and spend it instantly. ## Key benefits * **Simple setup**: Get up and running with minimal configuration and a few lines of code. * **Application monetization**: Collect a custom fee from end users without writing new code. * **Flexible configurations**: Specify custom RPC endpoints and wallet clients. * **Broad compatibility**: Works with Viem, Ethers, Solana, and Circle Wallets, integrating smoothly with existing developer workflows. * **Protocol abstraction**: Build against a single interface over underlying protocols such as [Gateway](https://developers.circle.com/gateway) and [CCTP](https://developers.circle.com/cctp). * **Composable workflows**: Combine multiple capabilities in one product flow without stitching together separate protocol integrations. ## Quick look The following examples show how each capability can be integrated with a single method call. ```typescript TypeScript theme={null} // Transfer 1.00 USDC from Ethereum to Arc const result = await kit.bridge({ from: { adapter: viemAdapter, chain: "Ethereum_Sepolia" }, to: { adapter: viemAdapter, chain: "Arc_Testnet" }, amount: "1.00", }); ``` Ready to start bridging? Follow the [Bridge tokens across blockchains](/app-kit/quickstarts/bridge-tokens-across-blockchains) quickstart. ```typescript TypeScript theme={null} // Swap 1.00 USDC for EURC on Arc Testnet const result = await kit.swap({ from: { adapter: viemAdapter, chain: "Arc_Testnet" }, tokenIn: "USDC", tokenOut: "EURC", amountIn: "1.00", }); ``` Ready to start swapping? Follow the [Swap tokens on a blockchain](/app-kit/quickstarts/swap-tokens-same-chain) quickstart. ```typescript TypeScript theme={null} // Deposit 1.00 USDC into the Unified Balance from Base const depositBase = await kit.unifiedBalance.deposit({ from: { adapter: viemAdapter, chain: "Base_Sepolia" }, amount: "1.00", token: "USDC", }); // Deposit 1.00 USDC into the Unified Balance from Arbitrum const depositArb = await kit.unifiedBalance.deposit({ from: { adapter: viemAdapter, chain: "Arbitrum_Sepolia" }, amount: "1.00", token: "USDC", }); // Spend 1.50 USDC from the Unified Balance on Arc const spendResult = await kit.unifiedBalance.spend({ from: { adapter: viemAdapter }, amountIn: "1.50", to: { adapter: viemAdapter, chain: "Arc_Testnet", recipientAddress: "0xRecipientAddress", }, }); ``` Ready to start using a Unified Balance? Follow the quickstarts: * [Deposit and spend a Unified Balance](/app-kit/quickstarts/unified-balance-deposit-and-spend) * [Use a delegate to deposit and spend a Unified Balance](/app-kit/quickstarts/unified-balance-delegate-deposit-and-spend) ```typescript TypeScript theme={null} // Send 1.00 USDC from one wallet to another on Arc Testnet const result = await kit.send({ from: { adapter: viemAdapter, chain: "Arc_Testnet" }, to: "RECIPIENT_ADDRESS", amount: "1.00", token: "USDC", }); ``` Ready to start sending tokens? Follow the [Send tokens across wallets](/app-kit/quickstarts/send-tokens-same-chain) quickstart. Want to combine capabilities? Follow the [Swap tokens across chains](/app-kit/quickstarts/swap-tokens-crosschain) quickstart to swap and bridge tokens in the same flow. # App Kit SDK: Bridge Source: https://docs.arc.io/app-kit/bridge Transfer USDC across blockchains with the App Kit SDK's Bridge capability The [App Kit SDK](/app-kit) includes the Bridge capability that lets you move USDC across blockchains in a few lines of code. It abstracts the underlying [CCTP](https://developers.circle.com/cctp) flow so you can bridge without orchestrating the low-level burn, attestation, and mint steps yourself. ## How it works Bridge moves USDC from a wallet on the source blockchain to a wallet on the destination blockchain. The SDK handles the underlying [CCTP](https://developers.circle.com/cctp) burn, attestation, and mint steps for you. ```mermaid theme={null} flowchart LR subgraph ChainA[Source blockchain] A[Wallet] end subgraph ChainB[Destination blockchain] B[Wallet] end A -->|"bridge USDC"| B ``` ## Quick look This code snippet bridges between an EVM and non-EVM chain in a single method call: ```typescript TypeScript theme={null} // Transfer 1.00 USDC from Solana to Arc const result = await kit.bridge({ from: { adapter: solanaAdapter, chain: "Solana_Devnet" }, to: { adapter: viemAdapter, chain: "Arc_Testnet" }, amount: "1.00", }); ``` For a complete end-to-end flow, follow the [quickstart](/app-kit/quickstarts/bridge-tokens-across-blockchains). ## Installation [Install the App Kit SDK](/app-kit/tutorials/installation) to use Bridge. If you only need to bridge and don't want to install the full App Kit SDK, follow the steps below to install the standalone Bridge Kit. ```bash npm theme={null} npm install @circle-fin/bridge-kit ``` ```bash yarn theme={null} yarn add @circle-fin/bridge-kit ``` Install the [adapters](/app-kit/tutorials/adapter-setups) you need for the chains you plan to bridge between. ```bash npm theme={null} npm install @circle-fin/adapter-viem-v2 viem ``` ```bash yarn theme={null} yarn add @circle-fin/adapter-viem-v2 viem ``` ```bash npm theme={null} npm install @circle-fin/adapter-ethers-v6 ethers ``` ```bash yarn theme={null} yarn add @circle-fin/adapter-ethers-v6 ethers ``` ```bash npm theme={null} npm install @circle-fin/adapter-solana-kit @solana/kit @solana/web3.js ``` ```bash yarn theme={null} yarn add @circle-fin/adapter-solana-kit @solana/kit @solana/web3.js ``` ```bash npm theme={null} npm install @circle-fin/adapter-circle-wallets ``` ```bash yarn theme={null} yarn add @circle-fin/adapter-circle-wallets ``` # How bridge fees work Source: https://docs.arc.io/app-kit/concepts/bridge-fees How custom fees, CCTP protocol fees, Forwarding Service fees, and source-paid (upfront) fees apply when bridging USDC with the App Kit SDK This guide explains which fees apply when bridging, how funds move through a transaction, and the best practices to follow when [implementing custom fees](/app-kit/tutorials/bridge/collect-bridge-fee). ## Fee breakdown The following fees can apply: | Fee | When it applies | Amount | Recipient | | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | | Custom fee | Conditionally. When you [implement custom bridge fees](/app-kit/tutorials/bridge/collect-bridge-fee). | You define (on top of the bridge amount). | 90% to your fee recipient; 10% to Arc | | Cross-Chain Transfer Protocol (CCTP) fee | Conditionally. On [`FAST`](/app-kit/tutorials/bridge/configure-transfer-speed) transfers only; `SLOW` (Standard) transfers skip this fee. | Varies by source blockchain. See [CCTP fees](https://developers.circle.com/cctp/technical-guide#fees). | [Circle CCTP](https://developers.circle.com/cctp) (the underlying protocol) | | Forwarding Service fee | Conditionally. When you enable the [Forwarding Service](/app-kit/tutorials/bridge/use-forwarding-service). | Per [Forwarding Service fees](https://developers.circle.com/cctp/concepts/forwarding-service#fees-and-execution). Deducted from mint on destination by default. | [Circle CCTP](https://developers.circle.com/cctp) | ## Pay fees on the source chain By default, CCTP Fast Transfer and Forwarding Service fees reduce the amount minted on the destination. You can instead [pay those fees on the source chain](/app-kit/tutorials/bridge/pay-fees-on-source) (`config.feePayment: "source"`) so the recipient receives the exact bridge amount. Source-paid fees are incompatible with custom fees. ## How funds flow through a transfer The following example shows what happens when a user wants 1,000 USDC to arrive at the destination after a Fast Transfer, with a 10 USDC custom fee on that transfer, and the [Forwarding Service](/app-kit/tutorials/bridge/use-forwarding-service) enabled. The bridge amount is 1,000.30 USDC so that after the example CCTP protocol fee (0.10 USDC) and Forwarding Service fee (0.20 USDC), 1,000 USDC is credited to the recipient: The user initiates a 1,000.30 USDC bridge transfer on the source blockchain (sized for 1,000 USDC net to the destination after the example fees). You add a 10 USDC (about 1%) custom fee. The source wallet signs a transaction for 1,010.30 USDC (bridge amount + custom fee). The 10 USDC custom fee is split on the source blockchain: * Arc receives 1 USDC (10%). * Your fee recipient receives 9 USDC (remaining 90%). The 1,000.30 USDC bridge amount is forwarded to CCTP. CCTP takes a protocol fee (0.10 USDC in this example) for a Fast Transfer. The Forwarding Service deducts its fee (0.20 USDC in this example) from the amount to be minted on the destination blockchain. The destination wallet receives 1,000.00 USDC on the destination blockchain. This flow is illustrated in the following diagram: ```mermaid theme={null} flowchart TB Source["*Source wallet*#160;"] --> Approved["Approved amount:
1,010.30 USDC"] Approved --> CustomFee["Custom fee:
-10 USDC"] Approved --> Transfer["Bridge amount:
1,000.30 USDC"] CustomFee --> ArcShare["10% of custom fee:
1 USDC"] ArcShare --> Arc["*Arc*#160;"] CustomFee --> PartnerShare["90% of custom fee:
9 USDC"] PartnerShare --> Partner["*Your fee recipient*#160;"] Transfer --> CCTP["*CCTP*#160;"] CCTP --> Transfer2["Net amount after fees:
1,000.00 USDC"] Transfer2 --> Destination["*Destination wallet*#160;"] CCTP --> FastFee["CCTP protocol fee (example):
-0.10 USDC"] CCTP --> ForwarderFee["Forwarding Service fee (example):
-0.20 USDC"] FastFee --> Circle["*Circle*#160;"] ForwarderFee --> Circle ``` ## Best practices for custom fees Follow these best practices when implementing custom fees: * Treat the custom fee as an amount added on top of the bridge transfer. Do not subtract it from the bridge amount. * Validate that the user's wallet balance covers both the bridge amount and the custom fee. The following code shows an example balance check: ```typescript TypeScript theme={null} const requiredBalance = parseFloat(amount) + parseFloat(customFee); if (userBalance < requiredBalance) { throw new Error(`Insufficient balance. Need ${requiredBalance} USDC`); } ``` * Use a fee recipient address on the source blockchain. Do not use an address on the destination. * In your UI, display the following to the user before they confirm the transaction: * The total source wallet debit: bridge amount + custom fee * The full fee breakdown: bridge amount, custom fee, CCTP Fast Transfer fee (if applicable), and Forwarding Service fee (if using the [Forwarding Service](/app-kit/tutorials/bridge/use-forwarding-service)) * Return human-readable decimal strings. For example, `10` rather than `10000000` for 10 USDC. The App Kit SDK handles base-unit conversion internally. # How swap fees work Source: https://docs.arc.io/app-kit/concepts/swap-fees How custom fees and provider fees apply when swapping tokens with the App Kit SDK This guide explains which fees apply when performing a swap, how funds move through a swap transaction, and best practices for [implementing custom swap fees](/app-kit/tutorials/swap/collect-swap-fee). ## Fee breakdown Two fees can apply to a swap: | Fee | When it applies | Amount | Recipient | | ------------ | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------- | | Custom fee | Conditionally. When you [configure custom swap fees](/app-kit/tutorials/swap/collect-swap-fee). | Percentage you define on the swap amount; collected before the swap executes. | 90% to your fee recipient; 10% to Arc | | Provider fee | Always. On every swap. | 2 basis points (0.02%) of the swap amount after any custom fee (1 bp = 0.01%). | Swap service provider (third-party liquidity service that executes the swap) | Your end user pays the swap amount plus all applicable fees. ## How funds flow through a swap This example traces a 1,000 USDT-to-USDC swap with a 1% custom fee: The user requests a swap of 1,000 USDT to USDC. You add a 1% custom fee. On 1,000 USDT, this equals 10 USDT. The source wallet signs a transaction authorizing 1,000 USDT. The App Kit SDK collects the 10 USDT custom fee and splits it: * Arc receives 1 USDT (10% of the custom fee). * Your fee recipient receives 9 USDT (90% of the custom fee). The swap service provider deducts a provider fee of 2 basis points (0.02%) from the remaining 990 USDT. This equals 0.198 USDT. The provider executes the swap with 989.802 USDT and the user receives the equivalent output in USDC. **Net result:** Of the original 1,000 USDT, 10 USDT goes to custom fee recipients, 0.198 USDT goes to the swap service provider, and the remaining 989.802 USDT is swapped for USDC. This flow is illustrated in the following diagram: ```mermaid theme={null} flowchart TB SourceWallet["*Source wallet*#160;"] --> ApprovedAmount["Approved amount:
1,000 USDT"] ApprovedAmount --> CustomFeeDeduction["Custom fee:
-10 USDT (1%)"] ApprovedAmount --> PostFeeSwapAmount["Swap amount:
990 USDT"] CustomFeeDeduction --> ArcShare["10% of custom fee:
1 USDT"] CustomFeeDeduction --> PartnerShare["90% of custom fee:
9 USDT"] ArcShare --> Arc["*Arc*#160;"] PartnerShare --> FeeRecipient["*Your fee recipient*#160;"] PostFeeSwapAmount --> SwapService["*Swap service*#160;"] SwapService --> SwapTransaction["Swap transaction:
989.802 USDT to USDC"] SwapService --> ProviderFee["Swap service fee:
-0.198 USDT (2 bps)"] SwapTransaction --> SourceWalletUsdc["*Source wallet*#160;
receives USDC"] ProviderFee --> SwapServiceProvider["*Swap service provider*#160;"] ``` ## Best practices for custom fees Follow these best practices when implementing custom fees for swaps: * Use a fee recipient address in the same network context where the swap originates. * Return fee amounts in human-readable decimal format (for example, `0.20` instead of `200000` for 0.20 USDC). The App Kit SDK handles base-unit conversion internally. # How Unified Balance fees work Source: https://docs.arc.io/app-kit/concepts/unified-balance-fees How fees apply when spending from a Unified Balance and how funds move through a spend transaction Several fees can apply when you spend from a Unified Balance, including a [custom fee](/app-kit/tutorials/unified-balance/collect-custom-spend-fees) you can implement. Fees affect how funds move through a spend and what remains in the Unified Balance total. Deposits work differently. Same-chain deposits do not charge Unified Balance protocol fees. [Fast deposits](/app-kit/tutorials/unified-balance/use-fast-deposits) can incur [CCTP Fast Transfer](https://developers.circle.com/cctp/technical-guide#fees) and [Forwarding Service](https://developers.circle.com/cctp/concepts/forwarding-service#fees-and-execution) fees before funds land in the Unified Balance. ## Fees breakdown Each spend can include the following fees: | Fee | When it applies | Amount | Recipient | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | | Custom spend fee | Conditionally. When you [implement custom spend fees](/app-kit/tutorials/unified-balance/collect-custom-spend-fees). | You define (carved from the spend amount). | 90% to your fee recipient; 10% to Arc | | Gateway protocol fee | Conditionally. On spends where the source and destination differ (crosschain). | 0.5 basis points (0.005%) of the spend amount from the Unified Balance at spend time; 0 if same blockchain. | [Circle Gateway](https://developers.circle.com/gateway) (protocol underlying Unified Balance) | | Gas | Always. On spends that execute burn intents on source blockchains. | Varies by source blockchain and network conditions; incurred per burn intent on source. | Source blockchain | | Forwarding Service fee | Conditionally. When you [use the Forwarding Service](/app-kit/tutorials/unified-balance/use-forwarding-service) for the destination mint. | Per [Forwarding Service fees](https://developers.circle.com/cctp/concepts/forwarding-service#fees-and-execution). Deducted from amount minted on destination. | Circle | ## Total balance and funds flow The following example shows what happens when a user wants 500 USDC to arrive at the destination from a Unified Balance of 1,000 USDC (previously deposited), you collect a 5 USDC custom fee on that spend, and the [Forwarding Service](/app-kit/tutorials/unified-balance/use-forwarding-service) is enabled: The user previously deposited 1,000 USDC from their wallet on the source blockchain into the Unified Balance. The user confirms a spend of 505.20 USDC from the Unified Balance. This is the amount needed to ensure exactly 500 USDC arrives at the destination after the custom fee and Forwarding Service fee are applied. See [best practices](#best-practices-for-custom-fees) for what to show the user before they confirm a spend. You deduct a 5 USDC custom fee from the spend amount. The user's source wallet signs three burn intents that move: * 500.20 USDC (the spend amount minus the 5 USDC custom fee) toward the destination mint. * 0.50 USDC (10% of the custom fee) to Arc. * 4.50 USDC (90% of the custom fee) to your fee recipient. Circle Gateway deducts a 0.025 USDC transfer fee from the Unified Balance (0.005% of 505.20 USDC). For a same-chain spend, this fee is 0. The source blockchains deduct 0.03 USDC from the Unified Balance as gas for the three burn intents. The Forwarding Service deducts its fee (0.20 USDC in this example) from the amount to be minted on the destination blockchain. The recipient's destination wallet receives 500 USDC on the destination blockchain: * 500.20 USDC minted. * Deduct 0.20 USDC Forwarding Service fee. * Net received: 500 USDC. After the spend, the user's Unified Balance total is 494.745 USDC: * Started at 1,000 USDC. * Deduct 505.20 USDC spend amount. * Deduct 0.025 USDC Gateway transfer fee. * Deduct 0.03 USDC gas. * Remaining: 494.745 USDC. This flow and Unified Balance running total is illustrated in the following diagram. ```mermaid theme={null} flowchart TB subgraph UB["Unified Balance total"] direction TB U0["1,000 USDC"] U1["494.80 USDC"] U2["494.775 USDC"] U3["494.745 USDC"] U0 -->|"Spend deducted:
-505.20 USDC"| U1 U1 -->|"Gateway fee:
-0.025 USDC"| U2 U2 -->|"Gas:
-0.030 USDC"| U3 end SPEND["Spend amount:
**505.20 USDC**"] B500["Spend amount:
**500.20 USDC**"] B05["10% of custom fee:
**0.50 USDC**"] B45["90% of custom fee:
**4.50 USDC**"] U0 --> SPEND SPEND --> B500 SPEND --> B05 SPEND --> B45 FWD["Forwarding fee:
**-0.20 USDC**"] GW["*Gateway*#160;"] OUT500["Spend amount:
**500 USDC**"] DEST["*Destination wallet*#160;"] B500 --> FWD FWD --> GW B500 --> OUT500 OUT500 --> DEST B05 --> ARC["*Arc*#160;"] B45 --> RECIP["*Your fee recipient*#160;"] ``` ## Best practices for custom fees Follow these best practices when implementing custom fees: * Use a fee recipient address on the source blockchain. Do not use an address on the destination. * Calculate the spend amount from the amount the user wants to receive at the destination. To receive a specific amount, the user must spend more than that from the Unified Balance to cover fees. * Before the user confirms a spend, show: * Spend summary: spend amount, fee breakdown (custom fee and Forwarding Service fee when applicable), and amount received at the destination. * Unified Balance summary: starting balance, each deduction (spend amount, Gateway transfer fee when applicable, gas), and remaining balance. ```text Example UI display theme={null} Spend amount: 505.20 USDC Forwarding Service fee: - 0.20 USDC Custom fee: - 5.00 USDC Amount received at destination: 500.00 USDC Unified Balance: 1,000.00 USDC Spend amount: - 505.20 USDC Gateway fee: - 0.025 USDC Gas (estimated): - 0.03 USDC Remaining balance: 494.745 USDC ``` * Return human-readable decimal strings. For example, return `"10"` rather than `"10000000"` for 10 USDC. The App Kit SDK handles base-unit conversion internally. * Validate that the user's Unified Balance can cover the spend amount, the Gateway protocol fee, and gas. For gas and fee estimates, see [estimate spend fees](/app-kit/tutorials/unified-balance/estimate-spend-fees). Example check: ```typescript TypeScript theme={null} // Example: calculate required spend from a target destination amount and validate balance const targetDestinationAmount = 500; // USDC the user wants to arrive at the destination const forwardingServiceFee = 0.2; // deducted from amount minted at destination const customFee = 5; // your custom fee in USDC const spendAmount = targetDestinationAmount + forwardingServiceFee + customFee; // 505.20 USDC const sameChain = false; // true when source and destination blockchain are the same const gatewayFee = sameChain ? 0 : spendAmount * 0.00005; // 0.005% when crosschain const gasEstimate = 0.05; // replace with a blockchain-appropriate estimate; the walkthrough above uses 0.03 for illustration const userBalance = 1000; // placeholder; in production parse totalConfirmedBalance from kit.unifiedBalance.getBalances const requiredBalance = spendAmount + gatewayFee + gasEstimate; if (userBalance < requiredBalance) { throw new Error(`Insufficient balance. Need ${requiredBalance} USDC`); } // requiredBalance is about 505.275 USDC here (505.20 spend + 0.025 gateway + 0.05 gas estimate) ``` # Quickstart: Bridge tokens across blockchains Source: https://docs.arc.io/app-kit/quickstarts/bridge-tokens-across-blockchains Use the App Kit SDK to bridge tokens between blockchains with a browser wallet or Circle Wallets adapter Bridge USDC between blockchains with the wallet model that matches your application. Use a browser wallet when the end user signs in the client, or use Circle Wallets when you manage developer-controlled wallets through Circle. Use this flow to bridge USDC with a connected browser wallet. The example uses Ethereum Sepolia and Arc Testnet. ## Prerequisites Before you begin, ensure that you've: * Installed [Node.js v22+](https://nodejs.org/). * Installed a browser wallet such as [MetaMask](https://metamask.io/) and added the [Ethereum Sepolia](https://chainlist.org/chain/11155111) and [Arc Testnet](https://docs.arc.io/arc/references/connect-to-arc#wallet-setup) networks. * Funded your wallet with testnet USDC from the [Circle Faucet](https://faucet.circle.com/). * Funded your wallet with ETH for Ethereum Sepolia from a [public faucet](https://www.alchemy.com/faucets/ethereum-sepolia). ## Step 1. Set up the project ### 1.1. Create the project and install dependencies Create a new directory, install the App Kit packages, and add local browser demo tooling: ```bash Shell theme={null} mkdir app-kit-bridge-browser-wallet cd app-kit-bridge-browser-wallet npm init -y npm pkg set type=module npm install @circle-fin/app-kit @circle-fin/adapter-viem-v2 viem npm install --save-dev typescript vite ``` Only need to bridge and want a lighter install than the full App Kit SDK? Install the standalone Bridge Kit instead: `@circle-fin/bridge-kit` ### 1.2. Configure TypeScript (optional) This step is optional. It helps prevent missing types in your IDE or editor. Create a `tsconfig.json` file configured for ESM and Node: ```bash Shell theme={null} cat <<'EOF' > tsconfig.json { "compilerOptions": { "target": "ESNext", "module": "ESNext", "moduleResolution": "bundler", "strict": true, "types": ["node"] } } EOF ``` ## Step 2. Connect a browser wallet This step shows the core browser wallet integration flow: discover an [`EIP-6963`](https://eips.ethereum.org/EIPS/eip-6963) provider, create an App Kit adapter from the selected provider, and pass that adapter into an App Kit SDK method. The snippets below keep wallet discovery, wallet connection, and adapter setup in small helper functions for readability. ### 2.1. Discover a browser wallet with `EIP-6963` This pattern is standards-based. The example uses MetaMask as the selected wallet, but the discovery flow works with any wallet that announces an `EIP-6963` provider. ```typescript TypeScript theme={null} import type { EIP1193Provider } from "viem"; type EIP6963ProviderInfo = { uuid: string; name: string; icon: string; rdns: string; }; type EIP6963ProviderDetail = { info: EIP6963ProviderInfo; provider: EIP1193Provider; }; declare global { interface WindowEventMap { "eip6963:announceProvider": CustomEvent; } } async function discoverBrowserWallets(): Promise { const providers = new Map(); const handleProviderAnnouncement = ( event: WindowEventMap["eip6963:announceProvider"], ) => { providers.set(event.detail.info.uuid, event.detail); }; window.addEventListener( "eip6963:announceProvider", handleProviderAnnouncement, ); window.dispatchEvent(new Event("eip6963:requestProvider")); await new Promise((resolve) => window.setTimeout(resolve, 250)); window.removeEventListener( "eip6963:announceProvider", handleProviderAnnouncement, ); return [...providers.values()]; } ``` ### 2.2. Connect the wallet and request account access After you select a provider, request account access before calling an App Kit SDK method. This should happen in a user-triggered action such as a `Connect wallet` button. ```typescript TypeScript theme={null} async function connectWallet(provider: EIP1193Provider) { await provider.request({ method: "eth_requestAccounts", params: undefined, // Required by the provider type even though this method has no params. }); const accounts = (await provider.request({ method: "eth_accounts", params: undefined, // Required by the provider type even though this method has no params. })) as string[]; return { connectedAddress: accounts[0] ?? null, }; } ``` Keep wallet connection and App Kit actions as separate user actions. This avoids overlapping wallet permission or chain-switch requests while a previous wallet prompt is still pending. ### 2.3. Create a Viem adapter from the selected wallet provider Use the discovered provider to request account access, then create the App Kit adapter that signs transactions in the browser: ```typescript TypeScript theme={null} import { createViemAdapterFromProvider } from "@circle-fin/adapter-viem-v2"; async function connectBrowserWallet() { const providers = await discoverBrowserWallets(); const selectedWallet = providers.find( ({ info }) => info.rdns === "io.metamask" || info.name === "MetaMask", ) ?? providers[0]; if (!selectedWallet) { throw new Error("No EIP-6963 browser wallet found"); } const { connectedAddress } = await connectWallet(selectedWallet.provider); const adapter = await createViemAdapterFromProvider({ provider: selectedWallet.provider, }); return { adapter, connectedAddress, walletName: selectedWallet.info.name, }; } ``` If multiple EVM wallets are installed, explicitly choose the wallet you want to use instead of relying on the first announced provider. The browser demo used to validate this quickstart prefers MetaMask when it is available. ## Step 3. Bridge USDC ### 3.1. Pass the browser wallet adapter into `kit.bridge()` This is the only App Kit-specific bridge call you need after the wallet is connected: ```typescript TypeScript theme={null} import { AppKit } from "@circle-fin/app-kit"; const kit = new AppKit(); async function bridgeUSDCWithBrowserWallet() { const { adapter, connectedAddress, walletName } = await connectBrowserWallet(); let result = await kit.bridge({ from: { adapter, chain: "Ethereum_Sepolia" }, to: { adapter, chain: "Arc_Testnet" }, amount: "1.00", }); if (result.state === "error") { result = await kit.retryBridge(result, { from: adapter, to: adapter, }); } console.log(`Submitted bridge with ${walletName}`, { connectedAddress, result, }); return result; } ``` Download the runnable [browser demo](https://github.com/circlefin/docs-examples/tree/master/app-kit-bridge-evm) to see the EVM-to-EVM bridge flow in action. ### 3.2. Observe bridge lifecycle events To inspect the bridge flow while it runs, subscribe to Bridge Kit events before you call `kit.bridge()`. This is useful for seeing the runtime step order and payload shape. The companion browser demos render these events into an on-page `
` element, but logging them to the console is enough for the core
    integration.

    ```typescript TypeScript theme={null}
    kit.on("*", (payload) => {
      console.log("Action:", payload);
    });
    ```

    
      Using other EVM chains? Change the `chain` values in `kit.bridge()` and ensure
      the connected wallet holds USDC on the source chain and enough gas to complete
      the transfer flow.
    

    
      You can customize your bridges to
      [collect a fee](/app-kit/tutorials/bridge/collect-bridge-fee), use the
      [Forwarding Service](/app-kit/tutorials/bridge/use-forwarding-service), or
      [estimate gas and provider fees](/app-kit/tutorials/bridge/estimate-costs)
      before bridging. Proceed only if the cost works for you.
    

    ### 3.3. Verify the transaction

    After `kit.bridge()` resolves, inspect the returned `steps` array. Each
    transaction step includes an `explorerUrl`. Use those links to confirm the
    approve, burn, and mint steps for the amount you bridged.

    The following code is an example of how an `approve` event might look in the
    browser console after a successful bridge. The values are examples only and are
    not a real transaction:

    ```bash Shell theme={null}
    Event received: {
      protocol: "cctp",
      version: "v2",
      traceId: "550afd44ba4c6d1d1bf4880b9ded3840",
      values: {
        name: "approve",
        state: "success",
        txHash: "0xdeadbeefcafebabe1234567890abcdef1234567890abcdef1234567890abcd",
        data: {
          txHash:
            "0xdeadbeefcafebabe1234567890abcdef1234567890abcdef1234567890abcd",
          status: "success",
          cumulativeGasUsed: 17138643n,
          gasUsed: 38617n,
          blockNumber: 8778959n,
          blockHash:
            "0xbeadfacefeed1234567890abcdef1234567890abcdef1234567890abcdef12",
          transactionIndex: 173,
          effectiveGasPrice: 1037232n,
        },
        explorerUrl:
          "https://testnet.arcscan.app/tx/0xdeadbeefcafebabe1234567890abcdef1234567890abcdef1234567890abcd",
      },
      method: "approve",
    }
    ```

    ## Extend: Add a Solana source wallet

    If you want a Solana browser wallet as the source, keep the EVM destination
    adapter from the browser wallet flow above and add a Solana source adapter. The
    examples use Solana Devnet and Arc Testnet, but you can use Solana and any
    [supported EVM chain](/app-kit/references/supported-blockchains) as the
    destination.

    In this browser wallet flow, both wallets run in the browser and the user signs
    transactions in wallet extensions. Treat wallet connection and bridging as
    separate user actions: connect the destination EVM wallet first, connect the
    Solana source wallet second, then call `kit.bridge()` after both adapters are
    available.

    ### Additional prerequisites

    Before you begin, ensure that you have:

    * Worked through the EVM browser wallet flow above first. This Solana path adds
      a Solana source wallet to that same browser-wallet pattern.
    * Installed a Solana browser wallet that exposes `window.solana`.
    * Funded your Solana wallet with testnet USDC from the
      [Circle Faucet](https://faucet.circle.com/).
    * Funded your Solana wallet with SOL for Solana Devnet transaction fees from the
      [Solana Faucet](https://faucet.solana.com/).

    ### Add the Solana dependencies

    Add the Solana adapter dependency to the same project:

    ```bash Shell theme={null}
    npm install @circle-fin/adapter-solana
    ```

    ### Connect the Solana source wallet

    This step extends the EVM browser wallet flow by adding a Solana source wallet.
    You will connect the Solana wallet, create a Solana source adapter, keep the EVM
    destination adapter, and pass both adapters into `kit.bridge()`.

    The snippets below keep each part of the flow in small helper functions for
    readability. The companion browser demo wires this same sequence through
    `handleEvmConnect()`, `handleSolanaConnect()`, and `handleBridge()` in a
    runnable UI.

    #### Connect the Solana wallet and request account access

    
      This pattern assumes a Solana browser wallet that exposes `window.solana`. Keep
      wallet connection and App Kit actions as separate user actions so the wallet is
      fully connected before you call an App Kit SDK method.
    

    ```typescript TypeScript theme={null}
    import type { CreateSolanaAdapterFromProviderParams } from "@circle-fin/adapter-solana";

    type SolanaWalletProvider = CreateSolanaAdapterFromProviderParams["provider"];

    declare global {
      interface Window {
        solana?: SolanaWalletProvider;
      }
    }

    async function connectSolanaWallet(provider: SolanaWalletProvider) {
      const connection = await provider.connect();

      return {
        connectedAddress:
          connection.publicKey?.toString() ??
          provider.publicKey?.toString() ??
          null,
      };
    }
    ```

    #### Keep the EVM destination adapter and add a Solana source adapter

    This Solana path builds on the EVM browser wallet flow above. Reuse the
    connected EVM wallet provider from that flow, then add a Solana provider and
    create one adapter for each chain:

    ```typescript TypeScript theme={null}
    import { createViemAdapterFromProvider } from "@circle-fin/adapter-viem-v2";
    import { createSolanaAdapterFromProvider } from "@circle-fin/adapter-solana";
    import type { EIP1193Provider } from "viem";

    async function createBridgeAdapters(
      evmProvider: EIP1193Provider,
      solanaProvider: SolanaWalletProvider,
    ) {
      const evmAdapter = await createViemAdapterFromProvider({
        provider: evmProvider,
      });

      const solanaAdapter = await createSolanaAdapterFromProvider({
        provider: solanaProvider,
      });

      return {
        evmAdapter,
        solanaAdapter,
      };
    }
    ```

    #### Pass the browser wallet adapters into `kit.bridge()`

    After you have a connected EVM provider from the earlier browser-wallet flow and
    a connected Solana provider from `window.solana`, create both adapters and pass
    them into `kit.bridge()`:

    ```typescript TypeScript theme={null}
    import { AppKit } from "@circle-fin/app-kit";
    import type { EIP1193Provider } from "viem";

    const kit = new AppKit();

    async function bridgeUSDCWithSolanaBrowserWallet(
      evmProvider: EIP1193Provider,
      solanaProvider: SolanaWalletProvider,
    ) {
      const { evmAdapter, solanaAdapter } = await createBridgeAdapters(
        evmProvider,
        solanaProvider,
      );

      const result = await kit.bridge({
        from: { adapter: solanaAdapter, chain: "Solana_Devnet" },
        to: { adapter: evmAdapter, chain: "Arc_Testnet" },
        amount: "1.00",
      });

      console.log(
        "Submitted bridge from Solana browser wallet to EVM destination",
        {
          result,
        },
      );

      return result;
    }
    ```

    #### Retry a failed bridge attempt

    If the first bridge attempt returns `state: "error"`, retry it with the same
    freshly created adapters:

    ```typescript TypeScript theme={null}
    let result = await kit.bridge({
      from: { adapter: solanaAdapter, chain: "Solana_Devnet" },
      to: { adapter: evmAdapter, chain: "Arc_Testnet" },
      amount: "1.00",
    });

    if (result.state === "error") {
      result = await kit.retryBridge(result, {
        from: solanaAdapter,
        to: evmAdapter,
      });
    }
    ```

    Download the runnable
    [browser demo](https://github.com/circlefin/docs-examples/tree/master/app-kit-bridge-solana)
    to see the Solana-to-EVM bridge flow in action.

    #### Observe bridge lifecycle events

    If you added the `kit.on("*", (payload) => { ... })` listener in the previous
    step, it already captures Solana bridge events, and no additional subscription
    is needed.

    ```typescript TypeScript theme={null}
    kit.on("*", (payload) => {
      console.log("Action:", payload);
    });
    ```

    
      Using a different EVM chain as the destination? Change the `to.chain` value and
      ensure the connected Solana wallet holds USDC on the source chain and enough
      native gas to complete the transfer flow.
    

    
      You can customize your bridges to
      [collect a fee](/app-kit/tutorials/bridge/collect-bridge-fee), use the
      [Forwarding Service](/app-kit/tutorials/bridge/use-forwarding-service), or
      [estimate gas and provider fees](/app-kit/tutorials/bridge/estimate-costs)
      before bridging. Proceed only if the cost works for you.
    

    #### Verify the transaction

    After `kit.bridge()` resolves, inspect the returned `steps` array. Each
    transaction step includes an `explorerUrl`. Use those links to confirm the burn,
    attestation, and mint steps for the amount you bridged.

    The following code is an example of how a `burn` step might look in the browser
    console after a successful bridge. The values are examples only and are not a
    real transaction:

    ```bash Shell theme={null}
    steps: [
      {
        name: "burn",
        state: "success",
        txHash: "5UfgJ5vVZxUxefDGqzqkVLHzHxVTyYH9StYyHKSNc7WLyFTmgL5RFGujWNqEbUBdNKRkHmx7ZRQR3FVhdEwxKHm",
        data: {
          txHash:
            "5UfgJ5vVZxUxefDGqzqkVLHzHxVTyYH9StYyHKSNc7WLyFTmgL5RFGujWNqEbUBdNKRkHmx7ZRQR3FVhdEwxKHm",
          status: "success",
          blockNumber: 312456789n,
          blockHash: "HxVTyYH9StYyHKSNc7WLyFTmgL5RFGujWNqEbUBdNK",
          transactionIndex: 0,
          gasUsed: 25000n,
          cumulativeGasUsed: 0n,
          effectiveGasPrice: 5000n,
          explorerUrl:
            "https://solscan.io/tx/5UfgJ5vVZxUxefDGqzqkVLHzHxVTyYH9StYyHKSNc7WLyFTmgL5RFGujWNqEbUBdNKRkHmx7ZRQR3FVhdEwxKHm?cluster=devnet",
        },
      },
    ];
    ```
  

  
    Use this flow to bridge USDC with developer-controlled wallets managed by Circle
    Wallets. The example uses Solana Devnet and Arc Testnet.

    
      When bridging from Arc Testnet with Circle Wallets, the bridge amount must
      exceed the CCTPv2 max fee. If the amount is too low, the burn step reverts with
      `"Max fee must be less than amount"`.
    

    ## Prerequisites

    Before you begin, ensure that you've:

    * Installed [Node.js v22+](https://nodejs.org/).
    * Obtained a
      [API Key](https://developers.circle.com/api-reference/keys#creating-an-api-key-for-developer-services)
      and
      [entity secret](https://developers.circle.com/wallets/dev-controlled/register-entity-secret)
      from the
      [Circle Console](https://developers.circle.com/w3s/circle-developer-account).
    * Created developer-controlled wallets on Arc Testnet and Solana Devnet using
      the Circle Console.
    * Funded your Arc Testnet wallet with testnet USDC from the
      [Circle Faucet](https://faucet.circle.com/).
    * Funded your Solana Devnet wallet with testnet USDC from the
      [Circle Faucet](https://faucet.circle.com/).
    * Funded your Solana Devnet wallet with SOL for transaction fees from the
      [Solana Faucet](https://faucet.solana.com/).

    ## Step 1. Set up the project

    ### 1.1. Create the project and install dependencies

    Create a project directory, initialize a Node.js project with an ES module type
    and start script, then install runtime and dev dependencies:

    ```bash Shell theme={null}
    mkdir app-kit-bridge-circle-wallets
    cd app-kit-bridge-circle-wallets
    npm init -y
    npm pkg set type=module
    npm pkg set scripts.start="tsx --env-file=.env index.ts"
    npm install @circle-fin/app-kit @circle-fin/adapter-circle-wallets tsx @solana/kit
    npm install --save-dev typescript @types/node
    ```

    
      Only need to bridge and want a lighter install than the full App Kit SDK?
      Install the standalone Bridge Kit instead: `@circle-fin/bridge-kit`
    

    ### 1.2. Configure TypeScript (optional)

    
      This step is optional. It helps prevent missing types in your IDE or editor.
    

    Create a `tsconfig.json` file configured for ESM and Node:

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

    ### 1.3. Set environment variables

    Create an `.env` file in the project directory:

    ```bash Shell theme={null}
    touch .env
    ```

    Add your credentials. Replace `YOUR_API_KEY` with your Circle Developer API key,
    `YOUR_ENTITY_SECRET` with your entity secret (64 lowercase alphanumeric
    characters), and `YOUR_EVM_WALLET_ADDRESS` and `YOUR_SOLANA_WALLET_ADDRESS` with
    the wallet addresses you control through Circle Wallets. You can fetch the
    addresses from the
    [Circle Developer Console](https://developers.circle.com/w3s/circle-developer-account)
    or the
    [list wallets](https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/get-wallets)
    endpoint:

    ```text .env theme={null}
    CIRCLE_API_KEY=YOUR_API_KEY
    CIRCLE_ENTITY_SECRET=YOUR_ENTITY_SECRET
    EVM_WALLET_ADDRESS=YOUR_EVM_WALLET_ADDRESS
    SOLANA_WALLET_ADDRESS=YOUR_SOLANA_WALLET_ADDRESS
    ```

    
      Edit `.env` files in your IDE or editor so credentials are not leaked to your
      shell history.
    

    ## Step 2. Bridge USDC

    ### 2.1. Create the script

    Create an `index.ts` file in the project directory and add the following code.
    This code sets up your script and bridges 1 USDC from Arc Testnet to Solana
    Devnet.

    
      Using a different blockchain as the source or destination? Change the `chain`
      values in `kit.bridge()` and ensure the source wallet has USDC. For this example
      that means keeping extra USDC on Arc Testnet for gas and SOL on Solana Devnet
      for Solana transaction fees.
    

    ```typescript TypeScript theme={null}
    // Import the App Kit SDK and the Circle Wallets adapter
    import { AppKit } from "@circle-fin/app-kit";
    import { createCircleWalletsAdapter } from "@circle-fin/adapter-circle-wallets";

    const kit = new AppKit();

    kit.on("*", (payload) => {
      console.log("Event received:", payload);
    });

    const adapter = createCircleWalletsAdapter({
      apiKey: process.env.CIRCLE_API_KEY!,
      entitySecret: process.env.CIRCLE_ENTITY_SECRET!,
    });

    console.log("---------------Starting Bridging---------------");

    const result = await kit.bridge({
      from: {
        adapter,
        chain: "Arc_Testnet",
        address: process.env.EVM_WALLET_ADDRESS!,
      },
      to: {
        adapter,
        chain: "Solana_Devnet",
        address: process.env.SOLANA_WALLET_ADDRESS!,
      },
      amount: "1",
    });

    console.dir(result, { depth: null, colors: true });
    ```

    
      You can customize your bridges to
      [collect a fee](/app-kit/tutorials/bridge/collect-bridge-fee), use the
      [Forwarding Service](/app-kit/tutorials/bridge/use-forwarding-service), or
      [estimate gas and provider fees](/app-kit/tutorials/bridge/estimate-costs)
      before bridging. Proceed only if the cost works for you.
    

    ### 2.2. Run the script

    Save the `index.ts` file and run the script in your terminal:

    ```bash Shell theme={null}
    npm run start
    ```

    ### 2.3. Verify the transaction

    As the bridge runs, the event listener logs `approve`, `burn`,
    `fetchAttestation`, and `mint` progress to the terminal. For transaction-backed
    steps, use `values.explorerUrl` from the event payload to open the Arc Testnet
    or Solana Devnet explorer and confirm the transfer.

    After the script finishes, inspect the final `result.steps` array. Each
    transaction step includes its own `explorerUrl`, which you can use to confirm
    the amount you bridged and the chain where that step executed.

    The following code is an example of how an `approve` event might look in the
    terminal output. The values are used in this example only and are not a real
    transaction:

    ```bash Shell theme={null}
    Event received: {
      protocol: "cctp",
      version: "v2",
      traceId: "550afd44ba4c6d1d1bf4880b9ded3840",
      values: {
        name: "approve",
        state: "success",
        txHash: "0xb4a6efb91a3a714822185b9310e54d0806040d38ef1b5282a9279522f09e6ccd",
        data: {
          txHash: "0xb4a6efb91a3a714822185b9310e54d0806040d38ef1b5282a9279522f09e6ccd",
          status: "success",
          cumulativeGasUsed: 706909n,
          gasUsed: 38596n,
          blockNumber: 42852493n,
          blockHash: "0xdb5e8026f29ffd7d7cad14e7afcc04b3287981767bf406be2e21f3c559ced13d",
          transactionIndex: 6,
          effectiveGasPrice: 22000000000n,
        },
        explorerUrl:
          "https://testnet.arcscan.app/tx/0xb4a6efb91a3a714822185b9310e54d0806040d38ef1b5282a9279522f09e6ccd",
      },
      method: "approve",
    }
    ```
  



# Quickstart: Send tokens across wallets on the same blockchain
Source: https://docs.arc.io/app-kit/quickstarts/send-tokens-same-chain

Use the App Kit SDK to send tokens from one wallet to another on the same blockchain

This quickstart walks you through sending tokens from one wallet to another on
the same blockchain. The example in this quickstart sends USDC on Arc Testnet,
but you can use another
[supported token or blockchain](/app-kit/references/supported-blockchains).


  
    Use this flow to send USDC with a connected browser wallet. The example uses Arc
    Testnet.

    ## Prerequisites

    Before you begin, ensure that you've:

    * Installed [Node.js v22+](https://nodejs.org/).
    * Installed a browser wallet such as [MetaMask](https://metamask.io/) and added
      the
      [Arc Testnet](https://docs.arc.io/arc/references/connect-to-arc#wallet-setup)
      network.
    * Funded your wallet with testnet USDC from the
      [Circle Faucet](https://faucet.circle.com/).

    ## Step 1. Set up the project

    ### 1.1. Create the project and install dependencies

    Create a new directory, install the App Kit packages, and add local browser demo
    tooling:

    ```bash Shell theme={null}
    mkdir app-kit-send-browser-wallet
    cd app-kit-send-browser-wallet
    npm init -y
    npm pkg set type=module
    npm install @circle-fin/app-kit @circle-fin/adapter-viem-v2 viem {extraPackages}
    npm install --save-dev typescript vite
    ```

    ### 1.2. Configure TypeScript (optional)

    
      This step is optional. It helps prevent missing types in your IDE or editor.
    

    Create a `tsconfig.json` file configured for ESM and Node:

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

    ## Step 2. Connect a browser wallet

    This step shows the core browser wallet integration flow: discover an
    [`EIP-6963`](https://eips.ethereum.org/EIPS/eip-6963) provider, create an App
    Kit adapter from the selected provider, and pass that adapter into an App Kit
    SDK method.

    The snippets below keep wallet discovery, wallet connection, and adapter setup
    in small helper functions for readability.

    ### 2.1. Discover a browser wallet with `EIP-6963`

    
      This pattern is standards-based. The example uses MetaMask as the selected
      wallet, but the discovery flow works with any wallet that announces an
      `EIP-6963` provider.
    

    ```typescript TypeScript theme={null}
    import type { EIP1193Provider } from "viem";

    type EIP6963ProviderInfo = {
      uuid: string;
      name: string;
      icon: string;
      rdns: string;
    };

    type EIP6963ProviderDetail = {
      info: EIP6963ProviderInfo;
      provider: EIP1193Provider;
    };

    declare global {
      interface WindowEventMap {
        "eip6963:announceProvider": CustomEvent;
      }
    }

    async function discoverBrowserWallets(): Promise {
      const providers = new Map();

      const handleProviderAnnouncement = (
        event: WindowEventMap["eip6963:announceProvider"],
      ) => {
        providers.set(event.detail.info.uuid, event.detail);
      };

      window.addEventListener(
        "eip6963:announceProvider",
        handleProviderAnnouncement,
      );
      window.dispatchEvent(new Event("eip6963:requestProvider"));

      await new Promise((resolve) => window.setTimeout(resolve, 250));
      window.removeEventListener(
        "eip6963:announceProvider",
        handleProviderAnnouncement,
      );

      return [...providers.values()];
    }
    ```

    ### 2.2. Connect the wallet and request account access

    After you select a provider, request account access before calling an App Kit
    SDK method. This should happen in a user-triggered action such as a
    `Connect wallet` button.

    ```typescript TypeScript theme={null}
    async function connectWallet(provider: EIP1193Provider) {
      await provider.request({
        method: "eth_requestAccounts",
        params: undefined, // Required by the provider type even though this method has no params.
      });

      const accounts = (await provider.request({
        method: "eth_accounts",
        params: undefined, // Required by the provider type even though this method has no params.
      })) as string[];

      return {
        connectedAddress: accounts[0] ?? null,
      };
    }
    ```

    
      Keep wallet connection and App Kit actions as separate user actions. This avoids
      overlapping wallet permission or chain-switch requests while a previous wallet
      prompt is still pending.
    

    ### 2.3. Create a Viem adapter from the selected wallet provider

    Use the discovered provider to request account access, then create the App Kit
    adapter that signs transactions in the browser:

    ```typescript TypeScript theme={null}
    import { createViemAdapterFromProvider } from "@circle-fin/adapter-viem-v2";

    async function connectBrowserWallet() {
      const providers = await discoverBrowserWallets();
      const selectedWallet =
        providers.find(
          ({ info }) => info.rdns === "io.metamask" || info.name === "MetaMask",
        ) ?? providers[0];

      if (!selectedWallet) {
        throw new Error("No EIP-6963 browser wallet found");
      }

      const { connectedAddress } = await connectWallet(selectedWallet.provider);

      const adapter = await createViemAdapterFromProvider({
        provider: selectedWallet.provider,
      });

      return {
        adapter,
        connectedAddress,
        walletName: selectedWallet.info.name,
      };
    }
    ```

    
      If multiple EVM wallets are installed, explicitly choose the wallet you want to
      use instead of relying on the first announced provider. The browser demo used to
      validate this quickstart prefers MetaMask when it is available.
    

    ## Step 3. Send USDC

    ### 3.1. Call `kit.send()`

    This is the only App Kit-specific send call you need after the wallet is
    connected:

    ```typescript TypeScript theme={null}
    import { AppKit } from "@circle-fin/app-kit";
    import type { SendParams } from "@circle-fin/app-kit";

    const kit = new AppKit();

    async function sendUSDCWithBrowserWallet() {
      const { adapter, connectedAddress, walletName } =
        await connectBrowserWallet();

      const sendParams: SendParams = {
        from: { adapter, chain: "Arc_Testnet" },
        to: "RECIPIENT_ADDRESS",
        amount: "1.00",
        token: "USDC",
      };

      const estimate = await kit.estimateSend(sendParams);
      const result = await kit.send(sendParams);

      console.log(`Submitted send with ${walletName}`, {
        connectedAddress,
        estimate,
        result,
      });
      return result;
    }
    ```

    
      Using another
      [token](/app-kit/references/supported-blockchains#supported-tokens) or
      [blockchain](/app-kit/references/supported-blockchains)? Change the `token` and
      `chain` values in `kit.send()` and ensure the connected wallet holds enough
      funds to complete the transfer.
    

    ### 3.2. Verify the transaction

    After `kit.send()` resolves, inspect the returned result. Use the transaction
    explorer URL to verify the amount and recipient on the blockchain.

    The following is an example of how the result of a successful send might look in
    the browser console. The values are used in this example only and are not a real
    transaction:

    ```bash Shell theme={null}
    {
      name: "transfer",
      state: "success",
      txHash: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
      explorerUrl: "https://testnet.arcscan.app/tx/0x1234567890abcdef...",
    }
    ```
  

  
    Use this flow to send USDC with a developer-controlled wallet managed by Circle
    Wallets. The example uses Arc Testnet.

    ## Prerequisites

    Before you begin, ensure that you've:

    * Installed [Node.js v22+](https://nodejs.org/).
    * Obtained a
      [API Key](https://developers.circle.com/api-reference/keys#creating-an-api-key-for-developer-services)
      and
      [entity secret](https://developers.circle.com/wallets/dev-controlled/register-entity-secret)
      from the
      [Circle Console](https://developers.circle.com/w3s/circle-developer-account).
    * Created a developer-controlled wallet on Arc Testnet using the Circle Console.
    * Funded your Arc Testnet wallet with testnet USDC from the
      [Circle Faucet](https://faucet.circle.com/).

    ## Step 1. Set up the project

    ### 1.1. Create the project and install dependencies

    Create a project directory, initialize a Node.js project with an ES module type
    and start script, then install runtime and dev dependencies:

    ```bash Shell theme={null}
    mkdir app-kit-send-circle-wallets
    cd app-kit-send-circle-wallets
    npm init -y
    npm pkg set type=module
    npm pkg set scripts.start="tsx --env-file=.env index.ts"
    npm install @circle-fin/app-kit @circle-fin/adapter-circle-wallets tsx {extraPackages}
    npm install --save-dev typescript @types/node
    ```

    ### 1.2. Configure TypeScript (optional)

    
      This step is optional. It helps prevent missing types in your IDE or editor.
    

    Create a `tsconfig.json` file configured for ESM and Node:

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

    ### 1.3. Set environment variables

    Create an `.env` file in the project directory:

    ```bash Shell theme={null}
    touch .env
    ```

    Add your credentials. Replace `YOUR_API_KEY` with your Circle Developer API key,
    `YOUR_ENTITY_SECRET` with your entity secret. You can fetch the address from the
    [Circle Developer Console](https://developers.circle.com/w3s/circle-developer-account)
    or the
    [list wallets](https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/get-wallets)
    endpoint:

    ```text .env theme={null}
    CIRCLE_API_KEY=YOUR_API_KEY
    CIRCLE_ENTITY_SECRET=YOUR_ENTITY_SECRET
    ```

    
      Edit `.env` files in your IDE or editor so credentials are not leaked to your
      shell history.
    

    ## Step 2. Send USDC

    ### 2.1. Create the script

    Create an `index.ts` file in the project directory and add the following code.
    This code sends 1.00 USDC from your Circle Wallets-controlled Arc Testnet wallet
    to a recipient on Arc Testnet:

    
      Using another
      [token](/app-kit/references/supported-blockchains#supported-tokens) or
      [blockchain](/app-kit/references/supported-blockchains)? Change the `token` and
      `chain` values in `kit.send()` and ensure the source wallet has enough funds to
      complete the transfer.
    

    ```typescript TypeScript theme={null}
    import { AppKit } from "@circle-fin/app-kit";
    import { createCircleWalletsAdapter } from "@circle-fin/adapter-circle-wallets";
    import type { SendParams } from "@circle-fin/app-kit";

    const kit = new AppKit();

    const sourceWalletAddress = "YOUR_SOURCE_WALLET_ADDRESS";
    const recipientAddress = "RECIPIENT_ADDRESS";

    const adapter = createCircleWalletsAdapter({
      apiKey: process.env.CIRCLE_API_KEY!,
      entitySecret: process.env.CIRCLE_ENTITY_SECRET!,
    });

    const sendParams: SendParams = {
      from: {
        adapter,
        chain: "Arc_Testnet",
        address: sourceWalletAddress,
      },
      to: recipientAddress,
      amount: "1.00",
      token: "USDC",
    };

    const estimate = await kit.estimateSend(sendParams);
    const result = await kit.send(sendParams);

    console.dir({ estimate, result }, { depth: null, colors: true });
    ```

    ### 2.2. Run the script

    Save the `index.ts` file and run the script in your terminal:

    ```bash Shell theme={null}
    npm run start
    ```

    ### 2.3. Verify the transaction

    After the script finishes, inspect the returned result in the terminal output.
    Use the transaction explorer URL to verify the amount and recipient on the
    blockchain.

    The following is an example of how the result of a successful send might look in
    the terminal output. The values are used in this example only and are not a real
    transaction:

    ```bash Shell theme={null}
    {
      estimate: { gas: 406817n, fee: '8138073262135265', gasPrice: 20004260545n },
      result: {
        name: 'send',
        state: 'success',
        txHash: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
        explorerUrl: 'https://testnet.arcscan.app/tx/0x1234567890abcdef123456789...'
      }
    }
    ```
  



# Quickstart: Swap tokens across blockchains
Source: https://docs.arc.io/app-kit/quickstarts/swap-tokens-crosschain

Use the App Kit SDK to swap one token on the source blockchain and receive a different token on a destination blockchain in a single call

This quickstart walks you through how to use the App Kit SDK's
[Swap](/app-kit/swap) capability to swap tokens across blockchains in a single
call. The example swaps 1.00 USDC on Base for native POL delivered to a wallet
on Polygon.

Swap is intended for server-side applications and is not specific to Circle
Wallets; you can use any compatible EVM adapter with
[supported tokens or blockchains](/app-kit/references/supported-blockchains).
Passing an API key to Swap is optional—without one, swap requests run against a
rate limit. Add one from the
[Circle Console](https://console.circle.com/api-keys) if you hit rate limit
errors or expect high request volume.


  If your Circle Wallet is a smart contract account (SCA), set
  `allowanceStrategy: "approve"`. USDC permit signatures use `ecrecover`, which
  does not accept the SCA's ERC-1271 signature, so the SDK uses an onchain
  `approve`.


## Prerequisites

Before you begin, ensure that you've:

* Installed [Node.js v22+](https://nodejs.org/).
* Obtained the following from the [Circle Console](https://console.circle.com/):
  * [API key](https://developers.circle.com/contracts/create-api-key): required
    for wallet operations, and optionally used to authenticate Swap requests
    (recommended for production or high-volume usage)
  * Entity secret: generated and registered using the
    [Circle Console](https://console.circle.com/wallets/dev/configurator/entity-secret)
    or the
    [SDK](https://developers.circle.com/wallets/dev-controlled/register-entity-secret#create-an-entity-secret-using-the-sdk)
* Created a developer-controlled wallet on Base.
* Funded your Base wallet with USDC for the swap amount and ETH for gas.

## Step 1. Set up the project

### 1.1. Create the project and install dependencies

Create a project directory, initialize a Node.js project with an ES module type
and start script, then install runtime and dev dependencies:

```bash Shell theme={null}
mkdir app-kit-swap-crosschain-circle-wallets
cd app-kit-swap-crosschain-circle-wallets
npm init -y
npm pkg set type=module
npm pkg set scripts.start="tsx --env-file=.env index.ts"
npm install @circle-fin/app-kit @circle-fin/adapter-circle-wallets tsx {extraPackages}
npm install --save-dev typescript @types/node
```


  Only need to swap and want a lighter install than the full App Kit SDK? Install
  the standalone Swap Kit instead: `@circle-fin/swap-kit`.

  For server-side scripts, you can use any compatible EVM adapter, including the
  private key adapter. Keep private keys on the server and configure the adapter
  in the [wallet adapter setup guide](/app-kit/tutorials/adapter-setups).


### 1.2. Configure TypeScript (optional)


  This step is optional. It helps prevent missing types in your IDE or editor.


Create a `tsconfig.json` file configured for ESM and Node:

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

### 1.3. Set environment variables

Create an `.env` file in the project directory:

```bash Shell theme={null}
touch .env
```

Add your server-side credentials. Replace `YOUR_API_KEY` with your Circle
Developer API key and `YOUR_ENTITY_SECRET` with your entity secret. The same API
key authenticates both the wallet operations and the swap request:

```text .env theme={null}
CIRCLE_API_KEY=YOUR_API_KEY
CIRCLE_ENTITY_SECRET=YOUR_ENTITY_SECRET
```


  Edit `.env` files in your IDE or editor so credentials are not leaked to your
  shell history.


## Step 2. Swap tokens

Create a script that estimates the route, submits the source-chain swap, waits
for crosschain delivery, and prints the final swap status.


  Unlike same-chain swaps, crosschain swaps return immediately with
  `progress.status: 'PENDING'`. The destination-chain leg lands shortly after. Use
  `kit.waitForSwap()` to poll until the swap reaches a terminal status: `DONE`,
  `FAILED`, or `NOT_FOUND`.


### 2.1. Create the script

Create an `index.ts` file in the project directory and add the following code.
This code swaps 1.00 USDC on Base for native POL delivered to your wallet on
Polygon.

Replace `YOUR_SOURCE_WALLET_ADDRESS` with your Base wallet address and
`YOUR_RECIPIENT_ADDRESS` with your Polygon recipient address. The recipient
address does not need POL to receive the swapped tokens.


  Using other [tokens](/app-kit/references/supported-blockchains#supported-tokens)
  or [blockchains](/app-kit/references/supported-blockchains)? Change the source
  `chain`, `tokenIn`, `tokenOut`, `to.chain`, and `recipientAddress` values in
  `swapParams`. Both blockchains must support crosschain swap.


```typescript TypeScript theme={null}
import { AppKit } from "@circle-fin/app-kit";
import { createCircleWalletsAdapter } from "@circle-fin/adapter-circle-wallets";
import type { SwapParams } from "@circle-fin/app-kit";

const kit = new AppKit();

const sourceWalletAddress = "YOUR_SOURCE_WALLET_ADDRESS";
const recipientAddress = "YOUR_RECIPIENT_ADDRESS";

const adapter = createCircleWalletsAdapter({
  apiKey: process.env.CIRCLE_API_KEY!,
  entitySecret: process.env.CIRCLE_ENTITY_SECRET!,
});

const swapParams: SwapParams = {
  from: {
    adapter,
    chain: "Base",
    address: sourceWalletAddress, // Omit the address if using Viem or Ethers adapters.
  },
  tokenIn: "USDC",
  tokenOut: "NATIVE",
  amountIn: "1",
  to: {
    chain: "Polygon",
    recipientAddress,
  },
  config: {
    apiKey: process.env.CIRCLE_API_KEY as string, // optional for swaps; recommended for production or high-volume usage
    allowanceStrategy: "approve",
  },
};

const estimate = await kit.estimateSwap(swapParams);
console.dir({ estimate }, { depth: null, colors: true });

const result = await kit.swap(swapParams);
console.dir({ result }, { depth: null, colors: true });

const status = await kit.waitForSwap({
  result,
  apiKey: process.env.CIRCLE_API_KEY!,
});

console.dir({ status }, { depth: null, colors: true });
```


  Customize your crosschain swaps to
  [collect a custom fee](/app-kit/tutorials/swap/collect-swap-fee), or set a
  [slippage tolerance or stop limit](/app-kit/tutorials/swap/set-slippage-tolerance-or-stop-limit).


### 2.2. Run the script

Save the `index.ts` file and run the script in your terminal:

```bash Shell theme={null}
npm run start
```


  Providers enforce route-specific minimum amounts. If `estimateSwap()` throws a
  `KitError` with code `INPUT_AMOUNT_OUT_OF_RANGE`, increase `amountIn` and
  estimate again (inside a `try`/`catch`) before calling `swap()`.


### 2.3. Verify the transactions

After the script finishes, find the returned status object in the terminal
output:

* Use the source `txHash` and `chainIn` to verify the source-chain swap on the
  Base block explorer.
* Use `status.destination.txHash` to verify the destination-chain delivery on
  the Polygon block explorer.

The following is an example of how the estimate, result, and final status might
look in the terminal output. Values vary by route, liquidity, and gas price.


  ```shell Estimate theme={null}
  {
    estimate: {
      tokenIn: 'USDC',
      tokenOut: 'NATIVE',
      amountIn: '1',
      chainIn: 'Base',
      chainOut: 'Polygon',
      fromAddress: '0xabcd...1234',
      toAddress: '0xabcd...1234',
      stopLimit: { amount: '12.1427586417541', token: 'NATIVE' },
      estimatedOutput: { amount: '12.518307878097', token: 'NATIVE' },
      fees: [
        { token: 'USDC', amount: '0.0002', type: 'provider' },
        { token: 'ETH', amount: '0.000010429566703815', type: 'gas' }
      ]
    }
  }
  ```

  ```shell Result theme={null}
  {
    result: {
      tokenIn: 'USDC',
      tokenOut: 'NATIVE',
      chainIn: 'Base',
      chainOut: 'Polygon',
      amountIn: '1.0',
      fromAddress: '0xabcd...1234',
      toAddress: '0xabcd...1234',
      txHash: '0x2f11...025e',
      explorerUrl: 'https://basescan.org/tx/0x2f11...025e',
      fees: [{ token: 'USDC', amount: '0.0002', type: 'provider' }],
      config: { allowanceStrategy: 'approve' },
      progress: { status: 'PENDING' }
    }
  }
  ```

  ```shell Final status theme={null}
  {
    status: {
      progress: {
        status: 'DONE',
        substatus: 'COMPLETED',
        substatusMessage: 'The transfer is complete.'
      },
      source: {
        txHash: '0x2f11...025e'
      },
      destination: {
        txHash: '0xd824...e331',
        token: {
          symbol: 'POL',
          address: '0x0000000000000000000000000000000000000000'
        },
        amount: '12.534043097347965275'
      }
    }
  }
  ```



# Quickstart: Swap tokens on a blockchain
Source: https://docs.arc.io/app-kit/quickstarts/swap-tokens-same-chain

Use the App Kit SDK to swap one token for another on the same blockchain

This quickstart walks you through how to use the App Kit SDK's
[Swap](/app-kit/swap) capability to swap tokens on the same blockchain. The
example swaps USDC for EURC on Arc Testnet with the Circle Wallets adapter.

Swap is intended for server-side applications and is not specific to Circle
Wallets; you can use any compatible EVM adapter with
[supported tokens or blockchains](/app-kit/references/supported-blockchains).
Passing an API key to Swap is optional—without one, swap requests run against a
rate limit. Add one from the
[Circle Console](https://console.circle.com/api-keys) if you hit rate limit
errors or expect high request volume.


  If your Circle Wallet is a smart contract account (SCA), set
  `allowanceStrategy: "approve"`. USDC permit signatures use `ecrecover`, which
  does not accept the SCA's ERC-1271 signature, so the SDK uses an onchain
  `approve`.


## Prerequisites

Before you begin, ensure that you've:

* Installed [Node.js v22+](https://nodejs.org/).
* Obtained the following from the [Circle Console](https://console.circle.com/):
  * [API key](https://developers.circle.com/contracts/create-api-key): required
    for wallet operations, and optionally used to authenticate Swap requests
    (recommended for production or high-volume usage)
  * Entity secret: generated and registered using the
    [Circle Console](https://console.circle.com/wallets/dev/configurator/entity-secret)
    or the
    [SDK](https://developers.circle.com/wallets/dev-controlled/register-entity-secret#create-an-entity-secret-using-the-sdk)
* Created a developer-controlled wallet on Arc Testnet.
* Funded your Arc Testnet wallet with enough testnet USDC for the swap amount
  and transaction fees from the [Circle Faucet](https://faucet.circle.com/).

## Step 1. Set up the project

### 1.1. Create the project and install dependencies

Create a project directory, initialize a Node.js project with an ES module type
and start script, then install runtime and dev dependencies:

```bash Shell theme={null}
mkdir app-kit-swap-same-chain-circle-wallets
cd app-kit-swap-same-chain-circle-wallets
npm init -y
npm pkg set type=module
npm pkg set scripts.start="tsx --env-file=.env index.ts"
npm install @circle-fin/app-kit @circle-fin/adapter-circle-wallets tsx {extraPackages}
npm install --save-dev typescript @types/node
```


  Only need to swap and want a lighter install than the full App Kit SDK? Install
  the standalone Swap Kit instead: `@circle-fin/swap-kit`.

  For server-side scripts, you can use any compatible EVM adapter, including the
  private key adapter. Keep private keys on the server and configure the adapter
  in the [wallet adapter setup guide](/app-kit/tutorials/adapter-setups).


### 1.2. Configure TypeScript (optional)


  This step is optional. It helps prevent missing types in your IDE or editor.


Create a `tsconfig.json` file configured for ESM and Node:

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

### 1.3. Set environment variables

Create an `.env` file in the project directory:

```bash Shell theme={null}
touch .env
```

Add your server-side credentials. Replace `YOUR_API_KEY` with your Circle
Developer API key and `YOUR_ENTITY_SECRET` with your entity secret. The same API
key authenticates both the wallet operations and the swap request:

```text .env theme={null}
CIRCLE_API_KEY=YOUR_API_KEY
CIRCLE_ENTITY_SECRET=YOUR_ENTITY_SECRET
```


  Edit `.env` files in your IDE or editor so credentials are not leaked to your
  shell history.


## Step 2. Swap tokens

### 2.1. Create the script

Create an `index.ts` file in the project directory and add the following code.
This code swaps 1.00 USDC for EURC from your Circle Wallets-controlled Arc
Testnet wallet. Replace `YOUR_SOURCE_WALLET_ADDRESS` with the wallet address
from the [Circle Console](https://console.circle.com/) or the
[list wallets](https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/get-wallets)
endpoint:


  Using another
  [token pair](/app-kit/references/supported-blockchains#supported-tokens) or
  [blockchain](/app-kit/references/supported-blockchains)? Change the `tokenIn`,
  `tokenOut`, and `chain` values in `swapParams` and ensure the source wallet has
  enough input tokens and gas.


```typescript TypeScript theme={null}
import { AppKit } from "@circle-fin/app-kit";
import { createCircleWalletsAdapter } from "@circle-fin/adapter-circle-wallets";
import type { SwapParams } from "@circle-fin/app-kit";

const kit = new AppKit();

const sourceWalletAddress = "YOUR_SOURCE_WALLET_ADDRESS";

const adapter = createCircleWalletsAdapter({
  apiKey: process.env.CIRCLE_API_KEY!,
  entitySecret: process.env.CIRCLE_ENTITY_SECRET!,
});

const swapParams: SwapParams = {
  from: {
    adapter,
    chain: "Arc_Testnet",
    address: sourceWalletAddress, // Omit the address if using Viem or Ethers adapters.
  },
  tokenIn: "USDC",
  tokenOut: "EURC",
  amountIn: "1.00",
  config: {
    apiKey: process.env.CIRCLE_API_KEY as string, // optional for swaps; recommended for production or high-volume usage
  },
};

const estimate = await kit.estimateSwap(swapParams);
console.dir({ estimate }, { depth: null, colors: true });

const result = await kit.swap(swapParams);
console.dir({ result }, { depth: null, colors: true });
```


  Customize your swaps to
  [collect a custom fee](/app-kit/tutorials/swap/collect-swap-fee),
  [set a slippage tolerance or stop limit](/app-kit/tutorials/swap/set-slippage-tolerance-or-stop-limit),
  or [get a pre-swap estimate](/app-kit/tutorials/swap/estimate-swap-rate).


### 2.2. Run the script

Save the `index.ts` file and run the script in your terminal:

```bash Shell theme={null}
npm run start
```


  Arc Testnet swap liquidity can be unstable. If a simulation reverts or a quote
  looks far from the expected rate, the testnet pool may be imbalanced or low on
  liquidity. Mainnet pools are typically deeper, but execution still depends on
  pool conditions, trade size, and slippage.


### 2.3. Verify the transaction

After the script finishes, inspect the returned result in the terminal output.
Use the transaction explorer URL to verify the swap transaction and confirm the
output amount.

The following is an example of how the estimate and result of a successful swap
might look in the terminal output.


  ```shell Estimate theme={null}
  {
    estimate: {
      tokenIn: 'USDC',
      tokenOut: 'EURC',
      amountIn: '1.00',
      chainIn: 'Arc_Testnet',
      chainOut: 'Arc_Testnet',
      chain: 'Arc_Testnet',
      fromAddress: '0xabcd...1234',
      toAddress: '0xabcd...1234',
      stopLimit: { amount: '0.959901', token: 'EURC' },
      estimatedOutput: { amount: '0.989589', token: 'EURC' },
      fees: [
        { token: 'USDC', amount: '0.0002', type: 'provider' },
        { token: 'USDC', amount: '0.021106828371967574', type: 'gas' }
      ]
    }
  }
  ```

  ```shell Result theme={null}
  {
    result: {
      tokenIn: 'USDC',
      tokenOut: 'EURC',
      chainIn: 'Arc_Testnet',
      chainOut: 'Arc_Testnet',
      amountIn: '1.0',
      fromAddress: '0xabcd...1234',
      toAddress: '0xabcd...1234',
      txHash: '0x43cb...c801',
      explorerUrl: 'https://testnet.arcscan.app/tx/0x43cb...c801',
      fees: [{ token: 'USDC', amount: '0.0002', type: 'provider' }],
      progress: {
        status: 'DONE',
        substatus: 'COMPLETED',
        substatusMessage: 'The transfer is complete.'
      },
      amountOut: '0.989589',
    }
  }
  ```



# Quickstart: Use a delegate to deposit and spend a Unified Balance
Source: https://docs.arc.io/app-kit/quickstarts/unified-balance-delegate-deposit-and-spend

Let a backend delegate wallet fund and spend from an owner's Unified Balance while the owner keeps custody

Delegation lets one address authorize another address to sign spends from its
Unified Balance. The owner account keeps custody of the funds, while the
delegate signs spend intents for authorized source blockchains.

Delegated spends are typically a server-side pattern: the owner authorizes a
delegate once, then a backend service signs future spend intents without asking
the owner wallet to sign each spend. The mechanism is wallet-agnostic; any
supported owner wallet can authorize a compatible EOA delegate.

In this quickstart, you’ll use Circle Wallets for both wallets: an owner wallet
that holds the Unified Balance and an EOA delegate wallet that signs spends.
You’ll use the delegate wallet to deposit into the owner's Unified Balance,
authorize the delegate on Base Sepolia, check the owner’s Unified Balance, and
spend on Arc Testnet with the Forwarding Service.

## Prerequisites

Before you begin, ensure that you've:

* Installed [Node.js v22+](https://nodejs.org/).
* Set up Circle Wallets:
  * Obtained an
    [API Key](https://developers.circle.com/api-reference/keys#creating-an-api-key-for-developer-services)
    from the
    [Circle Console](https://developers.circle.com/w3s/circle-developer-account).
  * Generated and registered an entity secret using the
    [Circle Console](https://console.circle.com/wallets/dev/configurator/entity-secret)
    or the
    [SDK](https://developers.circle.com/wallets/dev-controlled/register-entity-secret#create-an-entity-secret-using-the-sdk).
  * Created an owner wallet and an EOA delegate wallet on Base Sepolia. The
    owner wallet holds the Unified Balance, and the delegate wallet signs
    delegated spends after authorization.
* Funded the Base Sepolia wallets:
  * Owner wallet: testnet ETH from a
    [public faucet](https://www.alchemy.com/faucets/base-sepolia).
  * Delegate wallet: testnet USDC from the
    [Circle Faucet](https://faucet.circle.com/) and testnet ETH from a
    [public faucet](https://www.alchemy.com/faucets/base-sepolia).
* Obtained an Arc Testnet recipient address that will receive the USDC.

## Step 1. Set up your project

### 1.1. Create the project and install dependencies

Create a new directory and install the App Kit SDK with the Circle Wallets
adapter and supporting tools:

```bash Shell theme={null}
# Set up your directory and initialize a Node.js project
mkdir unified-balance-delegate
cd unified-balance-delegate
npm init -y
npm pkg set type=module

# Set up run scripts
npm pkg set scripts.deposit="tsx --env-file=.env delegate-deposit.ts"
npm pkg set scripts.authorize="tsx --env-file=.env delegate-authorize.ts"
npm pkg set scripts.balance="tsx --env-file=.env delegate-check-balance.ts"
npm pkg set scripts.spend="tsx --env-file=.env delegate-spend.ts"

# Install runtime dependencies
npm install @circle-fin/app-kit @circle-fin/adapter-circle-wallets tsx

# Install dev dependencies
npm install --save-dev typescript @types/node
```


  Only need a Unified Balance and want a lighter install than the full App Kit
  SDK? Install the standalone Unified Balance Kit instead:
  `@circle-fin/unified-balance-kit`


### 1.2. Configure TypeScript (optional)


  This step is optional. It helps prevent missing types in your IDE or editor.


Create a `tsconfig.json` file configured for ESM and Node:

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

### 1.3. Set environment variables

Create an `.env` file in the project directory:

```bash Shell theme={null}
touch .env
```

Add your credentials. Replace `YOUR_API_KEY` with your Circle Developer API key
and `YOUR_ENTITY_SECRET` with your entity secret:

```text .env theme={null}
CIRCLE_API_KEY=YOUR_API_KEY
CIRCLE_ENTITY_SECRET=YOUR_ENTITY_SECRET
```


  Edit `.env` files in your IDE or editor so credentials are not leaked to your
  shell history.


## Step 2. Deposit into the owner's Unified Balance

In this step, the delegate wallet deposits USDC into the owner's Unified Balance
from Base Sepolia.

### 2.1. Create the deposit script

Create a `delegate-deposit.ts` file. In this script, the delegate wallet
deposits 2.00 USDC from Base Sepolia into the owner's Unified Balance.

```typescript delegate-deposit.ts theme={null}
import { AppKit } from "@circle-fin/app-kit";
import { createCircleWalletsAdapter } from "@circle-fin/adapter-circle-wallets";

const DEPOSIT_AMOUNT = "2.00";

const kit = new AppKit();

kit.on("*", (payload) => {
  console.log("Event received:", payload);
});

const adapter = createCircleWalletsAdapter({
  apiKey: process.env.CIRCLE_API_KEY!,
  entitySecret: process.env.CIRCLE_ENTITY_SECRET!,
});

const ownerWalletAddress = "YOUR_BASE_SEPOLIA_OWNER_WALLET_ADDRESS";
const delegateWalletAddress = "YOUR_BASE_SEPOLIA_DELEGATE_WALLET_ADDRESS";

const result = await kit.unifiedBalance.depositFor({
  from: {
    adapter,
    chain: "Base_Sepolia",
    address: delegateWalletAddress,
  },
  amount: DEPOSIT_AMOUNT,
  token: "USDC",
  depositAccount: ownerWalletAddress,
});

console.dir(result, { depth: null, colors: true });
```


  `depositFor` is permissionless. Any wallet can fund another account's Unified
  Balance. This quickstart uses it so the delegate funds the owner before the
  owner grants spend authorization.


### 2.2. Run the deposit script

In your terminal, run:

```bash Shell theme={null}
npm run deposit
```

You'll see output like:

```bash Shell theme={null}
{
  amount: '2.00',
  token: 'USDC',
  depositedTo: '0x...',
  depositedBy: '0x...',
  chain: 'Base_Sepolia',
  txHash: '0x...',
  explorerUrl: 'https://sepolia.basescan.org/tx/0x...'
}
```

### 2.3. Verify the deposit

Open the `explorerUrl` from the deposit result to confirm the onchain
transaction on Base Sepolia.

## Step 3. Authorize the delegate

In this step, the owner wallet grants the delegate permission to spend from its
Unified Balance on a specific blockchain.

### 3.1. Create the authorize script

Create a `delegate-authorize.ts` file. In this script, the owner wallet
authorizes the delegate to spend from its Unified Balance on Base Sepolia:

```typescript delegate-authorize.ts theme={null}
import { AppKit } from "@circle-fin/app-kit";
import { createCircleWalletsAdapter } from "@circle-fin/adapter-circle-wallets";

const kit = new AppKit();

kit.on("*", (payload) => {
  console.log("Event received:", payload);
});

const adapter = createCircleWalletsAdapter({
  apiKey: process.env.CIRCLE_API_KEY!,
  entitySecret: process.env.CIRCLE_ENTITY_SECRET!,
});

const ownerWalletAddress = "YOUR_BASE_SEPOLIA_OWNER_WALLET_ADDRESS";
const delegateWalletAddress = "YOUR_BASE_SEPOLIA_DELEGATE_WALLET_ADDRESS";

async function main() {
  const status = await kit.unifiedBalance.getDelegateStatus({
    from: {
      adapter,
      chain: "Base_Sepolia",
      address: ownerWalletAddress,
    },
    delegateAddress: delegateWalletAddress,
  });

  if (status === "ready") {
    console.log(
      `Delegate ${delegateWalletAddress} is already authorized on Base_Sepolia.`,
    );
    return;
  }

  if (status === "pending") {
    console.log(
      `Delegate ${delegateWalletAddress} is still pending on Base_Sepolia. Wait and run this script again.`,
    );
    return;
  }

  // addDelegate: owner-signed transaction granting the delegate spend rights.
  const result = await kit.unifiedBalance.addDelegate({
    from: {
      adapter,
      chain: "Base_Sepolia",
      address: ownerWalletAddress,
    },
    delegateAddress: delegateWalletAddress,
  });

  console.dir(result, { depth: null, colors: true });
}

void main();
```


  `addDelegate` is an onchain transaction signed by the owner wallet. Once
  authorized, the delegate can spend repeatedly on the same blockchain without
  reauthorization. Authorize only delegate addresses you control, and revoke
  access when it is no longer needed. Authorization is source-blockchain specific.
  See [Manage Delegates](/app-kit/tutorials/unified-balance/manage-delegates) for
  details.


### 3.2. Run the authorize script

In your terminal, run:

```bash Shell theme={null}
npm run authorize
```

You'll see output like:

```bash Shell theme={null}
{
  account: '0x...',
  delegateAddress: '0x...',
  chain: 'Base_Sepolia',
  state: 'added',
  txHash: '0x...',
  explorerUrl: 'https://sepolia.basescan.org/tx/0x...'
}
```

If `status` is already `'ready'`, the script exits without calling
`addDelegate`. If `status` is `'pending'`, it asks you to wait and run the
script again. Otherwise it submits `addDelegate`.

## Step 4. Check the owner's Unified Balance

In this step, you check the owner's Unified Balance by address.

### 4.1. Create the balance check script

Create a `delegate-check-balance.ts` file. This script prints the owner's
confirmed and pending Unified Balance totals:

```typescript delegate-check-balance.ts theme={null}
import { AppKit } from "@circle-fin/app-kit";

const kit = new AppKit();

const ownerWalletAddress = "YOUR_BASE_SEPOLIA_OWNER_WALLET_ADDRESS";

const balances = await kit.unifiedBalance.getBalances({
  sources: {
    address: ownerWalletAddress,
    chains: ["Base_Sepolia"],
  },
  networkType: "testnet",
  includePending: true,
});

console.dir(balances, { depth: null, colors: true });
```


  You can check balances by address, adapter, chain, and network. See
  [Check Unified Balance](/app-kit/tutorials/unified-balance/check-unified-balance)
  for more options.


### 4.2. Run the balance check script

In your terminal, run:

```bash Shell theme={null}
npm run balance
```

You'll see output like:

```bash Shell theme={null}
{
  token: 'USDC',
  totalConfirmedBalance: '2.000000',
  breakdown: [
    {
      depositor: '0x...',
      totalConfirmed: '2.000000',
      breakdown: [
        {
          chain: 'Base_Sepolia',
          confirmedBalance: '2.000000',
          pendingBalance: '0.000000',
          pendingTransactions: []
        }
      ],
      totalPending: '0.000000'
    }
  ],
  totalPendingBalance: '0.000000'
}
```

After a deposit, funds can appear in `totalPendingBalance` before they are
reflected in `totalConfirmedBalance`. Wait until the owner's
`totalConfirmedBalance` is high enough for the spend you plan to make before you
continue.

## Step 5. Spend from the owner's balance

In this step, the delegate spends from the owner's Unified Balance on Arc
Testnet for the recipient. The
[Forwarding Service](/app-kit/tutorials/unified-balance/use-forwarding-service)
submits the destination mint, so you don't need a wallet on Arc Testnet.


  The Forwarding Service charges a fee that is deducted from the amount minted on
  the destination chain. The spend result includes the forwarding fee in the fee
  breakdown.


### 5.1. Create the spend script

Create a `delegate-spend.ts` file. This script spends 0.50 USDC from the owner's
Unified Balance on Arc Testnet for the recipient, signed by the delegate.

```typescript delegate-spend.ts theme={null}
import { AppKit } from "@circle-fin/app-kit";
import { createCircleWalletsAdapter } from "@circle-fin/adapter-circle-wallets";

const SPEND_AMOUNT = "0.50";

const kit = new AppKit();

kit.on("*", (payload) => {
  console.log("Event received:", payload);
});

const adapter = createCircleWalletsAdapter({
  apiKey: process.env.CIRCLE_API_KEY!,
  entitySecret: process.env.CIRCLE_ENTITY_SECRET!,
});

const ownerWalletAddress = "YOUR_BASE_SEPOLIA_OWNER_WALLET_ADDRESS";
const delegateWalletAddress = "YOUR_BASE_SEPOLIA_DELEGATE_WALLET_ADDRESS";
const recipientAddress = "YOUR_ARC_TESTNET_RECIPIENT_ADDRESS";

console.log(
  `Spending ${SPEND_AMOUNT} USDC on Arc_Testnet for ${recipientAddress}...\n`,
);

const result = await kit.unifiedBalance.spend({
  amount: SPEND_AMOUNT,
  token: "USDC",
  from: [
    {
      adapter,
      address: delegateWalletAddress,
      // Spend from the owner's balance; the delegate wallet signs.
      sourceAccount: ownerWalletAddress,
      allocations: [{ amount: SPEND_AMOUNT, chain: "Base_Sepolia" }],
    },
  ],
  to: {
    chain: "Arc_Testnet",
    recipientAddress,
    useForwarder: true,
  },
});

console.dir(result, { depth: null, colors: true });
```


  You can also customize your Unified Balance to
  [collect a custom fee](/app-kit/tutorials/unified-balance/collect-custom-spend-fees)
  from end users,
  [estimate fees](/app-kit/tutorials/unified-balance/estimate-spend-fees) before
  spending,
  [select source blockchains and allocations](/app-kit/tutorials/unified-balance/select-source-blockchains)
  to fund a balance.


### 5.2. Run the spend script

In your terminal, run:

```bash Shell theme={null}
npm run spend
```

The script logs SDK events and prints the spend result. You'll see output like:

```bash Shell theme={null}
{
  recipientAddress: '0x...',
  destinationChain: 'Arc_Testnet',
  txHash: '0x...',
  explorerUrl: 'https://testnet.arcscan.app/tx/0x...',
  allocations: [
    {
      amount: '0.5',
      chain: 'Base_Sepolia',
      sourceAccount: '0x...'
    }
  ],
  fees: [
    { type: 'provider', token: 'USDC', amount: '0.000025', ... },
    { type: 'gasFee', token: 'USDC', amount: '0.024137', ... },
    { type: 'forwarder', token: 'USDC', amount: '0.014162' }
  ],
  transferId: '...',
  expirationBlock: '...'
}
```

### 5.3. Verify the spend

Use the spend result to confirm that USDC arrived at the recipient address on
Arc Testnet. When you use the Forwarding Service, the result can include a
`transferId` instead of a locally submitted destination transaction hash. The
received amount can be less than the requested spend after fees. For more on
fees, see
[How Unified Balance fees work](/app-kit/concepts/unified-balance-fees).


# Quickstart: Deposit and spend a Unified Balance
Source: https://docs.arc.io/app-kit/quickstarts/unified-balance-deposit-and-spend

Deposit USDC from multiple source blockchains into a Unified Balance, then spend from the combined pool on another blockchain

Use this quickstart to deposit USDC into a Unified Balance, check the combined
balance, and spend from it on another blockchain. The examples deposit from Base
Sepolia and Solana Devnet, then spend on Arc Testnet.

Choose the wallet model that matches your application. Use a browser wallet when
the end user signs in the client, or use Circle Wallets when you manage
developer-controlled wallets through Circle.


  
    Use this flow to deposit and spend a Unified Balance with connected browser
    wallets. The examples use Base Sepolia, Solana Devnet, and Arc Testnet.

    ## Prerequisites

    Before you begin, ensure that you've:

    * Installed [Node.js v22+](https://nodejs.org/).
    * Created an EVM wallet using a wallet provider such as
      [MetaMask](https://metamask.io/) and added the
      [Base Sepolia](https://docs.base.org/docs/network-information#base-testnet-sepolia)
      and
      [Arc Testnet](https://docs.arc.io/arc/references/connect-to-arc#wallet-setup)
      networks.
    * Created a Solana wallet (for example, [Phantom](https://phantom.app/) or
      [Solflare](https://solflare.com/)) on Devnet.
    * Funded your wallets with testnet tokens:
      * Get testnet USDC from the [Circle Faucet](https://faucet.circle.com/) on
        Base Sepolia and Solana Devnet.
      * Get testnet ETH on Base Sepolia from a
        [public faucet](https://www.alchemy.com/faucets/base-sepolia) (needed for
        deposit and spend transactions on Base Sepolia).
      * Get SOL for Solana Devnet from the
        [Solana Faucet](https://faucet.solana.com/).
      * Fund the connected EVM wallet on Arc Testnet if needed (USDC on Arc can
        cover gas for the destination credit when you spend on Arc).
    * Obtained an Arc Testnet address that will receive USDC when you spend on Arc
      Testnet.

    ## Step 1. Set up the project

    ### 1.1. Create the project and install dependencies

    Create a new directory, install the App Kit packages, and add local browser demo
    tooling:

    ```bash Shell theme={null}
    mkdir app-kit-unified-balance-browser-wallet
    cd app-kit-unified-balance-browser-wallet
    npm init -y
    npm pkg set type=module
    npm install @circle-fin/app-kit @circle-fin/adapter-viem-v2 viem @circle-fin/adapter-solana
    npm install --save-dev typescript vite
    ```

    
      Only need a Unified Balance and want a lighter install than the full App Kit
      SDK? Install the standalone Unified Balance Kit instead:
      `@circle-fin/unified-balance-kit`
    

    ### 1.2. Configure TypeScript (optional)

    
      This step is optional. It helps prevent missing types in your IDE or editor.
    

    Create a `tsconfig.json` file configured for ESM and Node:

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

    ## Step 2. Connect browser wallets

    This step shows the core browser wallet integration flow: discover an
    [`EIP-6963`](https://eips.ethereum.org/EIPS/eip-6963) provider, create App Kit
    adapters from the selected providers, and pass those adapters into App Kit SDK
    methods.

    The snippets below keep wallet discovery, wallet connection, and adapter setup
    in small helper functions for readability.

    ### 2.1. Discover a browser wallet with `EIP-6963`

    
      This pattern is standards-based. The example uses MetaMask as the selected
      wallet, but the discovery flow works with any wallet that announces an
      `EIP-6963` provider.
    

    ```typescript TypeScript theme={null}
    import type { EIP1193Provider } from "viem";

    type EIP6963ProviderInfo = {
      uuid: string;
      name: string;
      icon: string;
      rdns: string;
    };

    type EIP6963ProviderDetail = {
      info: EIP6963ProviderInfo;
      provider: EIP1193Provider;
    };

    declare global {
      interface WindowEventMap {
        "eip6963:announceProvider": CustomEvent;
      }
    }

    async function discoverBrowserWallets(): Promise {
      const providers = new Map();

      const handleProviderAnnouncement = (
        event: WindowEventMap["eip6963:announceProvider"],
      ) => {
        providers.set(event.detail.info.uuid, event.detail);
      };

      window.addEventListener(
        "eip6963:announceProvider",
        handleProviderAnnouncement,
      );
      window.dispatchEvent(new Event("eip6963:requestProvider"));

      await new Promise((resolve) => window.setTimeout(resolve, 250));
      window.removeEventListener(
        "eip6963:announceProvider",
        handleProviderAnnouncement,
      );

      return [...providers.values()];
    }
    ```

    ### 2.2. Connect the wallet and request account access

    After you select a provider, request account access before calling an App Kit
    SDK method. This should happen in a user-triggered action such as a
    `Connect wallet` button.

    ```typescript TypeScript theme={null}
    async function connectWallet(provider: EIP1193Provider) {
      await provider.request({
        method: "eth_requestAccounts",
        params: undefined, // Required by the provider type even though this method has no params.
      });

      const accounts = (await provider.request({
        method: "eth_accounts",
        params: undefined, // Required by the provider type even though this method has no params.
      })) as string[];

      return {
        connectedAddress: accounts[0] ?? null,
      };
    }
    ```

    
      Keep wallet connection and App Kit actions as separate user actions. This avoids
      overlapping wallet permission or chain-switch requests while a previous wallet
      prompt is still pending.
    

    ### 2.3. Create an EVM adapter for Unified Balance

    This quickstart uses adapter-only balance checks and spend sources so the SDK
    can choose source blockchains automatically. Configure the EVM adapter with the
    EVM chains used in this flow so automatic allocation has chain context for
    balance discovery.

    ```typescript TypeScript theme={null}
    import { ArcTestnet, BaseSepolia } from "@circle-fin/app-kit/chains";
    import { createViemAdapterFromProvider } from "@circle-fin/adapter-viem-v2";

    async function connectEvmBrowserWallet() {
      const providers = await discoverBrowserWallets();
      const selectedWallet =
        providers.find(
          ({ info }) => info.rdns === "io.metamask" || info.name === "MetaMask",
        ) ?? providers[0];

      if (!selectedWallet) {
        throw new Error("No EIP-6963 browser wallet found");
      }

      const { connectedAddress } = await connectWallet(selectedWallet.provider);

      const adapter = await createViemAdapterFromProvider({
        provider: selectedWallet.provider,
        capabilities: {
          supportedChains: [BaseSepolia, ArcTestnet],
        },
      });

      return {
        adapter,
        connectedAddress,
        walletName: selectedWallet.info.name,
      };
    }
    ```

    ### 2.4. Connect the Solana wallet and create a Solana adapter

    
      This pattern assumes a Solana browser wallet that exposes `window.solana`. Keep
      wallet connection and App Kit actions as separate user actions so the wallet is
      fully connected before you call an App Kit SDK method.
    

    ```typescript TypeScript theme={null}
    import { SolanaDevnet } from "@circle-fin/app-kit/chains";
    import { createSolanaAdapterFromProvider } from "@circle-fin/adapter-solana";
    import type { CreateSolanaAdapterFromProviderParams } from "@circle-fin/adapter-solana";

    type SolanaWalletProvider = CreateSolanaAdapterFromProviderParams["provider"];

    declare global {
      interface Window {
        solana?: SolanaWalletProvider;
      }
    }

    async function connectSolanaWallet(provider: SolanaWalletProvider) {
      const connection = await provider.connect();

      return {
        connectedAddress:
          connection.publicKey?.toString() ??
          provider.publicKey?.toString() ??
          null,
      };
    }

    async function connectSolanaBrowserWallet() {
      const provider = window.solana;

      if (!provider) {
        throw new Error("No Solana browser wallet found");
      }

      const { connectedAddress } = await connectSolanaWallet(provider);

      const adapter = await createSolanaAdapterFromProvider({
        provider,
        capabilities: {
          supportedChains: [SolanaDevnet],
        },
      });

      return {
        adapter,
        connectedAddress,
      };
    }
    ```

    ## Step 3. Deposit into a Unified Balance

    In this step, you'll deposit from Base Sepolia and Solana Devnet. The EVM
    deposit uses the EVM browser wallet adapter from the previous step. The Solana
    deposit uses the Solana browser wallet adapter from the previous step.

    The examples in the remaining steps reuse the same `kit` instance and `Adapter`
    type.

    ### 3.1. Deposit from Base Sepolia

    Call `kit.unifiedBalance.deposit` with the connected EVM browser wallet adapter.
    Before the deposit, switch the browser wallet to Base Sepolia so the deposit
    authorization is signed on the source chain:

    ```typescript TypeScript theme={null}
    import { AppKit, type Adapter } from "@circle-fin/app-kit";
    import { resolveChainIdentifier } from "@circle-fin/adapter-viem-v2";

    const kit = new AppKit();

    async function depositFromBaseSepolia(evmAdapter: Adapter) {
      const chain = resolveChainIdentifier("Base_Sepolia");

      if (chain.type !== "evm") {
        throw new Error(`${chain.name} is not an EVM chain`);
      }

      await evmAdapter.ensureChain(chain);

      const result = await kit.unifiedBalance.deposit({
        from: { adapter: evmAdapter, chain: "Base_Sepolia" },
        amount: "2.00",
        token: "USDC",
      });

      console.dir(result, { depth: null });

      return result;
    }
    ```

    You'll see output like:

    ```text Shell theme={null}
    {
      amount: "2.00",
      token: "USDC",
      chain: "Base_Sepolia",
      txHash: "0x...",
      explorerUrl: "https://sepolia.basescan.org/tx/0x...",
      ...
    }
    ```

    ### 3.2. Deposit from Solana Devnet

    Call `kit.unifiedBalance.deposit` with the Solana browser wallet adapter:

    ```typescript TypeScript theme={null}
    async function depositFromSolanaDevnet(solanaAdapter: Adapter) {
      const result = await kit.unifiedBalance.deposit({
        from: { adapter: solanaAdapter, chain: "Solana_Devnet" },
        amount: "1.00",
        token: "USDC",
      });

      console.dir(result, { depth: null });

      return result;
    }
    ```

    You'll see output like:

    ```text Shell theme={null}
    {
      amount: "1.00",
      token: "USDC",
      chain: "Solana_Devnet",
      txHash: "2k41...",
      explorerUrl: "https://solscan.io/tx/2k41...?cluster=devnet",
      ...
    }
    ```

    ### 3.3. Verify the deposits

    Open the `explorerUrl` from each deposit result and confirm the onchain
    transactions on Base Sepolia and Solana Devnet. When both deposits are
    finalized, continue to the next step.

    ## Step 4. Check your Unified Balance

    In this step, you query your Unified Balance across the Base Sepolia and Solana
    Devnet depositors and print the confirmed and pending amounts.

    ### 4.1. Check balances

    Call `kit.unifiedBalance.getBalances` with the same browser wallet adapters you
    used for the deposits:

    ```typescript TypeScript theme={null}
    async function checkUnifiedBalance(
      evmAdapter: Adapter,
      solanaAdapter: Adapter,
    ) {
      const balances = await kit.unifiedBalance.getBalances({
        // Both wallets that deposited, one adapter per source.
        sources: [{ adapter: evmAdapter }, { adapter: solanaAdapter }],
        networkType: "testnet",
        includePending: true,
      });

      console.dir(balances, { depth: null });

      return balances;
    }
    ```

    You'll see output like:

    ```text Shell theme={null}
    {
      token: "USDC",
      totalConfirmedBalance: "3.00",
      totalPendingBalance: "0.00",
      breakdown: [
        {
          depositor: "0x...",
          totalConfirmed: "2.00",
          totalPending: "0.00",
          breakdown: [{ chain: "Base_Sepolia", confirmedBalance: "2.00", ... }]
        },
        {
          depositor: "...",
          totalConfirmed: "1.00",
          totalPending: "0.00",
          breakdown: [{ chain: "Solana_Devnet", confirmedBalance: "1.00", ... }]
        }
      ]
    }
    ```

    After a deposit, funds can appear in `totalPendingBalance` before they are
    reflected in `totalConfirmedBalance`. Wait until the confirmed balance is
    sufficient before you spend.

    ## Step 5. Spend from the combined balance

    In this step, you spend USDC on Arc Testnet from your Unified Balance.

    ### 5.1. Spend on Arc Testnet

    Collect the recipient address from your app UI, then pass it with the connected
    wallet adapters into the spend function. This code spends 2.50 USDC on Arc
    Testnet for the recipient.
    [The App Kit SDK chooses](/app-kit/tutorials/unified-balance/select-source-blockchains)
    how much USDC to use from each blockchain.

    ```typescript TypeScript theme={null}
    async function spendFromUnifiedBalance(
      evmAdapter: Adapter,
      solanaAdapter: Adapter,
      recipientAddress: string,
    ) {
      console.log(`Spending 2.50 USDC on Arc_Testnet for ${recipientAddress}...`);

      const result = await kit.unifiedBalance.spend({
        amount: "2.50",
        token: "USDC",
        from: [{ adapter: evmAdapter }, { adapter: solanaAdapter }],
        to: {
          adapter: evmAdapter,
          chain: "Arc_Testnet",
          recipientAddress,
        },
      });

      console.dir(result, { depth: null });

      return result;
    }
    ```

    
      You can customize your Unified Balance to
      [use fast deposits](/app-kit/tutorials/unified-balance/use-fast-deposits),
      [collect a custom fee](/app-kit/tutorials/unified-balance/collect-custom-spend-fees)
      from end users,
      [estimate fees](/app-kit/tutorials/unified-balance/estimate-spend-fees) before
      spending,
      [select source blockchains and allocations](/app-kit/tutorials/unified-balance/select-source-blockchains)
      to fund a balance, or use the
      [Forwarding Service](/app-kit/tutorials/unified-balance/use-forwarding-service).
      For normal movement out of a Unified Balance, use `spend`. Use
      [`removeFund`](/app-kit/tutorials/unified-balance/remove-funds-trustlessly) only
      as a trustless recovery path when the normal spend flow is unavailable; the
      process requires a seven-day delay before you can withdraw.
    

    When the spend completes, you should see output similar to:

    ```bash Shell theme={null}
    Spending 2.50 USDC on Arc_Testnet for 0x...

    { recipientAddress: "0x...", destinationChain: "Arc Testnet", txHash: "0x...", ... }
    ```

    ### 5.2. Verify the spend

    Use the `explorerUrl` from the spend result to confirm that USDC arrived at the
    recipient address on Arc Testnet. The received amount can be less than the
    requested spend after fees. For more on fees, see
    [How Unified Balance fees work](/app-kit/concepts/unified-balance-fees).
  

  
    Use this flow to deposit and spend a Unified Balance with developer-controlled
    wallets managed by Circle Wallets. The examples use Base Sepolia, Solana Devnet,
    and Arc Testnet.

    ## Prerequisites

    Before you begin, ensure that you've:

    * Installed [Node.js v22+](https://nodejs.org/).
    * Set up Circle Wallets:
      * Obtained a
        [API Key](https://developers.circle.com/api-reference/keys#creating-an-api-key-for-developer-services)
        and
        [entity secret](https://developers.circle.com/wallets/dev-controlled/register-entity-secret)
        from the
        [Circle Console](https://developers.circle.com/w3s/circle-developer-account).
      * Created developer-controlled wallets on Base Sepolia, Solana Devnet, and Arc
        Testnet.
    * Funded the wallets:
      * Base Sepolia: testnet USDC from the
        [Circle Faucet](https://faucet.circle.com/) and testnet ETH from a
        [public faucet](https://www.alchemy.com/faucets/base-sepolia).
      * Solana Devnet: testnet USDC from the
        [Circle Faucet](https://faucet.circle.com/) and SOL from the
        [Solana Faucet](https://faucet.solana.com/).
      * Arc Testnet: fund the destination wallet if needed. USDC on Arc can cover
        gas for the destination credit when you spend on Arc.

    ## Step 1. Set up the project

    ### 1.1. Create the project and install dependencies

    Create a new directory and install the App Kit SDK with the Circle Wallets
    adapter and supporting tools:

    ```bash Shell theme={null}
    # Set up your directory and initialize a Node.js project
    mkdir app-kit-unified-balance-circle-wallets
    cd app-kit-unified-balance-circle-wallets
    npm init -y
    npm pkg set type=module

    # Set up run scripts
    npm pkg set scripts.deposit:base="tsx --env-file=.env deposit-base.ts"
    npm pkg set scripts.deposit:solana="tsx --env-file=.env deposit-solana.ts"
    npm pkg set scripts.balance="tsx --env-file=.env check-balance.ts"
    npm pkg set scripts.spend="tsx --env-file=.env spend.ts"

    # Install runtime dependencies
    npm install @circle-fin/app-kit @circle-fin/adapter-circle-wallets tsx

    # Install dev dependencies
    npm install --save-dev typescript @types/node
    ```

    
      Only need a Unified Balance and want a lighter install than the full App Kit
      SDK? Install the standalone Unified Balance Kit instead:
      `@circle-fin/unified-balance-kit`
    

    ### 1.2. Configure TypeScript (optional)

    
      This step is optional. It helps prevent missing types in your IDE or editor.
    

    Create a `tsconfig.json` file configured for ESM and Node:

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

    ### 1.3. Set environment variables

    Create an `.env` file in the project directory:

    ```bash Shell theme={null}
    touch .env
    ```

    Add your credentials. Replace `YOUR_API_KEY` with your Circle Developer API key
    and `YOUR_ENTITY_SECRET` with your entity secret:

    ```text .env theme={null}
    CIRCLE_API_KEY=YOUR_API_KEY
    CIRCLE_ENTITY_SECRET=YOUR_ENTITY_SECRET
    ```

    
      Edit `.env` files in your IDE or editor so credentials are not leaked to your
      shell history.
    

    ## Step 2. Deposit into a Unified Balance

    Circle Wallets uses developer-controlled addresses, so each operation includes
    the wallet address explicitly.

    
      The API key, entity secret, and deposit wallet addresses must belong to the same
      Circle Developer entity. Otherwise, Circle Wallets may fail to find the wallet
      and return error `156001` during signing.
    

    ### 2.1. Deposit from Base Sepolia

    Create a `deposit-base.ts` file. This script deposits 2.00 USDC from your Base
    Sepolia Circle Wallets-controlled wallet into your Unified Balance.

    ```typescript TypeScript theme={null}
    import { AppKit } from "@circle-fin/app-kit";
    import { createCircleWalletsAdapter } from "@circle-fin/adapter-circle-wallets";

    const kit = new AppKit();

    kit.on("*", (payload) => {
      console.log("Event received:", payload);
    });

    const adapter = createCircleWalletsAdapter({
      apiKey: process.env.CIRCLE_API_KEY!,
      entitySecret: process.env.CIRCLE_ENTITY_SECRET!,
    });

    const baseWalletAddress = "YOUR_BASE_SEPOLIA_WALLET_ADDRESS";

    const result = await kit.unifiedBalance.deposit({
      from: {
        adapter,
        chain: "Base_Sepolia",
        address: baseWalletAddress,
      },
      amount: "2.00",
      token: "USDC",
    });

    console.dir(result, { depth: null, colors: true });
    ```

    Run the script:

    ```bash Shell theme={null}
    npm run deposit:base
    ```

    ### 2.2. Deposit from Solana Devnet

    Create a `deposit-solana.ts` file. This script deposits 1.00 USDC from your
    Solana Devnet Circle Wallets-controlled wallet into your Unified Balance.

    ```typescript TypeScript theme={null}
    import { AppKit } from "@circle-fin/app-kit";
    import { createCircleWalletsAdapter } from "@circle-fin/adapter-circle-wallets";

    const kit = new AppKit();

    kit.on("*", (payload) => {
      console.log("Event received:", payload);
    });

    const adapter = createCircleWalletsAdapter({
      apiKey: process.env.CIRCLE_API_KEY!,
      entitySecret: process.env.CIRCLE_ENTITY_SECRET!,
    });

    const solanaWalletAddress = "YOUR_SOLANA_DEVNET_WALLET_ADDRESS";

    const result = await kit.unifiedBalance.deposit({
      from: {
        adapter,
        chain: "Solana_Devnet",
        address: solanaWalletAddress,
      },
      amount: "1.00",
      token: "USDC",
    });

    console.dir(result, { depth: null, colors: true });
    ```

    Run the script:

    ```bash Shell theme={null}
    npm run deposit:solana
    ```

    ### 2.3. Verify the deposits

    Open the `explorerUrl` from each deposit result and confirm the onchain
    transactions on Base Sepolia and Solana Devnet. When both deposits are
    finalized, continue to the next step.

    
      You can customize your Unified Balance to
      [use fast deposits](/app-kit/tutorials/unified-balance/use-fast-deposits),
      [collect a custom fee](/app-kit/tutorials/unified-balance/collect-custom-spend-fees)
      from end users,
      [estimate fees](/app-kit/tutorials/unified-balance/estimate-spend-fees) before
      spending,
      [select source blockchains and allocations](/app-kit/tutorials/unified-balance/select-source-blockchains)
      to fund a balance, or use the
      [Forwarding Service](/app-kit/tutorials/unified-balance/use-forwarding-service).
      For normal movement out of a Unified Balance, use `spend`. Use
      [`removeFund`](/app-kit/tutorials/unified-balance/remove-funds-trustlessly) only
      as a trustless recovery path when the normal spend flow is unavailable; the
      process requires a seven-day delay before you can withdraw.
    

    ## Step 3. Check your Unified Balance

    After both deposits are finalized, create a `check-balance.ts` file. This script
    queries the confirmed and pending Unified Balance for the Base Sepolia and
    Solana Devnet source accounts.

    ```typescript TypeScript theme={null}
    import { AppKit } from "@circle-fin/app-kit";
    import { createCircleWalletsAdapter } from "@circle-fin/adapter-circle-wallets";

    const kit = new AppKit();

    const adapter = createCircleWalletsAdapter({
      apiKey: process.env.CIRCLE_API_KEY!,
      entitySecret: process.env.CIRCLE_ENTITY_SECRET!,
    });

    const baseWalletAddress = "YOUR_BASE_SEPOLIA_WALLET_ADDRESS";
    const solanaWalletAddress = "YOUR_SOLANA_DEVNET_WALLET_ADDRESS";

    const balances = await kit.unifiedBalance.getBalances({
      sources: [
        {
          adapter,
          address: baseWalletAddress,
          chains: ["Base_Sepolia"],
        },
        {
          adapter,
          address: solanaWalletAddress,
          chains: ["Solana_Devnet"],
        },
      ],
      networkType: "testnet",
      includePending: true,
    });

    console.dir(balances, { depth: null, colors: true });
    ```

    Run the script:

    ```bash Shell theme={null}
    npm run balance
    ```

    Wait until `totalConfirmedBalance` is high enough for the spend you plan to
    make.

    ## Step 4. Spend from the Unified Balance

    Create a `spend.ts` file. This script spends 2.50 USDC on Arc Testnet. The
    `from` sources do not include `allocations`, so the SDK chooses source
    blockchains automatically from the available confirmed balances. Each `from`
    entry needs its own adapter instance.

    ```typescript TypeScript theme={null}
    import { AppKit } from "@circle-fin/app-kit";
    import { createCircleWalletsAdapter } from "@circle-fin/adapter-circle-wallets";

    const kit = new AppKit();

    kit.on("*", (payload) => {
      console.log("Event received:", payload);
    });

    const evmAdapter = createCircleWalletsAdapter({
      apiKey: process.env.CIRCLE_API_KEY!,
      entitySecret: process.env.CIRCLE_ENTITY_SECRET!,
    });

    const solanaAdapter = createCircleWalletsAdapter({
      apiKey: process.env.CIRCLE_API_KEY!,
      entitySecret: process.env.CIRCLE_ENTITY_SECRET!,
    });

    const baseWalletAddress = "YOUR_BASE_SEPOLIA_WALLET_ADDRESS";
    const solanaWalletAddress = "YOUR_SOLANA_DEVNET_WALLET_ADDRESS";
    const arcWalletAddress = "YOUR_ARC_TESTNET_WALLET_ADDRESS";

    const result = await kit.unifiedBalance.spend({
      amount: "2.50",
      token: "USDC",
      from: [
        {
          adapter: evmAdapter,
          address: baseWalletAddress,
          sourceAccount: baseWalletAddress,
        },
        {
          adapter: solanaAdapter,
          address: solanaWalletAddress,
          sourceAccount: solanaWalletAddress,
        },
      ],
      to: {
        adapter: evmAdapter,
        chain: "Arc_Testnet",
        address: arcWalletAddress,
        recipientAddress: arcWalletAddress,
      },
    });

    console.dir(result, { depth: null, colors: true });
    ```

    Run the script:

    ```bash Shell theme={null}
    npm run spend
    ```

    ### 4.1. Verify the spend

    Use the `explorerUrl` from the spend result to confirm that USDC arrived at the
    recipient address on Arc Testnet. The spend result includes the allocations
    selected by the SDK, the destination recipient, and the Arc Testnet mint
    transaction hash.
  



# Error recovery and troubleshooting for bridges
Source: https://docs.arc.io/app-kit/references/bridge-error-recovery

Identify bridge transfer failures, recover partial bridge transfers, and implement error handling for App Kit SDK bridge transfers

Bridge transfers can encounter two error types. Hard errors stop execution. Soft
errors let you recover and retry the bridge transfer. The App Kit SDK provides
error handling that helps you respond in both cases.

Hard errors throw exceptions such as validation errors, configuration issues,
and authentication problems. Soft errors occur during the bridge transfer but
return enough transaction information for recovery. Examples include
insufficient balance, network timeouts, and RPC connectivity issues.

This guide helps you identify failure points, recover partial bridge transfers,
and implement error handling patterns.


  The examples below are focused recovery snippets, not complete runnable scripts.
  They assume you have already configured `kit`, `sourceAdapter`, and
  `destinationAdapter`. See [Adapter setups](/app-kit/tutorials/adapter-setups)
  for setup options.


## Bridge transfer failures

This section explains how to identify where a bridge transfer failed and resume
the bridge transfer manually.

### Transaction steps overview

Each bridge transfer uses Circle's CCTP protocol provider, which breaks each
transaction into various steps:

* `approve`: Allows the contract to spend USDC.
* `burn`: Burns USDC on the source blockchain and generates an attestation.
* `fetchAttestation`: Waits for Circle to sign the burn proof.
* `mint`: Mints USDC on the destination blockchain with the attestation.

### Bridge result details

When a bridge transfer fails, the App Kit SDK returns a `BridgeResult` object
showing which steps completed and which failed. Use this result to decide
whether to retry the transfer, inspect a transaction on a block explorer, or
surface a recoverable state to your application.

Focus on these `BridgeResult` properties during recovery:

* `result.state` - shows whether the bridge transfer succeeded or failed
  (`pending`, `success`, `error`)
* `result.steps` - each object contains:
  * `name`: the name of the step
  * `state`: the status of the step
  * `txHash`: the transaction hash if the step completed
  * `error`: an error message if the step failed

This example shows a returned `result` object for a transaction that failed when
fetching an attestation:

```bash Shell theme={null}
result.state: 'error'
result.steps: [
  { name: 'approve', state: 'success', txHash: '0x123...' },
  { name: 'burn', state: 'success', txHash: '0x456...' },
  { name: 'fetchAttestation', state: 'error', error: 'Network timeout' },
]
```

### Step analysis

This example shows how to check for completed steps and use a helper function to
find specific steps:

```typescript TypeScript theme={null}
// Start a bridge transfer that might fail
const result = await kit.bridge({
  from: { adapter: sourceAdapter, chain: "Ethereum_Sepolia" },
  to: { adapter: destinationAdapter, chain: "Arc_Testnet" },
  amount: "1.00",
});

// Check which steps completed successfully
console.log("Bridge transfer state:", result.state);
console.log("Steps:", result.steps);

// Helper function to find specific steps
const getStep = (stepName: string) =>
  result.steps.find((step) => step.name === stepName);
const approveStep = getStep("approve");
const burnStep = getStep("burn");
const attestationStep = getStep("fetchAttestation");
const mintStep = getStep("mint");
```

## Recovery scenarios

This section describes how you can implement recovery patterns.

### Retry a failed bridge transfer

If a bridge transfer fails, you can retry it with the `retry` method. Pass the
failed `BridgeResult` and the `to` and `from` adapters.

This example shows how the retry method works:

```typescript TypeScript theme={null}
const result = await kit.bridge({
  from: { adapter: sourceAdapter, chain: "Ethereum_Sepolia" },
  to: { adapter: destinationAdapter, chain: "Arc_Testnet" },
  amount: "1.00",
});

if (result.state === "error") {
  const retryResult = await kit.retry(result, {
    from: sourceAdapter,
    to: destinationAdapter,
  });
  console.dir(retryResult, { depth: null, colors: true });
} else {
  console.dir(result, { depth: null, colors: true });
}
```

### Retry after a failed mint step

This pattern shows how to retry when the mint step fails. The
`failingDestinationAdapter` placeholder represents a bad destination signer or
RPC setup used to exercise the retry path:

```typescript TypeScript theme={null}
import type { BridgeResult } from "@circle-fin/app-kit";

const findErrorStep = (result: BridgeResult) => {
  if (result.state === "error") {
    return result.steps.find((step) => step.state === "error");
  }
  return null;
};

const result = await kit.bridge({
  from: { adapter: sourceAdapter, chain: "Ethereum_Sepolia" },
  to: { adapter: failingDestinationAdapter, chain: "Arc_Testnet" },
  amount: "1.00",
});

console.log("INITIAL RESULT");
console.dir(result, { depth: null, colors: true });

if (result.state === "error") {
  const errorStep = findErrorStep(result);
  if (
    errorStep &&
    errorStep.errorMessage?.includes("gas required exceeds allowance") // This is an example error message
  ) {
    const retryResult = await kit.retry(result, {
      from: sourceAdapter,
      to: destinationAdapter,
    });
    console.log("RETRY RESULT");
    console.dir(retryResult, { depth: null, colors: true });
  }
}
```

## Common issues

This section lists common issues and solutions.

### Insufficient balance

Ensure you have enough USDC in your wallet before a bridge transfer to avoid an
insufficient balance error.

This EVM example checks your wallet balance:

```typescript TypeScript theme={null}
import { formatUnits } from "viem";

const balanceAction = await sourceAdapter.prepareAction(
  "usdc.balanceOf",
  {},
  { chain: "Arc_Testnet" },
);
const balance = await balanceAction.execute();
console.log(`USDC balance: ${formatUnits(BigInt(balance), 6)}`);
```

### Transaction stuck or failed

If a transaction is stuck or failed, check the transaction on a block explorer
with the returned `txHash`. For Solana bridge transfers, use Solana Explorer or
SolScan.

If the transaction failed during the bridge transfer, check the returned
`result.steps` to see which [transaction steps](#transaction-steps-overview)
completed.

## Best practices

Follow these practices for prevention, recovery, and monitoring to improve
reliability.

**Prevention**

* Test your integration on testnets before deploying on mainnet.
* Monitor gas prices and adjust during network congestion.
* Use dedicated RPC providers such as Alchemy or QuickNode.
* Implement multiple RPC fallbacks.
* Wrap all bridge transfers in try-catch including adapter setup and bridge
  calls.

**Recovery**

* Always save the bridge transfer state for recovery scenarios.
* Verify which steps completed before attempting recovery.
* Use appropriate timeouts and give network operations enough time to complete.
* Implement exponential backoff and use increasing delays for retry logic.

**Monitoring and debugging**

* Use block explorers to verify transaction status.
* Save intermediate results and persist bridge transfer state for recovery
  scenarios.


# App Kit SDK Reference
Source: https://docs.arc.io/app-kit/references/sdk-reference

API reference for App Kit's public interfaces, methods, and types

This reference guide describes the public interfaces, methods, and types
available in the App Kit SDK.

## AppKit Class

The `AppKit` is how you'll perform all stablecoin operations including
crosschain bridging, same-chain swaps, token transfers, and fee estimation. It
also enables you to add event listeners for bridge transfers.

### constructor(config?)

Creates a new `AppKit` instance.

```typescript theme={null}
constructor(config?: AppKitConfig)
```

**Parameters**

| Name   | Type                            | Description                                                                                      |
| ------ | ------------------------------- | ------------------------------------------------------------------------------------------------ |
| config | [`AppKitConfig`](#appkitconfig) | Optional configuration for fee estimation, developer fees, and the underlying UnifiedBalanceKit. |

**Usage**

```typescript theme={null}
import { AppKit } from "@circle-fin/app-kit";

// Minimal — all defaults
const kit = new AppKit();

// With unified balance config
const kitWithUb = new AppKit({
  unifiedBalance: { providers: [myCustomProvider] },
});
```

### AppKitConfig

```typescript theme={null}
type AppKitConfig = CreateContextParams & {
  developerFee?: Partial;
  disableAnalytics?: boolean;
  disableErrorReporting?: boolean;
  unifiedBalance?: UnifiedBalanceKitConfig;
};
```

**Properties**

| Name                  | Type                         | Description                                                                                                                                                                                                                                                                                                      |
| --------------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| developerFee          | `Partial` | Optional developer fee hooks passed through to BridgeKit when both are provided                                                                                                                                                                                                                                  |
| disableAnalytics      | boolean                      | Disable success analytics for the underlying EarnKit, SwapKit, and UnifiedBalanceKit.

When `true`, completed earn, swap, and unified balance operations will not POST analytics events. This does not disable error reporting; use AppKitConfig.disableErrorReporting for that. Defaults to `false`. | | disableErrorReporting | boolean | Disable error telemetry for all underlying kits.

When `true`, BridgeKit, SwapKit, EarnKit, and UnifiedBalanceKit will not POST error details to the telemetry endpoint. Defaults to `false`. | | unifiedBalance | UnifiedBalanceKitConfig | Optional config forwarded to the underlying UnifiedBalanceKit. | *** *** ## Methods ### bridge(params) Execute a crosschain USDC bridge transfer. Transfers USDC between different blockchain networks using Circle's Cross-Chain Transfer Protocol (CCTP). Supports both fast and standard transfer speeds with automatic attestation handling. ```typescript theme={null} bridge(params: BridgeParams): Promise ``` **Parameters** | Name | Type | Description | | ------ | ------------------------------- | ------------------------------------------------------------------- | | params | [`BridgeParams`](#bridgeparams) | Bridge parameters containing source, destination, amount, and token | #### BridgeParams Parameters for initiating a crosschain USDC bridge transfer. This type is used as the primary input to BridgeKit.bridge, allowing users to specify the source and destination adapters, transfer amount, and optional configuration. * The `from` field specifies the source adapter context (wallet and chain). * The `to` field specifies the destination, supporting both explicit and derived recipient addresses. * The `config` field allows customization of bridge behavior (e.g., transfer speed). * The `token` field is optional and defaults to `USDC`; other tokens are not currently supported. ```typescript theme={null} interface BridgeParams { amount: string; config?: BridgeConfig; from: AdapterContext; invocationMeta?: InvocationMeta; to: BridgeDestination; token?: "USDC"; } ``` **Properties** | Name | Type | Description | | -------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | amount | string | The amount to transfer | | config | BridgeConfig | Optional bridge configuration (e.g., transfer speed). If omitted, defaults will be used | | from | `AdapterContext` | The source adapter context (wallet and chain) for the transfer. | | invocationMeta | InvocationMeta | Optional invocation metadata for tracing and correlation.

When provided, the `traceId` is used to correlate all events emitted during the bridge operation. If not provided, an OpenTelemetry-compatible `traceId` will be auto-generated. | | to | `BridgeDestination` | The destination for the transfer, supporting explicit or derived recipient addresses | | token | `'USDC'` | The token to transfer. Defaults to `USDC`. If omitted, the provider will use `USDC` by default. | **Returns** [`Promise`](#bridgeresult) #### BridgeResult Result object returned after a successful crosschain bridge operation. This interface contains all the details about a completed bridge, including the bridge parameters, source and destination information, and the sequence of steps that were executed. ```typescript theme={null} interface BridgeResult { amount: string config?: BridgeConfig destination: { address: string; chain: ChainDefinition; recipientAddress?: string; useForwarder?: boolean } provider: string source: { address: string; chain: ChainDefinition } state: 'pending' \| 'success' \| 'error' steps: BridgeStep[] token: 'USDC' } ``` **Properties** | Name | Type | Description | | ----------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------- | | amount | string | The amount that was transferred (as a string to avoid precision issues) | | config | BridgeConfig | The bridge configuration that was used for this operation | | destination | `{ address: string; chain: ChainDefinition; recipientAddress?: string; useForwarder?: boolean }` | Information about the destination chain and address | | provider | string | The provider that was used for this operation | | source | `{ address: string; chain: ChainDefinition }` | Information about the source chain and address | | state | `'pending' \| 'success' \| 'error'` | The state of the transfer | | steps | BridgeStep\[] | Array of steps that were executed during the bridge process | | token | `'USDC'` | The token that was transferred (currently only USDC is supported) | **Usage Example** ```typescript theme={null} const result = await kit.bridge({ from: { adapter: sourceAdapter, chain: "Ethereum" }, to: { adapter: destAdapter, chain: "Polygon" }, amount: "100.50", token: "USDC", }); console.log(`Bridged ${result.amount} ${result.token} (${result.state})`); ``` *** ### estimateBridge(params) Estimate the bridge operation. Calculates gas costs, protocol fees, and optional custom fees for a crosschain bridge transfer without executing the transaction. Useful for displaying cost estimates to users before they confirm a transfer. ```typescript theme={null} estimateBridge(params: BridgeParams): Promise ``` **Parameters** | Name | Type | Description | | ------ | --------------------------------- | ------------------------------------------------------------------- | | params | [`BridgeParams`](#bridgeparams-2) | Bridge parameters containing source, destination, amount, and token | #### BridgeParams Parameters for initiating a crosschain USDC bridge transfer. This type is used as the primary input to BridgeKit.bridge, allowing users to specify the source and destination adapters, transfer amount, and optional configuration. * The `from` field specifies the source adapter context (wallet and chain). * The `to` field specifies the destination, supporting both explicit and derived recipient addresses. * The `config` field allows customization of bridge behavior (e.g., transfer speed). * The `token` field is optional and defaults to `USDC`; other tokens are not currently supported. ```typescript theme={null} interface BridgeParams { amount: string; config?: BridgeConfig; from: AdapterContext; invocationMeta?: InvocationMeta; to: BridgeDestination; token?: "USDC"; } ``` **Properties** | Name | Type | Description | | -------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | amount | string | The amount to transfer | | config | BridgeConfig | Optional bridge configuration (e.g., transfer speed). If omitted, defaults will be used | | from | `AdapterContext` | The source adapter context (wallet and chain) for the transfer. | | invocationMeta | InvocationMeta | Optional invocation metadata for tracing and correlation.

When provided, the `traceId` is used to correlate all events emitted during the bridge operation. If not provided, an OpenTelemetry-compatible `traceId` will be auto-generated. | | to | `BridgeDestination` | The destination for the transfer, supporting explicit or derived recipient addresses | | token | `'USDC'` | The token to transfer. Defaults to `USDC`. If omitted, the provider will use `USDC` by default. | **Returns** [`Promise`](#estimateresult) #### EstimateResult Cost estimation result for a crosschain transfer operation. This interface provides detailed information about the expected costs for a transfer, including gas fees on different chains and protocol fees. It also includes the input context (token, amount, source, destination) to provide a complete view of the transfer being estimated. ```typescript theme={null} interface EstimateResult { amount: string destination: { address: string; chain: Blockchain; recipientAddress?: string } fees: { amount: string \| null; error?: unknown; token: 'USDC'; type: 'kit' \| 'provider' \| 'forwarder' }[] gasFees: { blockchain: Blockchain; error?: unknown; fees: EstimatedGas \| null; name: string; token: string }[] source: { address: string; chain: Blockchain } token: 'USDC' } ``` **Properties** | Name | Type | Description | | ----------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | | amount | string | The amount being transferred | | destination | `{ address: string; chain: Blockchain; recipientAddress?: string }` | Information about the destination chain and address | | fees | `{ amount: string \| null; error?: unknown; token: 'USDC'; type: 'kit' \| 'provider' \| 'forwarder' }[]` | Array of protocol and service fees for the transfer | | gasFees | `{ blockchain: Blockchain; error?: unknown; fees: EstimatedGas \| null; name: string; token: string }[]` | Array of gas fees required for the transfer on different blockchains | | source | `{ address: string; chain: Blockchain }` | Information about the source chain and address | | token | `'USDC'` | The token being transferred | **Usage Example** ```typescript theme={null} const estimate = await kit.estimateBridge({ from: { adapter: sourceAdapter, chain: "Ethereum" }, to: { adapter: destAdapter, chain: "Polygon" }, amount: "100.50", token: "USDC", }); console.log("Estimated fees:", estimate.fees); ``` *** ### estimateSend(params) Estimate network fees for a send operation. Prepare the send (validation + recipient resolution) and returns the gas estimate without executing the actual transaction. This allows developers to show users the cost before committing to the transfer. ```typescript theme={null} estimateSend(params: SendParams): Promise ``` **Parameters** | Name | Type | Description | | ------ | --------------------------- | ------------------------------------------------------------------------- | | params | [`SendParams`](#sendparams) | Send parameters: source, destination (address or adapter), amount, token. | #### SendParams Parameters for sending USDC, USDT, native tokens, or custom ERC-20/SPL tokens. This interface is the canonical input for send operations in App Kit. It supports sending to either a destination Adapter (recipient derives from the adapter's default account) or an explicit recipient `string` address. * The `from` field provides the source signing context and chain. * The `to` field identifies the destination as an adapter or an explicit address. * The `amount` field is a human-readable decimal string (for example, `'10.5'`). * The `token` field selects the asset to move and defaults to `'USDC'`. ```typescript theme={null} interface SendParams { amount: string from: AdapterContext to: string \| Adapter token?: TokenAlias \| TokenAddress } ``` **Properties** | Name | Type | Description | | ------ | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | amount | string | The amount to transfer. | | from | AdapterContext | The source adapter context (wallet and chain) for the transfer. | | to | `string \| Adapter` | The destination for the transfer, supporting explicit or derived recipient addresses. | | token | `TokenAlias \| TokenAddress` | The token to transfer. Defaults to `USDC`. If omitted, the provider will use `USDC` by default.

Supports both known aliases and custom token contract addresses:
- Known aliases: `USDC`, `USDT`, `NATIVE`, `EURC` (`EURC` requires the chain to have an `eurcAddress` configured)
- Custom token addresses: EVM addresses or Solana SPL token mint addresses | **Returns** [`Promise`](#estimatedgas) #### EstimatedGas Estimated gas information for a blockchain transaction. This interface provides a unified way to represent gas costs across different blockchain networks, supporting both EVM-style gas calculations and other fee models. ```typescript theme={null} interface EstimatedGas { fee: string; gas: bigint; gasPrice: bigint; } ``` **Usage Examples** ```typescript theme={null} const estimate = await kit.estimateSend({ from: { adapter: sourceAdapter, chain: "Ethereum" }, to: recipientAdapter, amount: "100.50", token: "USDC", }); console.log("Estimated gas:", estimate.gas); ``` ```typescript theme={null} const estimate = await kit.estimateSend({ from: { adapter: sourceAdapter, chain: "Ethereum" }, to: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", amount: "50.0", token: "USDT", }); console.log("Estimated gas:", estimate.gas); ``` *** ### estimateSwap(params) Estimate the output and fees for a swap operation. Calculates the expected output amount, minimum output (with slippage), and fee breakdown for a token swap without executing the transaction. ```typescript theme={null} estimateSwap(params: SwapParams): Promise ``` **Parameters** | Name | Type | Description | | ------ | --------------------------- | ------------------------------------------------------------- | | params | [`SwapParams`](#swapparams) | Swap parameters containing source, tokens, amount, and config | #### SwapParams ```typescript theme={null} interface SwapParams< TFromAdapterCapabilities extends AdapterCapabilities = AdapterCapabilities, > { from: SwapAdapterContext; tokenIn: SupportedSwapToken; tokenOut: SupportedSwapToken; amountIn: string; to?: SwapDestination; config?: SwapConfig; } ``` **Properties** | Name | Type | Description | | -------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | amountIn | string | The amount of the input token to swap.

Expressed as a human-readable decimal string in token units (e.g., `'0.05'` for 0.05 USDC or 0.05 ETH). | | config | SwapConfig | Optional configuration for swap behavior.

If omitted, defaults will be used:
- `allowanceStrategy`: `permit` (fallback to `approve`)
- `slippageBps`: 300 (3%) | | from | `SwapAdapterContext` | The source adapter context (wallet and chain) for the swap. | | to | SwapDestination | Optional destination chain/address.

For same-chain swaps this may be omitted and the source wallet address is used as the recipient. crosschain swaps require `recipientAddress`. | | tokenIn | SupportedSwapToken | The input token to swap from.

Supports stablecoins (USDC, USDT, EURC, USDe, DAI, PYUSD), wrapped tokens (WBTC, WETH, WSOL, WAVAX, WPOL), and native tokens (NATIVE or chain-specific symbols). | | tokenOut | SupportedSwapToken | The output token to swap to.

Supports stablecoins (USDC, USDT, EURC, USDe, DAI, PYUSD), wrapped tokens (WBTC, WETH, WSOL, WAVAX, WPOL), and native tokens (NATIVE or chain-specific symbols). | **Returns** [`Promise`](#swapestimate) #### SwapEstimate Estimation result for a swap operation. Contains the provider's swap quote including minimum output (stop limit), estimated output amount, fee breakdown, and input context fields ```typescript theme={null} interface SwapEstimate { readonly tokenIn: SupportedSwapToken; readonly tokenOut: SupportedSwapToken; readonly amountIn: string; readonly chainIn: Blockchain; readonly chainOut: Blockchain; readonly chain: Blockchain; readonly fromAddress: string; readonly toAddress: string; readonly stopLimit: TokenAmount; readonly estimatedOutput: TokenAmount; readonly fees?: readonly ServiceSwapFee[]; } ``` **Properties** | Name | Type | Description | | --------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | amountIn | string | The input amount that will be swapped.

Expressed as a human-readable decimal string in token units (e.g., `'0.05'` for 0.05 USDC or 0.05 ETH). | | chain | Blockchain | **Deprecated.** Use SwapEstimate.chainIn instead. Still populated with the source chain for a deprecation window; will be removed in a future major release. | | chainIn | Blockchain | The source chain of the swap.

Returns the chain name (e.g. `Blockchain.Ethereum`). Use `getChainByEnum(chainIn)` to resolve back to a full `ChainDefinition`. | | chainOut | Blockchain | Destination chain of the swap.

Equal to `chainIn` for same-chain estimates; different for crosschain estimates. Always populated. | | estimatedOutput | TokenAmount | Estimated output amount with token information. | | fees | readonly ServiceSwapFee\[] | Detailed fee breakdown for the swap operation. | | fromAddress | string | The address that will initiate the swap. | | stopLimit | TokenAmount | Estimated minimum token out amount with token information.

This represents the minimum amount of tokens the user should receive after accounting for slippage. Amount is in human-readable decimal format. | | toAddress | string | The address that will receive the swapped tokens. | | tokenIn | SupportedSwapToken | The input token that will be swapped from. | | tokenOut | SupportedSwapToken | The output token that will be swapped to. | **Usage Example** ```typescript theme={null} const estimate = await kit.estimateSwap({ from: { adapter, chain: "Ethereum" }, tokenIn: "USDC", tokenOut: "USDT", amountIn: "100.50", config: { slippageBps: 300, apiKey: "TEST_API_KEY:id:secret", }, }); console.log("Stop limit:", estimate.stopLimit.amount, estimate.stopLimit.token); console.log( "Estimated output:", estimate.estimatedOutput.amount, estimate.estimatedOutput.token, ); console.log("Fees:", estimate.fees); ``` *** ### getSupportedChains(operationType) Get chains supported by AppKit operations. Returns blockchain networks that support specific stablecoin operations. When no operation type is specified, returns all chains supporting any operation (bridge, swap, earn, or unified balance). ```typescript theme={null} getSupportedChains(operationType?: OperationType): ChainDefinition[] ``` **Parameters** | Name | Type | Description | | ------------- | --------------------------------- | ------------------------------------------------------------------------------------------- | | operationType | [`OperationType`](#operationtype) | Optional operation type to filter chains (`bridge` \| `swap` \| `earn` \| `unifiedBalance`) | #### OperationType Union type of all supported operation types in the AppKit. This type ensures type safety when specifying operation types and enables proper parameter validation based on the selected operation. ```typescript theme={null} type OperationType = FeeOperationType | "earn" | "unifiedBalance"; ``` **Returns** `ChainDefinition[]` #### FeeOperationType Operation types that support the `getFee`/`getFeeRecipient` hooks. ```typescript theme={null} type FeeOperationType = "bridge" | "swap"; ``` #### GetSupportedChainsOptions Options for filtering supported chains. ```typescript theme={null} type GetSupportedChainsOptions = | { chainType: CCTPV2SupportedChainType | CCTPV2SupportedChainType[]; isTestnet?: boolean; forwarderSupported?: boolean; } | { chainType?: CCTPV2SupportedChainType | CCTPV2SupportedChainType[]; isTestnet: boolean; forwarderSupported?: boolean; } | { chainType?: CCTPV2SupportedChainType | CCTPV2SupportedChainType[]; isTestnet?: boolean; forwarderSupported: boolean; }; ``` **Properties** | Name | Type | Description | | ------------------ | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | forwarderSupported | `'source' \| 'destination'` | Filter chains by forwarder support. When set, only chains whose `gateway.forwarderSupported.source` or `gateway.forwarderSupported.destination` matches the specified value are returned.

- `undefined` (default) — no forwarder filtering; all supported chains are returned.
- `'source'` — only chains that support forwarding as a source.
- `'destination'` — only chains that support forwarding as a destination. | **Usage Examples** ```typescript theme={null} import { AppKit } from "@circle-fin/app-kit"; const kit = new AppKit(); const allChains = kit.getSupportedChains(); console.log(`Total supported chains: ${allChains.length}`); allChains.forEach((chain) => { console.log(`- ${chain.name} (${chain.type})`); }); ``` ```typescript theme={null} const kit = new AppKit(); const bridgeChains = kit.getSupportedChains("bridge"); console.log( "Chains supporting bridge:", bridgeChains.map((c) => c.name), ); ``` *** ### getSwapStatus(params) Fetch the current status of a swap from the Stablecoin Service. Delegates to SwapKit.getSwapStatus. Performs a single HTTP request and returns the service's snapshot of the swap's state. For crosschain swaps the status can remain `'PENDING'` for several minutes while attestation and destination-chain mint complete; callers are responsible for polling — re-calling this method with a delay — until `progress.status` is terminal (`'DONE'`, `'FAILED'`, or `'NOT_FOUND'`). Use AppKit.waitForSwap if you'd rather not write the polling loop yourself. ```typescript theme={null} getSwapStatus(params: GetSwapStatusParams): Promise ``` **Parameters** | Name | Type | Description | | ------ | --------------------------------------------- | -------------------------------------------------------------- | | params | [`GetSwapStatusParams`](#getswapstatusparams) | `txHash` and `chainIn`, plus optional `chainOut` and `apiKey`. | #### GetSwapStatusParams Parameters for SwapKit.getSwapStatus. ```typescript theme={null} interface GetSwapStatusParams { txHash: string; chainIn: SwapChainIdentifier | Blockchain; chainOut?: SwapChainIdentifier | Blockchain; apiKey?: string; kitKey?: string; } ``` **Properties** | Name | Type | Description | | -------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | apiKey | string | Circle API key used as a bearer credential for the status request. Treat this value as a secret and do not log it.

Optional — when omitted, the request is made without an `Authorization` header (permissionless mode). | | chainIn | `SwapChainIdentifier \| Blockchain` | Chain the swap was initiated on.

Accepts a Blockchain enum value, a string literal of the enum, or a full ChainDefinition (for convenience when piping through resolved parameters). | | chainOut | `SwapChainIdentifier \| Blockchain` | Destination chain for crosschain swaps.

Must be supplied when the source and destination chains differ. Omit (or match `chainIn`) for same-chain swaps. | | kitKey | string | **Deprecated.** Use GetSwapStatusParams.`apiKey` instead. Still honored when `apiKey` is omitted, and `apiKey` takes precedence when both are set. Circle API key used as a bearer credential for the status request. | | txHash | string | The source-chain transaction hash returned by SwapKit.swap. | **Returns** [`Promise`](#swapstatusresult) #### SwapStatusResult Result of a swap status lookup — a single snapshot of the swap's state at the time of the call. ```typescript theme={null} interface SwapStatusResult { readonly progress: SwapProgress; readonly source?: SwapSourceLeg; readonly destination?: SwapDestinationLeg; } ``` **Properties** | Name | Type | Description | | ----------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | destination | SwapDestinationLeg | Destination-leg transaction, token, and amount metadata. Omitted until the service reports destination-chain data (typically once the swap reaches `'DONE'`). The received amount lives under `destination.amount`. | | progress | SwapProgress | Lifecycle snapshot: `status`, `substatus`, and `substatusMessage`. | | source | SwapSourceLeg | Source-leg transaction and token metadata. `source.txHash` is populated from the service's `sendingTxHash`; `source.token` is shape-reserved pending service support. | **Usage Examples** ````typescript theme={null} Single snapshot: ```typescript const result = await kit.swap(swapParams) const status = await kit.getSwapStatus({ txHash: result.txHash, chainIn: result.chainIn, chainOut: result.chainOut, apiKey: process.env.CIRCLE_API_KEY, }) console.log(status.progress.status, status.progress.substatus) ```` ````typescript theme={null} Poll until terminal (or just call `kit.waitForSwap` instead): ```typescript let status = await kit.getSwapStatus({ txHash: result.txHash, chainIn: result.chainIn, chainOut: result.chainOut, apiKey: process.env.CIRCLE_API_KEY, }) while (status.progress.status === 'PENDING') { await new Promise((r) => setTimeout(r, 3_000)) status = await kit.getSwapStatus({ txHash: result.txHash, chainIn: result.chainIn, chainOut: result.chainOut, apiKey: process.env.CIRCLE_API_KEY, }) } ```` *** ### getTokenRates(params) Fetch cached USD rates for one or more tokens from the Stablecoin Service. Two lookup modes are supported: * Per-chain dump: omit `tokens` to retrieve every rate cached for `chain`. * Targeted lookup: supply `tokens` (up to 100) to retrieve a specific set of rates on `chain`. Each entry may be a registered token symbol (e.g. `'USDC'`, `'EURC'`), the literal `'NATIVE'`, the chain's native gas symbol (e.g. `'ETH'` on Ethereum), or a raw EVM address / Solana mint. Unknown strings are rejected with a `KitError`. The rates pipeline is broader than the swap pipeline — `chain` accepts any Blockchain value or ChainDefinition, not just the swap-supported subset. Chains the cron does not track return an empty `rates` map. Response keys preserve the service's canonical casing: EVM hex addresses are lowercased, Solana base58 mints are case-preserved. Lowercase EVM addresses before indexing into `result.rates[chain]`. Native gas rates: `'NATIVE'` (or a chain's native currency symbol) translates to the chain's native sentinel address — `0xEee…` for EVM, `1111…` for Solana — before querying the service. ```typescript theme={null} getTokenRates(params: GetTokenRatesParams): Promise ``` **Parameters** | Name | Type | Description | | ------ | --------------------------------------------- | --------------------------------------------- | | params | [`GetTokenRatesParams`](#gettokenratesparams) | `chain`, plus optional `tokens` and `apiKey`. | #### GetTokenRatesParams Parameters for SwapKit.getTokenRates. ```typescript theme={null} interface GetTokenRatesParams { chain: ChainIdentifier; tokens?: readonly TokenSymbol[]; apiKey?: string; kitKey?: string; } ``` **Properties** | Name | Type | Description | | ------ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | apiKey | string | Circle API key used as a bearer credential for the rates request. Treat this value as a secret and do not log it.

Optional — when omitted, the request is made without an `Authorization` header (permissionless mode). | | chain | ChainIdentifier | Chain to look up rates for. Accepts a Blockchain enum value, a string literal of the enum, or a full ChainDefinition. | | kitKey | string | **Deprecated.** Use GetTokenRatesParams.`apiKey` instead. Still honored when `apiKey` is omitted, and `apiKey` takes precedence when both are set. Circle API key used as a bearer credential for the rates request. | | tokens | readonly TokenSymbol\[] | Optional list of tokens (max 100) to look up on `chain`. Each entry may be:

- A registered TokenSymbol (e.g. `'USDC'`, `'EURC'`). The kit resolves it to the chain's address via the built-in `TokenRegistry`. Symbol matching is case-insensitive.
- The literal `'NATIVE'`, or the chain's native gas symbol (e.g. `'ETH'` on Ethereum, `'POL'` on Polygon). Both translate to the chain's native sentinel address — `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE` for EVM, `11111111111111111111111111111111` for Solana — before querying the service.
- A raw on-chain address (EVM `0x` hex) or mint (Solana base58), passed through verbatim (EVM lowercased to match service response casing).

Omit the field to get every cached rate for the chain.

Unknown strings that are neither a registered symbol nor a well-formed address are rejected with a `KitError` to surface typos at the call site instead of returning a silently empty rate map. | **Returns** `Promise` #### ChainIdentifier Chain identifier that can be used in transfer parameters and factory functions. This can be either: * A ChainDefinition object * A Blockchain enum value (e.g., Blockchain.Ethereum) * A string literal of the blockchain value (e.g., "Ethereum") ```typescript theme={null} type ChainIdentifier = ChainDefinition | Blockchain | unknown; ``` **Usage Example** ```typescript theme={null} const { rates } = await kit.getTokenRates({ chain: "Ethereum", tokens: ["USDC", "EURC"], apiKey: process.env.CIRCLE_API_KEY, }); const usdc = rates["Ethereum"]?.["0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"]; console.log(`USDC: $${usdc?.priceUSD ?? "unknown"}`); ``` *** ### off(action) Unregister an event handler for a specific AppKit action. This method removes a previously registered event handler. You must pass the exact same handler function reference that was used during registration. Use the wildcard `*` to remove handlers listening to all actions. ```typescript theme={null} off(action: K, handler: (payload: AppKitActions[K]) => void): void off(action: '*', handler: (payload: AppKitActions[keyof AppKitActions]) => void): void ``` **Parameters** | Name | Type | Description | | ------- | ---------------------------- | ----------------------------------------------------------- | | action | `K` | The namespaced action name or `*` for all actions | | handler | `(payload: unknown) => void` | The handler function to remove (must be the same reference) | **Usage Example** ```typescript theme={null} import { AppKit } from "@circle-fin/app-kit"; const kit = new AppKit(); // Define handler const handler = (payload) => { console.log("Approval:", payload); }; // Register kit.on("bridge.approve", handler); // Later, unregister kit.off("bridge.approve", handler); ``` *** ### on(action) Register an event handler for a specific AppKit action. Subscribe to step events from bridge, earn, or unified balance operations. Action names are namespaced: `bridge.`, `earn.`, and `unifiedBalance.`. Use `''` to receive every action. Handlers receive strongly-typed payloads for the chosen action. Multiple handlers may be registered for the same action. ```typescript theme={null} on(action: K, handler: (payload: AppKitActions[K]) => void): void on(action: '*', handler: (payload: AppKitActions[keyof AppKitActions]) => void): void ``` **Parameters** | Name | Type | Description | | ------- | ---------------------------- | ------------------------------------------------- | | action | `K` | The namespaced action name or `*` for all actions | | handler | `(payload: unknown) => void` | Callback invoked when the action occurs | **Usage Example** ```typescript theme={null} import { AppKit } from "@circle-fin/app-kit"; const kit = new AppKit(); // Listen to specific bridge action kit.on("bridge.approve", (payload) => { console.log("Approval transaction:", payload.values.txHash); }); // Listen to earn deposit steps kit.on("earn.deposit", (payload) => { console.log("Earn deposit step:", payload.values.state); }); // Listen to unified balance action kit.on("unifiedBalance.gateway.spend.succeeded", (payload) => { console.log("Spend succeeded:", payload.data); }); // Listen to all actions kit.on("*", (payload) => { console.log("Action:", payload); }); ``` *** ### removeCustomFeePolicy(operation) Remove an AppKit-level custom fee policy for one operation. Bridge and swap policies are removed from AppKit's persistent context so future operations fall back to legacy fee hooks. Unified balance policies are also removed from the namespaced Unified Balance Kit. ```typescript theme={null} removeCustomFeePolicy(operation: AppKitCustomFeePolicyScope): void ``` **Parameters** | Name | Type | Description | | --------- | --------- | ---------------------------------------------------- | | operation | `unknown` | Operation whose custom fee policy should be removed. | **Usage Example** ```typescript theme={null} kit.removeCustomFeePolicy("bridge"); ``` *** ### retryBridge(result) Retry a failed crosschain USDC bridge transfer. Resume a bridge operation that failed due to a transient error. Use isRetryableError to check whether a failed step's error is eligible for retry before calling this method. ```typescript theme={null} retryBridge(result: BridgeResult, retryContext: RetryContext): Promise ``` **Parameters** | Name | Type | Description | | ------------ | --------------------------------- | --------------------------------------------------------------- | | result | [`BridgeResult`](#bridgeresult-2) | The bridge result from the failed operation | | retryContext | [`RetryContext`](#retrycontext) | The retry context with source and optional destination adapters | #### BridgeResult Result object returned after a successful crosschain bridge operation. This interface contains all the details about a completed bridge, including the bridge parameters, source and destination information, and the sequence of steps that were executed. ```typescript theme={null} interface BridgeResult { amount: string config?: BridgeConfig destination: { address: string; chain: ChainDefinition; recipientAddress?: string; useForwarder?: boolean } provider: string source: { address: string; chain: ChainDefinition } state: 'pending' \| 'success' \| 'error' steps: BridgeStep[] token: 'USDC' } ``` **Properties** | Name | Type | Description | | ----------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------- | | amount | string | The amount that was transferred (as a string to avoid precision issues) | | config | BridgeConfig | The bridge configuration that was used for this operation | | destination | `{ address: string; chain: ChainDefinition; recipientAddress?: string; useForwarder?: boolean }` | Information about the destination chain and address | | provider | string | The provider that was used for this operation | | source | `{ address: string; chain: ChainDefinition }` | Information about the source chain and address | | state | `'pending' \| 'success' \| 'error'` | The state of the transfer | | steps | BridgeStep\[] | Array of steps that were executed during the bridge process | | token | `'USDC'` | The token that was transferred (currently only USDC is supported) | #### RetryContext Context for retry operations containing source and destination adapter contexts. This interface provides the necessary context for retry operations, including both the source adapter context (where the retry originates) and the destination adapter context (where the retry is targeted). This ensures that retry operations have access to both the source and destination chain information needed for validation and execution. The destination adapter (`to`) is optional to support forwarder-only destinations where Circle's Orbit relayer handles the mint transaction without requiring a destination adapter. When `to` is undefined, the retry operation relies on IRIS API confirmation instead of on-chain transaction confirmation. ```typescript theme={null} interface RetryContext { from: Adapter; to?: Adapter; } ``` **Properties** | Name | Type | Description | | ---- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | from | `Adapter` | The source adapter context for the retry operation | | to | `Adapter` | The destination adapter context for the retry operation.

Optional for forwarder-only destinations where Circle's Orbit relayer handles the mint transaction. When undefined, the retry operation relies on IRIS API confirmation (`forwardState === 'CONFIRMED'`) instead of on-chain transaction confirmation via the adapter. | **Returns** [`Promise`](#bridgeresult-3) #### BridgeResult Result object returned after a successful crosschain bridge operation. This interface contains all the details about a completed bridge, including the bridge parameters, source and destination information, and the sequence of steps that were executed. ```typescript theme={null} interface BridgeResult { amount: string config?: BridgeConfig destination: { address: string; chain: ChainDefinition; recipientAddress?: string; useForwarder?: boolean } provider: string source: { address: string; chain: ChainDefinition } state: 'pending' \| 'success' \| 'error' steps: BridgeStep[] token: 'USDC' } ``` **Properties** | Name | Type | Description | | ----------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------- | | amount | string | The amount that was transferred (as a string to avoid precision issues) | | config | BridgeConfig | The bridge configuration that was used for this operation | | destination | `{ address: string; chain: ChainDefinition; recipientAddress?: string; useForwarder?: boolean }` | Information about the destination chain and address | | provider | string | The provider that was used for this operation | | source | `{ address: string; chain: ChainDefinition }` | Information about the source chain and address | | state | `'pending' \| 'success' \| 'error'` | The state of the transfer | | steps | BridgeStep\[] | Array of steps that were executed during the bridge process | | token | `'USDC'` | The token that was transferred (currently only USDC is supported) | **Usage Example** ```typescript theme={null} import { AppKit, isRetryableError } from "@circle-fin/app-kit"; const kit = new AppKit(); const result = await kit.bridge({ from: { adapter: sourceAdapter, chain: "Ethereum" }, to: { adapter: destAdapter, chain: "Polygon" }, amount: "100.50", }); const failedStep = result.steps.find((s) => s.error); if ( result.state === "error" && failedStep?.error && isRetryableError(failedStep.error) ) { const retried = await kit.retryBridge(result, { from: sourceAdapter, to: destAdapter, }); console.log("Retry result:", retried.state); } ``` *** ### send(params) Execute a send operation for known token aliases (USDC, USDT, NATIVE) or custom ERC-20/SPL tokens. For custom tokens, the token address must be provided. This method handles the complete send transfer flow using the underlying AppKit infrastructure. It supports sending to either a destination adapter or an explicit recipient address, with full type safety and comprehensive error handling. ```typescript theme={null} send(params: SendParams): Promise ``` **Parameters** | Name | Type | Description | | ------ | ----------------------------- | ----------------------------------------------------------------- | | params | [`SendParams`](#sendparams-2) | Send parameters containing source, destination, amount, and token | #### SendParams Parameters for sending USDC, USDT, native tokens, or custom ERC-20/SPL tokens. This interface is the canonical input for send operations in App Kit. It supports sending to either a destination Adapter (recipient derives from the adapter's default account) or an explicit recipient `string` address. * The `from` field provides the source signing context and chain. * The `to` field identifies the destination as an adapter or an explicit address. * The `amount` field is a human-readable decimal string (for example, `'10.5'`). * The `token` field selects the asset to move and defaults to `'USDC'`. ```typescript theme={null} interface SendParams { amount: string from: AdapterContext to: string \| Adapter token?: TokenAlias \| TokenAddress } ``` **Properties** | Name | Type | Description | | ------ | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | amount | string | The amount to transfer. | | from | AdapterContext | The source adapter context (wallet and chain) for the transfer. | | to | `string \| Adapter` | The destination for the transfer, supporting explicit or derived recipient addresses. | | token | `TokenAlias \| TokenAddress` | The token to transfer. Defaults to `USDC`. If omitted, the provider will use `USDC` by default.

Supports both known aliases and custom token contract addresses:
- Known aliases: `USDC`, `USDT`, `NATIVE`, `EURC` (`EURC` requires the chain to have an `eurcAddress` configured)
- Custom token addresses: EVM addresses or Solana SPL token mint addresses | **Returns** [`Promise`](#bridgestep) #### BridgeStep A step in the bridge process. ```typescript theme={null} interface BridgeStep { batched?: boolean batchId?: string data?: unknown error?: unknown errorCategory?: BridgeStepErrorCategory errorMessage?: string explorerUrl?: string forwarded?: boolean name: string state: 'pending' \| 'success' \| 'error' \| 'noop' txHash?: string } ``` **Properties** | Name | Type | Description | | ------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | batched | boolean | Whether this step was executed as part of an EIP-5792 batched `wallet_sendCalls` request.

- `true`: The step was included in a batched call bundle
- `undefined`: The step was executed individually (sequential flow) | | batchId | string | The wallet-assigned batch identifier from `wallet_sendCalls`.

Present only when batched is `true`. Can be used with `wallet_getCallsStatus` to query the status of the entire bundle. | | data | unknown | Optional data for the step | | error | unknown | Optional raw error object (can be Viem/Ethers/Chain error) | | errorCategory | BridgeStepErrorCategory | Optional machine-readable classification of the error.

Present when the step is in `state: 'error'` and the SDK was able to categorize the failure. See BridgeStepErrorCategory for the list of categories and how they map to underlying error shapes. | | errorMessage | string | Optional human-readable error message | | explorerUrl | string | Optional explorer URL for viewing this transaction on a block explorer | | forwarded | boolean | Whether this step was executed via Circle's Forwarder (relay service). Only applicable for mint steps.

- `true`: The mint was handled by Circle's Orbit relayer
- `false`: The user submitted the mint transaction directly
- `undefined`: Not applicable (non-mint steps) | | name | string | Human-readable name of the step (e.g., "Approve", "Burn", "Mint") | | state | `'pending' \| 'success' \| 'error' \| 'noop'` | The state of the step | | txHash | string | Optional transaction hash for this step (if applicable) | **Usage Examples** ```typescript theme={null} // Send USDC to a recipient adapter (same chain) const result = await kit.send({ from: { adapter: sourceAdapter, chain: "Ethereum" }, to: recipientAdapter, amount: "100.50", token: "USDC", }); console.log("Send completed:", result.txHash); ``` ```typescript theme={null} // Send a custom token to an explicit address const result = await kit.send({ from: { adapter: sourceAdapter, chain: "Ethereum" }, to: "0x742d35Cc4634C0532925a3b8D1d7", amount: "100.50", token: "0x6B175474E89094C44Da98b954EedeAC495271d0F", // DAI on Ethereum }); console.log("Send completed:", result.txHash); ``` ```typescript theme={null} // Send USDT to an explicit address const result = await kit.send({ from: { adapter: sourceAdapter, chain: "Ethereum" }, to: "0x742d35Cc4634C0532925a3b8D1d7", amount: "50.25", token: "USDT", }); console.log("Send completed:", result.txHash); ``` *** ### setCustomFeePolicy(policy) Set operation-scoped custom fee policies. Configure custom fees for only the operations that need them. Bridge and swap policies are forwarded to the underlying kits when those operations run. Unified balance policies are applied immediately to the namespaced Unified Balance Kit. ```typescript theme={null} setCustomFeePolicy(policy: AppKitCustomFeePolicy): void ``` **Parameters** | Name | Type | Description | | ------ | ------------------------------------------------- | ----------------------------------------------- | | policy | [`AppKitCustomFeePolicy`](#appkitcustomfeepolicy) | Partial custom fee policy grouped by operation. | #### AppKitCustomFeePolicy Operation-scoped custom fee policies configured at the AppKit level. Each property is optional so consumers can enable custom fees only for the operation they use. AppKit forwards the supplied policy to the matching underlying kit when that operation runs. ```typescript theme={null} interface AppKitCustomFeePolicy { bridge?: CustomFeePolicy; swap?: CustomFeePolicy; unifiedBalance?: CustomFeePolicy; } ``` **Properties** | Name | Type | Description | | -------------- | --------------- | ------------------------------------------------------------------ | | bridge | CustomFeePolicy | Custom fee policy forwarded to BridgeKit bridge operations. | | swap | CustomFeePolicy | Custom fee policy forwarded to SwapKit swap operations. | | unifiedBalance | CustomFeePolicy | Custom fee policy forwarded to UnifiedBalanceKit spend operations. | **Usage Example** ```typescript theme={null} kit.setCustomFeePolicy({ bridge: { computeFee: () => "1.00", resolveFeeRecipientAddress: () => "0x1234567890123456789012345678901234567890", }, }); ``` *** ### swap(params) Execute a same-chain token swap operation. Swaps between USDC, USDT, and native tokens on the same blockchain with configurable slippage tolerance and allowance strategies. ```typescript theme={null} swap(params: SwapParams): Promise ``` **Parameters** | Name | Type | Description | | ------ | ----------------------------- | ------------------------------------------------------------- | | params | [`SwapParams`](#swapparams-2) | Swap parameters containing source, tokens, amount, and config | #### SwapParams ```typescript theme={null} interface SwapParams< TFromAdapterCapabilities extends AdapterCapabilities = AdapterCapabilities, > { from: SwapAdapterContext; tokenIn: SupportedSwapToken; tokenOut: SupportedSwapToken; amountIn: string; to?: SwapDestination; config?: SwapConfig; } ``` **Properties** | Name | Type | Description | | -------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | amountIn | string | The amount of the input token to swap.

Expressed as a human-readable decimal string in token units (e.g., `'0.05'` for 0.05 USDC or 0.05 ETH). | | config | SwapConfig | Optional configuration for swap behavior.

If omitted, defaults will be used:
- `allowanceStrategy`: `permit` (fallback to `approve`)
- `slippageBps`: 300 (3%) | | from | `SwapAdapterContext` | The source adapter context (wallet and chain) for the swap. | | to | SwapDestination | Optional destination chain/address.

For same-chain swaps this may be omitted and the source wallet address is used as the recipient. crosschain swaps require `recipientAddress`. | | tokenIn | SupportedSwapToken | The input token to swap from.

Supports stablecoins (USDC, USDT, EURC, USDe, DAI, PYUSD), wrapped tokens (WBTC, WETH, WSOL, WAVAX, WPOL), and native tokens (NATIVE or chain-specific symbols). | | tokenOut | SupportedSwapToken | The output token to swap to.

Supports stablecoins (USDC, USDT, EURC, USDe, DAI, PYUSD), wrapped tokens (WBTC, WETH, WSOL, WAVAX, WPOL), and native tokens (NATIVE or chain-specific symbols). | **Returns** [`Promise`](#swapresult) #### SwapResult Result of an executed swap operation. Captures the source-chain execution outcome for a swap transaction. ```typescript theme={null} interface SwapResult { readonly tokenIn: SupportedSwapToken; readonly tokenOut: SupportedSwapToken; readonly chainIn: Blockchain; readonly chainOut: Blockchain; readonly chain: Blockchain; readonly amountIn: string; readonly fromAddress: string; readonly toAddress: string; readonly config?: SwapResultConfig; readonly txHash: string; readonly explorerUrl?: string; fees?: readonly ServiceSwapFee[]; readonly progress: SwapProgress; readonly amountOut?: string; } ``` **Properties** | Name | Type | Description | | ----------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | amountIn | string | The input amount that was swapped, as a human-readable decimal string in token units (e.g. `'0.05'` for 0.05 USDC or 0.05 ETH). | | amountOut | string | Output amount when `.swap()` can confirm the result inline.

Expressed as a human-readable decimal string in output-token units (run through the output-token transform), never base units.

Populated for same-chain swaps when the best-effort status peek reaches `DONE`. Undefined for crosschain swaps; call SwapKit.getSwapStatus to retrieve destination-leg details. | | chain | Blockchain | **Deprecated.** Use SwapResult.chainIn instead. Still populated with the source chain for a deprecation window; will be removed in a future major release. | | chainIn | Blockchain | The source chain of the swap.

Returns the chain name (e.g. `Blockchain.Ethereum`). Use `getChainByEnum(chainIn)` to resolve back to a full `ChainDefinition`. | | chainOut | Blockchain | Destination chain of the swap.

Equal to `chainIn` for same-chain swaps; different for crosschain swaps. Always populated so consumers don't have to fall back to `chainIn` when the destination is implicit. | | config | SwapResultConfig | The swap configuration that was used for this operation. | | explorerUrl | string | The formatted explorer URL for the source-chain transaction. Only present when `txHash` is non-empty. | | fees | readonly ServiceSwapFee\[] | Detailed fee breakdown for the swap operation. Includes both provider fees (charged by the DEX aggregator/protocol) and kit fees. | | fromAddress | string | The address that initiated the swap. | | progress | SwapProgress | Lifecycle snapshot: `status`, `substatus`, and `substatusMessage`.

`status` is `'DONE'` when the swap completed end-to-end (same-chain only), `'PENDING'` while the destination leg is still in-flight, or a terminal failure value (`'FAILED'` / `'NOT_FOUND'`). | | toAddress | string | The address that received the swapped tokens. | | tokenIn | SupportedSwapToken | The input token that was swapped from. | | tokenOut | SupportedSwapToken | The output token that was swapped to. | | txHash | string | The source-chain transaction hash for the executed swap. | **Usage Example** ```typescript theme={null} const result = await kit.swap({ from: { adapter, chain: "Ethereum" }, tokenIn: "USDC", tokenOut: "USDT", amountIn: "100.50", config: { slippageBps: 300, // 3% slippage allowanceStrategy: "permit", apiKey: "TEST_API_KEY:id:secret", }, }); console.log("Swap completed:", result.txHash); ``` *** ### waitForSwap(params) Poll the Stablecoin Service until a swap reaches a terminal status (`'DONE'`, `'FAILED'`, `'NOT_FOUND'`) or `timeoutMs` elapses. Delegates to SwapKit.waitForSwap. Use this after `kit.swap()` to collapse the `while (status === 'PENDING')` polling loop into a single awaitable. Same-chain swaps return on the first poll because they are already terminal at `swap` time; crosschain swaps follow an escalating backoff (3s → 6s → 12s → 24s → 24s) until the wait budget expires. Timeouts surface as a RETRYABLE `KitError` so callers can re-invoke with the same `txHash`. ```typescript theme={null} waitForSwap(params: WaitForSwapParams): Promise ``` **Parameters** | Name | Type | Description | | ------ | ----------------------------------------- | ---------------------------------------------------------------------------------- | | params | [`WaitForSwapParams`](#waitforswapparams) | Wait configuration: identifiers, API key, and optional `timeoutMs` / `onProgress`. | #### WaitForSwapParams Parameters for SwapKit.waitForSwap. ```typescript theme={null} type WaitForSwapParams = WaitForSwapResultParams | WaitForSwapDiscreteParams; ``` **Returns** [`Promise`](#swapstatusresult-2) #### SwapStatusResult Result of a swap status lookup — a single snapshot of the swap's state at the time of the call. ```typescript theme={null} interface SwapStatusResult { readonly progress: SwapProgress; readonly source?: SwapSourceLeg; readonly destination?: SwapDestinationLeg; } ``` **Properties** | Name | Type | Description | | ----------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | destination | SwapDestinationLeg | Destination-leg transaction, token, and amount metadata. Omitted until the service reports destination-chain data (typically once the swap reaches `'DONE'`). The received amount lives under `destination.amount`. | | progress | SwapProgress | Lifecycle snapshot: `status`, `substatus`, and `substatusMessage`. | | source | SwapSourceLeg | Source-leg transaction and token metadata. `source.txHash` is populated from the service's `sendingTxHash`; `source.token` is shape-reserved pending service support. | #### WaitForSwapResultParams `waitForSwap` parameter shape that pipes a SwapResult straight through — the most ergonomic form when you've just called SwapKit.swap or SwapKit.executeSwap. ```typescript theme={null} interface WaitForSwapResultParams extends WaitForSwapCommonParams { readonly result: SwapResult; } ``` **Properties** | Name | Type | Description | | ---------- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | apiKey | string | Circle API key used as a bearer credential. Treat as a secret and do not log it.

Optional — when omitted, the request is made without an `Authorization` header (permissionless mode). | | kitKey | string | **Deprecated.** Use `apiKey` instead. Still honored when `apiKey` is omitted, and `apiKey` takes precedence when both are set. Circle API key used as a bearer credential. | | onProgress | (status: SwapStatusResult) => void | Fired on every poll that produces a status snapshot. Useful for surfacing `substatus` transitions to a UI (e.g. spinner copy, log lines). Not called on retried 429/5xx responses. | | result | SwapResult | The SwapResult returned by SwapKit.swap or SwapKit.executeSwap. `txHash`, `chainIn`, and `chainOut` are read directly from this object. | | timeoutMs | number | Overall wait budget, in milliseconds. The promise rejects with a RETRYABLE KitError when this elapses without a terminal status. crosschain swaps typically settle within 30s–3min; pad the budget if production traffic shows occasional outliers.

Default: `300_000` (5 minutes). | #### WaitForSwapDiscreteParams `waitForSwap` parameter shape for callers that don't have a SwapResult on hand — e.g. picking up an in-flight swap from a persisted record or a copy-pasted tx hash. ```typescript theme={null} interface WaitForSwapDiscreteParams extends WaitForSwapCommonParams { readonly txHash: string; readonly chainIn: SwapChainIdentifier | Blockchain; readonly chainOut?: SwapChainIdentifier | Blockchain; } ``` **Properties** | Name | Type | Description | | ---------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | apiKey | string | Circle API key used as a bearer credential. Treat as a secret and do not log it.

Optional — when omitted, the request is made without an `Authorization` header (permissionless mode). | | chainIn | `SwapChainIdentifier \| Blockchain` | Chain the swap was initiated on. Accepts a Blockchain enum value, a string literal of the enum, or a full ChainDefinition. | | chainOut | `SwapChainIdentifier \| Blockchain` | Destination chain for crosschain swaps. Omit (or match `chainIn`) for same-chain swaps. | | kitKey | string | **Deprecated.** Use `apiKey` instead. Still honored when `apiKey` is omitted, and `apiKey` takes precedence when both are set. Circle API key used as a bearer credential. | | onProgress | (status: SwapStatusResult) => void | Fired on every poll that produces a status snapshot. Useful for surfacing `substatus` transitions to a UI (e.g. spinner copy, log lines). Not called on retried 429/5xx responses. | | timeoutMs | number | Overall wait budget, in milliseconds. The promise rejects with a RETRYABLE KitError when this elapses without a terminal status. crosschain swaps typically settle within 30s–3min; pad the budget if production traffic shows occasional outliers.

Default: `300_000` (5 minutes). | | txHash | string | The source-chain transaction hash returned by SwapKit.swap or SwapKit.executeSwap. | **Usage Examples** ````typescript theme={null} Pipe a `SwapResult` straight in — the common case: ```typescript const result = await kit.swap(swapParams) const final = await kit.waitForSwap({ result, apiKey: process.env.CIRCLE_API_KEY, onProgress: (snap) => console.log(snap.progress.status), }) if (final.progress.status === 'DONE') { console.log(`Received ${final.destination?.amount}`) } ```` ````typescript theme={null} Resume from a persisted tx hash (no `SwapResult` on hand): ```typescript const final = await kit.waitForSwap({ txHash: persisted.txHash, chainIn: persisted.chainIn, chainOut: persisted.chainOut, apiKey: process.env.CIRCLE_API_KEY, }) ```` *** ## kit.unifiedBalance Methods `unifiedBalance` is a property on every `AppKit` instance. Call these methods as `kit.unifiedBalance.methodName()`. ### addDelegate(params) Grant spending rights to another address on the owner's account. ```typescript theme={null} addDelegate(params: UpdateDelegateParams): Promise ``` **Parameters** | Name | Type | Description | | ------ | ----------------------------------------------- | ----------------------------------------------------- | | params | [`UpdateDelegateParams`](#updatedelegateparams) | The owner's adapter context and the delegate address. | #### UpdateDelegateParams Parameters for adding or removing a delegate on a Gateway account. ```typescript theme={null} interface UpdateDelegateParams { delegateAddress: string; from: AdapterContext; token?: SupportedTokenInput; } ``` **Properties** | Name | Type | Description | | --------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------- | | delegateAddress | string | The address being added or removed as an authorized delegate. | | from | `AdapterContext` | The owner's adapter context identifying the account and chain to which the delegate will be authorized. | | token | SupportedTokenInput | The token for which delegation applies. Accepts any case (e.g. `'usdc'`, `'Usdc'`); normalised to uppercase internally. | **Returns** [`Promise`](#updatedelegateresult) #### UpdateDelegateResult Result returned after a successful add or remove delegate operation. ```typescript theme={null} interface UpdateDelegateResult { account: string chain: Blockchain delegateAddress: string explorerUrl?: string state: 'added' \| 'removed' txHash: string } ``` **Properties** | Name | Type | Description | | --------------- | ---------------------- | ---------------------------------------------------------------------------- | | account | string | The Gateway account that was modified. | | chain | Blockchain | The chain on which the delegate was updated. | | delegateAddress | string | The delegate address that was added or removed. | | explorerUrl | string | Link to view the transaction details on the appropriate blockchain explorer. | | state | `'added' \| 'removed'` | Whether the delegate was added or removed. | | txHash | string | Unique identifier returned by the blockchain once the transaction is mined. | **Usage Example** ```typescript theme={null} const result = await kit.unifiedBalance.addDelegate({ from: { adapter, chain: "Ethereum" }, delegateAddress: "0xDelegate…", }); ``` *** ### deposit(params) Deposit USDC into the caller's account on a specific chain. ```typescript theme={null} deposit(params: DepositParams): Promise ``` **Parameters** | Name | Type | Description | | ------ | --------------------------------- | ----------------------------------------------------------- | | params | [`DepositParams`](#depositparams) | Deposit details including the depositor context and amount. | #### DepositParams Parameters for depositing tokens into the caller's own Gateway account on a specific chain. ```typescript theme={null} interface DepositParams< TAdapterCapabilities extends AdapterCapabilities = AdapterCapabilities, TChainIdentifier extends UnifiedBalanceChainIdentifier = UnifiedBalanceChainIdentifier, > { from: AdapterContext; amount: string; token?: SupportedTokenInput; allowanceStrategy?: AllowanceStrategy; } ``` **Properties** | Name | Type | Description | | ----------------- | ------------------- | ----------------------------------------------------------------------------------------------------- | | allowanceStrategy | AllowanceStrategy | The token allowance strategy to authorize the deposit. | | amount | string | The amount of tokens to deposit (human-readable decimal string). | | from | `AdapterContext` | The adapter context identifying the depositor and chain. | | token | SupportedTokenInput | The token to deposit. Accepts any case (e.g. `'usdc'`, `'Usdc'`); normalised to uppercase internally. | **Returns** [`Promise`](#depositresult) #### DepositResult Result returned after a successful deposit operation. ```typescript theme={null} interface DepositResult { amount: string; token: SupportedToken; depositedTo: string; depositedBy: string; chain: Blockchain; txHash: string; explorerUrl?: string; } ``` **Properties** | Name | Type | Description | | ----------- | -------------- | ---------------------------------------------------------------------------- | | amount | string | The deposited amount (human-readable decimal string). | | chain | Blockchain | The chain on which the deposit occurred. | | depositedBy | string | The address that signed and funded the deposit. | | depositedTo | string | The Gateway account address credited by the deposit. | | explorerUrl | string | Link to view the transaction details on the appropriate blockchain explorer. | | token | SupportedToken | The token that was deposited. | | txHash | string | Unique identifier returned by the blockchain once the transaction is mined. | **Usage Example** ```typescript theme={null} const result = await kit.unifiedBalance.deposit({ from: { adapter, chain: "Ethereum" }, amount: "100", token: "USDC", }); ``` *** ### depositFor(params) Deposit USDC into another account (not the caller's). ```typescript theme={null} depositFor(params: DepositForParams): Promise ``` **Parameters** | Name | Type | Description | | ------ | --------------------------------------- | ------------------------------------------------------------------------------------------- | | params | [`DepositForParams`](#depositforparams) | Deposit details including the depositor context, amount, and the account address to credit. | #### DepositForParams Parameters for depositing tokens into another Gateway account. ```typescript theme={null} interface DepositForParams< TAdapterCapabilities extends AdapterCapabilities = AdapterCapabilities, TChainIdentifier extends UnifiedBalanceChainIdentifier = UnifiedBalanceChainIdentifier, > extends Omit< DepositParams, "allowanceStrategy" > { depositAccount: string; } ``` **Properties** | Name | Type | Description | | -------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | amount | string | The amount of tokens to deposit (human-readable decimal string). | | depositAccount | string | The Gateway account address to credit with the deposit.

When provided the deposit is credited to this account rather than the caller's own. | | from | `AdapterContext` | The adapter context identifying the depositor and chain. | | token | SupportedTokenInput | The token to deposit. Accepts any case (e.g. `'usdc'`, `'Usdc'`); normalised to uppercase internally. | **Returns** [`Promise`](#depositresult-2) #### DepositResult Result returned after a successful deposit operation. ```typescript theme={null} interface DepositResult { amount: string; token: SupportedToken; depositedTo: string; depositedBy: string; chain: Blockchain; txHash: string; explorerUrl?: string; } ``` **Properties** | Name | Type | Description | | ----------- | -------------- | ---------------------------------------------------------------------------- | | amount | string | The deposited amount (human-readable decimal string). | | chain | Blockchain | The chain on which the deposit occurred. | | depositedBy | string | The address that signed and funded the deposit. | | depositedTo | string | The Gateway account address credited by the deposit. | | explorerUrl | string | Link to view the transaction details on the appropriate blockchain explorer. | | token | SupportedToken | The token that was deposited. | | txHash | string | Unique identifier returned by the blockchain once the transaction is mined. | **Usage Example** ```typescript theme={null} const result = await kit.unifiedBalance.depositFor({ from: { adapter, chain: "Ethereum" }, amount: "50", depositAccount: "0x742d35Cc6634c0532925A3B844D97D35e2B60E53", }); ``` *** ### estimateSpend(params) Estimate the fees for a spend operation without executing it. ```typescript theme={null} estimateSpend(params: SpendParams): Promise ``` **Parameters** | Name | Type | Description | | ------ | ----------------------------- | ------------------------------------------------------------- | | params | [`SpendParams`](#spendparams) | The same spend parameters used in AppKitUnifiedBalance.spend. | #### SpendParams Parameters for spending (minting) USDC on a destination chain from one or more Gateway account sources. ```typescript theme={null} type SpendParams< TFromAdapterCapabilities extends AdapterCapabilities = AdapterCapabilities, TToAdapterCapabilities extends AdapterCapabilities = AdapterCapabilities, TChainIdentifier extends UnifiedBalanceChainIdentifier = UnifiedBalanceChainIdentifier, > = | SpendParamsWithFrom< TFromAdapterCapabilities, TToAdapterCapabilities, TChainIdentifier > | SpendParamsRetry< TFromAdapterCapabilities, TToAdapterCapabilities, TChainIdentifier >; ``` **Returns** [`Promise`](#estimatespendresult) #### EstimateSpendResult Cost estimation for a spend (mint) operation. ```typescript theme={null} interface EstimateSpendResult { fees: FeeEntry[]; } ``` **Properties** | Name | Type | Description | | ---- | ----------- | ----------------------------------------------- | | fees | FeeEntry\[] | Itemised fee breakdown for the spend operation. | **Usage Example** ```typescript theme={null} const estimate = await kit.unifiedBalance.estimateSpend({ from: { adapter, allocations: [{ amount: "100", chain: "Ethereum" }] }, to: { adapter, chain: "Base" }, token: "USDC", }); ``` *** ### getBalances(params) Fetch aggregated and per-chain balances for one or more accounts. ```typescript theme={null} getBalances(params: GetBalancesParams): Promise ``` **Parameters** | Name | Type | Description | | ------ | ----------------------------------------- | ------------------------------------------------------ | | params | [`GetBalancesParams`](#getbalancesparams) | Balance query parameters (adapter or account address). | #### GetBalancesParams Parameters for the balances and pending-deposits API requests. Specify the `token` to query and one or more `sources` identifying the accounts or adapters whose balances should be retrieved. When `includePending` is true, the result includes pending balances and pending transaction details per chain. ```typescript theme={null} interface GetBalancesParams< TAdapterCapabilities extends AdapterCapabilities = AdapterCapabilities, > { token?: SupportedTokenInput; sources: Sources; includePending?: boolean; networkType?: NetworkType; } ``` **Properties** | Name | Type | Description | | -------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------- | | includePending | boolean | When true, the result includes pending balances. When false or omitted, only confirmed balances are returned. | | networkType | NetworkType | Network to use when no chains are specified on a source. When omitted, mainnet is used when chains cannot be derived. | | sources | `Sources` | One or more sources identifying the accounts or adapters to query. Accepts a single object or an array. | | token | SupportedTokenInput | The token to query balances for. Accepts any case (e.g. `'usdc'`, `'Usdc'`); normalised to uppercase internally. | **Returns** [`Promise`](#getbalancesresult) #### GetBalancesResult Result returned from the provider's `getBalances` method (combined confirmed and pending). When `includePending` is false (default), only `totalConfirmedBalance` and `breakdown` with confirmed fields are returned. When `includePending` is true, `totalPendingBalance` is present and breakdown entries include pending amounts and `pendingTransactions` per chain. ```typescript theme={null} interface GetBalancesResult { token: SupportedToken; totalConfirmedBalance: string; totalPendingBalance?: string; breakdown: BalanceWithPendingBreakdown[]; } ``` **Properties** | Name | Type | Description | | --------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------- | | breakdown | BalanceWithPendingBreakdown\[] | Per-account, per-chain breakdown. | | token | SupportedToken | Token that was queried. | | totalConfirmedBalance | string | Total confirmed balance across all accounts and chains (human-readable decimal string). | | totalPendingBalance | string | Total pending balance across all accounts and chains. Present only when `includePending` is true. | #### NetworkType Network type for balance queries when the target chain(s) are not explicitly specified. Default is mainnet. ```typescript theme={null} type NetworkType = "mainnet" | "testnet"; ``` #### BalanceWithPendingBreakdown Per-account balance breakdown used in GetBalancesResult. When `includePending` is true, `totalPending` is present and each chain entry may include `pendingBalance` and `pendingTransactions`. ```typescript theme={null} interface BalanceWithPendingBreakdown { depositor: string; totalConfirmed: string; totalPending?: string; breakdown: ChainBalanceBreakdown[]; } ``` **Properties** | Name | Type | Description | | -------------- | ------------------------ | -------------------------------------------------------------------------------- | | breakdown | ChainBalanceBreakdown\[] | Per-chain breakdown for this depositor. | | depositor | string | Gateway account (depositor) address. | | totalConfirmed | string | Total confirmed balance across chains (human-readable decimal string). | | totalPending | string | Total pending balance across chains. Present only when `includePending` is true. | #### ChainBalanceBreakdown Per-chain balance within a breakdown in GetBalancesResult. When `includePending` is true, `pendingBalance` and `pendingTransactions` are present. ```typescript theme={null} interface ChainBalanceBreakdown { chain: Blockchain; confirmedBalance: string; pendingBalance?: string; pendingTransactions?: PendingBalanceTransaction[]; } ``` **Properties** | Name | Type | Description | | ------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------- | | chain | Blockchain | The chain. | | confirmedBalance | string | Confirmed balance (human-readable decimal string). | | pendingBalance | string | Pending balance on this chain. Present only when GetBalancesParams.includePending is true. | | pendingTransactions | PendingBalanceTransaction\[] | Pending deposit transactions on this chain. Present only when GetBalancesParams.includePending is true. | #### PendingBalanceTransaction A pending transaction included in GetBalancesResult when `includePending` is true. ```typescript theme={null} interface PendingBalanceTransaction { transactionHash: string; amount: string; blockTimestamp: string; } ``` **Usage Example** ```typescript theme={null} const balances = await kit.unifiedBalance.getBalances({ token: "USDC", sources: { address: "0x1234…abcd" }, }); ``` *** ### getDelegateStatus(params) Check the finality-aware delegate status of an address. ```typescript theme={null} getDelegateStatus(params: GetDelegateStatusParams): Promise ``` **Parameters** | Name | Type | Description | | ------ | ----------------------------------------------------- | -------------------------------------------------- | | params | [`GetDelegateStatusParams`](#getdelegatestatusparams) | The adapter context and delegate address to check. | #### GetDelegateStatusParams Parameters for checking the delegate status of an address on a Gateway account. ```typescript theme={null} interface GetDelegateStatusParams { delegateAddress: string; from: AdapterContext; token?: SupportedTokenInput; } ``` **Properties** | Name | Type | Description | | --------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------- | | delegateAddress | string | The address to check for delegate status. | | from | `AdapterContext` | The adapter context identifying the account owner and chain. | | token | SupportedTokenInput | The token for which delegation is checked. Accepts any case (e.g. `'usdc'`, `'Usdc'`); normalised to uppercase internally. | **Returns** [`Promise`](#delegatestatus) #### DelegateStatus The finality-aware status of a delegate on a Gateway account. * `'none'` — not a delegate on-chain. * `'pending'` — delegate set on-chain but Gateway hasn't finalized it yet; spend will fail until the status advances to `'ready'`. * `'ready'` — finalized at Gateway; spend will succeed. ```typescript theme={null} type DelegateStatus = "none" | "pending" | "ready"; ``` **Usage Example** ```typescript theme={null} const status = await kit.unifiedBalance.getDelegateStatus({ from: { adapter, chain: 'Ethereum' }, delegateAddress: '0xDelegate…', }) if (status === 'ready') { // safe to spend } ``` *** ### getSupportedChains(token) Get all chains supported by the unified balance operations. ```typescript theme={null} getSupportedChains(token?: SupportedToken, options?: GetSupportedChainsOptions): ChainDefinition[] ``` **Parameters** | Name | Type | Description | | ------- | --------------------------------------------------------- | -------------------------------------------- | | token | `'USDC'` | Optional token filter (defaults to USDC). | | options | [`GetSupportedChainsOptions`](#getsupportedchainsoptions) | Optional filtering (e.g. forwarder support). | #### GetSupportedChainsOptions Options for filtering supported chains. ```typescript theme={null} type GetSupportedChainsOptions = | { chainType: CCTPV2SupportedChainType | CCTPV2SupportedChainType[]; isTestnet?: boolean; forwarderSupported?: boolean; } | { chainType?: CCTPV2SupportedChainType | CCTPV2SupportedChainType[]; isTestnet: boolean; forwarderSupported?: boolean; } | { chainType?: CCTPV2SupportedChainType | CCTPV2SupportedChainType[]; isTestnet?: boolean; forwarderSupported: boolean; }; ``` **Properties** | Name | Type | Description | | ------------------ | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | forwarderSupported | `'source' \| 'destination'` | Filter chains by forwarder support. When set, only chains whose `gateway.forwarderSupported.source` or `gateway.forwarderSupported.destination` matches the specified value are returned.

- `undefined` (default) — no forwarder filtering; all supported chains are returned.
- `'source'` — only chains that support forwarding as a source.
- `'destination'` — only chains that support forwarding as a destination. | **Returns** `ChainDefinition[]` **Usage Example** ```typescript theme={null} const chains = kit.unifiedBalance.getSupportedChains(); const usdcChains = kit.unifiedBalance.getSupportedChains("USDC"); ``` *** ### initiateRemoveFund(params) Initiate a trustless recovery removal from an account. Use `spend` for normal movement out of a Unified Balance. `removeFund` is a recovery path for situations where the normal spend flow is unavailable. Calling this method starts the 7-day withdrawal delay before the removal can be completed. ```typescript theme={null} initiateRemoveFund(params: InitiateRemoveFundParams): Promise ``` **Parameters** | Name | Type | Description | | ------ | ------------------------------------------------------- | ------------------------------------------------------- | | params | [`InitiateRemoveFundParams`](#initiateremovefundparams) | The account owner's adapter context, amount, and token. | #### InitiateRemoveFundParams Parameters for initiating a delayed recovery fund removal from a Gateway account. ```typescript theme={null} interface InitiateRemoveFundParams< TAdapterCapabilities extends AdapterCapabilities = AdapterCapabilities, TChainIdentifier extends UnifiedBalanceChainIdentifier = UnifiedBalanceChainIdentifier, > { from: AdapterContext; amount: string; token?: SupportedTokenInput; } ``` **Properties** | Name | Type | Description | | ------ | ------------------- | ---------------------------------------------------------------------------------------------------- | | amount | string | The amount to remove (human-readable decimal string). | | from | `AdapterContext` | The account owner's adapter context identifying the account, chain, and address. | | token | SupportedTokenInput | The token to remove. Accepts any case (e.g. `'usdc'`, `'Usdc'`); normalised to uppercase internally. | **Returns** [`Promise`](#initiateremovefundresult) #### InitiateRemoveFundResult Result returned after successfully initiating a fund removal. ```typescript theme={null} interface InitiateRemoveFundResult { amount: string; token: SupportedToken; account: string; chain: Blockchain; withdrawingBalance: string; withdrawalBlock: number; txHash: string; explorerUrl?: string; } ``` **Properties** | Name | Type | Description | | ------------------ | -------------- | ---------------------------------------------------------------------------- | | account | string | The Gateway account from which this removal will occur. | | amount | string | The amount requested for removal. | | chain | Blockchain | The chain on which this removal was initiated. | | explorerUrl | string | Link to view the transaction details on the appropriate blockchain explorer. | | token | SupportedToken | The token type (always USDC). | | txHash | string | Unique identifier returned by the blockchain once the transaction is mined. | | withdrawalBlock | number | The block number at which the removal can be completed. | | withdrawingBalance | string | The balance currently in the withdrawing state for this account. | **Usage Example** ```typescript theme={null} const result = await kit.unifiedBalance.initiateRemoveFund({ from: { adapter, chain: "Ethereum" }, amount: "50", token: "USDC", }); ``` *** ### off(action) Unregister an event handler for a specific gateway lifecycle action. Removes a previously registered event handler. You must pass the exact same handler function reference that was used during registration. ```typescript theme={null} off(action: string, handler: (payload: unknown) => void): void ``` **Parameters** | Name | Type | Description | | ------- | ---------------------------- | ----------------------------------------- | | action | `string` | The action name or `'*'` for all actions. | | handler | `(payload: unknown) => void` | The exact handler reference to remove. | **Usage Example** ```typescript theme={null} const kit = new AppKit(); const handler = (payload: unknown) => console.log(payload); kit.unifiedBalance.on("gateway.deposit.succeeded", handler); kit.unifiedBalance.off("gateway.deposit.succeeded", handler); ``` *** ### on(action) Register an event handler for a specific gateway lifecycle action. Subscribe to events emitted during gateway operations such as deposit, spend, getBalances, etc. Handlers receive strongly-typed payloads based on the action name. Multiple handlers can be registered for the same action, and all will be invoked when the action occurs. Use the wildcard `'*'` to listen to all actions. Note: TypeScript autocomplete may only show `'*'` due to internal type erasure. The following action names are available at runtime and can be imported as `GatewayActionName` from `@circle-fin/provider-gateway-v1`: | Action | Stages | | ---------------------------- | ------------------------------------------------------------------ | | `gateway.deposit` | `.started` `.succeeded` `.failed` | | `gateway.depositFor` | `.started` `.succeeded` `.failed` | | `gateway.spend` | `.started` `.succeeded` `.failed` | | `gateway.spend.step` | `.buildBurnIntents` `.signBurnIntents` `.fetchAttestation` `.mint` | | `gateway.estimateSpend` | `.started` `.succeeded` `.failed` | | `gateway.getBalances` | `.started` `.succeeded` `.failed` | | `gateway.addDelegate` | `.started` `.succeeded` `.failed` | | `gateway.removeDelegate` | `.started` `.succeeded` `.failed` | | `gateway.initiateRemoveFund` | `.started` `.succeeded` `.failed` | | `gateway.removeFund` | `.started` `.succeeded` `.failed` | ```typescript theme={null} on(action: string, handler: (payload: unknown) => void): void ``` **Parameters** | Name | Type | Description | | ------- | ---------------------------- | ------------------------------------------ | | action | `string` | The action name or `'*'` for all actions. | | handler | `(payload: unknown) => void` | Callback to invoke when the action occurs. | **Usage Example** ```typescript theme={null} const kit = new AppKit(); kit.unifiedBalance.on("gateway.spend.started", (payload) => { console.log("Spend started:", payload); }); kit.unifiedBalance.on("*", (payload) => { console.log("Event:", payload); }); ``` *** ### removeCustomFeePolicy() Remove the custom fee policy for the kit. ```typescript theme={null} removeCustomFeePolicy(): void ``` **Usage Example** ```typescript theme={null} kit.unifiedBalance.removeCustomFeePolicy(); ``` *** ### removeDelegate(params) Revoke spending rights from a delegate on the owner's account. ```typescript theme={null} removeDelegate(params: UpdateDelegateParams): Promise ``` **Parameters** | Name | Type | Description | | ------ | ------------------------------------------------- | ----------------------------------------------------- | | params | [`UpdateDelegateParams`](#updatedelegateparams-2) | The owner's adapter context and the delegate address. | #### UpdateDelegateParams Parameters for adding or removing a delegate on a Gateway account. ```typescript theme={null} interface UpdateDelegateParams { delegateAddress: string; from: AdapterContext; token?: SupportedTokenInput; } ``` **Properties** | Name | Type | Description | | --------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------- | | delegateAddress | string | The address being added or removed as an authorized delegate. | | from | `AdapterContext` | The owner's adapter context identifying the account and chain to which the delegate will be authorized. | | token | SupportedTokenInput | The token for which delegation applies. Accepts any case (e.g. `'usdc'`, `'Usdc'`); normalised to uppercase internally. | **Returns** [`Promise`](#updatedelegateresult-2) #### UpdateDelegateResult Result returned after a successful add or remove delegate operation. ```typescript theme={null} interface UpdateDelegateResult { account: string chain: Blockchain delegateAddress: string explorerUrl?: string state: 'added' \| 'removed' txHash: string } ``` **Properties** | Name | Type | Description | | --------------- | ---------------------- | ---------------------------------------------------------------------------- | | account | string | The Gateway account that was modified. | | chain | Blockchain | The chain on which the delegate was updated. | | delegateAddress | string | The delegate address that was added or removed. | | explorerUrl | string | Link to view the transaction details on the appropriate blockchain explorer. | | state | `'added' \| 'removed'` | Whether the delegate was added or removed. | | txHash | string | Unique identifier returned by the blockchain once the transaction is mined. | **Usage Example** ```typescript theme={null} const result = await kit.unifiedBalance.removeDelegate({ from: { adapter, chain: "Ethereum" }, delegateAddress: "0xDelegate…", }); ``` *** ### removeFeeRecipients() Remove the declarative fee recipient map for the kit. ```typescript theme={null} removeFeeRecipients(): void ``` **Usage Example** ```typescript theme={null} kit.unifiedBalance.removeFeeRecipients(); ``` *** ### removeFund(params) Complete a trustless recovery removal after the withdrawal delay. Use `spend` for normal movement out of a Unified Balance. `removeFund` is a recovery path for situations where the normal spend flow is unavailable. Both EVM and Solana removals require a 7-day withdrawal delay after `initiateRemoveFund` before funds can be removed. ```typescript theme={null} removeFund(params: RemoveFundParams): Promise ``` **Parameters** | Name | Type | Description | | ------ | --------------------------------------- | ----------------------------------------------------------- | | params | [`RemoveFundParams`](#removefundparams) | The account owner context matching the original initiation. | #### RemoveFundParams Parameters for completing a recovery fund removal after the withdrawal delay. ```typescript theme={null} interface RemoveFundParams< TAdapterCapabilities extends AdapterCapabilities = AdapterCapabilities, TChainIdentifier extends UnifiedBalanceChainIdentifier = UnifiedBalanceChainIdentifier, > { from: AdapterContext; token?: SupportedTokenInput; } ``` **Properties** | Name | Type | Description | | ----- | ------------------- | ---------------------------------------------------------------------------------------------------- | | from | `AdapterContext` | The account owner's adapter context. Must match the one used when initiating the fund removal. | | token | SupportedTokenInput | The token to remove. Accepts any case (e.g. `'usdc'`, `'Usdc'`); normalised to uppercase internally. | **Returns** [`Promise`](#removefundresult) #### RemoveFundResult Result returned after successfully completing a fund removal. ```typescript theme={null} interface RemoveFundResult { amount: string; token: SupportedToken; account: string; chain: Blockchain; txHash: string; explorerUrl?: string; } ``` **Properties** | Name | Type | Description | | ----------- | -------------- | ---------------------------------------------------------------------------- | | account | string | The Gateway account from which the tokens were removed. | | amount | string | The final removed amount. | | chain | Blockchain | The chain on which the removal occurred. | | explorerUrl | string | Link to view the transaction details on the appropriate blockchain explorer. | | token | SupportedToken | The token type (always USDC). | | txHash | string | Unique identifier returned by the blockchain once the transaction is mined. | **Usage Example** ```typescript theme={null} const result = await kit.unifiedBalance.removeFund({ from: { adapter, chain: "Ethereum" }, token: "USDC", }); ``` *** ### setCustomFeePolicy(policy) Set a custom fee policy for spend operations. Once set, every subsequent `spend()` and `estimateSpend()` call will include the computed fee unless overridden by per-call `config.customFee`. ```typescript theme={null} setCustomFeePolicy(policy: CustomFeePolicy): void ``` **Parameters** | Name | Type | Description | | ------ | ------------------------------------- | ---------------------------------------------------- | | policy | [`CustomFeePolicy`](#customfeepolicy) | The fee computation and recipient resolution policy. | #### CustomFeePolicy ```typescript theme={null} interface CustomFeePolicy { computeFee: SpendFeeFunction; resolveFeeRecipientAddress?: SpendFeeRecipientFunction; } ``` **Usage Example** ```typescript theme={null} kit.unifiedBalance.setCustomFeePolicy({ computeFee: () => "0.10", resolveFeeRecipientAddress: () => "0xFeeRecipient…", }); ``` *** ### setFeeRecipients(config) Set a declarative fee recipient map, keyed by chain type. Once set, `spend()`/`estimateSpend()` resolve the fee recipient by looking up the spend's destination chain type in this map — taking priority over `customFeePolicy`'s `resolveFeeRecipientAddress` callback. ```typescript theme={null} setFeeRecipients(config: FeeRecipientsConfig): void ``` **Parameters** | Name | Type | Description | | ------ | --------------------------------------------- | ---------------------------------------------------------------------------------------- | | config | [`FeeRecipientsConfig`](#feerecipientsconfig) | Fee recipient addresses keyed by chain type (e.g. `{ evm: '0x...', solana: 'Sol...' }`). | #### FeeRecipientsConfig ```typescript theme={null} type FeeRecipientsConfig = Partial>; ``` **Usage Example** ```typescript theme={null} kit.unifiedBalance.setFeeRecipients({ evm: "0x1234567890123456789012345678901234567890", solana: "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM", }); ``` *** ### spend(params) Spend (mint) USDC on a destination chain by pulling funds from one or more account sources. ```typescript theme={null} spend(params: SpendParams): Promise ``` **Parameters** | Name | Type | Description | | ------ | ------------------------------- | ---------------------------------------------------------- | | params | [`SpendParams`](#spendparams-2) | Spend details including source(s), destination, and token. | #### SpendParams Parameters for spending (minting) USDC on a destination chain from one or more Gateway account sources. ```typescript theme={null} type SpendParams< TFromAdapterCapabilities extends AdapterCapabilities = AdapterCapabilities, TToAdapterCapabilities extends AdapterCapabilities = AdapterCapabilities, TChainIdentifier extends UnifiedBalanceChainIdentifier = UnifiedBalanceChainIdentifier, > = | SpendParamsWithFrom< TFromAdapterCapabilities, TToAdapterCapabilities, TChainIdentifier > | SpendParamsRetry< TFromAdapterCapabilities, TToAdapterCapabilities, TChainIdentifier >; ``` **Returns** [`Promise`](#spendresult) #### SpendResult Result returned after a successful spend (mint) operation. ```typescript theme={null} interface SpendResult { allocations?: AllocationResult[]; recipientAddress: string; destinationChain: Blockchain; txHash: string; explorerUrl?: string; fees?: FeeEntry[]; transferId?: string; expirationBlock?: string; steps?: SpendStep[]; } ``` **Properties** | Name | Type | Description | | ---------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | allocations | AllocationResult\[] | Flattened list of allocations that were executed, each with its source Gateway account. Only present for non-retry spends. | | destinationChain | Blockchain | The destination chain on which the recipient received the USDC. | | expirationBlock | string | Block height after which the transfer attestation expires. Returned by the Gateway transfer API. | | explorerUrl | string | Link to view the transaction details on the appropriate blockchain explorer. | | fees | FeeEntry\[] | Fee breakdown (provider, gasFee, optional kit, forwarder) for the executed spend. Same shape as estimateSpend fees. Omitted when using config.retry. | | recipientAddress | string | The address that received the minted USDC. | | steps | SpendStep\[] | Ordered list of steps executed during the spend operation. | | transferId | string | Gateway transfer identifier. Present when `useForwarder` is enabled and can be used to query transfer status via `GET /v1/transfer/{id}`. | | txHash | string | Unique identifier returned by the blockchain once the transaction is mined. | #### AllocationResult Extends Allocation with the resolved source Gateway account address. ```typescript theme={null} interface AllocationResult extends Allocation { sourceAccount: string; } ``` **Properties** | Name | Type | Description | | ------------- | ----------------------------- | ------------------------------------------------------------------- | | amount | string | The amount to pull from this chain (human-readable decimal string). | | chain | UnifiedBalanceChainIdentifier | The chain from which to pull the funds. | | sourceAccount | string | The Gateway account address from which this amount was pulled. | #### UnifiedBalanceChainIdentifier Type representing valid unified-balance chain identifiers. Constrains chain parameters to only accept chains that support Gateway V1 operations. Accepts: * An UnifiedBalanceChain enum value (e.g., `UnifiedBalanceChain.Ethereum`) * A string literal matching an UnifiedBalanceChain value (e.g., `'Ethereum'`) * A ChainDefinition object for a supported chain ```typescript theme={null} type UnifiedBalanceChainIdentifier = ChainDefinition | UnifiedBalanceChain | unknown; ``` #### UnifiedBalanceChain Enumeration of blockchains that support Gateway V1 operations (deposit, spend, balance, delegate, removeFund). Derived from the full Blockchain enum but filtered to only include chains with active Gateway V1 contract support. When new chains gain Gateway V1 support, they are added to this enum. ```typescript theme={null} enum UnifiedBalanceChain { Arbitrum = 'Arbitrum' Arbitrum_Sepolia = 'Arbitrum_Sepolia' Arc_Testnet = 'Arc_Testnet' Avalanche = 'Avalanche' Avalanche_Fuji = 'Avalanche_Fuji' Base = 'Base' Base_Sepolia = 'Base_Sepolia' Ethereum = 'Ethereum' Ethereum_Sepolia = 'Ethereum_Sepolia' HyperEVM = 'HyperEVM' HyperEVM_Testnet = 'HyperEVM_Testnet' Optimism = 'Optimism' Optimism_Sepolia = 'Optimism_Sepolia' Polygon = 'Polygon' Polygon_Amoy_Testnet = 'Polygon_Amoy_Testnet' Sei = 'Sei' Sei_Testnet = 'Sei_Testnet' Solana = 'Solana' Solana_Devnet = 'Solana_Devnet' Sonic = 'Sonic' Sonic_Testnet = 'Sonic_Testnet' Unichain = 'Unichain' Unichain_Sepolia = 'Unichain_Sepolia' World_Chain = 'World_Chain' World_Chain_Sepolia = 'World_Chain_Sepolia' } ``` **Values** `Arbitrum`, `Arbitrum_Sepolia`, `Arc_Testnet`, `Avalanche`, `Avalanche_Fuji`, `Base`, `Base_Sepolia`, `Ethereum`, `Ethereum_Sepolia`, `HyperEVM`, `HyperEVM_Testnet`, `Optimism`, `Optimism_Sepolia`, `Polygon`, `Polygon_Amoy_Testnet`, `Sei`, `Sei_Testnet`, `Solana`, `Solana_Devnet`, `Sonic`, `Sonic_Testnet`, `Unichain`, `Unichain_Sepolia`, `World_Chain`, `World_Chain_Sepolia` #### SpendStep Data payload for a single step in a spend operation. ```typescript theme={null} interface SpendStep { data?: unknown error?: unknown errorMessage?: string explorerUrl?: string name: string state: 'pending' \| 'success' \| 'error' txHash?: string } ``` **Properties** | Name | Type | Description | | ------------ | ----------------------------------- | ----------------------------------------------------------------------- | | data | unknown | Optional data for the step. | | error | unknown | Optional raw error object. | | errorMessage | string | Optional human-readable error message. | | explorerUrl | string | Optional explorer URL for viewing this transaction on a block explorer. | | name | string | Human-readable name of the step (e.g., "buildBurnIntents", "mint"). | | state | `'pending' \| 'success' \| 'error'` | The state of the step. | | txHash | string | Optional transaction hash for this step (if applicable). | **Usage Example** ```typescript theme={null} const result = await kit.unifiedBalance.spend({ from: { adapter, allocations: [{ amount: "100", chain: "Ethereum" }] }, to: { adapter, chain: "Base" }, token: "USDC", }); ``` *** ## Supporting Types ### Common #### AdapterContext Represents the context of an adapter used for crosschain operations. An AdapterContext must always specify both the adapter and the chain explicitly. The address field behavior is determined by the adapter's address control model: * Developer-controlled adapters: The `address` field is required because each operation must explicitly specify which address to use. * User-controlled adapters: The `address` field is forbidden because the address is automatically resolved from the connected wallet or signer. * Legacy adapters: The `address` field remains optional for backward compatibility. This ensures clear, debuggable code where the intended chain is always visible at the call site, and address requirements are enforced at compile time based on adapter capabilities. ```typescript theme={null} type AdapterContext = { adapter: Adapter; chain: TChainIdentifier; } & AddressField>; ``` **Properties** | Name | Type | Description | | ------- | ------------------------------- | --------------------------------------------------------------------------------------- | | adapter | `Adapter` | The adapter instance for blockchain operations | | chain | TChainIdentifier | The chain reference, which can be a ChainDefinition, Blockchain enum, or string literal | *** #### DeveloperFeeHooks ```typescript theme={null} interface DeveloperFeeHooks { getFee: (params: BridgeParams) => bigint \| Promise getFeeRecipient: (chain: ChainDefinition) => string \| Promise } ``` **Properties** | Name | Type | Description | | --------------- | ------------------------------------------------------- | --------------------------------------------------------------------- | | getFee | `(params: BridgeParams) => bigint \| Promise` | Returns the developer fee in USDC's smallest units (e.g. 6 decimals). | | getFeeRecipient | `(chain: ChainDefinition) => string \| Promise` | Returns the fee recipient for the given chain. | *** ### Chains #### BaseChainDefinition Base information that all chain definitions must include. ```typescript theme={null} interface BaseChainDefinition { cctp: CCTPConfig \| null chain: Blockchain eurcAddress: string \| null explorerUrl: string gateway?: GatewayConfig isTestnet: boolean kitContracts?: Partial> name: string nativeCurrency: Currency rpcEndpoints: readonly string[] title?: string usdcAddress: string \| null usdtAddress: string \| null } ``` **Properties** | Name | Type | Description | | -------------- | ------------------------------------------ | -------------------------------------------------------------------------- | | cctp | `CCTPConfig \| null` | Optional CCTP configuration. | | chain | Blockchain | The blockchain identifier from the Blockchain enum. | | eurcAddress | `string \| null` | The contract address for EURC. | | explorerUrl | string | Template URL for the blockchain explorer to view transactions. | | gateway | GatewayConfig | Optional Gateway contract configuration for Gateway protocol support. | | isTestnet | boolean | Indicates whether this is a testnet or mainnet. | | kitContracts | `Partial>` | Optional kit-specific contract addresses for enhanced chain functionality. | | name | string | The display name of the blockchain. | | nativeCurrency | Currency | Information about the native currency of the blockchain. | | rpcEndpoints | readonly string\[] | Default RPC endpoints for connecting to the blockchain network. | | title | string | Optional title or alternative name for the blockchain. | | usdcAddress | `string \| null` | The contract address for USDC. | | usdtAddress | `string \| null` | The contract address for USDT. | *** #### Blockchain Enumeration of all blockchains known to this library. This enum contains every blockchain that has a chain definition, regardless of whether bridging is currently supported. For chains that support bridging via CCTPv2, see BridgeChain. ```typescript theme={null} enum Blockchain { Algorand = 'Algorand' Algorand_Testnet = 'Algorand_Testnet' Aptos = 'Aptos' Aptos_Testnet = 'Aptos_Testnet' Arbitrum = 'Arbitrum' Arbitrum_Sepolia = 'Arbitrum_Sepolia' Arc_Testnet = 'Arc_Testnet' Avalanche = 'Avalanche' Avalanche_Fuji = 'Avalanche_Fuji' Base = 'Base' Base_Sepolia = 'Base_Sepolia' Celo = 'Celo' Celo_Alfajores_Testnet = 'Celo_Alfajores_Testnet' Codex = 'Codex' Codex_Testnet = 'Codex_Testnet' Cronos = 'Cronos' Cronos_Testnet = 'Cronos_Testnet' Edge = 'Edge' Edge_Testnet = 'Edge_Testnet' Ethereum = 'Ethereum' Ethereum_Sepolia = 'Ethereum_Sepolia' Hedera = 'Hedera' Hedera_Testnet = 'Hedera_Testnet' HyperEVM = 'HyperEVM' HyperEVM_Testnet = 'HyperEVM_Testnet' Injective = 'Injective' Injective_Testnet = 'Injective_Testnet' Ink = 'Ink' Ink_Testnet = 'Ink_Testnet' Linea = 'Linea' Linea_Sepolia = 'Linea_Sepolia' Monad = 'Monad' Monad_Testnet = 'Monad_Testnet' Morph = 'Morph' Morph_Testnet = 'Morph_Testnet' NEAR = 'NEAR' NEAR_Testnet = 'NEAR_Testnet' Noble = 'Noble' Noble_Testnet = 'Noble_Testnet' Optimism = 'Optimism' Optimism_Sepolia = 'Optimism_Sepolia' Pharos = 'Pharos' Pharos_Testnet = 'Pharos_Testnet' Plume = 'Plume' Plume_Testnet = 'Plume_Testnet' Polkadot_Asset_Hub = 'Polkadot_Asset_Hub' Polkadot_Westmint = 'Polkadot_Westmint' Polygon = 'Polygon' Polygon_Amoy_Testnet = 'Polygon_Amoy_Testnet' Robinhood = 'Robinhood' Robinhood_Testnet = 'Robinhood_Testnet' Sei = 'Sei' Sei_Testnet = 'Sei_Testnet' Solana = 'Solana' Solana_Devnet = 'Solana_Devnet' Sonic = 'Sonic' Sonic_Testnet = 'Sonic_Testnet' Stellar = 'Stellar' Stellar_Testnet = 'Stellar_Testnet' Sui = 'Sui' Sui_Testnet = 'Sui_Testnet' Unichain = 'Unichain' Unichain_Sepolia = 'Unichain_Sepolia' World_Chain = 'World_Chain' World_Chain_Sepolia = 'World_Chain_Sepolia' X_Layer = 'X_Layer' X_Layer_Testnet = 'X_Layer_Testnet' XDC = 'XDC' XDC_Apothem = 'XDC_Apothem' ZKSync_Era = 'ZKSync_Era' ZKSync_Sepolia = 'ZKSync_Sepolia' } ``` **Values** `Algorand`, `Algorand_Testnet`, `Aptos`, `Aptos_Testnet`, `Arbitrum`, `Arbitrum_Sepolia`, `Arc_Testnet`, `Avalanche`, `Avalanche_Fuji`, `Base`, `Base_Sepolia`, `Celo`, `Celo_Alfajores_Testnet`, `Codex`, `Codex_Testnet`, `Cronos`, `Cronos_Testnet`, `Edge`, `Edge_Testnet`, `Ethereum`, `Ethereum_Sepolia`, `Hedera`, `Hedera_Testnet`, `HyperEVM`, `HyperEVM_Testnet`, `Injective`, `Injective_Testnet`, `Ink`, `Ink_Testnet`, `Linea`, `Linea_Sepolia`, `Monad`, `Monad_Testnet`, `Morph`, `Morph_Testnet`, `NEAR`, `NEAR_Testnet`, `Noble`, `Noble_Testnet`, `Optimism`, `Optimism_Sepolia`, `Pharos`, `Pharos_Testnet`, `Plume`, `Plume_Testnet`, `Polkadot_Asset_Hub`, `Polkadot_Westmint`, `Polygon`, `Polygon_Amoy_Testnet`, `Robinhood`, `Robinhood_Testnet`, `Sei`, `Sei_Testnet`, `Solana`, `Solana_Devnet`, `Sonic`, `Sonic_Testnet`, `Stellar`, `Stellar_Testnet`, `Sui`, `Sui_Testnet`, `Unichain`, `Unichain_Sepolia`, `World_Chain`, `World_Chain_Sepolia`, `X_Layer`, `X_Layer_Testnet`, `XDC`, `XDC_Apothem`, `ZKSync_Era`, `ZKSync_Sepolia` *** #### ChainDefinition Public chain definition type. ```typescript theme={null} type ChainDefinition = EVMChainDefinition | NonEVMChainDefinition; ``` *** #### Currency Represents basic information about a currency or token. ```typescript theme={null} interface Currency { decimals: number; name: string; symbol: string; } ``` *** #### EVMChainDefinition Represents chain definitions for Ethereum Virtual Machine (EVM) compatible blockchains. ```typescript theme={null} interface EVMChainDefinition { cctp: CCTPConfig \| null chain: Blockchain chainId: number eurcAddress: string \| null explorerUrl: string gateway?: GatewayConfig isTestnet: boolean kitContracts?: Partial> name: string nativeCurrency: Currency rpcEndpoints: readonly string[] title?: string type: 'evm' usdcAddress: string \| null usdtAddress: string \| null } ``` **Properties** | Name | Type | Description | | -------------- | ------------------------------------------ | -------------------------------------------------------------------------- | | cctp | `CCTPConfig \| null` | Optional CCTP configuration. | | chain | Blockchain | The blockchain identifier from the Blockchain enum. | | chainId | number | The unique identifier for the blockchain. | | eurcAddress | `string \| null` | The contract address for EURC. | | explorerUrl | string | Template URL for the blockchain explorer to view transactions. | | gateway | GatewayConfig | Optional Gateway contract configuration for Gateway protocol support. | | isTestnet | boolean | Indicates whether this is a testnet or mainnet. | | kitContracts | `Partial>` | Optional kit-specific contract addresses for enhanced chain functionality. | | name | string | The display name of the blockchain. | | nativeCurrency | Currency | Information about the native currency of the blockchain. | | rpcEndpoints | readonly string\[] | Default RPC endpoints for connecting to the blockchain network. | | title | string | Optional title or alternative name for the blockchain. | | type | `'evm'` | Discriminator for EVM chains. | | usdcAddress | `string \| null` | The contract address for USDC. | | usdtAddress | `string \| null` | The contract address for USDT. | *** #### KitContractType Available kit contract types for enhanced chain functionality. ```typescript theme={null} type KitContractType = "bridge" | "adapter"; ``` *** #### NonEVMChainDefinition Represents chain definitions for non-EVM blockchains. ```typescript theme={null} interface NonEVMChainDefinition { cctp: CCTPConfig \| null chain: Blockchain eurcAddress: string \| null explorerUrl: string gateway?: GatewayConfig isTestnet: boolean kitContracts?: Partial> name: string nativeCurrency: Currency rpcEndpoints: readonly string[] title?: string type: 'solana' \| 'algorand' \| 'avalanche' \| 'aptos' \| 'near' \| 'stellar' \| 'sui' \| 'hedera' \| 'noble' \| 'polkadot' usdcAddress: string \| null usdtAddress: string \| null } ``` **Properties** | Name | Type | Description | | -------------- | ----------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | | cctp | `CCTPConfig \| null` | Optional CCTP configuration. | | chain | Blockchain | The blockchain identifier from the Blockchain enum. | | eurcAddress | `string \| null` | The contract address for EURC. | | explorerUrl | string | Template URL for the blockchain explorer to view transactions. | | gateway | GatewayConfig | Optional Gateway contract configuration for Gateway protocol support. | | isTestnet | boolean | Indicates whether this is a testnet or mainnet. | | kitContracts | `Partial>` | Optional kit-specific contract addresses for enhanced chain functionality. | | name | string | The display name of the blockchain. | | nativeCurrency | Currency | Information about the native currency of the blockchain. | | rpcEndpoints | readonly string\[] | Default RPC endpoints for connecting to the blockchain network. | | title | string | Optional title or alternative name for the blockchain. | | type | `'solana' \| 'algorand' \| 'avalanche' \| 'aptos' \| 'near' \| 'stellar' \| 'sui' \| 'hedera' \| 'noble' \| 'polkadot'` | Discriminator for non-EVM chains. | | usdcAddress | `string \| null` | The contract address for USDC. | | usdtAddress | `string \| null` | The contract address for USDT. | *** #### TokenInfo Represents the metadata associated with a token. ```typescript theme={null} interface TokenInfo { decimals: number; name: string; symbol: string; } ``` *** ### Event Actions #### AppKitActions All actions available in AppKit. ```typescript theme={null} type AppKitActions = AppKitBridgeActions & AppKitUnifiedBalanceActions & AppKitEarnActions; ``` *** #### AppKitBridgeActions Prefixed bridge actions for AppKit. All BridgeKit events are prefixed with `bridge.` to namespace them within the AppKit event system. ```typescript theme={null} type AppKitBridgeActions = PrefixActions<"bridge", DefaultBridgeKitActions>; ``` *** #### AppKitEarnActions Prefixed earn actions for AppKit. Earn step events are exposed under the `earn.` namespace (for example `earn.deposit`, `earn.approve`, `earn.withdraw`) so they can be subscribed to via `kit.on()` alongside bridge and unified balance events. ```typescript theme={null} type AppKitEarnActions = PrefixActions<"earn", EarnActions>; ``` *** #### AppKitUnifiedBalanceActions Prefixed unified balance actions for AppKit. All UnifiedBalanceKit (Gateway) events are prefixed with `unifiedBalance.` to namespace them within the AppKit event system. ```typescript theme={null} type AppKitUnifiedBalanceActions = PrefixActions< "unifiedBalance", GatewayV1Actions >; ``` *** ### Bridge #### BridgeConfig Configuration options for customizing bridge behavior. ```typescript theme={null} interface BridgeConfig { batchTransactions?: boolean customFee?: CustomFee maxFee?: string transferSpeed?: TransferSpeed \| 'FAST' \| 'SLOW' } ``` **Properties** | Name | Type | Description | | ----------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | batchTransactions | boolean | Enable or disable EIP-5792 batched transaction execution.

When `true` (or `undefined` / omitted), the bridge will attempt to batch the approve and burn calls into a single `wallet_sendCalls` request if the connected wallet supports it. Set to `false` to explicitly opt out and always use the sequential approve -> burn flow. | | customFee | CustomFee | The custom fee to charge for the transfer.

Whatever value you provide here is added on top of the transfer amount. The user must have enough balance for `amount + customFee`, and the wallet signs for that total on the source chain. The custom fee is split automatically:

- 10% routes to Circle.
- 90% routes to your `recipientAddress`.

The original transfer amount proceeds through CCTPv2 unchanged, and the protocol fee (1–14 bps in FAST mode, 0% in STANDARD) is taken from that transfer amount. | | maxFee | string | The maximum fee to pay for the burn operation.

Provide the amount as a base-10 numeric string representing the token amount in human-readable format. For example: to set a maximum fee of 1 USDC, pass `"1"`. Decimal values are supported (e.g., `"0.5"` for half a USDC). | | transferSpeed | `TransferSpeed \| 'FAST' \| 'SLOW'` | The transfer speed mode for CCTPv2 transfers.

Controls whether to use fast burn mode (FAST) or standard mode (SLOW). Fast burn may reduce transfer time but could have different fee implications. | *** #### CCTPConfig Configuration for the Cross-Chain Transfer Protocol (CCTP). ```typescript theme={null} interface CCTPConfig { contracts: CCTPContracts; domain: number; forwarderSupported: { destination: boolean; source: boolean }; } ``` **Properties** | Name | Type | Description | | ------------------ | ------------------------------------------- | -------------------------------------------------------------------------- | | contracts | CCTPContracts | The contracts required for CCTP. | | domain | number | The CCTP domain identifier. | | forwarderSupported | `{ destination: boolean; source: boolean }` | Indicates whether the chain supports forwarder for source and destination. | *** #### CCTPMergedConfig Merged CCTP contract configuration. Used by chains that deploy a single unified CCTP contract. This simplified architecture is used by newer chain integrations. ```typescript theme={null} interface CCTPMergedConfig { confirmations: number; contract: string; tokenMessengerWithFees?: string; type: "merged"; } ``` *** #### CCTPSplitConfig Split CCTP contract configuration. Used by chains that deploy separate TokenMessenger and MessageTransmitter contracts. This is the traditional CCTP architecture used by most EVM chains. ```typescript theme={null} interface CCTPSplitConfig { confirmations: number; messageTransmitter: string; tokenMessenger: string; tokenMessengerWithFees?: string; type: "split"; } ``` *** #### TransferSpeed Transfer speed options for crosschain operations. Defines the available speed modes for CCTPv2 transfers, affecting both transfer time and potential fee implications. ```typescript theme={null} enum TransferSpeed { FAST = 'FAST' SLOW = 'SLOW' } ``` **Values** `FAST`, `SLOW` *** ### Swap #### AllowanceStrategy Allowance strategy for token approvals during swap operations. Defines how token allowances should be granted to the swap contract: * `permit`: Use EIP-2612 permit signature (gas-efficient, no approval transaction) * `approve`: Traditional approval transaction The default strategy is `permit` with fallback to `approve` if permit is not supported. ```typescript theme={null} type AllowanceStrategy = "approve" | "permit" | "authorize"; ``` *** #### SwapConfig Configuration options for swap operations. Controls swap behavior including allowance strategy, slippage tolerance, minimum output amounts, custom fees, and kit identification. ```typescript theme={null} interface SwapConfig { allowanceStrategy?: AllowanceStrategy; slippageBps?: number; stopLimit?: string; customFee?: { percentageBps: number; recipientAddress: string; }; apiKey?: string; kitKey?: string; } ``` **Properties** | Name | Type | Description | | ----------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | allowanceStrategy | AllowanceStrategy | Strategy for granting token allowances to the swap contract.

Defaults to `permit` with fallback to `approve`. | | apiKey | string | Circle API key used to authenticate service-backed swap requests.

Format: `_API_KEY::`. A legacy `KIT_KEY::` value is also accepted.

Treat this value as a credential. Do not log it, embed it in client-side source, or expose it in telemetry. | | customFee | `{ percentageBps: number; recipientAddress: string; }` | Custom fee configuration for this swap (percentage-based approach).

Allows specifying a percentage fee and recipient address at the transaction level. This is mutually exclusive with kit-level callback fee policy. If both are set, transaction-level takes precedence.

For complex fee logic (VIP tiers, database lookups), use kit-level callback approach via `setCustomFeePolicy()` instead. | | kitKey | string | **Deprecated.** Use SwapConfig.`apiKey` instead. Still honored when `apiKey` is omitted, and `apiKey` takes precedence when both are set. Circle API key used to authenticate service-backed swap requests. | | slippageBps | number | Maximum acceptable slippage in basis points (BPS).

1 BPS = 0.01%, so 300 BPS = 3% slippage. Defaults to 300 BPS (3%). | | stopLimit | string | Minimum acceptable output amount in human-readable format (stop-limit).

If the estimated output falls below this value, the swap will fail. Expressed as a decimal string (e.g., `0.4` for 0.4 USDT). The value is automatically converted to base units using the tokenOut decimals. | *** #### SwapDestinationLeg Destination-leg transaction and token information reported by the service. Present on SwapStatusResult once the service reports destination data. Omitted while the destination leg is still in-flight. ```typescript theme={null} interface SwapDestinationLeg { readonly txHash?: string; readonly token?: { readonly symbol?: string; readonly address?: string; }; readonly amount?: string; } ``` **Properties** | Name | Type | Description | | ------ | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | amount | string | Amount received on the destination leg, as a human-readable decimal string (e.g. `'0.082927'`).

Present once the destination leg has landed and the service has reported both `amountOut` and `receivingTokenDecimals`. Omitted otherwise — never a raw base-unit value, so consumers can render `destination.amount` directly in a UI without worrying about unit conversion. | | token | `{ readonly symbol?: string; readonly address?: string; }` | Destination-token metadata reported by the service. | | txHash | string | Destination-chain transaction hash reported by the service. | *** #### SwapProgress Lifecycle snapshot for a swap. Grouped so progress signals live in one place on both SwapResult and SwapStatusResult. ```typescript theme={null} interface SwapProgress { readonly status: SwapStatus; readonly substatus?: string; readonly substatusMessage?: string; } ``` **Properties** | Name | Type | Description | | ---------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | status | SwapStatus | Current swap status. Only `'DONE'`, `'FAILED'`, and `'NOT_FOUND'` are terminal. `'PENDING'` means the caller should re-invoke SwapKit.getSwapStatus until a terminal status is observed. | | substatus | string | Provider-specific status detail (e.g. `'WAIT_DESTINATION_TRANSACTION'`, `'COMPLETED'`). Surfaced as-is from the Stablecoin Service. | | substatusMessage | string | Human-readable provider status explanation. | *** #### SwapSourceLeg Source-leg transaction and token information reported by the service. Populated on SwapStatusResult from the service's `sendingTxHash`. `token` is shape-reserved for future service support (the status endpoint does not report source-token metadata today) so consumers can rely on a symmetric `source` / `destination` layout. ```typescript theme={null} interface SwapSourceLeg { readonly txHash?: string; readonly token?: { readonly symbol?: string; readonly address?: string; }; } ``` **Properties** | Name | Type | Description | | ------ | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | token | `{ readonly symbol?: string; readonly address?: string; }` | Source-token metadata. Shape-reserved — not populated by the current Stablecoin Service status endpoint, but exposed here so this field mirrors SwapDestinationLeg.token if/when the service starts returning it. | | txHash | string | Source-chain transaction hash reported by the service. | *** #### SwapStatus All possible status values for a swap tracked by the Stablecoin Service. `'PENDING'` means the swap is still in-flight. Callers should keep calling SwapKit.getSwapStatus on `'PENDING'` until the status becomes terminal (see SwapTerminalStatus). ```typescript theme={null} type SwapStatus = SwapTerminalStatus | "PENDING"; ``` *** #### SwapTerminalStatus Terminal status values for a swap tracked by the Stablecoin Service. `'DONE'` indicates the swap completed (including any crosschain delivery). `'FAILED'` indicates the swap failed after on-chain submission. `'NOT_FOUND'` indicates the service has no record of the transaction. ```typescript theme={null} type SwapTerminalStatus = "DONE" | "FAILED" | "NOT_FOUND"; ``` *** ### Unified Balance #### FeeAllocation Per-chain breakdown of a fee amount. ```typescript theme={null} interface FeeAllocation { chain: Blockchain; amount: string; } ``` **Properties** | Name | Type | Description | | ------ | ---------- | ------------------------------------------------------------- | | amount | string | The fee amount on this chain (human-readable decimal string). | | chain | Blockchain | The chain to which this portion of the fee applies. | *** #### FeeEntry A single fee line item within an estimate. ```typescript theme={null} interface FeeEntry { type: FeeType; token: string; amount: string; allocations?: FeeAllocation[]; recipientAddress?: string; } ``` **Properties** | Name | Type | Description | | ---------------- | ---------------- | ------------------------------------------------------------------------------------------ | | allocations | FeeAllocation\[] | Per-chain breakdown of the fee. Omitted for flat fees (e.g. forwarder). | | amount | string | Aggregate fee amount (human-readable decimal string). | | recipientAddress | string | When `type === 'kit'`, the address that receives the kit fee. Omitted for other fee types. | | token | string | The token in which this fee is denominated (e.g. `'USDC'`, `'ETH'`). | | type | FeeType | The category of this fee. | *** #### FeeType Fee category describing the origin of a fee line item. * `'provider'` — Fee charged by the crosschain provider (e.g. protocol fee). * `'gasFee'` — On-chain gas cost denominated in the transfer token (e.g. USDC). * `'kit'` — Fee charged by the kit / developer integration. * `'forwarder'` — Fee charged by Circle's Forwarding Service for automatic mint. ```typescript theme={null} type FeeType = "provider" | "gasFee" | "kit" | "forwarder"; ``` *** #### SupportedToken ```typescript theme={null} type SupportedToken = (typeof SUPPORTED_TOKENS)[number]; ``` *** #### SupportedTokenInput Case-insensitive variant of SupportedToken for user-facing input. Accepts the canonical uppercase form, fully lowercase, and title-case (e.g. `'USDC'`, `'usdc'`, `'Usdc'`). Arbitrary mixed-case input like `'uSdC'` is handled at runtime by the Zod schema and normalizeToken, so exhaustive compile-time permutations are unnecessary. Avoiding a recursive `CasePermutations` type prevents 2^N type-literal explosion as the token list grows. ```typescript theme={null} type SupportedTokenInput = | SupportedToken | Lowercase | Capitalize>; ``` *** #### UnifiedBalanceKitConfig Configuration options for initializing a UnifiedBalanceKit instance. When no providers are specified, the kit uses the default Gateway v1 provider. Any additional providers supplied via config are appended to the defaults. ```typescript theme={null} interface UnifiedBalanceKitConfig { disableAnalytics?: boolean; disableErrorReporting?: boolean; excludeDefaultProviders?: boolean; providers?: TExtraProviders; } ``` **Properties** | Name | Type | Description | | ----------------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | disableAnalytics | boolean | Disable analytics telemetry (success events with `txHash`).

When `true`, the SDK will not POST to the telemetry endpoint after successful verb operations. Defaults to `false` (enabled). | | disableErrorReporting | boolean | Disable error telemetry.

When `true`, the SDK will not POST error details to the telemetry endpoint when public methods throw. Defaults to `false` (enabled). | | excludeDefaultProviders | boolean | Skip the built-in default providers when assembling the context.

By default the kit prepends the standard Gateway v1 provider so `providers` is treated as additive. Pass `true` to use only the providers supplied in UnifiedBalanceKitConfig.providers.

Useful when stubbing the gateway in integration tests, when running against a self-hosted gateway replacement, or any time you want full control over which provider serves a given chain. | | providers | TExtraProviders | Optional array of additional Gateway providers.

If not provided, default providers will be initialized. | *** ## Event Types ### Bridge Events Bridge events are emitted for each provider in the kit. Events for the built-in CCTP provider follow its transaction steps. You can subscribe to each event multiple times with different callbacks. Bridge events are prefixed with `bridge.` to namespace them within AppKit: | Event | Description | | -------------------- | -------------------------------------------- | | `bridge.approve` | Token approval transaction completed | | `bridge.burn` | Source chain burn transaction completed | | `bridge.attestation` | CCTP attestation received | | `bridge.mint` | Destination chain mint transaction completed | | `*` | Wildcard listener for all events | **Usage Example** ```typescript theme={null} import { AppKit } from "@circle-fin/app-kit"; const kit = new AppKit(); // Listen to specific bridge action kit.on("bridge.approve", (payload) => { console.log("Approval transaction:", payload.values.txHash); }); // Listen to earn deposit steps kit.on("earn.deposit", (payload) => { console.log("Earn deposit step:", payload.values.state); }); // Listen to unified balance action kit.on("unifiedBalance.gateway.spend.succeeded", (payload) => { console.log("Spend succeeded:", payload.data); }); // Listen to all actions kit.on("*", (payload) => { console.log("Action:", payload); }); ``` # App Kits supported blockchains and tokens Source: https://docs.arc.io/app-kit/references/supported-blockchains Review supported blockchains, tokens, and token aliases available for App Kits ## Supported blockchains The App Kit SDK supports the following capabilities and wallet adapters for each supported blockchain. Swap is only available on mainnet and on Arc testnet. On Arc testnet, Swap supports USDC, EURC, and cirBTC only. | Blockchain | Capabilities | Adapters | | :----------------- | :---------------------------------- | :--------------------------- | | Arbitrum | Send, Bridge, Swap, Unified Balance | Viem, Ethers, Circle Wallets | | Arc (testnet only) | Send, Bridge, Swap, Unified Balance | Viem, Ethers, Circle Wallets | | Avalanche | Send, Bridge, Swap, Unified Balance | Viem, Ethers, Circle Wallets | | Base | Send, Bridge, Swap, Unified Balance | Viem, Ethers, Circle Wallets | | Codex | Send, Bridge | Viem, Ethers | | Cronos | Send, Bridge | Viem, Ethers, Circle Wallets | | EDGE | Send, Bridge | Viem, Ethers | | Ethereum | Send, Bridge, Swap, Unified Balance | Viem, Ethers, Circle Wallets | | HyperEVM | Send, Bridge, Swap, Unified Balance | Viem, Ethers | | Injective | Send, Bridge | Viem, Ethers | | Ink | Send, Bridge, Swap | Viem, Ethers | | Linea | Send, Bridge, Swap | Viem, Ethers | | Monad | Send, Bridge, Swap | Viem, Ethers | | Morph | Send, Bridge | Viem, Ethers | | OP Mainnet | Send, Bridge, Swap, Unified Balance | Viem, Ethers, Circle Wallets | | Pharos | Send, Bridge | Viem, Ethers | | Plasma | Send, Bridge | Viem, Ethers, Circle Wallets | | Plume | Send, Bridge, Swap | Viem, Ethers | | Polygon PoS | Send, Bridge, Swap, Unified Balance | Viem, Ethers, Circle Wallets | | Sei | Send, Bridge, Swap, Unified Balance | Viem, Ethers | | Solana | Send, Bridge, Swap, Unified Balance | Solana, Circle Wallets | | Sonic | Send, Bridge, Swap, Unified Balance | Viem, Ethers | | Unichain | Send, Bridge, Swap, Unified Balance | Viem, Ethers, Circle Wallets | | World Chain | Send, Bridge, Swap, Unified Balance | Viem, Ethers | | X Layer | Send, Bridge | Viem, Ethers | | XDC | Send, Bridge, Swap | Viem, Ethers | ### Blockchain identifiers For most blockchains, the identifier is the blockchain name with any spaces replaced by underscores (for example, Arc testnet → `Arc_Testnet`). Identifiers are case-sensitive and match the `BridgeChain` enum exported from `@circle-fin/app-kit`: ```ts theme={null} import { BridgeChain } from "@circle-fin/app-kit"; const chain = BridgeChain.Arc_Testnet; // or "Arc_Testnet" ``` Some blockchains have identifier deviations: | Blockchain | Identifier | | :--------------- | :--------------------- | | EDGE | `Edge` | | EDGE Testnet | `Edge_Testnet` | | OP Mainnet | `Optimism` | | OP Sepolia | `Optimism_Sepolia` | | Pharos Atlantic | `Pharos_Testnet` | | Polygon PoS | `Polygon` | | Polygon PoS Amoy | `Polygon_Amoy_Testnet` | ## Discover supported blockchains programmatically The App Kit SDK provides a way to fetch supported blockchain data in code. This is useful for building a blockchain picker, validating user input against supported blockchains, or looking up metadata like explorer URLs and testnet flags. Each method returns `ChainDefinition[]` with fields like `name`, `isTestnet`, `explorerUrl`, and protocol config. See [`getSupportedChains`](/app-kit/references/sdk-reference#getsupportedchains-operationtype) for the method signature and [`BaseChainDefinition`](/app-kit/references/sdk-reference#basechaindefinition) for the full field list. ### App Kit SDK Pass an optional `operationType` to narrow by capability: ```ts theme={null} import { AppKit } from "@circle-fin/app-kit"; const kit = new AppKit(); const allChains = kit.getSupportedChains(); const bridgeChains = kit.getSupportedChains("bridge"); const swapChains = kit.getSupportedChains("swap"); const unifiedBalanceChains = kit.getSupportedChains("unifiedBalance"); ``` The top-level App Kit SDK accepts only `operationType`. Bridge filters are available on the [standalone](#standalone-kits) Bridge Kit. Unified Balance forwarder filters are available on the namespaced API: ```ts theme={null} kit.unifiedBalance.getSupportedChains({ forwarderSupported: "destination", }); ``` ### Standalone kits If you import a kit package directly, call `getSupportedChains()` on that kit. Each package accepts different optional filters: * **Bridge Kit**: `chainType`, `isTestnet`, `forwarderSupported` * **Swap Kit**: none—filter the returned array yourself * **Unified Balance Kit**: `forwarderSupported` as `"source"` or `"destination"` ```ts theme={null} // Bridge Kit kit.getSupportedChains({ chainType: "evm", isTestnet: false, forwarderSupported: true, }); // Swap Kit kit.getSupportedChains().filter((c) => !c.isTestnet); // Unified Balance Kit kit.getSupportedChains({ forwarderSupported: "destination" }); ``` ## Supported tokens You can specify either a contract address or an [alias](#token-aliases) for any supported token. Each capability supports different tokens: * **[Send](/app-kit/send)**: Any token. Use [token aliases](#token-aliases) for common tokens or the token's contract address for other tokens. * **[Bridge](/app-kit/bridge)**: USDC only. * **[Swap](/app-kit/swap)**: * Any tokens with enough liquidity to trade with major stablecoins such as USDC, EURC, USDT, USDe, DAI, and PYUSD. * Native tokens on blockchains where Swap is supported. * On Arc testnet, only USDC, EURC, and cirBTC. * **[Unified Balance](/app-kit/unified-balance)**: USDC only. ### Token aliases App Kits works with any supported token when you supply the contract address. For convenience, you can use the following aliases for the most common tokens instead: * `USDC` * `EURC` * `USDT` * `USDe` * `DAI` * `PYUSD` * `cirBTC` * `NATIVE`: Uses the blockchain's native token. # Troubleshoot Unified Balance deposits Source: https://docs.arc.io/app-kit/references/unified-balance-error-recovery Identify deposit failures, retry quote errors safely, and recover fast deposits without submitting duplicate source transactions Recovery depends on the source transaction receipt. Before a fast deposit burns USDC, you can correct the request and try again. After a successful burn, monitor the existing deposit or escalate it. If the submission result is unclear, check the source blockchain before you try again. Quote and progress states apply only to crosschain `FAST` deposits. `STANDARD` same blockchain deposits throw structured errors and don't return `progress`. Don't call `deposit()` or `depositFor()` again while the source transaction is pending or after its receipt confirms the burn. A `PENDING` result means that the kit submitted the source transaction, but it doesn't prove that the transaction succeeded. Check the receipt before you choose a recovery action. The snippets assume that you've configured `kit` and a source `adapter`. See [Use fast deposits](/app-kit/tutorials/unified-balance/use-fast-deposits) for the complete deposit flow. ## Identify the failure stage Start with the result. Use it and the source blockchain history to choose a safe next step. | Observation | Source transaction status | Action | | ---------------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------ | | `estimateDeposit()` throws | Not submitted | Correct the request, then estimate again. | | `deposit()` throws and no burn transaction was sent | Not submitted | Correct the error, request a fresh estimate, and submit again. | | Wallet or RPC error while submitting the transaction | Unknown until you check the source blockchain | Check the wallet activity or block explorer before you submit another deposit. | | `progress.status` is `PENDING` | Submitted; receipt not verified by the result | Check the source receipt before you poll IRIS or retry. | | `progress.status` is `FAILED` | Burn confirmed; destination relay failed | Check the destination balance, then contact Circle support with the source `txHash`. | | `progress.status` is `DONE` | Burn and destination deposit confirmed | Save the destination relay `txHash`. No recovery action is required. | For a fast deposit, `txHash` identifies different transactions depending on the result: * `DONE`: the destination relay transaction. * `PENDING` or `FAILED`: the source burn transaction. When the result is `PENDING`, look up the source `txHash` and use the source transaction state to choose the next step: | Source transaction state | Action | | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | | Successful receipt | The burn occurred. Poll IRIS and don't submit another deposit. | | Reverted receipt | The burn didn't occur. Correct the cause, request a fresh estimate, and submit again. | | Pending | Keep monitoring the source transaction. Don't submit another deposit. | | Replaced | Inspect the replacement transaction before you act. A successful receipt alone doesn't prove that the burn occurred. | | Dropped or not found | Check the signing wallet's activity and nonce. Don't retry until you confirm that the transaction can't be executed. Then request a fresh estimate. | Check what the replacement transaction did. Compare its contract address and call data with the original burn, or look for the burn event: * Same burn succeeded: Poll IRIS with the replacement transaction hash. Don't submit another deposit. * Still pending: Keep monitoring the replacement. Don't submit another deposit. * Canceled, unrelated, or reverted: The burn didn't occur. Request a fresh estimate before you retry. If a wallet or Remote Procedure Call (RPC) request fails while it submits the burn, the transaction might still have reached the source blockchain. Don't automatically retry an ambiguous submission. First check the signing wallet's recent transactions for a matching USDC burn. ## Inspect structured errors Unified Balance operations throw a `KitError` for request validation, balance, RPC, and onchain failures. Record the error name, message, `recoverability`, and cause. These fields help you tell a request error from an infrastructure error. ```typescript TypeScript theme={null} import { isKitError, type DepositResult } from "@circle-fin/app-kit"; type DepositEstimate = Awaited< ReturnType >; try { const estimate: DepositEstimate = await kit.unifiedBalance.estimateDeposit({ from: { adapter, chain: "Ethereum_Sepolia" }, amount: "10", token: "USDC", to: { chain: "Arc_Testnet" }, config: { transferSpeed: "FAST" }, }); const result: DepositResult = await kit.unifiedBalance.deposit({ ...estimate, from: { adapter, chain: "Ethereum_Sepolia" }, }); console.log(result.progress?.status, result.txHash); } catch (error: unknown) { if (isKitError(error)) { console.error({ name: error.name, code: error.code, recoverability: error.recoverability, message: error.message, cause: error.cause, }); } throw error; } ``` Don't use `recoverability` alone to decide whether to submit another deposit. First verify whether the source burn occurred. ## Resolve request validation errors Fast deposit configuration errors use the `INPUT_VALIDATION_FAILED` error name. Fast deposits require an Ethereum Virtual Machine (EVM) source blockchain. Use the field and reason in the error message to correct the request. | Condition | Resolution | | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `transferSpeed` is `FAST`, but `to` is omitted | Add a supported destination blockchain. | | `to` is set, but `transferSpeed` is omitted or `STANDARD` | Set `config.transferSpeed` to `FAST`, or remove `to` for a standard deposit. | | `to.chain` isn't a supported fast deposit destination | Select a destination from the [fast deposit support table](/app-kit/tutorials/unified-balance/use-fast-deposits#supported-blockchains). | | The source is a fast finality destination | Select a supported source blockchain. The kit doesn't fall back to a standard deposit. | | The source isn't an EVM blockchain | Use a supported EVM source blockchain. | | The source and destination use different environments | Use either two mainnet blockchains or two testnet blockchains. | | `allowanceStrategy` is set with `to` | Omit `allowanceStrategy`. Fast deposits manage the USDC approval. | | `quote` is set for a standard deposit | Remove `quote`, or configure a fast deposit and request a new estimate. | If the preflight check reports an insufficient USDC balance, fund the source wallet with the required amount shown in the error. The required balance includes the deposit amount and forwarder fee. The wallet also needs enough of the source blockchain's native token to pay for any required approval and the burn gas. ## Resolve fee quote errors The kit validates a supplied fast deposit quote before it submits the source burn. Quote rejection reasons aren't `KitError` names. The kit translates them into either `INPUT_VALIDATION_FAILED` messages that request a new estimate or a fatal internal error. ### Resolve caller quote errors The kit detects these reasons before it submits a source burn for the current call: | Quote rejection reason | Meaning | Action | | ------------------------ | ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | `QUOTE_EXPIRED` | The quote's validity window ended. | Request a new estimate. | | `NON_ZERO_NONCE` | The quote was already used. | Find the transaction that consumed it. Recover that deposit instead if it matches the current intent. | | `SOURCE_DOMAIN_MISMATCH` | The quote belongs to a different source blockchain. | Request an estimate for the intended source blockchain. | | `SIGNATURE_INVALID` | The quote signature is invalid. | Discard the quote and request a new estimate. | | `QUOTE_ARGS_MISMATCH` | The quote doesn't match the final deposit parameters. | Request a new estimate after you finalize the amount, source, destination, recipient, and configuration. | Call `estimateDeposit()` again with the final amount, source, destination, recipient, and transfer configuration. Pass the returned estimate to `deposit()` without changing its quoted fields: ```typescript TypeScript theme={null} import type { DepositResult } from "@circle-fin/app-kit"; type DepositEstimate = Awaited< ReturnType >; const estimate: DepositEstimate = await kit.unifiedBalance.estimateDeposit({ from: { adapter, chain: "Ethereum_Sepolia" }, amount: "10", token: "USDC", to: { chain: "Arc_Testnet" }, config: { transferSpeed: "FAST" }, }); const result: DepositResult = await kit.unifiedBalance.deposit({ ...estimate, from: { adapter, chain: "Ethereum_Sepolia" }, }); console.log(result.progress?.status, result.txHash); ``` Request a quote shortly before submission instead of caching it. If a new quote fails with the same reason, stop retrying and contact Circle support with the full structured error. For `NON_ZERO_NONCE`, request a new quote only after you confirm that you aren't trying to recover the deposit that used the old quote. ### Report an internal quote error The following reasons indicate that the kit constructed an invalid forwarding payload: * `EMPTY_HOOK_DATA` * `FORWARD_FEE_WITHOUT_HOOK` * `FORWARD_HOOK_WITHOUT_FEE` These errors have `SERVICE_INTERNAL_ERROR` as their error name and `FATAL` as their `recoverability`. Requesting another quote with the same SDK and parameters won't correct the payload. Stop retrying and report the error to Circle support. ## Handle fast deposit progress After it submits the source transaction, the kit waits about 60 seconds for Circle's relayer. It then returns one of these states: | Status | Meaning | Action | | --------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | | `DONE` | The relayer confirmed the destination deposit. | Store the destination relay `txHash`. | | `PENDING` | The wait ended without a confirmed relay. The source transaction receipt isn't verified. | Check the returned source `txHash`. Poll IRIS only after the receipt succeeds. | | `FAILED` | The source burn completed, but the relayer reported a terminal destination failure. | Check the destination balance, then contact Circle support. | `PENDING` doesn't by itself mean that the deposit failed or that funds are in transit. The source transaction can still be pending, reverted, or dropped when the kit returns. ### Poll a pending deposit After the source receipt succeeds, query the IRIS messages endpoint with the source Cross-Chain Transfer Protocol (CCTP) domain ID and the source burn transaction hash. For domain IDs, see [Supported chains and domains](https://developers.circle.com/cctp/concepts/supported-chains-and-domains#cctp-domains). **Mainnet:** ```bash Shell theme={null} curl "https://iris-api.circle.com/v2/messages/{sourceDomainId}?transactionHash={sourceTxHash}" ``` **Testnet:** ```bash Shell theme={null} curl "https://iris-api-sandbox.circle.com/v2/messages/{sourceDomainId}?transactionHash={sourceTxHash}" ``` Use `messages[].forwardState` to choose the next action: | `forwardState` | Action | | ----------------------------- | ------------------------------------------------------------------------- | | `PENDING`, `null`, or omitted | Continue polling with backoff. | | `CONFIRMED` or `COMPLETE` | Treat the deposit as complete when `forwardTxHash` is present. | | `FAILED` | Stop polling. Check the destination balance, then contact Circle support. | The top-level message `status` describes the CCTP attestation. It doesn't confirm that the forwarding transaction completed. Use `forwardState` and `forwardTxHash` for the destination deposit. ### Escalate a failed deposit An IRIS `FAILED` forward state means that Circle's forwarding attempt failed. Another party might still complete the destination mint using the same CCTP message. Before you contact Circle support, [check the recipient's Unified Balance](/app-kit/tutorials/unified-balance/check-unified-balance). If the balance wasn't credited, provide: * The source burn transaction hash from the deposit result. * The source and destination blockchains. * The deposit account and amount. * The approximate submission time and SDK version. * The structured error or IRIS response, with secrets removed. Circle support can inspect the existing burn and attestation and determine whether the destination relay can be submitted again. This recovery reuses the existing burn; it doesn't require a new deposit. The Unified Balance deposit API doesn't provide a public retry method for an existing source burn. Don't reuse Bridge Kit's `retry()` method with a Unified Balance deposit result, and don't submit a new deposit as a recovery attempt. ## Prevent duplicate deposits * Persist the source transaction hash and deposit state before starting background monitoring. * Prevent duplicate submissions while a wallet request or deposit is pending. * Request a fresh estimate immediately before `deposit()` or `depositFor()`. * Pass quoted fields to the deposit unchanged. * Use backoff when polling IRIS, and stop when the state is terminal. * Alert on repeated internal quote errors and `FAILED` relay states instead of retrying them automatically. # App Kit SDK: Send Source: https://docs.arc.io/app-kit/send Send tokens between wallets on the same blockchain with the App Kit SDK's Send capability The [App Kit SDK](/app-kit) includes the Send capability that lets you send tokens from one wallet to another on the same blockchain. You can use an available [token alias](/app-kit/references/supported-blockchains#token-aliases) or the token's contract address. ## How it works Send transfers tokens between two wallets on the same blockchain in a single method call. The SDK resolves the token by alias or contract address, signs with the sender's adapter, and submits the transfer. ```mermaid theme={null} flowchart LR A[Sender wallet] -->|"transfer"| B[Recipient wallet] ``` ## Quick look This code snippet sends USDC from one wallet to another in a single method call: ```typescript TypeScript theme={null} // Send 1.00 USDC to a wallet on Arc Testnet const result = await kit.send({ from: { adapter, chain: "Arc_Testnet" }, to: "RECIPIENT_ADDRESS", amount: "1.00", token: "USDC", }); ``` For a complete end-to-end flow, follow the quickstart: [Send Tokens Across Wallets](/app-kit/quickstarts/send-tokens-same-chain). ## Installation [Install the App Kit SDK](/app-kit/tutorials/installation) to start sending tokens on the same blockchain. # App Kit SDK: Swap Source: https://docs.arc.io/app-kit/swap Swap stablecoins and native tokens with the App Kit SDK's Swap capability The [App Kit SDK](/app-kit) includes the Swap capability that lets you swap two tokens in a few lines of code. Swap [supports](/app-kit/references/supported-blockchains#supported-tokens) many stablecoins (including USDC, EURC, USDT, USDe, DAI, and PYUSD) and native tokens on supported blockchains. Swap is only available on mainnet and on Arc testnet. On Arc testnet, Swap supports USDC, EURC, and cirBTC only. ## Quick look This example swaps 1 USDC for EURC on Arc Testnet: ```typescript TypeScript theme={null} const result = await kit.swap({ from: { adapter: viemAdapter, chain: "Arc_Testnet" }, tokenIn: "USDC", tokenOut: "EURC", amountIn: "1.00", }); ``` See [Swap tokens on a blockchain](/app-kit/quickstarts/swap-tokens-same-chain) for a full working example. This example swaps 1 USDC on Base for native POL delivered on Polygon in a single call: ```typescript TypeScript theme={null} const result = await kit.swap({ from: { adapter: viemAdapter, chain: "Base" }, tokenIn: "USDC", tokenOut: "NATIVE", amountIn: "1.00", to: { chain: "Polygon", recipientAddress: "0xYourRecipientAddress", }, }); ``` See [Swap tokens across blockchains](/app-kit/quickstarts/swap-tokens-crosschain) for a full working example. ## API key Swap accepts an optional [Circle API key](https://developers.circle.com/api-reference/keys) obtained from the [Circle Console](https://console.circle.com/api-keys). Pass one as `apiKey` in the swap config. Without one, requests share a rate limit. Production traffic typically needs a key to avoid hitting it. API keys are environment-specific. Testnet and mainnet each need their own key. Moving from a kit key? Kit keys are legacy, and new ones cannot be created. Existing kit keys remain valid, but new integrations use an API key. Rename the `kitKey` parameter in your swap config to `apiKey` when you upgrade the SDK. ## Installation [Install the App Kit SDK](/app-kit/tutorials/installation) to use Swap. If you only need to swap and don't want to install the full App Kit SDK, install the standalone Swap Kit for your preferred adapter below. ```bash npm theme={null} npm install @circle-fin/swap-kit @circle-fin/adapter-viem-v2 viem ``` ```bash yarn theme={null} yarn add @circle-fin/swap-kit @circle-fin/adapter-viem-v2 viem ``` ```bash npm theme={null} npm install @circle-fin/swap-kit @circle-fin/adapter-ethers-v6 ethers ``` ```bash yarn theme={null} yarn add @circle-fin/swap-kit @circle-fin/adapter-ethers-v6 ethers ``` ```bash npm theme={null} npm install @circle-fin/swap-kit @circle-fin/adapter-solana-kit @solana/kit @solana/web3.js ``` ```bash yarn theme={null} yarn add @circle-fin/swap-kit @circle-fin/adapter-solana-kit @solana/kit @solana/web3.js ``` ```bash npm theme={null} npm install @circle-fin/swap-kit @circle-fin/adapter-circle-wallets ``` ```bash yarn theme={null} yarn add @circle-fin/swap-kit @circle-fin/adapter-circle-wallets ``` # Adapter setups Source: https://docs.arc.io/app-kit/tutorials/adapter-setups Configure Viem, Ethers, Solana, or Circle Wallets adapters for the App Kit SDK The App Kit SDK uses adapters to connect SDK calls to a client and signer. The client reads blockchain data and submits signed transactions while the signer authorizes transactions from a private key, browser wallet, or wallet provider. The following adapters are supported: * [`viem`](https://viem.sh/) v2 for EVM blockchains * [`ethers`](https://ethers.org/) v6 for EVM blockchains * [`solana`](https://solana.com/) for the Solana blockchain * [`circle-wallets`](https://developers.circle.com/wallets) for Circle-managed developer-controlled and user-controlled wallets Before setting up an adapter, you should already have created the wallet, account, API keys, and policies in your wallet provider. Pick your adapter to see its setup options. Use the Viem adapter for EVM apps that use `viem` accounts or wallet clients. ## Server-side wallet Use a server-side wallet when your backend signs transactions. The signer can be a private key or a wallet provider. Create one adapter from your wallet private key. The adapter works across EVM blockchains and uses built-in public RPC endpoints. Default RPC URLs are shared and may be rate-limited. For a more stable connection, configure a [custom RPC](#custom-rpc). ```typescript TypeScript theme={null} import { createViemAdapterFromPrivateKey } from "@circle-fin/adapter-viem-v2"; const adapter = createViemAdapterFromPrivateKey({ privateKey: process.env.PRIVATE_KEY as string, }); ``` Use Turnkey Company Wallets with the Viem adapter for backend signing. The setup uses `@turnkey/viem` to create a Turnkey-backed `viem` account, then passes that account to the Viem adapter. Set these environment variables from your Turnkey account: ```text .env theme={null} TURNKEY_API_PUBLIC_KEY=YOUR_TURNKEY_API_PUBLIC_KEY TURNKEY_API_PRIVATE_KEY=YOUR_TURNKEY_API_PRIVATE_KEY TURNKEY_ORGANIZATION_ID=YOUR_TURNKEY_ORGANIZATION_ID TURNKEY_WALLET_ADDRESS=YOUR_TURNKEY_ETHEREUM_WALLET_ADDRESS ``` Adapter setup: ```typescript TypeScript theme={null} import { AppKit } from "@circle-fin/app-kit"; import { ViemAdapter } from "@circle-fin/adapter-viem-v2"; import { Turnkey as TurnkeyServerSDK } from "@turnkey/sdk-server"; import { createAccount } from "@turnkey/viem"; import { createPublicClient, createWalletClient, http } from "viem"; const turnkey = new TurnkeyServerSDK({ apiBaseUrl: "https://api.turnkey.com", apiPublicKey: process.env.TURNKEY_API_PUBLIC_KEY!, apiPrivateKey: process.env.TURNKEY_API_PRIVATE_KEY!, defaultOrganizationId: process.env.TURNKEY_ORGANIZATION_ID!, }); const account = await createAccount({ client: turnkey.apiClient(), organizationId: process.env.TURNKEY_ORGANIZATION_ID!, signWith: process.env.TURNKEY_WALLET_ADDRESS!, }); const appKit = new AppKit(); const supportedChains = (await appKit.getSupportedChains()).filter( (chain) => chain.type === "evm", ); const adapter = new ViemAdapter( { getPublicClient: ({ chain }) => createPublicClient({ chain, transport: http(), }), getWalletClient: ({ chain }) => createWalletClient({ account, chain, transport: http(), }), }, { addressContext: "user-controlled", supportedChains, }, ); ``` **What to know about this setup**: * Turnkey supports both `viem` and `ethers`. This setup uses `viem` because `@turnkey/viem` creates a Turnkey-backed account directly, which keeps the adapter setup shorter. * Use this setup only on your backend. `TURNKEY_API_PRIVATE_KEY` authenticates requests to Turnkey and must not be exposed in browser code. * For crosschain flows, create one adapter per blockchain if your wallet client or RPC setup is blockchain-specific. ## Browser wallet This setup expects a wallet extension in the browser (for example `window.ethereum` or `window.solana`), not a Node.js server. You can use wallets like [MetaMask](https://metamask.io/) or [Phantom](https://phantom.com/): Use EIP-6963 to discover injected browser wallets, select a provider, request access to the user's accounts, then pass the provider to the Viem adapter. The example selects MetaMask by reverse-DNS identifier. To use any injected wallet, omit `requiredRdns`. ```typescript TypeScript theme={null} import { createViemAdapterFromProvider, type CreateViemAdapterFromProviderParams, } from "@circle-fin/adapter-viem-v2"; type BrowserWalletProvider = CreateViemAdapterFromProviderParams["provider"]; type EIP6963ProviderDetail = { info: { uuid: string; name: string; icon: string; rdns: string; }; provider: BrowserWalletProvider; }; declare global { interface WindowEventMap { "eip6963:announceProvider": CustomEvent; } } async function getInjectedWalletProvider( requiredRdns?: string, ): Promise { const providers = new Map(); const onAnnounce = ((event: CustomEvent) => { providers.set(event.detail.info.uuid, event.detail); }) as EventListener; window.addEventListener("eip6963:announceProvider", onAnnounce); window.dispatchEvent(new Event("eip6963:requestProvider")); await new Promise((resolve) => window.setTimeout(resolve, 250)); window.removeEventListener("eip6963:announceProvider", onAnnounce); const provider = requiredRdns ? [...providers.values()].find(({ info }) => info.rdns === requiredRdns) ?.provider : [...providers.values()][0]?.provider; if (!provider) { throw new Error( requiredRdns ? `No EIP-6963 wallet found for ${requiredRdns}` : "No EIP-6963 browser wallet found", ); } return provider; } const provider = await getInjectedWalletProvider("io.metamask"); // For any injected wallet, use: // const provider = await getInjectedWalletProvider(); await provider.request({ method: "eth_requestAccounts", params: undefined, }); const adapter = await createViemAdapterFromProvider({ provider, }); ``` Use `window.ethereum` only when the wallet does not support EIP-6963 or your app intentionally accepts the default injected provider. ```typescript TypeScript theme={null} import { createViemAdapterFromProvider } from "@circle-fin/adapter-viem-v2"; import type { EIP1193Provider } from "viem"; declare global { interface Window { ethereum?: EIP1193Provider; } } if (!window.ethereum) { throw new Error("No wallet provider found"); } const adapter = await createViemAdapterFromProvider({ provider: window.ethereum, }); ``` ## Custom RPC By default, adapters use built-in RPC endpoints, which may be rate-limited or unreliable. Override them with your own provider. [Alchemy](https://www.alchemy.com/), [QuickNode](https://www.quicknode.com/), and [chainlist.org](https://chainlist.org/) are common places to source endpoints. This example uses Alchemy. The `getPublicClient`/`getWalletClient` override pairs with any signer setup that uses `viem`, such as a private key or browser wallet. ```typescript TypeScript theme={null} import { createViemAdapterFromPrivateKey } from "@circle-fin/adapter-viem-v2"; import { EthereumSepolia, ArcTestnet } from "@circle-fin/app-kit/chains"; import { createPublicClient, createWalletClient, http } from "viem"; const RPC_BY_CHAIN_NAME: Record = { [EthereumSepolia.name]: `https://eth-sepolia.g.alchemy.com/v2/${process.env.ALCHEMY_KEY}`, [ArcTestnet.name]: `https://arc-testnet.g.alchemy.com/v2/${process.env.ALCHEMY_KEY}`, }; const adapter = createViemAdapterFromPrivateKey({ privateKey: process.env.PRIVATE_KEY as string, getPublicClient: ({ chain }) => { const rpcUrl = RPC_BY_CHAIN_NAME[chain.name]; if (!rpcUrl) { throw new Error(`No RPC configured for chain: ${chain.name}`); } return createPublicClient({ chain, transport: http(rpcUrl, { retryCount: 3, timeout: 10000, }), }); }, getWalletClient: ({ chain, account }) => { const rpcUrl = RPC_BY_CHAIN_NAME[chain.name]; if (!rpcUrl) { throw new Error(`No RPC configured for chain: ${chain.name}`); } return createWalletClient({ account, chain, transport: http(rpcUrl, { retryCount: 3, timeout: 10000, }), }); }, }); ``` Use the Ethers adapter for EVM apps that use `ethers` providers or signers. ## Server-side wallet Use a server-side wallet when your backend signs transactions. The signer can be a private key or a wallet provider. Create one adapter from your wallet private key. The adapter works across EVM blockchains and uses built-in public RPC endpoints. Default RPC URLs are shared and may be rate-limited. For a more stable connection, configure a custom RPC. ```typescript TypeScript theme={null} import { createEthersAdapterFromPrivateKey } from "@circle-fin/adapter-ethers-v6"; const adapter = createEthersAdapterFromPrivateKey({ privateKey: process.env.PRIVATE_KEY as string, }); ``` Use Privy server wallets with the Ethers adapter when Privy signs for your backend. Privy server wallets sign through the Privy wallet API by `walletId`, so the setup needs a small custom `ethers` signer. Set these environment variables from your Privy app and server wallet: ```text .env theme={null} PRIVY_APP_ID=YOUR_PRIVY_APP_ID PRIVY_APP_SECRET=YOUR_PRIVY_APP_SECRET PRIVY_AUTHORIZATION_PRIVATE_KEY=YOUR_PRIVY_AUTHORIZATION_PRIVATE_KEY PRIVY_WALLET_ID=YOUR_PRIVY_SERVER_WALLET_ID PRIVY_WALLET_ADDRESS=YOUR_PRIVY_SERVER_WALLET_ADDRESS ``` Use a server-owned Privy wallet. `PRIVY_AUTHORIZATION_PRIVATE_KEY` must belong to the wallet owner or an authorized signer for `PRIVY_WALLET_ID`. Create the `PrivyServerSigner.ts` helper first. The adapter setup imports this helper in the next step. ```typescript TypeScript expandable theme={null} import { PrivyClient, type Hex, type Quantity } from "@privy-io/server-auth"; import { ethers } from "ethers"; interface PrivyServerSignerOptions { walletId: string; walletAddress: string; privy: PrivyClient; provider: ethers.Provider; } type JsonSafe = string | number | boolean | null | JsonSafe[] | { [key: string]: JsonSafe }; type PrivySignTransactionRequest = Parameters< PrivyClient["walletApi"]["ethereum"]["signTransaction"] >[0]; type PrivySignMessageRequest = Parameters< PrivyClient["walletApi"]["ethereum"]["signMessage"] >[0]; type PrivySignTypedDataRequest = Parameters< PrivyClient["walletApi"]["ethereum"]["signTypedData"] >[0]; export class PrivyServerSigner extends ethers.AbstractSigner { private walletId: string; private walletAddress: string; private privy: PrivyClient; constructor(options: PrivyServerSignerOptions) { super(options.provider); this.walletId = options.walletId; this.walletAddress = options.walletAddress; this.privy = options.privy; } connect(provider: ethers.Provider): PrivyServerSigner { return new PrivyServerSigner({ walletId: this.walletId, walletAddress: this.walletAddress, privy: this.privy, provider, }); } async getAddress(): Promise { return this.walletAddress; } async signTransaction(tx: ethers.TransactionRequest): Promise { const toQuantity = ( value: ethers.BigNumberish | null | undefined, ): Quantity | undefined => value != null ? (ethers.toBeHex(value) as Hex) : undefined; const toHex = (value: unknown): Hex | undefined => value != null ? (value.toString() as Hex) : undefined; const request: PrivySignTransactionRequest = { walletId: this.walletId, chainType: "ethereum", transaction: { to: toHex(tx.to), nonce: tx.nonce != null ? Number(tx.nonce) : undefined, chainId: tx.chainId != null ? Number(tx.chainId) : undefined, data: toHex(tx.data), value: toQuantity(tx.value), type: (tx.type ?? 2) as 0 | 1 | 2, gasLimit: toQuantity(tx.gasLimit), maxFeePerGas: toQuantity(tx.maxFeePerGas), maxPriorityFeePerGas: toQuantity(tx.maxPriorityFeePerGas), }, }; const { signedTransaction } = await this.privy.walletApi.ethereum.signTransaction(request); return signedTransaction; } async signMessage(message: string | Uint8Array): Promise { const request: PrivySignMessageRequest = { walletId: this.walletId, chainType: "ethereum", message: typeof message === "string" ? message : ethers.hexlify(message), }; const { signature } = await this.privy.walletApi.ethereum.signMessage(request); return signature; } async signTypedData( domain: ethers.TypedDataDomain, types: Record, value: Record, ): Promise { const toJsonSafe = (input: unknown): JsonSafe => { if (typeof input === "bigint") { return input.toString(); } if (Array.isArray(input)) { return input.map(toJsonSafe); } if (input && typeof input === "object") { return Object.fromEntries( Object.entries(input).map(([key, nestedValue]) => [ key, toJsonSafe(nestedValue), ]), ); } return input as JsonSafe; }; const primaryType = Object.keys(types).find((key) => key !== "EIP712Domain") ?? ""; const jsonSafeDomain = toJsonSafe(domain) as Record; const jsonSafeMessage = toJsonSafe(value) as Record; const request: PrivySignTypedDataRequest = { walletId: this.walletId, typedData: { domain: jsonSafeDomain, types, message: jsonSafeMessage, primaryType, }, }; const { signature } = await this.privy.walletApi.ethereum.signTypedData(request); return signature; } } ``` After adding the helper, create the adapter: ```typescript TypeScript theme={null} import { AppKit } from "@circle-fin/app-kit"; import { EthersAdapter } from "@circle-fin/adapter-ethers-v6"; import { PrivyClient } from "@privy-io/server-auth"; import { ethers } from "ethers"; import { PrivyServerSigner } from "./PrivyServerSigner"; const privy = new PrivyClient( process.env.PRIVY_APP_ID!, process.env.PRIVY_APP_SECRET!, { walletApi: { authorizationPrivateKey: process.env.PRIVY_AUTHORIZATION_PRIVATE_KEY!, }, }, ); const appKit = new AppKit(); const supportedChains = (await appKit.getSupportedChains()).filter( (chain) => chain.type === "evm", ); const arcTestnet = supportedChains.find( (chain) => chain.chain === "Arc_Testnet", ); if (!arcTestnet) { throw new Error("Arc Testnet is not supported"); } const provider = new ethers.JsonRpcProvider(arcTestnet.rpcEndpoints[0]); const signer = new PrivyServerSigner({ walletId: process.env.PRIVY_WALLET_ID!, walletAddress: process.env.PRIVY_WALLET_ADDRESS!, privy, provider, }); const adapter = new EthersAdapter( { signer, getProvider: ({ chain }) => new ethers.JsonRpcProvider(chain.rpcEndpoints[0]), }, { addressContext: "user-controlled", supportedChains, }, ); ``` **What to know about this setup**: * Privy's stock `createEthersSigner` is built for user-linked wallets. Server wallets sign by `walletId`, which is why this setup uses `PrivyServerSigner`. * User-owned embedded wallets are not server wallets. For this setup, create a server-owned wallet or add your backend key as an authorized signer. * Automatic chain switching does not support this custom signer. For crosschain flows, create one adapter per blockchain. ## Browser wallet This setup expects a wallet extension in the browser (for example `window.ethereum` or `window.solana`), not a Node.js server. You can use wallets like [MetaMask](https://metamask.io/) or [Phantom](https://phantom.com/): In crosschain flows, use the Viem adapter for injected browser wallets. The Ethers adapter can work for same-chain browser wallet operations, but the `BrowserProvider` from `ethers` can throw a network-change error when the wallet switches blockchains during a bridge or other crosschain flow. Use EIP-6963 to discover injected browser wallets, select a provider, request access to the user's accounts, then pass the provider to the Ethers adapter. The example selects MetaMask by reverse-DNS identifier. To use any injected wallet, omit `requiredRdns`. ```typescript TypeScript theme={null} import { createEthersAdapterFromProvider, type CreateEthersAdapterFromProviderParams, } from "@circle-fin/adapter-ethers-v6"; type BrowserWalletProvider = CreateEthersAdapterFromProviderParams["provider"]; type EIP6963ProviderDetail = { info: { uuid: string; name: string; icon: string; rdns: string; }; provider: BrowserWalletProvider; }; declare global { interface WindowEventMap { "eip6963:announceProvider": CustomEvent; } } async function getInjectedWalletProvider( requiredRdns?: string, ): Promise { const providers = new Map(); const onAnnounce = ((event: CustomEvent) => { providers.set(event.detail.info.uuid, event.detail); }) as EventListener; window.addEventListener("eip6963:announceProvider", onAnnounce); window.dispatchEvent(new Event("eip6963:requestProvider")); await new Promise((resolve) => window.setTimeout(resolve, 250)); window.removeEventListener("eip6963:announceProvider", onAnnounce); const provider = requiredRdns ? [...providers.values()].find(({ info }) => info.rdns === requiredRdns) ?.provider : [...providers.values()][0]?.provider; if (!provider) { throw new Error( requiredRdns ? `No EIP-6963 wallet found for ${requiredRdns}` : "No EIP-6963 browser wallet found", ); } return provider; } const provider = await getInjectedWalletProvider("io.metamask"); // For any injected wallet, use: // const provider = await getInjectedWalletProvider(); await provider.request({ method: "eth_requestAccounts", params: undefined, }); const adapter = await createEthersAdapterFromProvider({ provider, }); ``` Use `window.ethereum` only when the wallet does not support EIP-6963 or your app intentionally accepts the default injected provider. ```typescript TypeScript theme={null} import { createEthersAdapterFromProvider } from "@circle-fin/adapter-ethers-v6"; import type { Eip1193Provider } from "ethers"; declare global { interface Window { ethereum?: Eip1193Provider; } } if (!window.ethereum) { throw new Error("No wallet provider found"); } const adapter = await createEthersAdapterFromProvider({ provider: window.ethereum, }); ``` ## Custom RPC By default, adapters use built-in RPC endpoints, which may be rate-limited or unreliable. Override them with your own provider. [Alchemy](https://www.alchemy.com/), [QuickNode](https://www.quicknode.com/), and [chainlist.org](https://chainlist.org/) are common places to source endpoints. This example uses Alchemy. The `getProvider` override pairs with any signer setup that uses `ethers`, such as a private key or browser wallet. ```typescript TypeScript theme={null} import { createEthersAdapterFromPrivateKey } from "@circle-fin/adapter-ethers-v6"; import { EthereumSepolia, ArcTestnet } from "@circle-fin/app-kit/chains"; import { JsonRpcProvider } from "ethers"; const RPC_BY_CHAIN_NAME: Record = { [EthereumSepolia.name]: `https://eth-sepolia.g.alchemy.com/v2/${process.env.ALCHEMY_KEY}`, [ArcTestnet.name]: `https://arc-testnet.g.alchemy.com/v2/${process.env.ALCHEMY_KEY}`, }; const adapter = createEthersAdapterFromPrivateKey({ privateKey: process.env.PRIVATE_KEY as string, getProvider: ({ chain }) => { const rpcUrl = RPC_BY_CHAIN_NAME[chain.name]; if (!rpcUrl) { throw new Error(`No RPC configured for chain: ${chain.name}`); } return new JsonRpcProvider(rpcUrl); }, }); ``` Use the Solana adapter for Solana wallets and RPC clients. ## Server-side wallet Use a server-side wallet when your backend signs transactions. ### Private key Create one adapter from your Solana private key. Solana accepts Base58, Base64, or JSON array format private keys. Default RPC URLs are shared and may be rate-limited. For a more stable connection, configure a custom RPC. ```typescript TypeScript theme={null} import { createSolanaKitAdapterFromPrivateKey } from "@circle-fin/adapter-solana-kit"; const adapter = createSolanaKitAdapterFromPrivateKey({ privateKey: process.env.PRIVATE_KEY as string, }); ``` ## Browser wallet This setup expects a wallet extension in the browser (for example `window.ethereum` or `window.solana`), not a Node.js server. You can use wallets like [MetaMask](https://metamask.io/) or [Phantom](https://phantom.com/): ```typescript TypeScript theme={null} import { createSolanaKitAdapterFromProvider } from "@circle-fin/adapter-solana-kit"; type SolanaKitWalletProvider = Parameters< typeof createSolanaKitAdapterFromProvider >[0]["provider"]; declare global { interface Window { solana?: SolanaKitWalletProvider; } } if (!window.solana) { throw new Error("No Solana wallet provider found"); } const adapter = await createSolanaKitAdapterFromProvider({ provider: window.solana, }); ``` ## Custom RPC By default, adapters use built-in RPC endpoints, which may be rate-limited or unreliable. Override them with your own provider. [Alchemy](https://www.alchemy.com/), [QuickNode](https://www.quicknode.com/), and [chainlist.org](https://chainlist.org/) are common places to source endpoints. This example uses Alchemy. The `getRpc` override pairs with any Solana signer setup, such as a private key or browser wallet. ```typescript TypeScript theme={null} import { createSolanaKitAdapterFromPrivateKey } from "@circle-fin/adapter-solana-kit"; import { createSolanaRpc } from "@solana/kit"; const adapter = createSolanaKitAdapterFromPrivateKey({ privateKey: process.env.SOLANA_PRIVATE_KEY as string, getRpc: () => createSolanaRpc( `https://solana-devnet.g.alchemy.com/v2/${process.env.ALCHEMY_KEY}`, ), }); ``` server-side only Use the Circle Wallets adapter if you already [manage wallets through Circle](https://developers.circle.com/wallets). The adapter is suited for backend apps and works with [developer-controlled](https://developers.circle.com/wallets/dev-controlled) or [user-controlled](https://developers.circle.com/wallets/user-controlled) wallets so you can use the App Kit SDK on [supported blockchains](/app-kit/references/supported-blockchains#adapters) without managing private keys yourself. Pick the custody model that matches your app. Use the developer-controlled Circle Wallets adapter when your backend initiates wallet actions. ## Setup The developer-controlled wallets adapter requires these credentials from the [Circle Console](https://console.circle.com): * **[API Key](https://developers.circle.com/w3s/circle-developer-account#creating-an-api-key-for-developer-services):** Environment-prefixed (examples: `TEST_API_KEY:abc123:def456`, `LIVE_API_KEY:xyz:uvw`) or Base64-encoded * **[Entity Secret](https://developers.circle.com/wallets/dev-controlled/register-entity-secret):** 64 lowercase alphanumeric characters This code block initializes the Circle Wallets adapter: ```typescript TypeScript theme={null} import { createCircleWalletsAdapter } from "@circle-fin/adapter-circle-wallets"; const adapter = createCircleWalletsAdapter({ apiKey: process.env.CIRCLE_API_KEY!, entitySecret: process.env.CIRCLE_ENTITY_SECRET!, }); ``` ## Usage When you use the developer-controlled adapter in an App Kit operation, pass the wallet address in the operation context. The address tells the App Kit SDK which Circle Wallet acts on that blockchain: ```typescript TypeScript theme={null} from: { adapter, chain: "Base_Sepolia", address: walletAddress, } ``` Use the wallet address for the same blockchain as `chain`. * The wallet address you pass to App Kit operations determines whether Circle handles the transaction as a dev-controlled externally owned account (EOA) or smart contract account (SCA) wallet. See [Account type comparison](https://developers.circle.com/wallets/account-types#account-type-comparison) to understand the difference. * **Dev-controlled EOA**: Each Circle Wallet exists on one blockchain. For crosschain flows, provision one wallet per source blockchain. The onchain addresses differ. * **Dev-controlled SCA**: Each Circle Wallet exists on one blockchain. For crosschain flows, provision one SCA per source blockchain. The Circle Wallets adapter does not support [gas sponsorship](https://developers.circle.com/wallets/gas-station) for crosschain transfers that originate on Solana. For those transactions, the user's Solana wallet must hold sufficient SOL to pay network fees. Use the user-controlled adapter when end users approve wallet actions in your app. The adapter runs on your server. Challenge completion runs in the client with the [User-Controlled Wallets Web SDK](https://developers.circle.com/sdks/user-controlled/web-sdk). ## Setup Before creating a user-controlled wallet adapter, the user must have a configured signing method and credentials from your existing user-controlled wallet login flow. If not, adapter creation fails with Circle error code `155110`. The user-controlled adapter requires these server-side credentials: * **[API Key](https://developers.circle.com/w3s/circle-developer-account#creating-an-api-key-for-developer-services):** Environment-prefixed (examples: `TEST_API_KEY:abc123:def456`, `LIVE_API_KEY:xyz:uvw`) or Base64-encoded * **`userToken`:** From your existing UCW authentication flow. See [Authentication methods](https://developers.circle.com/wallets/user-controlled/authentication-methods). Your client also needs the `encryptionKey` from that same login (and your Circle App ID) to complete challenges with the Web SDK. The adapter does not take `encryptionKey`. This code block initializes the user-controlled Circle Wallets adapter: ```typescript TypeScript theme={null} import { createCircleUserWalletAdapter } from "@circle-fin/adapter-circle-wallets/ucw/server"; const adapter = await createCircleUserWalletAdapter({ apiKey: process.env.CIRCLE_API_KEY!, userToken, // from your existing UCW login chain: "Arc_Testnet", onChallenge: ({ challengeId }) => { // notify your client to complete the challenge }, }); ``` ## Usage When you use the user-controlled adapter in an App Kit operation, pass only `adapter` and `chain` in the operation context. Omit `address`; the adapter resolves the wallet address: ```typescript TypeScript theme={null} from: { adapter, chain: "Arc_Testnet", } ``` * **Account type:** The user-controlled adapter uses smart contract account (SCA) wallets. See [Account type comparison](https://developers.circle.com/wallets/account-types#account-type-comparison). * **Approval flow:** An App Kit method such as `kit.send` waits for user approval. The adapter calls `onChallenge` with a `challengeId`; use it to notify the client only. Do not return a signature. The client completes the challenge with the [Web SDK](https://developers.circle.com/sdks/user-controlled/web-sdk), while the adapter polls Circle until approval completes. Keep the client-side challenge UI and listener available for the entire duration of the server-side call. * **When challenges fire:** Passing `chain` or `chains` can find or create the user's SCA wallet, so a challenge may fire during adapter creation or a later App Kit operation. Crosschain operations may emit multiple challenges; keep listening until the operation finishes. * **Chain selection:** Pass `chain` for a single-chain operation. For crosschain operations, pass `chains` (an array of every blockchain involved) when creating the adapter. Use the same adapter instance for both the source (`from`) and destination (`to`): ```typescript TypeScript theme={null} from: { adapter, chain: "Arc_Testnet" }, to: { adapter, chain: "Base_Sepolia" }, ``` * **Challenge delivery:** Deliver each `challengeId` to the client through your own channel, such as SSE, WebSocket, or polling. # How-to: Collect custom bridge fees Source: https://docs.arc.io/app-kit/tutorials/bridge/collect-bridge-fee Configure custom fees to collect on bridge transfers The App Kit SDK lets you collect a custom fee from your end users on each bridge transfer. For how custom fees fit into the overall fees breakdown, see [How bridge fees work](/app-kit/concepts/bridge-fees). If you use this feature, Arc keeps 10% of the custom fee you collect from your end users. Custom fees are not supported with [`feePayment: "source"`](/app-kit/tutorials/bridge/pay-fees-on-source). Combining them (including a kit-level custom fee policy) throws a validation error. ## Prerequisites Before you begin, ensure that you've: * [Installed the App Kit SDK](/app-kit/tutorials/installation) * [Configured an adapter](/app-kit/tutorials/adapter-setups) These are required so any example below runs with a valid `kit` and `adapter`. ## Set a custom fee on a bridge This example adds a 0.10 USDC fee to a single bridge transfer: ```typescript TypeScript theme={null} const result = await kit.bridge({ from: { adapter, chain: "Ethereum_Sepolia" }, to: { adapter, chain: "Arc_Testnet" }, amount: "1.00", // Collect a fee of 0.10 USDC on this bridge transfer config: { customFee: { value: "0.10", recipientAddress: "0xYourFeeRecipientAddress", }, }, }); ``` # How-to: Configure transfer speed and maximum cost Source: https://docs.arc.io/app-kit/tutorials/bridge/configure-transfer-speed Set a transfer speed and maximum fee to balance performance and cost By default, the App Kit SDK uses CCTP's [Fast Transfer](https://developers.circle.com/cctp) feature to optimize for speed. However, [this can incur higher fees](https://developers.circle.com/cctp/technical-guide#fees). If performance is not a concern, you can specify a slow transfer speed, which uses CCTP's Standard Transfer feature. To balance performance and cost, you can set the maximum fee you want to pay on fast bridge transfers. The transfer switches to slow if the cost is above your limit. ## Prerequisites Before you begin, ensure that you've: * [Installed the App Kit SDK](/app-kit/tutorials/installation) * [Configured an adapter](/app-kit/tutorials/adapter-setups) These are required so any example below runs with a valid `kit` and `adapter`. ## Configure slow speed This code configures a slow transfer speed: ```typescript TypeScript theme={null} const result = await kit.bridge({ from: { adapter, chain: "Ethereum_Sepolia" }, to: { adapter, chain: "Arc_Testnet" }, amount: "1.00", config: { transferSpeed: "SLOW" }, }); ``` ## Configure maximum fee Set the maximum fee that you want to pay for the CCTP fast burn. For bridges from Arc Testnet, the amount must exceed the CCTPv2 max fee (around 1.4 USDC). If the amount is too low, the burn step can revert with `"Max fee must be less than amount"`. This code configures a maximum fee of 0.10 USDC: ```typescript TypeScript theme={null} const result = await kit.bridge({ from: { adapter, chain: "Ethereum_Sepolia" }, to: { adapter, chain: "Arc_Testnet", }, amount: "1.00", config: { transferSpeed: "FAST", // Fast transfer speed (can be omitted) maxFee: "0.10", // Max 0.10 USDC fee }, }); ``` # How-to: Estimate costs before bridging Source: https://docs.arc.io/app-kit/tutorials/bridge/estimate-costs Estimate gas and provider fees before making bridge transfers The App Kit SDK can estimate gas fees and provider fees before making a bridge transfer, and only proceed if the costs are acceptable. ## Prerequisites Before you begin, ensure that you've: * [Installed the App Kit SDK](/app-kit/tutorials/installation) * [Configured an adapter](/app-kit/tutorials/adapter-setups) These are required so any example below runs with a valid `kit` and `adapter`. ## Estimate costs This example estimates costs for a bridge transfer of 1.00 USDC from Ethereum Sepolia to Arc Testnet: ```typescript TypeScript theme={null} import type { BridgeParams } from "@circle-fin/app-kit"; const params: BridgeParams = { from: { adapter, chain: "Ethereum_Sepolia" }, to: { adapter, chain: "Arc_Testnet" }, amount: "1.00", }; // Estimate costs const estimate = await kit.estimateBridge(params); console.log( `Estimating ${params.amount} USDC from ${params.from.chain} to ${params.to.chain}`, ); console.log("Estimated fees:", estimate.fees); const providerFee = estimate.fees.find( (fee) => fee.type === "provider", )?.amount; // Only proceed if the provider fee is less than 0.10 USDC // Also proceed if the `providerFee` variable is null/undefined since there is no provider fee to charge if (providerFee == null || parseFloat(providerFee) < 0.1) { // Execute the bridge transfer const result = await kit.bridge(params); console.log("Bridge transfer completed:", result); } else { // Provider fee is 0.10 USDC or higher - abort the bridge transfer console.log("Provider fee is above threshold:", providerFee, "USDC"); } ``` # How-to: Pay fees on the source chain Source: https://docs.arc.io/app-kit/tutorials/bridge/pay-fees-on-source Pay CCTP fees on the source chain so the recipient receives the exact bridge amount By default, CCTP Fast Transfer and Forwarding Service fees are taken from the amount minted on the destination chain. You can instead pay those fees [upfront](https://developers.circle.com/cctp/concepts/upfront-fees) on the source chain so the recipient receives the exact bridge `amount`. For how this fits into the overall fee model, see [How bridge fees work](/app-kit/concepts/bridge-fees). ## Prerequisites Before you begin, ensure that you've: * [Installed the App Kit SDK](/app-kit/tutorials/installation) * [Configured an adapter](/app-kit/tutorials/adapter-setups) These are required so any example below runs with a valid `kit` and `adapter`. ## Estimate and bridge with source-paid fees This example estimates, then bridges 1.00 USDC from Ethereum Sepolia to Arc Testnet. The recipient is credited the exact `amount`; the source wallet pays `amount` plus the quoted fee. Source-paid fees use a **two-step** flow: 1. Call `estimateBridge` with `feePayment: "source"`. The result includes a `quote` — a signed, time-bound fee quote from Circle's Fee Service. 2. Call `bridge` with the **same** transfer parameters **and** that `quote`. The SDK submits the burn with the quote so the fee is collected on the source chain and the destination mint stays unreduced. ```typescript TypeScript theme={null} const estimate = await kit.estimateBridge({ from: { adapter, chain: "Ethereum_Sepolia" }, to: { adapter, chain: "Arc_Testnet", recipientAddress: "0xRecipientAddress", useForwarder: true, // Required with feePayment: "source" }, amount: "1.00", config: { feePayment: "source" }, }); // Pass estimate.quote into bridge so the burn uses the signed quote const result = await kit.bridge({ from: { adapter, chain: "Ethereum_Sepolia" }, to: { adapter, chain: "Arc_Testnet", recipientAddress: "0xRecipientAddress", useForwarder: true, }, amount: "1.00", config: { feePayment: "source" }, quote: estimate.quote, }); ``` When `feePayment` is `"source"`, TypeScript narrows the estimate as `ReceiveExactEstimateResult` (no cast or `in` narrowing needed). Without `feePayment: "source"`, the return type remains the plain estimate result. `quote` is the Fee Service's raw signed quote: a `0x`-prefixed, byte-aligned hex string. Treat it as opaque — do not decode, edit, or reconstruct it. Pass it through unchanged. When you pass `quote`, Bridge Kit validates that exact quote before and after approval and fails closed if it is invalid, mismatched, expired, or too close to expiry. It never silently replaces a caller-supplied quote. Omitting `quote` opts into SDK-managed fetching and refresh. On a supplied-quote failure, call `estimateBridge()` again and retry with the new quote. A successful estimate looks like this (values vary by route and quote): ```typescript TypeScript theme={null} { token: "USDC", amount: "1.0", amountReceived: "1.0", // Exact recipient amount (== amount) feeTotal: "0.076527", // Source-chain USDC fee (sum of feeItems) totalDebit: "1.076527", // amount + feeTotal fees: [ { type: "forwarder", token: "USDC", amount: "0.076397" }, { type: "provider", token: "USDC", amount: "0.00013" }, ], feeItems: [ { type: "FORWARD", amount: "0.076397", args: [/* ... */], argsHash: "0x..." }, { type: "PRE_FINALITY", amount: "0.00013", args: [/* ... */], argsHash: "0x..." }, ], quoteExpiry: { mode: "BLOCK_NUMBER", expiresAtBlock: 45947392, blockEstimatedAt: 1787663073, }, quote: "0x...", // Pass this hex string unchanged to kit.bridge // Also includes source, destination, and gasFees from the plain estimate } ``` `fees` is the familiar estimate breakdown (`forwarder` / `provider`). `feeItems` is the signed Fee Service line items (`FORWARD`, `PRE_FINALITY`) that back the upfront quote. Step events (`bridge.approve`, `bridge.burn`, `bridge.mint`, and others) fire on this path the same way as other bridge transfers. See [Bridge events](/app-kit/references/sdk-reference#bridge-events) in the SDK reference. ## Check which chains support source fees Not every bridge source supports paying fees on the source chain. List the ones that do: ```typescript TypeScript theme={null} const sourceFeeChains = kit.getSupportedChains("bridge", { sourceFeeSupported: true, }); console.log(sourceFeeChains.map((chain) => chain.chain)); ``` ## Requirements and limitations ### Forwarding Service is required `to.useForwarder: true` is required with `feePayment: "source"`. Omitting it (or setting it to `false`) throws a validation error. ### Custom fees are not supported You cannot combine `feePayment: "source"` with a per-call `config.customFee` or a kit-level custom fee policy (`setCustomFeePolicy`). The SDK throws by design. Collect [custom bridge fees](/app-kit/tutorials/bridge/collect-bridge-fee) only on the default (destination) fee path. ### Use `SLOW` on Standard Transfer–only sources `transferSpeed` defaults to `FAST`. Some source chains support only CCTP Standard Transfer (for example Avalanche, Polygon PoS, Sei, XDC, and their testnets). On those chains, keep `feePayment: "source"` and set `transferSpeed: "SLOW"`. Using `FAST` (or omitting `transferSpeed`) can fail with a Quote API error such as `PRE_FINALITY_UNAVAILABLE` (often surfaced as HTTP 422). ```typescript TypeScript theme={null} const result = await kit.bridge({ from: { adapter, chain: "Avalanche_Fuji" }, to: { adapter, chain: "Arc_Testnet", recipientAddress: "0xRecipientAddress", useForwarder: true, }, amount: "1.00", config: { feePayment: "source", transferSpeed: "SLOW", }, }); ``` ### Error results can preserve a successful burn If the burn confirms but the Forwarding Service / relayer returns no destination transaction hash, the SDK returns an error-state `BridgeResult` that still includes the burn step (retryable), instead of throwing away that progress. See [Error recovery](/app-kit/references/bridge-error-recovery) for how to inspect `result.state` and `result.steps`. # How-to: Specify a recipient address Source: https://docs.arc.io/app-kit/tutorials/bridge/specify-recipient-address Send bridged USDC to a different address on the destination chain By default, bridged USDC arrives at the same address as your signing wallet on the destination chain. For bridge transfers, you can specify a recipient address so the tokens are minted to that address instead. ## Prerequisites Before you begin, ensure that you've: * [Installed the App Kit SDK](/app-kit/tutorials/installation) * [Configured an adapter](/app-kit/tutorials/adapter-setups) These are required so any example below runs with a valid `kit` and `adapter`. ## Specify a recipient This example bridges 1.00 USDC from your wallet address on Ethereum Sepolia to a different address on Arc Testnet: ```typescript TypeScript theme={null} const result = await kit.bridge({ from: { adapter, chain: "Ethereum_Sepolia" }, to: { adapter, chain: "Arc_Testnet", recipientAddress: "0xRecipientAddress", // Specify a recipient address }, amount: "1.00", }); ``` # How-to: Use Forwarding Service Source: https://docs.arc.io/app-kit/tutorials/bridge/use-forwarding-service Use Forwarding Service on bridge transfers You can use the [Forwarding Service](https://developers.circle.com/cctp/concepts/forwarding-service) on bridge transfers. When enabled, it fetches the attestation and submits the mint on the destination chain. You don't need to poll for attestations or have access to a wallet on the destination chain. ## Prerequisites Before you begin, ensure that you've: * [Installed the App Kit SDK](/app-kit/tutorials/installation) * [Configured an adapter](/app-kit/tutorials/adapter-setups) These are required so any example below runs with a valid `kit` and `adapter`. ## Use with adapters on both chains Set `useForwarder: true` when you have adapters for both chains but want the Forwarding Service to submit the mint transaction on the destination: ```typescript TypeScript theme={null} const result = await kit.bridge({ from: { adapter, chain: "Ethereum_Sepolia" }, to: { adapter, chain: "Arc_Testnet", useForwarder: true, }, amount: "1.00", }); ``` ## Use without a destination adapter When you don't have access to a wallet on the destination chain, such as with server-side or custodial bridge flows, omit the destination adapter and pass `recipientAddress` with `useForwarder: true`: ```typescript TypeScript theme={null} const result = await kit.bridge({ from: { adapter, chain: "Ethereum_Sepolia" }, to: { recipientAddress: "0xRecipientAddress", chain: "Arc_Testnet", useForwarder: true, }, amount: "1.00", }); ``` In this mode, mint confirmation comes from the Circle Iris API response rather than an onchain receipt. Because the Forwarding Service submits the mint transaction, no locally signed transaction hash is returned and the mint step's `data` field is `undefined`. ## Forwarding fee The Forwarding Service charges a [fee](https://developers.circle.com/cctp/concepts/forwarding-service#fees-and-execution) that is deducted from the amount minted on the destination chain by default. When you [pay fees on the source chain](/app-kit/tutorials/bridge/pay-fees-on-source) (`config.feePayment: "source"`), that fee is collected upfront in source-chain USDC instead, and the recipient receives the exact bridge amount. When you [estimate costs](/app-kit/tutorials/bridge/estimate-costs) for a bridge transfer, the result includes the forwarding fee. See [How bridge fees work](/app-kit/concepts/bridge-fees) for details. # Install the App Kit SDK Source: https://docs.arc.io/app-kit/tutorials/installation Install the App Kit SDK and your preferred adapter to start building apps Installing the App Kit SDK gives you every capability (Send, Bridge, Swap, and Unified Balance) in a single package. To get started, install the latest version and the adapter that fits your environment. An API key from [Circle Console](https://console.circle.com/api-keys) is optional for [Swap](/app-kit/swap) but recommended for production. Without one, swap requests run against a rate limit. Add one if you hit rate limit errors or expect high request volume. Upgrading an existing Swap integration? Rename `kitKey` to `apiKey` in the swap config. ```bash npm theme={null} npm install @circle-fin/app-kit ``` ```bash yarn theme={null} yarn add @circle-fin/app-kit ``` The App Kit SDK includes every capability in a single package. Or, install kits individually: ```bash npm theme={null} npm install @circle-fin/bridge-kit ``` ```bash yarn theme={null} yarn add @circle-fin/bridge-kit ``` ```bash npm theme={null} npm install @circle-fin/swap-kit ``` ```bash yarn theme={null} yarn add @circle-fin/swap-kit ``` ```bash npm theme={null} npm install @circle-fin/unified-balance-kit ``` ```bash yarn theme={null} yarn add @circle-fin/unified-balance-kit ``` ```bash npm theme={null} npm install @circle-fin/adapter-viem-v2 viem ``` ```bash yarn theme={null} yarn add @circle-fin/adapter-viem-v2 viem ``` ```bash npm theme={null} npm install @circle-fin/adapter-ethers-v6 ethers ``` ```bash yarn theme={null} yarn add @circle-fin/adapter-ethers-v6 ethers ``` ```bash npm theme={null} npm install @circle-fin/adapter-solana-kit @solana/kit @solana/web3.js ``` ```bash yarn theme={null} yarn add @circle-fin/adapter-solana-kit @solana/kit @solana/web3.js ``` If bridging, you should also install an EVM adapter (Viem or Ethers). For the [Bridge tokens across blockchains](/app-kit/quickstarts/bridge-tokens-across-blockchains#solana) quickstart and [adapter setup](/app-kit/tutorials/adapter-setups#solana) examples, the same adapter is used with `@solana/kit` and `@solana/web3.js`. server-side only If you have a [Circle Wallets](https://developers.circle.com/wallets) account, you can use the Circle Wallets adapter to connect to developer-controlled wallets and Circle Contracts. ```bash npm theme={null} npm install @circle-fin/adapter-circle-wallets ``` ```bash yarn theme={null} yarn add @circle-fin/adapter-circle-wallets ``` # How-to: Collect custom swap fees Source: https://docs.arc.io/app-kit/tutorials/swap/collect-swap-fee Configure custom fees to collect on token swaps You can configure your swaps to collect a custom fee from end users on each swap call. For how custom fees fit into the overall fees breakdown, see [How swap fees work](/app-kit/concepts/swap-fees). If you use this feature, Arc keeps 10% of the custom fee you collect from your end users. ## Prerequisites Before you begin, ensure that you've: * [Installed the App Kit SDK](/app-kit/tutorials/installation) * [Configured an adapter](/app-kit/tutorials/adapter-setups) These are required so any example below runs with a valid `kit` and `adapter`. ## Set a custom fee on a swap This example adds a 1% (100 basis points) fee to a single swap call: ```typescript TypeScript theme={null} const output = await kit.swap({ from: { adapter, chain: "Arc_Testnet" }, tokenIn: "USDC", amountIn: "1.00", tokenOut: "EURC", config: { apiKey: process.env.CIRCLE_API_KEY as string, // optional — configure for production or high-volume usage customFee: { percentageBps: 100, // 1% fee recipientAddress: "0xYourFeeRecipientAddress", }, }, }); ``` # How-to: Estimate swap rate Source: https://docs.arc.io/app-kit/tutorials/swap/estimate-swap-rate Get a pre-swap estimate of the amount to receive before swapping You can get a pre-swap estimate of the amount you'll receive before swapping. ## Prerequisites Before you begin, ensure that you've: * [Installed the App Kit SDK](/app-kit/tutorials/installation) * [Configured an adapter](/app-kit/tutorials/adapter-setups) These are required so any example below runs with a valid `kit` and `adapter`. ## Estimate swap amount This example estimates how much EURC you'll receive when swapping 1.00 USDC for EURC: ```typescript TypeScript theme={null} const params = { from: { adapter, chain: "Arc_Testnet" }, tokenIn: "USDC", amountIn: "1.00", tokenOut: "EURC", config: { apiKey: process.env.CIRCLE_API_KEY as string, // optional — configure for production or high-volume usage }, }; // Estimate swap amount const estimate = await kit.estimateSwap(params); console.log(`Estimated output: ${estimate.estimatedOutput}`); // Proceed to swap const result = await kit.swap(params); ``` An estimate does not guarantee the actual amount you'll receive on a swap, which can vary due to market fluctuations. To avoid receiving less than expected, you can [set a slippage tolerance or stop limit](/app-kit/tutorials/swap/set-slippage-tolerance-or-stop-limit). # How-to: Get token rates Source: https://docs.arc.io/app-kit/tutorials/swap/get-token-rates Query cached USD prices for swap-supported tokens before quoting a swap Token rates are read-only cached USD prices for [supported tokens](/app-kit/references/supported-blockchains#supported-tokens). Use them to show a fiat estimate before a user confirms a swap, sort token lists by USD value, or compare a swap output against current market prices. ## Prerequisites Before you begin, ensure that you've: * [Installed the App Kit SDK](/app-kit/tutorials/installation) * Obtained an [API key](https://developers.circle.com/contracts/create-api-key) (optional, recommended for production or high-volume usage) ## Look up token rates Pass `chain` and a `tokens` array to look up rates for one or more tokens on the specified blockchain. The App Kit SDK supports [token aliases](/app-kit/references/supported-blockchains#supported-tokens) (such as `"USDC"`, `"EURC"`, `"USDT"`). ```typescript TypeScript theme={null} import { AppKit } from "@circle-fin/app-kit"; const kit = new AppKit(); const result = await kit.getTokenRates({ chain: "Ethereum", tokens: ["USDC"], apiKey: process.env.CIRCLE_API_KEY as string, // optional — configure for production or high-volume usage }); console.dir(result, { depth: null, colors: true }); ``` The response is keyed by blockchain, then by token address: ```bash Shell theme={null} { rates: { Ethereum: { "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48": { priceUSD: "0.999615", fetchedAt: 1781758931073, decimals: 6, }, }, }, } ``` Each rate includes: * `priceUSD`: Latest cached USD price for the token, as a decimal string. * `fetchedAt`: Unix timestamp (milliseconds) of when the price was last refreshed. Use this to decide whether to refetch or warn the user about a stale quote. * `decimals`: Number of decimal places for the token. If a cached price doesn't exist for a requested token, that token's address is omitted from the blockchain's rate map. ## List all token rates Omit `tokens` to get every cached rate for the specified blockchain. This is useful for populating a token picker or refreshing a price column: ```typescript TypeScript theme={null} import { AppKit } from "@circle-fin/app-kit"; const kit = new AppKit(); const result = await kit.getTokenRates({ chain: "Base", apiKey: process.env.CIRCLE_API_KEY as string, // optional — configure for production or high-volume usage }); console.dir(result, { depth: null, colors: true }); ``` The response has the same shape as the single-token lookup, with one entry per cached token: ```bash Shell theme={null} { rates: { Base: { "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": { priceUSD: "1.000112", fetchedAt: 1781758931073, decimals: 6, }, "0xd46f9d14bd720f10714e2a2af5e0d617b2a0ba8c": { priceUSD: "0.000002619246974", fetchedAt: 1781829306975, decimals: 18, }, "0xae8fc9288685516d2eca717056ca69303b348752": { priceUSD: "74227.78149573994", fetchedAt: 1781829306975, decimals: 18, }, }, }, } ``` # How-to: Set slippage tolerance or stop limit on swaps Source: https://docs.arc.io/app-kit/tutorials/swap/set-slippage-tolerance-or-stop-limit Set a slippage or stop limit to protect against rate changes Set a slippage tolerance or stop limit to avoid receiving less than expected on a swap due to market fluctuations: * A **slippage tolerance** lets you set the maximum percentage difference you're willing to accept between the estimated and actual swap amount, expressed in bps (for example, 100 bps = 1%). * A **stop limit** lets you set the exact minimum amount you want to receive. If both are configured, the stop limit takes precedence. ## Prerequisites Before you begin, ensure that you've: * [Installed the App Kit SDK](/app-kit/tutorials/installation) * [Configured an adapter](/app-kit/tutorials/adapter-setups) These are required so any example below runs with a valid `kit` and `adapter`. ## Set slippage tolerance This example sets a slippage tolerance of 100 bps (1%): ```typescript TypeScript theme={null} const output = await kit.swap({ from: { adapter, chain: "Arc_Testnet" }, tokenIn: "USDC", amountIn: "1.00", tokenOut: "EURC", config: { apiKey: process.env.CIRCLE_API_KEY as string, // optional — configure for production or high-volume usage slippageBps: 100, // Set the slippage tolerance to 100 bps }, }); ``` With `slippageBps` as `100` as in the example, you'll receive at least 99% of the estimated amount. Meaning if the swap rate is 1 USDC to 1 EURC, you'll receive at least 0.99 EURC. The `slippageBps` default is 300 bps. `0` represents no tolerance, which increases transaction failure rates. ## Set stop limit `stopLimit` defines the minimum amount of the token type specified in `tokenOut` that you'll receive on a swap. This example sets a stop limit of 0.95 EURC, meaning you won't receive any less than that on a swap: ```typescript TypeScript theme={null} const output = await kit.swap({ from: { adapter, chain: "Arc_Testnet" }, tokenIn: "USDC", amountIn: "1.00", tokenOut: "EURC", config: { apiKey: process.env.CIRCLE_API_KEY as string, // optional — configure for production or high-volume usage stopLimit: "0.95", // Set stop limit to 0.95 EURC }, }); ``` # How-to: Specify a recipient address Source: https://docs.arc.io/app-kit/tutorials/swap/specify-recipient-address Send swapped tokens to a different address on the same chain By default, the tokens you receive from a swap arrive at the same address as your signing wallet. For same-chain swaps, you can specify a different recipient so the output tokens go to that address instead. Your signing wallet still signs and funds the swap. For crosschain swaps, see [Swap tokens across blockchains](/app-kit/quickstarts/swap-tokens-crosschain). ## Prerequisites Before you begin, ensure that you've: * [Installed the App Kit SDK](/app-kit/tutorials/installation) * [Configured an adapter](/app-kit/tutorials/adapter-setups) These are required so any example below runs with a valid `kit` and `adapter`. ## Specify a recipient Set the recipient with the `to` parameter. For a same-chain swap, `to.chain` defaults to the source chain (`from.chain`), so you only need to set `to.recipientAddress`. This example swaps 1.00 USDC for EURC on Arc Testnet and sends the EURC to a different address than the one that signs the swap: ```typescript TypeScript theme={null} const result = await kit.swap({ from: { adapter, chain: "Arc_Testnet" }, tokenIn: "USDC", tokenOut: "EURC", amountIn: "1.00", to: { recipientAddress: "0xRecipientAddress", }, config: { apiKey: process.env.CIRCLE_API_KEY as string, // optional — configure for production or high-volume usage }, }); ``` `to.recipientAddress` sets where the output tokens are sent. It's different from `config.customFee.recipientAddress`, which sets the recipient of a [custom swap fee](/app-kit/tutorials/swap/collect-swap-fee). # How-to: Check Unified Balance total Source: https://docs.arc.io/app-kit/tutorials/unified-balance/check-unified-balance Check your Unified Balance total and how much USDC sits on each source blockchain Check how much USDC is in your Unified Balance in total and on each source blockchain. ## Prerequisites Before you begin, ensure that you've: * [Installed the App Kit SDK](/app-kit/tutorials/installation) * [Configured an adapter](/app-kit/tutorials/adapter-setups) These are required so any example below runs with a valid `kit` and `adapter`. ## Check balances by adapter Query balances with the same adapters you use for deposits. If you query more than one signing ecosystem, such as EVM and Solana, pass one source for each adapter. Omitting the chains parameter returns balances for all supported chains available to that source and `networkType`. ```typescript TypeScript theme={null} const balances = await kit.unifiedBalance.getBalances({ sources: [{ adapter: evmAdapter }, { adapter: solanaAdapter }], }); // Log confirmed and pending totals plus per-depositor, per-chain breakdown. console.dir(balances, { depth: null }); ``` You will see an output similar to: ```bash Shell expandable theme={null} { token: "USDC", totalConfirmedBalance: "166.954132", breakdown: [ { depositor: "1234abcd1234abcd1234abcd1234abcd1234abcd1234", totalConfirmed: "13.124021", breakdown: [ { chain: "Solana", confirmedBalance: "13.124021", }, ], }, { depositor: "0x0123abcd0123abcd0123abcd0123abcd0123abcd", totalConfirmed: "153.830111", breakdown: [ { chain: "Ethereum", confirmedBalance: "11.998900", }, { chain: "Base", confirmedBalance: "19.926391", }, { chain: "Avalanche", confirmedBalance: "19.458595", }, { chain: "Arbitrum", confirmedBalance: "9.989950", }, { chain: "Sonic", confirmedBalance: "11.939700", }, { chain: "World_Chain", confirmedBalance: "10.929750", }, { chain: "Sei", confirmedBalance: "25.891695", }, { chain: "HyperEVM", confirmedBalance: "9.699800", }, { chain: "Optimism", confirmedBalance: "9.998450", }, { chain: "Polygon", confirmedBalance: "13.997930", }, { chain: "Unichain", confirmedBalance: "9.998950", } ], }, ], } ``` ## Check balances by address You can also query by account address. This is useful when you need to inspect an account's Unified Balance without signing. ```typescript TypeScript theme={null} const balances = await kit.unifiedBalance.getBalances({ sources: { address: "0xWalletAddress", chains: ["Base_Sepolia"], }, }); console.dir(balances, { depth: null }); ``` You will see an output similar to: ```bash Shell theme={null} { token: "USDC", totalConfirmedBalance: "0.082000", breakdown: [ { depositor: "0xWalletAddress", totalConfirmed: "0.082000", breakdown: [ { chain: "Base_Sepolia", confirmedBalance: "0.082000" } ] } ] } ``` ## Query specific chains Use `chains` to limit the response to the chains you care about. If you omit `chains`, App Kit returns balances for the supported chains available to that source and `networkType`. Note that all chains in the array must be on the same network, you cannot mix testnet and mainnet chains. ```typescript TypeScript theme={null} const balances = await kit.unifiedBalance.getBalances({ sources: { adapter, chains: ["Base_Sepolia", "Arc_Testnet"], }, }); ``` You will see an output similar to: ```bash Shell theme={null} { token: "USDC", totalConfirmedBalance: "32.207843", breakdown: [ { depositor: "0x0123abcd0123abcd0123abcd0123abcd0123abcd", totalConfirmed: "32.207843", breakdown: [ { chain: "Arc_Testnet", confirmedBalance: "12.281452" }, { chain: "Base_Sepolia", confirmedBalance: "19.926391" } ] } ] } ``` ## Select a network Set `networkType` to query testnet balances. If you omit `networkType`, App Kit uses mainnet when chains cannot be derived from the source. This is not an issue if you include the chains parameter. ```typescript TypeScript theme={null} const balances = await kit.unifiedBalance.getBalances({ sources: { address: "0xWalletAddress" }, networkType: "testnet", }); ``` You will see an output similar to: ```bash Shell expandable theme={null} { token: "USDC", totalConfirmedBalance: "0.282000", breakdown: [ { depositor: "0xWalletAddress", totalConfirmed: "0.282000", breakdown: [ { chain: "Ethereum_Sepolia", confirmedBalance: "0.000000", }, { chain: "Base_Sepolia", confirmedBalance: "0.200000", }, { chain: "Avalanche_Fuji", confirmedBalance: "0.000000", }, { chain: "Arbitrum_Sepolia", confirmedBalance: "0.000000", }, { chain: "Sonic_Testnet", confirmedBalance: "0.000000", }, { chain: "World_Chain_Sepolia", confirmedBalance: "0.000000", }, { chain: "Sei_Testnet", confirmedBalance: "0.000000", }, { chain: "HyperEVM_Testnet", confirmedBalance: "0.000000", }, { chain: "Arc_Testnet", confirmedBalance: "0.082000", }, { chain: "Optimism_Sepolia", confirmedBalance: "0.000000", }, { chain: "Polygon_Amoy_Testnet", confirmedBalance: "0.000000", }, { chain: "Unichain_Sepolia", confirmedBalance: "0.000000", } ], } ], } ``` ## Include pending balances Set `includePending: true` to include pending balance totals and pending transaction details in the response. ```typescript TypeScript theme={null} const balances = await kit.unifiedBalance.getBalances({ sources: { adapter }, includePending: true, }); ``` You will see an output similar to: ```bash Shell theme={null} { token: "USDC", totalConfirmedBalance: "0.000000", breakdown: [ { depositor: "1234abcd1234abcd1234abcd1234abcd1234abcd1234", totalConfirmed: "0.000000", breakdown: [ { chain: "Solana", confirmedBalance: "0.000000", pendingBalance: "0.000000", pendingTransactions: [], } ], totalPending: "0.000000", } ], totalPendingBalance: "0.000000", } ``` ## Query multiple sources Pass an array when you need balances across multiple sources. Each source can use an adapter or an address. ```typescript TypeScript theme={null} const balances = await kit.unifiedBalance.getBalances({ sources: [ { adapter: evmAdapter, chains: ["Arc_Testnet"], }, { address: "SolanaWalletAddress", chains: ["Solana_Devnet"], }, ], }); ``` You will see an output similar to: ```bash Shell theme={null} { token: "USDC", totalConfirmedBalance: "25.405473", breakdown: [ { depositor: "0x0123abcd0123abcd0123abcd0123abcd0123abcd", totalConfirmed: "12.281452", breakdown: [ { chain: "Arc_Testnet", confirmedBalance: "12.281452", } ], }, { depositor: "SolanaWalletAddress", totalConfirmed: "13.124021", breakdown: [ { chain: "Solana_Devnet", confirmedBalance: "13.124021", } ], } ], } ``` # How-to: Collect custom spend fees Source: https://docs.arc.io/app-kit/tutorials/unified-balance/collect-custom-spend-fees Configure custom fees to collect when spending from a Unified Balance The App Kit SDK lets you collect a custom fee from your end users each time they spend from a Unified Balance. Custom fees can only be configured for spend transactions on destination blockchains, not deposits on source blockchains. To learn how custom fees fit into the overall fee breakdown, see [How Unified Balance fees work](/app-kit/concepts/unified-balance-fees). If you use this feature, Arc keeps 10% of the custom fee you collect from your end users. ## Prerequisites Before you begin, ensure that you've: * [Installed the App Kit SDK](/app-kit/tutorials/installation) * [Configured an adapter](/app-kit/tutorials/adapter-setups) These are required so any example below runs with a valid `kit` and `adapter`. ## Set a custom fee on a spend This example spends 1.00 USDC from Base Sepolia and adds a 0.01 USDC custom fee on the spend: ```typescript TypeScript theme={null} const result = await kit.unifiedBalance.spend({ amount: "1.00", from: { adapter, allocations: [{ amount: "1.00", chain: "Base_Sepolia" }], }, to: { adapter, chain: "Arc_Testnet", recipientAddress: "0xRecipientAddress", }, config: { customFee: { value: "0.01", // 0.01 USDC collected as fee recipientAddress: "0xYourFeeWalletAddress", }, }, }); ``` # How-to: Estimate spend fees Source: https://docs.arc.io/app-kit/tutorials/unified-balance/estimate-spend-fees Estimate fees before spending from a Unified Balance The App Kit SDK can provide an estimate of the fees you'll incur before spending from a Unified Balance. ## Prerequisites Before you begin, ensure that you've: * [Installed the App Kit SDK](/app-kit/tutorials/installation) * [Configured an adapter](/app-kit/tutorials/adapter-setups) These are required so any example below runs with a valid `kit` and `adapter`. ## Estimate fees before spending This sample estimates then spends 1.00 USDC from Base Sepolia to Arc Testnet when you specify explicit amounts from a source blockchain: For automatic routing (no explicit `allocations`), see [Select source blockchains](/app-kit/tutorials/unified-balance/select-source-blockchains). ```typescript TypeScript theme={null} const params = { amount: "1.00", from: { adapter, allocations: [{ amount: "1.00", chain: "Base_Sepolia" }], }, to: { adapter, chain: "Arc_Testnet", recipientAddress: "0xRecipientAddress", }, }; const estimate = await kit.unifiedBalance.estimateSpend(params); console.log("Estimated fees:", estimate.fees); const result = await kit.unifiedBalance.spend(params); ``` Estimated fees may differ from actual fees due to network conditions at execution time. Review the estimate before proceeding. ### Example fee response The JSON below shows the shape of the fees array. ```json JSON theme={null} { "fees": [ { "type": "provider", "token": "USDC", "amount": "0.00005", "allocations": [{ "chain": "Base Sepolia", "amount": "0.00005" }] }, { "type": "gasFee", "token": "USDC", "amount": "3.311005", "allocations": [ { "chain": "Ethereum Sepolia", "amount": "3.30" }, { "chain": "Base Sepolia", "amount": "0.011005" } ] }, { "type": "kit", "token": "USDC", "amount": ".1", "allocations": [{ "chain": "Ethereum Sepolia", "amount": ".1" }], "recipientAddress": "0x2222222222222222222222222222222222222222" } ] } ``` Fee `type` values can include: * **`provider`**: Protocol transfer fee when the spend is crosschain. Not charged for same-chain spends. * **`gasFee`**: Onchain gas paid on source blockchains as part of the spend. * **`kit`**: Developer custom fee from your [custom fee policy](/app-kit/tutorials/unified-balance/collect-custom-spend-fees). * **`forwarder`**: Forwarding Service fee when the spend [uses the forwarder](/app-kit/tutorials/unified-balance/use-forwarding-service) (not shown in the sample above). See [How Unified Balance fees work](/app-kit/concepts/unified-balance-fees) for a conceptual fee breakdown. # How-to: Manage delegates Source: https://docs.arc.io/app-kit/tutorials/unified-balance/manage-delegates Check, add, and remove delegates for a Unified Balance A delegate is an address that you authorize to spend from your Unified Balance on a given blockchain. In practice, a delegate is often a backend service signing spends on behalf of a user. How delegates work: * Delegation is blockchain-specific. * Authorizing a delegate on one blockchain does not grant them access on other blockchains. * An authorized delegate can call `spend` with `sourceAccount` set to your address so funds are drawn from your Unified Balance. You can check, add, and remove delegates at any time. For an end-to-end flow on how a delegate can deposit and spend from a Unified Balance, see the [delegate quickstart](/app-kit/quickstarts/unified-balance-delegate-deposit-and-spend). ## Prerequisites Before you begin, ensure that you've: * [Installed the App Kit SDK](/app-kit/tutorials/installation) * [Configured an adapter](/app-kit/tutorials/adapter-setups) These are required so any example below runs with a valid `kit` and `adapter`. ## Check delegate status The following example reads delegate status for an address. ```typescript TypeScript theme={null} const status = await kit.unifiedBalance.getDelegateStatus({ from: { adapter, chain: "Base_Sepolia" }, delegateAddress: "0xDelegateAddress", }); console.log("Delegate status:", status); // 'none' | 'pending' | 'ready' ``` `getDelegateStatus` resolves to `'none'` when no delegate is set, `'pending'` while delegation is still confirming, and `'ready'` when the delegate is active and authorized to spend. ### Poll until ready Use `getDelegateStatus` in a poll loop to wait until the delegate is active before spending. Confirmation time varies by blockchain. For example, on Ethereum, Base, and Arbitrum it can take up to 15 minutes, while on Arc and Avalanche it is near-instant. If the delegate was added well before the spend, `getDelegateStatus` returns `'ready'` immediately and no polling is needed. This example polls until the delegate is ready before spending: ```typescript TypeScript theme={null} let status = await kit.unifiedBalance.getDelegateStatus({ from: { adapter, chain: "Base_Sepolia" }, delegateAddress: "0xDelegateAddress", }); while (status === "pending") { await new Promise((r) => setTimeout(r, 10_000)); // wait 10 seconds status = await kit.unifiedBalance.getDelegateStatus({ from: { adapter, chain: "Base_Sepolia" }, delegateAddress: "0xDelegateAddress", }); } if (status === "ready") { console.log("Delegate is ready. Safe to spend."); } ``` ## Add a delegate The following example authorizes a delegate to spend from a Unified Balance on Base Sepolia. ```typescript TypeScript theme={null} await kit.unifiedBalance.addDelegate({ from: { adapter, chain: "Base_Sepolia" }, delegateAddress: "0xDelegateAddress", }); console.log("Delegate added."); ``` Delegation is blockchain-specific. To authorize a delegate on multiple blockchains, call `addDelegate` for each blockchain. ## Remove a delegate This example removes a delegate from a Unified Balance on Base Sepolia: ```typescript TypeScript theme={null} await kit.unifiedBalance.removeDelegate({ from: { adapter, chain: "Base_Sepolia" }, delegateAddress: "0xDelegateAddress", }); console.log("Delegate removed."); ``` # How-to: Remove funds trustlessly Source: https://docs.arc.io/app-kit/tutorials/unified-balance/remove-funds-trustlessly Initiate and complete a trustless withdrawal from a Unified Balance The App Kit SDK supports trustless withdrawals from a Unified Balance as a recovery path that keeps funds under your control. Trustless removal requires two onchain transactions: first, call `initiateRemoveFund` to start the seven-day waiting period. After the waiting period ends, call `removeFund` to return the funds to your wallet. `removeFund` is designed as a trustless escape hatch for fallback or recovery scenarios only, such as when your normal spend path is unavailable. It is not intended for day-to-day payouts, withdrawals, or crosschain movement. In normal situations, [use `spend`](/app-kit/quickstarts/unified-balance-deposit-and-spend) to move funds instantly from a Unified Balance. ## Prerequisites Before you begin, ensure that you've: * [Installed the App Kit SDK](/app-kit/tutorials/installation) * [Configured an adapter](/app-kit/tutorials/adapter-setups) These are required so any example below runs with a valid `kit` and `adapter`. ## Initiate and complete a removal First, call `initiateRemoveFund` to record the request to remove funds and start the seven-day waiting period. This example initiates a removal of 1 USDC on Base Sepolia: ```typescript TypeScript theme={null} const initiateResult = await kit.unifiedBalance.initiateRemoveFund({ from: { adapter, chain: "Base_Sepolia", }, amount: "1.00", }); console.log("Remove fund initiated:", initiateResult); ``` `initiateRemoveFund` returns a result object that includes the transaction details for the pending removal. In step 2, pass the same `adapter` and `chain` values to `removeFund` to complete it (you can reuse the same variables as in this example). After the seven-day waiting period ends, call `removeFund` to return funds to the wallet for that adapter on that blockchain: ```typescript TypeScript theme={null} const removeResult = await kit.unifiedBalance.removeFund({ from: { adapter, chain: "Base_Sepolia", }, }); console.log("Remove fund complete:", removeResult); ``` Keep these constraints in mind for EVM removals: * Calling `removeFund` before the waiting period has elapsed fails with an error. Wait seven days after calling `initiateRemoveFund` before calling `removeFund`. * Funds go to the wallet associated with the adapter on the specified blockchain. * Only one pending `removeFund` request is allowed per blockchain and address. First, call `initiateRemoveFund` to record the request to remove funds and start the seven-day waiting period. This example initiates a removal of 1 USDC on Solana Devnet: ```typescript TypeScript theme={null} const from = { adapter, chain: "Solana_Devnet" as const }; const initiateResult = await kit.unifiedBalance.initiateRemoveFund({ from, amount: "1.00", }); console.log("Initiate result:", initiateResult); ``` After the seven-day waiting period ends, use the same `from` context as step 1 to call `removeFund` and complete the removal: ```typescript TypeScript theme={null} const removeResult = await kit.unifiedBalance.removeFund({ from }); console.log("Remove result:", removeResult); ``` Keep these constraints in mind for Solana removals: * Calling `removeFund` before the waiting period has elapsed fails with an error. Wait seven days after calling `initiateRemoveFund` before calling `removeFund`. * Funds return to the wallet for the Solana adapter you use in `from`. * The sample uses `"Solana_Devnet" as const` so TypeScript narrows the chain type for `from`. # How-to: Select source blockchains for a Unified Balance spend Source: https://docs.arc.io/app-kit/tutorials/unified-balance/select-source-blockchains Control how USDC is drawn from your Unified Balance across blockchains when you spend, using automatic routing or explicit amounts A spend pulls USDC from the balances you have deposited on supported blockchains. Choose one of these approaches to control which blockchains supply the USDC: * **Automatic routing**: Specify a total spend amount and let the App Kit SDK choose which blockchains to draw from based on your confirmed balances. * **Explicit amounts**: Set how much USDC comes from each source blockchain yourself. ## Prerequisites Before you begin, ensure that you've: * [Installed the App Kit SDK](/app-kit/tutorials/installation) * [Configured an adapter](/app-kit/tutorials/adapter-setups) These are required so any example below runs with a valid `kit` and `adapter`. ## The App Kit SDK allocates for you The App Kit SDK chooses how to fund the spend from your confirmed balances. The kit prefers the destination blockchain first, then pulls from your other blockchains from highest balance to lowest. Ethereum mainnet is the exception: it is always last, including when it is the spend destination. Pass `amount`, `from` with only an `adapter` (no `allocations`), and `to`. This example spends 2.00 USDC with automatic routing: ```typescript TypeScript theme={null} const result = await kit.unifiedBalance.spend({ amount: "2.00", from: { adapter }, to: { adapter, chain: "Arc_Testnet", recipientAddress: "0xRecipientAddress", }, }); ``` ## Explicit per-chain allocations List each blockchain and how much USDC to draw in `from.allocations`. Each entry contains an `amount` and `chain`. When you use `allocations`, their amounts must add up to the top-level `amount`. This example spends 2.00 USDC (1.00 USDC from Arc Testnet and 1.00 USDC from Base Sepolia) delivered to the recipient on Arc Testnet: ```typescript TypeScript theme={null} const result = await kit.unifiedBalance.spend({ amount: "2.00", from: { adapter, allocations: [ { amount: "1.00", chain: "Arc_Testnet" }, { amount: "1.00", chain: "Base_Sepolia" }, ], }, to: { adapter, chain: "Arc_Testnet", recipientAddress: "0xRecipientAddress", }, }); ``` You can pass multiple adapters in `from` when you need separate sources (for example EVM and Solana). Each source uses the same shape: `adapter` and optional `allocations`. The top-level `amount` must still match the sum of all allocation amounts you provide. ## Validation rules * **Amount**: The top-level `amount` is always required. It is the total USDC for the spend. * **Sums**: If you pass `allocations`, the kit checks that they sum to `amount`. If they do not match, you get a clear error with both values. * **Consistency**: Either every `from` entry specifies `allocations`, or none do. Mixing sources with allocations and sources without is not supported. If you need a computed split first, use `estimateSpend`, then pass the returned `allocations` into `spend`. * **Retry**: Retrying only the mint step after a failure uses a separate `retrySpend` flow and parameters, not a partial `spend` call. # How-to: Use fast deposits Source: https://docs.arc.io/app-kit/tutorials/unified-balance/use-fast-deposits Deposit USDC into a Unified Balance from a source blockchain onto a fast finality destination without waiting for source chain finality Standard deposits wait for source chain finality, which can take 15 minutes or longer. Fast deposits use [CCTP's Fast Transfer](https://developers.circle.com/cctp/concepts/finality-and-block-confirmations#fast-transfer-attestation-times) to credit the destination in seconds while the source settles in the background. They are opt-in: pass `config: { transferSpeed: "FAST" }` with `to`. Omitting it fails validation rather than falling back to a slower deposit. ## Prerequisites Before you begin, ensure that you've: * [Installed the App Kit SDK](/app-kit/tutorials/installation) * [Configured an adapter](/app-kit/tutorials/adapter-setups) These are required so any example below runs with a valid `kit` and `adapter`. ## Supported blockchains The following blockchains support a fast transfer as the source or destination. This list differs from Unified Balance's [supported blockchains](/app-kit/references/supported-blockchains): some sources are not full deposit blockchains, and only a subset of Unified Balance blockchains are fast deposit destinations. For chain identifiers used in code, see [Chain identifiers](/app-kit/references/supported-blockchains#chain-identifiers). | | Mainnet | Testnet | | ----------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | Source | Arbitrum, Codex, Ethereum, Ink, Linea, OP Mainnet, Plume, Unichain, World Chain | Arbitrum Sepolia, Codex Testnet, Ethereum Sepolia, Ink Testnet, Linea Sepolia, OP Sepolia, Plume Testnet, Unichain Sepolia, World Chain Sepolia | | Destination | Avalanche, Polygon PoS | Avalanche Fuji, Polygon PoS Amoy, Arc Testnet | ## Optional: set an allowance in advance A fast deposit uses the `TokenMessengerWithFees` contract to spend the deposit amount and forwarder fee from your wallet. If the remaining USDC allowance is too low, the kit submits an approval transaction and waits for it to be mined before it submits the burn transaction. To remove that wait from the deposit path, set an allowance before you estimate and execute the deposit. The allowance applies only to the same signing wallet and source blockchain. Each fast deposit consumes part of it, so the optimization lasts only while the remaining allowance covers the deposit amount plus the forwarder fee. All supported EVM source blockchains share the same address in an environment: | Environment | `TokenMessengerWithFees` address | | ----------- | -------------------------------------------- | | Mainnet | `0x71f54F818671cD0D7ea140Da213e5C8b5C92a408` | | Testnet | `0x8745D906D67C346E5eb1aEEED38Eb87F34DF0C0A` | Approval isn't part of the Unified Balance API. This example uses a user-controlled EVM adapter on Ethereum Sepolia: ```typescript TypeScript theme={null} import { EthereumSepolia } from "@circle-fin/app-kit/chains"; const sourceChain = EthereumSepolia; const tokenMessengerWithFees: string = "0x8745D906D67C346E5eb1aEEED38Eb87F34DF0C0A"; const maxUint256: bigint = 2n ** 256n - 1n; function isEvmTransactionHash(value: string): value is `0x${string}` { return /^0x[0-9a-fA-F]{64}$/.test(value); } const approval = await adapter.prepareAction( "usdc.approve", { amount: maxUint256, delegate: tokenMessengerWithFees }, { chain: sourceChain }, ); const approvalTransactionHash: string = await approval.execute(); if (!isEvmTransactionHash(approvalTransactionHash)) { throw new Error("The approval returned an invalid EVM transaction hash."); } type ApprovalReceipt = Awaited>; const approvalReceipt: ApprovalReceipt = await adapter.waitForTransaction( approvalTransactionHash, { timeout: 60_000 }, sourceChain, ); if (approvalReceipt.status !== "success") { throw new Error("The USDC approval transaction reverted."); } ``` `maxUint256` grants the largest possible `uint256` USDC allowance. Later fast deposits from the same wallet and source blockchain can skip the approval wait. The allowance stays active until you change or revoke it. Use a bounded amount if you want to limit how much USDC the contract can spend. Pre-approving moves the approval cost earlier; it doesn't remove the cost. If you use a developer-controlled adapter, also include its wallet `address` in the `prepareAction` context. ## Estimate fees before depositing Fast deposits carry two fees: * **Gas fee**: the cost to submit the source transaction. Paid in the source blockchain's native token, such as ETH. * **Forwarder fee**: charged in USDC from your source chain wallet on top of the deposit amount. Your wallet needs deposit + fee available; the full deposit amount arrives on the destination chain. Use `estimateDeposit` to preview fees before committing. The returned `quote` locks in the forwarder fee, preventing fee changes from affecting your deposit. For broader fee estimation guidance, see [Estimate spend fees](/app-kit/tutorials/unified-balance/estimate-spend-fees). ```typescript TypeScript theme={null} const estimate = await kit.unifiedBalance.estimateDeposit({ from: { adapter, chain: "Ethereum_Sepolia" }, amount: "10", token: "USDC", to: { chain: "Arc_Testnet" }, config: { transferSpeed: "FAST" }, }); console.log(estimate.fees); // [ // { type: 'gasFee', token: 'ETH', amount: '0.0012' }, // { type: 'forwarder', token: 'USDC', amount: '0.50' }, // ] ``` The gas fee estimate may differ from the actual fee due to network conditions at execution time. Review the estimate before proceeding. ## Execute the fast deposit Pass the `estimate` object into `deposit()` to reuse the locked fee quote. If you omit the quote, the kit fetches a fresh one. If a supplied quote expired, `deposit()` throws an error. Call `estimateDeposit()` again and pass the new estimate without changing its quoted fields. ```typescript TypeScript theme={null} const result = await kit.unifiedBalance.deposit({ ...estimate, // spreads amount, to, config, and quote from: { adapter, chain: "Ethereum_Sepolia" }, }); ``` `allowanceStrategy` cannot be used with the `to` parameter. Fast deposits manage USDC approval internally. A fast finality source blockchain (for example Avalanche or Polygon PoS) is rejected rather than silently falling back to a standard deposit. Use a [supported source](#supported-blockchains) instead. ## Deposit for another account `depositFor` is permissionless. Any wallet can fund another account's Unified Balance. Estimate first, then pass the same `to` and `config` fields as `deposit`: ```typescript TypeScript theme={null} const estimate = await kit.unifiedBalance.estimateDeposit({ from: { adapter, chain: "Ethereum_Sepolia" }, amount: "1.00", token: "USDC", to: { chain: "Arc_Testnet" }, config: { transferSpeed: "FAST" }, }); const result = await kit.unifiedBalance.depositFor({ ...estimate, // spreads amount, to, config, and quote from: { adapter, chain: "Ethereum_Sepolia" }, depositAccount: "0xDepositAccountAddress", }); ``` ## Handle the deposit result Circle's relayer runs in the background. `deposit()` returns a `progress` field alongside the transaction details: | Status | Meaning | | --------- | --------------------------------------------------------------------------------------------------------------- | | `DONE` | The relayer confirmed the destination deposit. `txHash` is the relay transaction hash. | | `PENDING` | The kit timed out (\~60 s). `txHash` is the submitted source transaction, whose receipt might still be pending. | | `FAILED` | The source burn completed, but the relayer returned a terminal destination failure. | ```typescript TypeScript theme={null} switch (result.progress?.status) { case "DONE": console.log("Deposited. Relay tx:", result.txHash); break; case "PENDING": console.log("Transfer in progress. Source tx:", result.txHash); break; case "FAILED": console.error( "Relay failed. Contact support with source tx:", result.txHash, ); break; } ``` `PENDING` doesn't confirm that the source transaction succeeded. Check the source `txHash` before you act. Don't submit another deposit while its receipt is pending or after the receipt succeeds. For receipt, polling, and recovery guidance, see [Troubleshoot Unified Balance deposits](/app-kit/references/unified-balance-error-recovery). Example result shape: ```json JSON theme={null} { "amount": "1.00", "token": "USDC", "depositedTo": "0xDepositAccountAddress", "depositedBy": "0xSignerAddress", "chain": "Arc_Testnet", "txHash": "0x…", "explorerUrl": "https://testnet.arcscan.app/tx/0x…", "sourceChain": "Ethereum_Sepolia", "destinationChain": "Arc_Testnet", "fees": [ { "type": "gasFee", "token": "ETH", "amount": "0.000689…" }, { "type": "forwarder", "token": "USDC", "amount": "0.023517" } ], "progress": { "status": "DONE" } } ``` # How-to: Use Forwarding Service Source: https://docs.arc.io/app-kit/tutorials/unified-balance/use-forwarding-service Use Forwarding Service when spending from a Unified Balance You can use the [Forwarding Service](https://developers.circle.com/cctp/concepts/forwarding-service) when spending from a Unified Balance on the destination blockchain. When enabled, it fetches the attestations from source blockchains and submits the mint on the destination blockchain. You don't need to poll for attestations or have access to a wallet on the destination. ## Prerequisites Before you begin, ensure that you've: * [Installed the App Kit SDK](/app-kit/tutorials/installation) * [Configured an adapter](/app-kit/tutorials/adapter-setups) These are required so any example below runs with a valid `kit` and `adapter`. ## Use with adapters on all blockchains Set `useForwarder: true` when you have adapters on all source and destination blockchains but want the Forwarding Service to submit the mint transaction on the destination: ```typescript TypeScript theme={null} const result = await kit.unifiedBalance.spend({ amount: "1.00", from: { adapter, allocations: [{ amount: "1.00", chain: "Base_Sepolia" }], }, to: { adapter, chain: "Arc_Testnet", useForwarder: true, }, token: "USDC", }); ``` ## Use without a destination adapter When you don't have access to a wallet on the destination blockchain, such as with server-side or custodial spend flows, omit the destination adapter and pass `recipientAddress` with `useForwarder: true`: ```typescript TypeScript theme={null} const result = await kit.unifiedBalance.spend({ amount: "1.00", from: { adapter, allocations: [{ amount: "1.00", chain: "Base_Sepolia" }], }, to: { chain: "Arc_Testnet", recipientAddress: "0xRecipientAddress", useForwarder: true, }, token: "USDC", }); ``` In this mode, the Forwarding Service submits the mint transaction for you. Use the spend result's `transferId`, `txHash`, or `explorerUrl` fields to track the destination mint when they are returned. ## Forwarding fee The Forwarding Service charges a [fee](https://developers.circle.com/cctp/concepts/forwarding-service#fees-and-execution) that is deducted from the amount minted on the destination chain. When you [estimate spend fees](/app-kit/tutorials/unified-balance/estimate-spend-fees) for a spend transaction, the result includes the forwarding fee. See [How Unified Balance fees work](/app-kit/concepts/unified-balance-fees) for details. # App Kit SDK: Unified Balance Source: https://docs.arc.io/app-kit/unified-balance Create a chain-agnostic USDC balance and spend it instantly on any blockchain with the App Kit SDK's Unified Balance capability. The [App Kit SDK](/app-kit) includes the Unified Balance capability that combines USDC from multiple blockchains into a single, instantly spendable balance. It is built on top of [Circle Gateway](https://developers.circle.com/gateway) and handles the Gateway workflow for deposits and spends across EVM and non-EVM blockchains. ## How it works Unified Balance works by depositing funds held across multiple blockchains into a single, chain-agnostic Unified Balance. Those funds are then available to spend instantly on any blockchain. The process is illustrated below: ```mermaid theme={null} flowchart LR A[Chain A wallet] -->|"deposits funds"| UB[(Unified Balance)] B[Chain B wallet] -->|"deposits funds"| UB C[Chain C wallet] -->|"deposits funds"| UB UB -->|"spends funds"| D[Chain D wallet] ``` ### What to know about wallet models * Some wallet models cannot sign their own Unified Balance spends, including Circle Wallets SCAs and Privy server wallets. Use the [delegate workflow](/app-kit/quickstarts/unified-balance-delegate-deposit-and-spend): the wallet remains the depositor, and an authorized EOA signs each spend. * Circle Wallets are chain-specific. For Unified Balance flows that draw from multiple source blockchains, use the wallet address for each source blockchain. * For Unified Balance spends, create one source per wallet and chain as needed. The Circle Wallets adapter is stateless, so you can reuse the same adapter configuration and pass the wallet address in each operation context. * Circle Wallets smart contract account (SCA) deposits require `allowanceStrategy: "approve"`. USDC permit signatures use `ecrecover`, which does not accept the SCA's ERC-1271 signature, so the SDK uses an onchain `approve`. Unified Balance is built on Circle Gateway. For production considerations related to deposits, spends, and fund removal, see the [Gateway implementation checklist](https://developers.circle.com/gateway/references/technical-guide#implementation-considerations). ## Quick look This code snippet creates a Unified Balance by depositing funds from two blockchains to spend on a third: ```typescript TypeScript theme={null} // Deposit 1.00 USDC into the Unified Balance from Base const depositBase = await kit.unifiedBalance.deposit({ from: { adapter: viemAdapter, chain: "Base_Sepolia" }, amount: "1.00", token: "USDC", }); // Deposit 1.00 USDC into the Unified Balance from Arbitrum const depositArb = await kit.unifiedBalance.deposit({ from: { adapter: viemAdapter, chain: "Arbitrum_Sepolia" }, amount: "1.00", token: "USDC", }); // Spend 1.50 USDC from the Unified Balance on Arc const spendResult = await kit.unifiedBalance.spend({ amount: "1.50", from: { adapter: viemAdapter }, to: { adapter: viemAdapter, chain: "Arc_Testnet", recipientAddress: "0xRecipientAddress", }, }); ``` For a complete end-to-end flow, follow the quickstart for your scenario: * [Deposit and spend a Unified Balance](/app-kit/quickstarts/unified-balance-deposit-and-spend) * [Use a delegate to deposit and spend a Unified Balance](/app-kit/quickstarts/unified-balance-delegate-deposit-and-spend) ## Installation [Install the App Kit SDK](/app-kit/tutorials/installation) to use Unified Balance. If you only need Unified Balance and don't want to install the full App Kit SDK, follow the steps below to install the standalone Unified Balance Kit. ```bash npm theme={null} npm install @circle-fin/unified-balance-kit ``` ```bash yarn theme={null} yarn add @circle-fin/unified-balance-kit ``` Use the [adapter setup guide](/app-kit/tutorials/adapter-setups) to choose the right wallet model for your app, including browser wallets and Circle Wallets. ```bash npm theme={null} npm install @circle-fin/adapter-viem-v2 viem ``` ```bash yarn theme={null} yarn add @circle-fin/adapter-viem-v2 viem ``` ```bash npm theme={null} npm install @circle-fin/adapter-ethers-v6 ethers ``` ```bash yarn theme={null} yarn add @circle-fin/adapter-ethers-v6 ethers ``` ```bash npm theme={null} npm install @circle-fin/adapter-solana-kit @solana/kit @solana/web3.js ``` ```bash yarn theme={null} yarn add @circle-fin/adapter-solana-kit @solana/kit @solana/web3.js ``` ```bash npm theme={null} npm install @circle-fin/adapter-circle-wallets ``` ```bash yarn theme={null} yarn add @circle-fin/adapter-circle-wallets ``` # Arc Network Source: https://docs.arc.io/arc-chain A purpose-built Layer-1 blockchain for stablecoin-native financial applications with USDC as gas, sub-second deterministic finality, and full EVM compatibility. Arc is a purpose-built Layer-1 blockchain for stablecoin-native financial applications, with USDC as gas, sub-second deterministic finality, and full EVM compatibility. It supports payments, lending, FX, treasury management, and agentic commerce at scale. Beyond the network, Arc provides [App Kits](/app-kit) for crosschain payment workflows, [AI tooling](/ai/mcp) for agent-based development, and a growing [ecosystem of infrastructure partners](/build#developer-tools). ## Key features USDC is the native gas token. EURC and USYC are natively supported. No volatile token required. Transaction fees are denominated in USDC, so costs stay predictable regardless of market volatility. Transactions finalize in under one second with no risk of chain reorganization. Deploy existing Solidity contracts and use standard Ethereum tooling like Hardhat, Foundry, and Viem. Run Solidity contracts confidentially in Arc Privacy Sector (APS), an opt-in execution environment that finalizes alongside the public EVM. `SLH-DSA-SHA2-128s` wallet signatures protect accounts against future quantum threats. ## Stablecoins on Arc Arc is designed from the ground up around stablecoins. USDC serves as the native [gas token](/arc/references/gas-and-fees), EURC enables euro-denominated transfers, and USYC provides onchain yield. There is no volatile native token. For details on the dual-interface model, native representations, and supported assets, see [stablecoin native model](/arc/concepts/stablecoin-native-model). ## Network architecture Arc separates consensus from execution so each layer can optimize independently while maintaining full compatibility with the Ethereum ecosystem. * **[Consensus layer](/arc/concepts/consensus-layer):** Built on Malachite BFT, delivering sub-second finality and high throughput across a permissioned validator set. * **[Execution layer](/arc/concepts/execution-layer):** Powered by Reth, a Rust Ethereum client that runs the EVM so Solidity contracts, development tools, and wallet infrastructure work without modification. For a complete view of how the layers interact, see the [system overview](/arc/concepts/system-overview). ## Network details | **Property** | **Value** | | ----------------------- | ------------------------- | | Consensus | Malachite BFT | | Execution environment | EVM (Osaka hard fork) | | Gas token | USDC | | Block time | \~0.48 s (testnet) | | Chain ID | 5042002 | | Finality | Deterministic, sub-second | | Validator participation | Permissioned | | Developer access | Permissionless | For RPC endpoints and connection details, see [RPC endpoints](/arc/references/rpc-endpoints). For deployed contract addresses, see [contract addresses](/arc/references/contract-addresses). For how Arc validators are selected and how the network is operated, see [deployment model](/arc/concepts/deployment-model). ## Start building Ready to build on Arc? Head to the [Build](/build) section for quickstarts and tutorials covering smart contract deployment, crosschain bridging, App Kits workflows, and AI agent integration. # Batched transactions Source: https://docs.arc.io/arc/concepts/batched-transactions How Multicall3From batches contract calls on Arc while preserving the original sender. Batched transactions let an application submit multiple contract calls in a single Arc transaction. The predeployed `Multicall3From` contract (`0x522fAf9A91c41c443c66765030741e4AaCe147D0`) batches calls like common Multicall3 contracts. Unlike a common batching contract, it preserves the original externally owned account (EOA) wallet as `msg.sender` for each target call. To send batch USDC transfers end to end, see [Send batch USDC transfers](/arc/tutorials/batch-usdc-transfers). ## How a batch call works `Multicall3From` routes each subcall through Arc's `CallFrom` precompile. `CallFrom` executes calls on behalf of the original transaction sender. The [transaction extension contracts](/arc/references/contract-addresses#transaction-extensions) use this precompile to keep the original EOA wallet as `msg.sender` for target calls. For a batch of USDC transfers: 1. Your wallet calls `Multicall3From.aggregate3(...)`. 2. `Multicall3From` forwards each encoded USDC `transfer(...)` through `CallFrom`. 3. The USDC contract sees your wallet as the sender for each transfer. In a normal batching contract, the target contract usually sees the batching contract as `msg.sender`. The contract exposes the `aggregate3(...)` entry point: ```solidity theme={null} struct Call3 { address target; bool allowFailure; bytes callData; } struct Result { bool success; bytes returnData; } function aggregate3(Call3[] calldata calls) external returns (Result[] memory); ``` `target` is the contract to call. `allowFailure` controls whether a failed subcall reverts the full batch. `callData` is the encoded calldata for the target contract. ## Event verification `Multicall3From` does not emit a batch-specific event. Verify the target contract events emitted by each subcall. For USDC transfers, a successful batch emits one USDC `Transfer` event per successful transfer. | Event | Field | Expected value | | :--------- | :------ | :----------------------------------------------------------------------------- | | `Transfer` | `from` | The wallet that called `Multicall3From.aggregate3(...)`, not `Multicall3From`. | | `Transfer` | `to` | The recipient for that subcall. | | `Transfer` | `value` | The USDC amount in base units. | The `from` field is the key sender-preservation check. It confirms that the USDC contract saw your wallet as `msg.sender` for each subcall. ## Unsupported patterns and guardrails `Multicall3From` has explicit guardrails: * Submit `aggregate3(...)` from an EOA. Calls routed through an intermediary contract with a different `msg.sender` are rejected by `CallFrom`'s sender-spoofing constraint. * Do not use value-forwarding patterns. `Multicall3From` does not support `aggregate3Value` because `CallFrom` does not forward value on Arc. * Set `allowFailure` to `false` when every subcall must succeed. If a subcall fails with `allowFailure: false`, the whole batch reverts. * Set `allowFailure` to `true` only when your application explicitly handles failed subcalls. Before you use batched transactions in production, test your exact call set and failure policy against Arc Testnet with the same wallet and indexing infrastructure you plan to use in your application. # Consensus layer Source: https://docs.arc.io/arc/concepts/consensus-layer Arc's Malachite consensus layer orders, validates, and finalizes blocks using a Tendermint-based Proof-of-Authority model. Arc's consensus layer is built on [Malachite](https://github.com/circlefin/malachite/), a high-performance, open-source implementation of the Tendermint Byzantine Fault Tolerant (BFT) protocol. BFT consensus ensures the network reaches agreement on a single history of transactions even if some validators behave maliciously or go offline. Arc uses a Proof-of-Authority (PoA) validator set to order transactions, produce blocks, and deliver [deterministic finality](/arc/concepts/deterministic-finality) -- the guarantee that committed blocks are permanent and can never be reversed or reorganized -- in under one second. ## How Malachite consensus works For how this fits into the broader architecture, see the [system overview](/arc/concepts/system-overview). Each block passes through a four-step pipeline. A rotating proposer assembles transactions, and all validators participate in two rounds of voting before the block is committed. 1. **Propose** -- A validator selected as proposer for the current round bundles pending transactions into a block and broadcasts it. 2. **Pre-vote** -- Every validator evaluates the proposed block and broadcasts a vote on its validity. 3. **Pre-commit** -- Validators broadcast a second vote. If more than two-thirds of validators pre-commit to the same block, it proceeds to commit. 4. **Commit** -- The block is finalized and appended to the chain. Every transaction in the block is irreversible. This two-phase voting process (pre-vote + pre-commit) guarantees that two conflicting blocks can never both be finalized, making reorganizations impossible. ```mermaid theme={null} sequenceDiagram participant P as Proposer (Validator) participant V1 as Validator 1 participant V2 as Validator 2 participant V3 as Validator 3 Note over P,V3: Arc Consensus (Malachite BFT) P->>V1: 1. Propose block P->>V2: 1. Propose block P->>V3: 1. Propose block V1->>P: 2. Pre-vote (block valid) V2->>P: 2. Pre-vote (block valid) V3->>P: 2. Pre-vote (block valid) V1->>P: 3. Pre-commit (confirm vote) V2->>P: 3. Pre-commit (confirm vote) V3->>P: 3. Pre-commit (confirm vote) Note over P,V3: ≥ 2/3 of validators pre-commit for the block P->>P: 4. Finalize block (local commit) V1->>V1: 4. Finalize block (local commit) V2->>V2: 4. Finalize block (local commit) V3->>V3: 4. Finalize block (local commit) Note over P,V3: All validators finalize block locally (under 1s) ``` ## Proof-of-Authority validator set Arc uses a **permissioned Proof-of-Authority (PoA)** model instead of anonymous economic staking. Validators are selected, known institutions with compliance obligations and operational guarantees. For details on operating a validator node, see [running a node](/arc/concepts/running-a-node). * **SOC 2 certified** -- Validators meet audited security and availability standards. * **Geographic distribution** -- Nodes run across multiple global regions to reduce correlated downtime. * **Rotating proposer** -- Block production rotates among validators to ensure fairness and liveness. * **Uptime SLAs** -- Each validator commits to operational availability requirements. This design replaces anonymous economic incentives with institutional accountability, providing stronger assurances for regulated finance. ## Performance characteristics Performance also depends on the [execution layer](/arc/concepts/execution-layer), which processes transactions within each block. Malachite delivers optimistic responsiveness: blocks are produced as fast as the network permits, with no artificial delays or extra timeouts. | Metric | Value | Conditions | | :-------------- | :---------- | :--------------------------------- | | Throughput | 3,000+ TPS | 20 globally distributed validators | | Finality | \<350 ms | Benchmark conditions | | Peak throughput | 10,000+ TPS | 4 validators | ## Security guarantees Arc combines protocol-level safety with institutional safeguards: | Guarantee | Description | | :------------- | :---------------------------------------------------------------------------------------------------------- | | Safety | With fewer than one-third faulty validators, consensus guarantees that no conflicting blocks are finalized. | | Liveness | The network continues to produce blocks as long as two-thirds or more of validators are online and honest. | | Accountability | Validators are regulated institutions, making malicious behavior costly beyond protocol penalties. | | Resilience | Geographic distribution reduces the risk of correlated outages or targeted attacks. | The [deployment model](/arc/concepts/deployment-model) provides additional detail on how validators are geographically distributed. ## Roadmap The Malachite roadmap includes multi-proposer support (multiple validators propose blocks in parallel for higher throughput), a protocol optimization that reduces consensus from three rounds to two for lower latency, and a potential transition from Proof-of-Authority to permissioned Proof-of-Stake to broaden validator participation while maintaining compliance requirements. # Network deployment model Source: https://docs.arc.io/arc/concepts/deployment-model Arc's phased network rollout moves from devnet through public testnet to mainnet, with permissioned validators and permissionless developer access at every stage. Arc follows a phased deployment model, progressively expanding network access and decentralization. The Public Testnet is the current active network, and developer access is permissionless at every phase: you can deploy contracts and send transactions without approval. ## Deployment phases | Phase | Status | Description | | :------------------ | :------- | :--------------------------------------------------------------------------------------------------------------------------- | | **Devnet** | Internal | Internal protocol testing -- Circle's development network for protocol iteration. Not publicly accessible. | | **Private Testnet** | Complete | Invitation-only testnet used for early integration testing with select partners. | | **Public Testnet** | Live | Mirrors mainnet behavior without using real assets. Permissionless testnet (chain ID `5042002`) available to all developers. | | **Private Mainnet** | Upcoming | Production network where real value flows. Initial launch with approximately 20 permissioned validators and limited access. | | **Public Mainnet** | Upcoming | Full public mainnet with open access for all users and applications. | For connection details, see [RPC endpoints](/arc/references/rpc-endpoints). To explore testnet activity, visit [testnet.arcscan.app](https://testnet.arcscan.app). ## Testnet performance The Public Testnet has demonstrated production-grade performance since launch. The following metrics are from the Q1 2026 monthly review. | Metric | Value | | :--------------------- | :------------------------------------------------- | | **Uptime** | 100% | | **Average block time** | \~0.48 s | | **Transactions** | \~30.7M | | **Unique wallets** | 916K | | **Block explorer** | [testnet.arcscan.app](https://testnet.arcscan.app) | ## Validator model at launch Arc launches with a **permissioned Proof-of-Authority (PoA)** validator set -- a consensus model in which a fixed group of known, vetted institutions produce and validate blocks, rather than anonymous participants staking tokens. Approximately 20 SOC 2-certified validators operate across multiple global regions, each bound by uptime SLAs and compliance obligations. Block production rotates among validators to ensure fairness and liveness. For full details on the validator set, see [Consensus layer](/arc/concepts/consensus-layer#proof-of-authority-validator-set). ## Future evolution Arc may transition from Proof-of-Authority to a **permissioned Proof-of-Stake (PoS)** model -- a consensus mechanism where validators lock tokens as collateral rather than relying solely on institutional selection. This broadens validator participation while maintaining the compliance requirements and institutional accountability that define the network. See the [consensus layer roadmap](/arc/concepts/consensus-layer#roadmap) for details. # Deterministic finality and settlement Source: https://docs.arc.io/arc/concepts/deterministic-finality Arc's deterministic finality delivers irreversible transaction settlement in under one second. Arc provides deterministic finality: every transaction is either unconfirmed or final, with no intermediate state. This guarantee comes from the network's [consensus layer](/arc/concepts/consensus-layer), which uses a Byzantine Fault Tolerant (BFT) consensus protocol—once two-thirds or more of validators sign off on a block, that block is irreversible. Every transaction in a committed block is immediately and irreversibly settled. There are no confirmation windows, no reorganization risk, and no probabilistic uncertainty. ## Finality comparison Arc's [Malachite BFT consensus](/arc/concepts/consensus-layer), a Byzantine Fault Tolerant protocol, finalizes blocks in under one second. This is orders of magnitude faster than the finality guarantees on other networks: | Network | Finality | Notes | | :-------------------- | :-------- | :------------------------------------------------------------------ | | **Arc** | \<1 s | Deterministic. Final on commit. | | **Ethereum L1** | 12-15 min | Two epochs of attestations required for finality. | | **Typical L2 rollup** | \~7 days | Withdrawal finality depends on the challenge or proof window to L1. | ## Use cases Sub-second deterministic finality enables use cases that are impractical on slower networks: | Use case | How finality helps | | :--------------------- | :----------------------------------------------------------------------------------------------------------------------------- | | Point-of-sale payments | A merchant can confirm payment and release goods without waiting for additional block confirmations. | | Crossborder settlement | Transfers between counterparties finalize instantly, eliminating the settlement windows that introduce counterparty risk. | | Institutional clearing | Trades and margin calls settle with immediate certainty, matching the expectations of traditional financial infrastructure. | | Composable workflows | Multi-step onchain flows (such as swap-then-bridge) execute sequentially without polling or confirmation delays between steps. | ## Developer benefits Deterministic finality simplifies application design by removing the edge cases that probabilistic chains force you to handle. You don't need retry logic, rollback mechanisms, or confirmation-count thresholds. A confirmed transaction stays confirmed. Safely trigger downstream actions (webhooks, database writes, notifications) as soon as a block is committed, without waiting for additional confirmations. Your application only needs to track two transaction states—unconfirmed and final—rather than tracking a sliding confirmation window. Settlement finality is auditable and provable, meeting the assurance requirements of regulated financial institutions. # Execution layer Source: https://docs.arc.io/arc/concepts/execution-layer Arc's execution layer runs Reth, a Rust Ethereum client that maintains blockchain state, executes EVM transactions, and extends the pipeline with stablecoin-native modules including a USDC-denominated Fee Manager and the CallFrom precompile. Arc's execution layer is built on [Reth](https://github.com/paradigmxyz/reth), a Rust implementation of the Ethereum execution client. Reth maintains the full blockchain state, executes every transaction through the Ethereum Virtual Machine (EVM), and produces the state root -- a cryptographic hash that summarizes the entire ledger state -- that the [consensus layer](/arc/concepts/consensus-layer) finalizes. Arc extends this foundation with modules purpose-built for stablecoin-native finance. ## What Reth does Reth handles three jobs on every block: 1. **Maintains the ledger.** Tracks accounts, balances, smart contracts, and transaction history. Every state change is recorded and addressable. 2. **Executes transactions.** Applies EVM logic for smart contract calls and transfers, deducts gas fees through the Fee Manager, and routes through Arc-specific modules where applicable. 3. **Produces the state root.** Computes a Merkle root -- a single hash derived from a tree of all state data -- of the updated state after all transactions in a block have been applied. The consensus layer finalizes this root, making the block irreversible. Reth is written in Rust for performance, memory safety, and modular extensibility. Arc leverages this architecture to plug in stablecoin-native modules without modifying core EVM execution. ## Arc-specific extensions Arc extends the standard Ethereum execution pipeline with modules that run alongside core EVM logic at the protocol level -- meaning they are built into the blockchain itself, not deployed as user-space smart contracts. You benefit from these capabilities without deploying custom contracts or external services. | Extension | Status | Function | | :----------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------ | | **[Fee Manager](/arc/concepts/stable-fee-design)** | Live | Stabilizes gas fees using USDC as the unit of account with EWMA smoothing. | | **[CallFrom precompile](/arc/references/contract-addresses#transaction-extensions)** | Live | Preserves `msg.sender` across delegated calls, powering the Memo and Multicall3From contracts. | | **[Arc Privacy Sector (APS)](/arc/concepts/opt-in-privacy)** | Planned | Confidential execution environment for Solidity contracts, composing synchronously with the public EVM. | | **Stablecoin Services** | Planned | Powers cross-currency settlement, paymaster-sponsored transactions, and multi-stablecoin gas payments. | ### Fee Manager The Fee Manager replaces Ethereum's per-block EIP-1559 base fee recalculation with an EWMA-smoothed fee curve denominated in USDC. Short demand spikes are absorbed by the smoothing window rather than propagated into sudden fee jumps. The base fee targets approximately \$0.01 per transaction under normal conditions. For the full fee model, see [stable fee design](/arc/concepts/stable-fee-design). For runtime parameters, see [gas and fees](/arc/references/gas-and-fees). ### CallFrom precompile Standard Ethereum opcodes `CALL` and `DELEGATECALL` -- used when one contract invokes another -- change `msg.sender` (the address of the immediate caller) at each hop in the call chain. The CallFrom precompile preserves the original caller's address through the call chain. Two predeployed contracts use this precompile: * **Memo** (`0x5294...`) -- Attaches memo metadata to contract calls and emits indexed `Memo` events. * **Multicall3From** (`0x522f...`) -- Batches multiple calls like Multicall3, but each subcall retains the original `msg.sender`. For contract addresses and integration details, see [transaction extensions](/arc/references/contract-addresses#transaction-extensions). For how this differs from standard Ethereum behavior, see [EVM compatibility](/arc/references/evm-differences). ## Protocol precompiles Arc exposes five custom precompiles in the `0x1800..` address range -- shorthand for the addresses `0x1800000000000000000000000000000000000000` through `0x1800000000000000000000000000000000000004`. These are built into the execution layer at the protocol level rather than deployed as user-space contracts, and they back the protocol-level features listed above. | Precompile | Address | Function | | :------------------------ | :------------- | :--------------------------------------------------------------- | | **Native Coin Authority** | `0x1800..0000` | Mint, burn, and transfer operations for the native USDC balance. | | **Native Coin Control** | `0x1800..0001` | Address blocklist for the native coin. | | **System Accounting** | `0x1800..0002` | Gas fee ring buffer used by the Fee Manager. | | **Call From** | `0x1800..0003` | Powers the Memo and Multicall3From contracts described earlier. | | **PQ Signature Verify** | `0x1800..0004` | Post-quantum `SLH-DSA-SHA2-128s` signature verification. | You typically interact with these precompiles indirectly through the predeployed contracts and protocol features they support, rather than calling them directly. ### Arc Privacy Sector (APS) Arc Privacy Sector (APS) is on the roadmap and not yet available on Arc. Arc Privacy Sector (APS) is a confidential execution environment for Solidity contracts that runs alongside Arc's public EVM. Contracts deployed to APS keep state and transaction data confidential while finalizing in the same block as public state, under default-deny contract isolation. See [opt-in privacy](/arc/concepts/opt-in-privacy) for the planned design. ### Stablecoin Services Stablecoin Services are on the roadmap and not yet available on Arc. Stablecoin Services will provide cross-currency settlement, paymaster-sponsored transactions, and multi-stablecoin gas payments at the protocol level. See [stablecoin native model](/arc/concepts/stablecoin-native-model) for the design rationale. ## Execution pipeline A transaction moves through the execution layer in a linear pipeline. Reth applies each transaction to the current state and produces a new state root, which the consensus layer then finalizes into an irreversible block. ```mermaid theme={null} flowchart LR TP["Mempool\n(unconfirmed txns)"] --> EVM["EVM execution\n(contracts & transfers)"] EVM --> FM["Fee Manager\n(USDC gas accounting)"] FM --> MC["Module calls\n(Privacy, Stablecoin Services)"] MC --> SU["State update\n(accounts, storage, logs)"] SU --> SR["State root\n(Merkle hash)"] class FM node-fee-manager; class MC node-module-calls; ``` 1. **Mempool.** Pending transactions wait in the mempool (a holding area for unconfirmed transactions) after passing initial validation (valid signature, sufficient balance, proper nonce). 2. **EVM execution.** Reth applies each transaction sequentially, running smart contract bytecode and processing native transfers. 3. **Fee Manager.** Gas fees are deducted in USDC using the EWMA-smoothed base fee. This step runs on every transaction. 4. **Module calls.** If the transaction invokes Arc Privacy Sector (APS) or Stablecoin Services, the relevant module processes the call. Standard transactions skip this step. 5. **State update.** Reth writes the resulting changes (account balances, contract storage, event logs) to the state database. 6. **State root.** Reth computes a Merkle root over the full updated state. This root serves as a cryptographic commitment -- a tamper-evident fingerprint of the entire ledger -- that the consensus layer finalizes. # Opt-in privacy Source: https://docs.arc.io/arc/concepts/opt-in-privacy Arc Privacy Sector (APS) is Arc's confidential execution environment for Solidity contracts, running alongside the public EVM with synchronous composability and default-deny contract isolation. Arc provides opt-in privacy through **Arc Privacy Sector (APS)**, a confidential execution environment for Solidity contracts that runs alongside Arc's public EVM. Deploy existing Solidity contracts into APS with minimal modifications to keep contract state and transaction data confidential while preserving atomicity and consensus consistency with the public ledger. Privacy is opt-in: applications choose when business or regulatory requirements warrant keeping data off the public ledger. Privacy features are on the roadmap and not yet available on Arc. ## How Arc Privacy Sector (APS) works Arc runs one blockchain with two execution environments: **Arc** for public state and **APS** for private state. Both environments produce state roots that are committed together in each block, advancing in lockstep within the same consensus rounds. To submit a private transaction, encrypt a standard EVM transaction using the APS network public key and send the ciphertext as calldata to an Arc precompile. From the public ledger's perspective, this looks like an ordinary precompile call—the encrypted payload is opaque. Validators running APS inside hardware enclaves decrypt, verify, and execute the transaction against private state using standard EVM rules. The privacy precompile returns only an acknowledgement and a predefined gas cost. No execution results, return values, or event logs are exposed to the public ledger. After consensus finalizes the block, a light client inside each enclave verifies the block's commit certificate before making private state queryable. Retrieve transaction results from an RPC node by providing the transaction hash and proving authorization. ## Synchronous composability APS finalizes private and public state in the same block by the same validator set. Private and public contracts compose atomically without cross-chain bridges or asynchronous messaging. Bridge assets between public and private contracts through controlled precompile operations that execute within a single block. Both sides of the bridge see consistent state at each block height. ## Contract isolation by default APS enforces a **default-deny** isolation model. When you deploy a contract, every function and storage slot is inaccessible to external callers and other contracts by default. Privacy holds even if you forget to add explicit protections. Exposure is opt-in through three mechanisms: * **Function-level access policies.** Each function selector carries an access policy: `Open`, `Restricted`, or `Locked`. Restricted functions require the caller to have an explicit grant. Locked functions revert unconditionally. * **Trust domains.** A contract's admin can grant trust to specific contracts via `addTrustee`. Without an active trust relationship, introspection opcodes like `EXTCODESIZE`, `EXTCODEHASH`, and `BALANCE` return zero. Trust is unidirectional and revocable. * **Runtime visibility enforcement.** Solidity visibility modifiers (`public`, `external`, `internal`, `private`) are enforced at runtime on every `CALL` and `DELEGATECALL`, preventing crafted calldata from reaching unintended entrypoints. Additional isolation behavior: event logging is disabled by default (events require an explicit precompile), cross-boundary probes are masked, revert reasons are sanitized, and gas reporting to external observers is constant-time. ## Deploying existing Solidity contracts APS reuses standard EVM bytecode and tooling. Core business rules, state management, and function implementations carry over unchanged. The primary adaptation is how contracts expose their interfaces to external callers, through access policy and trust domain configuration. For example, deploy a standard OpenZeppelin ERC-20 token into APS without modifying the bytecode, then use the trust domain API to make `transfer` open to any caller while restricting `balanceOf` to explicitly trusted contracts—without redeploying. ## Post-quantum encryption APS uses hybrid post-quantum cryptography to protect against harvest-now, decrypt-later attacks: * Public-key encryption uses X-Wing KEM (combining X25519 and ML-KEM-768) with HKDF-SHA256 and AES-256-GCM. * Symmetric encryption uses AES-256-GCM-SIV for contract state and AES-256-GCM for state roots. * Node-to-node communication uses TLS 1.3 with the X25519MLKEM768 hybrid key agreement. See [Post-quantum security](/arc/concepts/post-quantum-security) for Arc's broader quantum-resilience roadmap. ## Key management APS uses a single master secret key (MSK) distributed across validators through Shamir threshold secret sharing. The MSK can only be reconstructed inside attested enclaves and is never exposed to validator hosts. All derived keys for transaction decryption, per-contract state encryption, and state root encryption come from the MSK. Each validator organization runs an independent KMS with an attestation policy that governs which enclave profiles may access key material. Seed nodes provide fast MSK recovery if a validator restarts. Key rotation is supported when the validator set changes. # Post-quantum security Source: https://docs.arc.io/arc/concepts/post-quantum-security Arc's post-quantum roadmap covers wallet signatures, validator authentication, private state, and supporting infrastructure against quantum-era attacks. Arc's post-quantum roadmap covers wallet signatures, validator authentication, private smart contract state, and offchain infrastructure. Post-quantum features are on the roadmap and not yet available on Arc. ## Why post-quantum security matters Most public-key cryptography used today is vulnerable to large-scale quantum computers. If those computers become practical, blockchains face two risks: * **Signature forgery.** A quantum computer that breaks public-key cryptography can forge signatures that secure wallets, authorize transactions, and authenticate network participants. * **Harvest-now, decrypt-later attacks.** Encrypted data captured today can be stored and decrypted later when quantum attacks become practical, exposing private transaction details, balances, and other sensitive data. Because blockchain data is long-lived, post-quantum protections need to be in place before quantum attacks are widely available. ## Post-quantum roadmap Arc's roadmap phases each layer in a production-aligned sequence. | Milestone | Release target | Scope | Why it matters | | --------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | Post-quantum wallet signatures | Mainnet launch | Arc mainnet launches support for a post-quantum wallet signature scheme. | Classical wallet signatures are the most immediate quantum risk to user funds. | | Post-quantum privacy | Near-term | Arc Privacy mainnet introduces post-quantum protections for encrypted state and node-to-node communication in privacy mode. | Private balances, counterparties, and transaction details face harvest-now, decrypt-later risk without post-quantum encryption. | | Offchain infrastructure upgrades | Mid-term | Circle upgrades infrastructure such as TLS, encrypted data flows, and related operational systems. | Offchain infrastructure uses the same vulnerable cryptography as onchain systems. | | Post-quantum validator signatures | Long-term | Arc adds a quantum-resistant signature scheme for validators. | Consensus signatures protect the integrity of the Arc ledger. | ## Post-quantum wallet signatures Arc introduces beta support for a post-quantum wallet signature based on `SLH-DSA-SHA2-128s` at mainnet launch. Adoption is opt-in. Ecosystem constraints to keep in mind: * Hardware wallet support will take time to mature. * Post-quantum standards are still evolving, so long-term signature choices may change. Expect a transition period as tooling, wallet support, and integrations mature. ## Post-quantum privacy [Arc Privacy](/arc/concepts/opt-in-privacy) addresses the harvest-now, decrypt-later threat. When Arc Privacy launches, encrypted state and private transaction flows use post-quantum cryptography. In practice, public-key encryption uses X-Wing KEM (combining X25519 and ML-KEM-768) with HKDF-SHA256 and AES-256-GCM. Attackers can capture encrypted data today and attempt to decrypt it later when quantum attacks become practical. Post-quantum encryption at the platform level protects sensitive balances, transaction details, and recipients without requiring you to implement custom post-quantum cryptography. ## Post-quantum validator signatures Validator authentication also requires post-quantum protection to keep the ledger resilient. Arc adds post-quantum validator signatures in a later phase. This sequencing is intentional: validator upgrades must be introduced carefully to preserve throughput, latency, and operational reliability. Because Arc uses sub-second finality, the window to exploit validator signatures is narrower than the wallet-signature risk, making wallets the higher-priority target. ## Offchain infrastructure Quantum resilience extends beyond the chain itself. Circle's roadmap includes upgrading node-to-node communication to TLS 1.3 with the X25519MLKEM768 hybrid key agreement, as well as encrypted data flows, access controls, cloud environments, and other operational systems that support Arc. Offchain traffic and stored data face the same post-quantum threat vectors as onchain data when vulnerable cryptography remains in use. # Running a node Source: https://docs.arc.io/arc/concepts/running-a-node Arc node architecture and the role of full nodes in independently verifying the blockchain. Anyone can run an Arc node without permission. A node gives you independent verification of every block and transaction on the network, plus direct API access through a local JSON-RPC endpoint. Before setting up a node, review the [Node Requirements](/arc/references/node-requirements) for hardware and software prerequisites, then follow [Run an Arc Node](/arc/tutorials/run-an-arc-node) for step-by-step setup instructions. ## What your node does An Arc node performs three functions: * **Verifies every block.** Each block is cryptographically verified against the signatures of the validator set before it is accepted. Your node independently confirms that validators finalized each block. * **Executes every transaction.** Every transaction is re-executed locally through the EVM. Your node maintains its own copy of the complete blockchain state. * **Exposes a local RPC endpoint.** Your node provides a standard Ethereum JSON-RPC API (`http://localhost:8545`) for querying blocks, balances, and transactions, and for submitting calls directly against your own verified state. ## What your node does not do An Arc node is a full node, not a validator: * **Does not participate in consensus.** Your node does not propose or vote on blocks. Only permissioned [validators](/arc/concepts/consensus-layer#proof-of-authority-validator-set) participate in the consensus process. * **Does not observe consensus messages.** Your node does not join the consensus gossip network. It verifies finalized decisions by checking the cryptographic signatures on each block. ## Node architecture An Arc node runs two processes that work together: * **Consensus Layer (CL):** Built on [Malachite](/arc/concepts/consensus-layer), a high-performance Tendermint BFT implementation. The CL fetches blocks from the network, verifies their cryptographic signatures, and passes them to the EL for execution. * **Execution Layer (EL):** Built on [Reth](https://reth.rs/), a Rust implementation of the Ethereum execution client. The EL executes transactions, maintains blockchain state, and serves the JSON-RPC API. The two processes communicate through either local IPC sockets (when running on the same host) or RPC (when running on separate hosts): * **IPC mode:** The EL and CL share two Unix sockets on the same machine. This is the default and simplest configuration. * **RPC mode:** The CL connects to the EL over HTTP using the Engine API and a shared JWT secret. Use this when the EL and CL run on different hosts. ```mermaid theme={null} flowchart LR subgraph Node["Arc Node"] CL["Consensus Layer (Malachite)"] EL["Execution Layer (Reth)"] end Network["Arc Network"] -->|"Fetches blocks"| CL CL -->|"Engine API"| EL CL -->|"ETH RPC"| EL EL -->|"JSON-RPC API"| Client["Your Applications"] ``` ## Why run your own node Running your own node instead of relying on a third-party [node provider](/arc/tools/node-providers) gives you several advantages: * **Independent verification.** You verify every block and transaction yourself, rather than trusting a third party's RPC responses. * **Data sovereignty.** Your blockchain data stays on your own infrastructure. No third party observes your queries or transaction patterns. * **No rate limits.** You control your own RPC endpoint without usage restrictions, request quotas, or throttling. * **Lower latency.** A local RPC endpoint eliminates network round-trips to external providers, which matters for latency-sensitive applications. If you prefer managed infrastructure, see [Node Providers](/arc/tools/node-providers) for a list of third-party RPC services. To learn more about the layers that make up an Arc node, see [System Overview](/arc/concepts/system-overview), [Consensus Layer](/arc/concepts/consensus-layer), and [Execution Layer](/arc/concepts/execution-layer). # Stable fee design Source: https://docs.arc.io/arc/concepts/stable-fee-design Arc's fee market uses EWMA-smoothed base fees denominated in USDC and minimal priority fees, keeping transaction costs predictable and resistant to short-term demand spikes. Arc's fee market builds on [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) but replaces per-block base fee recalculation with an exponentially weighted moving average (EWMA) of block utilization. This smoothing mechanism ensures that short demand spikes do not propagate into sudden fee jumps, keeping transaction costs stable and easy to estimate. The base fee floor targets approximately \$0.001 per ERC-20 transfer under normal conditions. For why Arc uses USDC as its gas token, see [Stablecoin native model](/arc/concepts/stablecoin-native-model). For runtime parameters and RPC calls, see [Gas and fees](/arc/references/gas-and-fees). Any relayer, paymaster, or funded wallet account must hold USDC, the native token of Arc. ## How EWMA fee adjustment works Standard EIP-1559 recalculates the base fee every block based on whether the previous block exceeded or fell short of a target gas utilization. This approach can produce sharp fee swings when a single block fills quickly. Arc replaces this per-block step function with an EWMA over a configurable smoothing window. Instead of reacting fully to one block's utilization, the protocol blends recent utilization into a running average. The base fee then adjusts proportionally to the smoothed signal rather than the raw per-block value. ```text theme={null} utilization_ewma(n) = alpha * block_utilization(n) + (1 - alpha) * utilization_ewma(n - 1) base_fee(n) = adjust(base_fee(n - 1), utilization_ewma(n), target_utilization) # adjust() scales base_fee proportionally to (utilization_ewma / target_utilization), # clamped to [minimum_base_fee, maximum_base_fee] ``` * `alpha` controls how quickly the average responds to new data. A smaller alpha means more smoothing and slower reaction to spikes. * `target_utilization` is the equilibrium point the protocol steers toward. When the EWMA exceeds the target, the base fee rises; when it falls under the target, the base fee decreases. The result is a fee curve that trends toward equilibrium gradually, making cost estimation straightforward even during bursts of activity. ## Key parameters | Parameter | Value | Notes | | :--------------------------- | :------------------------------------------------ | :-------------------------------------------------------------- | | **Base fee floor** | \~\$0.001 per ERC-20 transfer | Design-time target under normal load | | **Testnet minimum base fee** | 20 Gwei | Transactions under this threshold are rejected from the mempool | | **Maximum base fee** | 20,000 Gwei | Hard ceiling that bounds worst-case cost | | **Gas throughput** | 30M gas/block (\~60M gas/sec at 0.5 s block time) | Protocol-level capacity limit | | **Smoothing method** | EWMA of block utilization | Replaces EIP-1559 per-block recalculation | These parameters reflect the current testnet configuration and may be adjusted before mainnet launch. See [Gas and fees](/arc/references/gas-and-fees) for the latest runtime values. ## Priority fees Like standard EIP-1559, Arc transactions include a priority fee (tip) in addition to the base fee. The tip incentivizes the sequencer to include a transaction promptly. Under normal network conditions, a minimal or zero tip is sufficient because the base fee alone covers inclusion. Set `maxPriorityFeePerGas` to `0` for most transactions. During periods of sustained congestion, a small tip can signal higher urgency. Query `eth_maxPriorityFeePerGas` on the Arc RPC for a recommended value. ## Why smoothing matters Without smoothing, a single high-utilization block can double the base fee instantly, and a series of empty blocks can drop it just as fast. This volatility forces developers to build retry logic, fee-bumping heuristics, and complex estimation strategies. With EWMA smoothing: * **Gradual adjustment.** Fees move in small increments, so a short burst of transactions does not cause a sudden price shock. * **Bounded range.** The hard ceiling at 20,000 Gwei ensures that even sustained congestion cannot push fees to extreme levels. * **Simple estimation.** You can call `eth_gasPrice`, `eth_feeHistory`, or `eth_maxPriorityFeePerGas` on the Arc RPC and trust that the returned values will remain accurate for a reasonable submission window. ## Developer benefits Building on Arc's stable fee design simplifies cost management and removes common integration challenges. ERC-20 transfers target \~\$0.001, and EWMA smoothing keeps fees stable. You can quote costs to users with confidence. Short-term demand spikes are absorbed by the smoothing window, so fees don't surprise your users during traffic bursts. Dollar-denominated, bounded fees mean transaction costs map directly to a known USDC amount. No post-hoc conversion is needed for cost tracking or reconciliation. Sponsor transactions on behalf of users through [account abstraction](/arc/tools/account-abstraction) or accept fees in multiple stablecoins without custom workarounds. # Stablecoin native model Source: https://docs.arc.io/arc/concepts/stablecoin-native-model Arc is built around stablecoins as first-class assets, with USDC as the native gas token, EURC for euro-denominated transfers, and USYC for yield-bearing use cases. Transaction fees on Arc are denominated in dollars, not a volatile asset. USDC, EURC, and USYC are integrated at the protocol level. No bridging or wrapping is required. ## USDC as the native gas token On most EVM blockchains, a volatile token (such as ETH) serves as the native asset for gas fees and value transfer. Arc replaces this with USDC. Every transaction fee, native balance, and native transfer on Arc is denominated in USDC. This means: * Fees are predictable in dollar terms. * You hold a single asset for both gas and application-level transfers. * There is no need to acquire or manage a separate gas token. ### Two interfaces, one balance USDC on Arc has two interfaces that share the same underlying balance: | Interface | Decimals | Purpose | | :--------- | :------- | :----------------------------------------------------- | | **Native** | 18 | Gas accounting, native sends, and `msg.value` | | **ERC-20** | 6 | application-level transfers, approvals, and allowances | The native representation uses 18 decimals, consistent with how ETH works on Ethereum. The ERC-20 interface at [`0x3600000000000000000000000000000000000000`](/arc/references/contract-addresses#usdc) uses 6 decimals to match the standard USDC representation on other EVM networks. This design also affects how DeFi protocols should handle USDC on Arc. On other EVM blockchains, a WETH-style wrapper contract gives the native asset an ERC-20 interface. On Arc, USDC is the native token and already has a built-in ERC-20 interface. The wrapping step does not exist; protocols should use this address directly. No wrapper is deployed or supported. Don't alias the [EIP-7528](https://eips.ethereum.org/EIPS/eip-7528) native-asset sentinel (`0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE`) that DeFi SDKs use in routing tables as a stand-in for native ETH to this address: aliasing it conflates native value transfers with ERC-20 transfers. For the protocol-level details, see [EVM differences](/arc/references/evm-differences). This native asset design behaves differently from other EVM chains in ways that can break common assumptions: * `USDC.balanceOf(addr)` (6 decimals) and `addr.balance` / `eth_getBalance` (18 decimals) represent the same underlying balance. The ERC-20 interface truncates the last 12 decimal places of the native value; amounts smaller than 1×10⁻⁶ USDC are not represented in `balanceOf` but are still present in the native balance. * Transfers through either interface affect the same underlying balance. * A native transfer can revert even with a sufficient balance, for example because of the blocklist or zero-address rules. If you're porting an existing contract, see the [Porting contracts to Arc checklist](/arc/tutorials/porting-contracts-to-arc) and [value transfer rules](/arc/references/evm-differences#value-transfer-rules) for the full set of differences. For fee parameters and gas pricing details, see [Gas and fees](/arc/references/gas-and-fees). For the rationale behind stable fee denomination, see [Stable fee design](/arc/concepts/stable-fee-design). ### Indexing USDC movements Every USDC send, ERC-20 transfer, mint, and burn emits a `Transfer` log (18 decimals) from the system emitter, Arc's [EIP-7708](https://eips.ethereum.org/EIPS/eip-7708) implementation. Gas deductions don't emit events. The ERC-20 USDC contract additionally logs its own `Transfer` (6 decimals) for ERC-20-interface activity, so a single ERC-20 transfer emits both; match on the emitter address to avoid double-counting. For the full event reference, including emitter addresses, the mint and burn mapping, and legacy native events, see [USDC system events](/arc/references/usdc-system-events). For a step-by-step walkthrough, see [Index Arc events](/integrate/infrastructure/indexing-events). ## Natively supported stablecoins Beyond USDC, Arc natively supports additional stablecoin assets at the protocol level, meaning they are deployed as pre-configured contracts at genesis, not bridged or wrapped from other blockchains. ### EURC EURC is Circle's euro-denominated stablecoin. It is deployed as a standard ERC-20 token on Arc with 6 decimals, enabling euro-denominated payments, FX workflows, and multi-currency applications without relying on third-party bridges. For the EURC contract address, see [Contract addresses](/arc/references/contract-addresses#eurc). ### USYC USYC is a yield-bearing token representing shares of a tokenized money market fund, a regulated investment vehicle that holds short-duration U.S. Treasury securities and distributes yield to token holders. It provides onchain access to regulated, low-risk yield for institutional participants. USYC is only accessible to institutions outside the United States, subject to eligibility restrictions and a \$100,000 USD minimum investment. For token addresses and testnet faucet instructions, see [Contract addresses](/arc/references/contract-addresses). ## Design rationale Arc's stablecoin-native model is a deliberate architectural choice, not a compatibility layer added after launch. Three principles guided the design. ### No volatile native token Traditional blockchains require users to hold a volatile native token to pay fees. This creates friction for financial applications: users must acquire the token, manage price exposure, and convert between the fee token and the assets they actually want to use. Arc eliminates this by making USDC the native token from day one. ### Single gas denomination at launch Arc launches with USDC as the sole gas token rather than supporting multi-token gas payments from the start. A single denomination simplifies the fee market, avoids oracle dependencies for gas price conversion, and keeps the protocol's economic model straightforward. Multi-stablecoin gas payment through paymasters (smart contracts that sponsor or redirect gas fees on behalf of a user) is not supported at launch. ### Stablecoins as first-class primitives USDC, EURC, and USYC are not tokens added after launch. They are integrated at the protocol level, which means applications can compose across stable assets with consistent interfaces and without external dependencies. This positions Arc as infrastructure specifically designed for payments, lending, FX, and treasury management. # System overview Source: https://docs.arc.io/arc/concepts/system-overview Arc separates consensus from execution across two layers: Malachite orders and finalizes blocks, Reth processes transactions and maintains state. Arc is a two-layer blockchain. The **consensus layer** (Malachite) orders transactions and finalizes blocks. The **execution layer** (Reth) processes those transactions, applies state changes, and produces a new state root. This separation lets each layer optimize independently while delivering sub-second deterministic finality and full EVM compatibility. ## Two-layer architecture ### Consensus layer (Malachite) The consensus layer runs [Malachite](/arc/concepts/consensus-layer), a high-performance implementation of the Tendermint BFT protocol. Its job is to agree on transaction order and finalize blocks across a permissioned validator set. Malachite uses a two-phase voting process (pre-vote and pre-commit) that requires more than two-thirds of validators to agree before a block is committed. Once committed, the block is final and irreversible. Key properties: * **Deterministic finality** in under one second, with no reorganization risk * **High throughput** of 3,000+ TPS with 20 validators (benchmarked at \<350 ms finality) * **Proof-of-Authority** validator set composed of regulated institutions ### Execution layer (Reth) The execution layer runs [Reth](/arc/concepts/execution-layer), a Rust implementation of the Ethereum execution client. It maintains the full blockchain state (accounts, balances, contracts, storage), executes EVM transactions, and extends the standard execution pipeline with Arc-specific modules. Core responsibilities: * **Maintain the ledger** -- track accounts, balances, smart contracts, and transaction history * **Execute transactions** -- apply EVM logic, deduct gas fees, and route through Arc modules * **Produce the state root** -- compute a Merkle root of the updated state for consensus to finalize ## Transaction lifecycle A transaction moves through both layers in sequence. The consensus layer orders and finalizes, then the execution layer processes and applies state changes. ```mermaid theme={null} flowchart LR A["Submit via\nJSON-RPC"] --> B["Mempool\n(validation)"] B --> C["Proposer bundles\ninto block"] C --> D["Pre-vote\n(validity check)"] D --> E["Pre-commit\n(2/3+ threshold)"] E --> F["Commit\n(block finalized)"] F --> G["Execution\n(EVM + modules)"] G --> H["State root\n(Merkle hash)"] class A node-submit; class F,H node-finalized; ``` 1. **Submit.** A user or application sends a signed transaction to an Arc node via JSON-RPC. 2. **Mempool.** The node validates the transaction for correctness (valid signature, sufficient balance, proper nonce) and holds it in the transaction pool. 3. **Propose.** A rotating validator is selected as proposer for the current round. It bundles pending transactions from the mempool into a candidate block and broadcasts it to the validator set. 4. **Pre-vote.** Each validator evaluates the proposed block and broadcasts a vote indicating whether the block is valid. 5. **Pre-commit.** Validators broadcast a second round of votes. If more than two-thirds of validators pre-commit to the same block, the block proceeds to commit. 6. **Commit.** The block is finalized and appended to the chain. Every transaction in the block is now irreversible. 7. **Execution.** Reth applies each transaction to the current state. The EVM processes smart contract logic and transfers, while Arc modules (Fee Manager, and in the future, Arc Privacy Sector (APS) and Stablecoin Services) handle their respective functions. 8. **State root.** Reth computes a Merkle root of the updated state. This root serves as a cryptographic commitment to the entire post-block state. The entire lifecycle completes in under one second. For a deep dive into the consensus protocol, see [consensus layer](/arc/concepts/consensus-layer). For details on how Reth processes transactions, see [execution layer](/arc/concepts/execution-layer). ## Arc-specific modules Arc extends the standard EVM execution pipeline with modules that provide stablecoin-native capabilities. These modules run inside the execution layer and process transactions alongside standard EVM logic. | Module | Status | Function | | :--------------------------------------------------------------- | :------ | :----------------------------------------------------------------------------------------------------------- | | **[Fee Manager](/arc/concepts/stable-fee-design)** | Live | Stabilizes gas fees using USDC as the unit of account. Uses EWMA smoothing to prevent short-term fee spikes. | | **[Arc Privacy Sector (APS)](/arc/concepts/opt-in-privacy)** | Planned | Confidential execution environment for Solidity contracts, composing synchronously with the public EVM. | | **[Stablecoin Services](/arc/concepts/stablecoin-native-model)** | Planned | Powers crosscurrency settlement, paymaster-sponsored transactions, and multi-stablecoin gas payments. | Arc Privacy Sector (APS) and Stablecoin Services are on the roadmap and not yet available on Arc. Arc also includes the **CallFrom precompile**, which preserves `msg.sender` across delegated calls. This precompile is used by the Memo contract and Multicall3From to enable batched and annotated transactions. For details on how these differ from standard Ethereum behavior, see [EVM compatibility](/arc/references/evm-differences). ## Developer benefits * **Instant settlement.** Deterministic finality means you can trigger downstream actions (webhooks, database writes, notifications) as soon as a block commits, with no confirmation polling or reorg handling. * **Familiar stack, native extensions.** Deploy Solidity contracts with standard Ethereum tooling. Fee stabilization, privacy, and stablecoin primitives are built into the execution layer, not bolt-on services. # Transaction memos Source: https://docs.arc.io/arc/concepts/transaction-memos How the Memo contract attaches metadata to a contract call on Arc while preserving the original sender. Transaction memos attach application metadata, such as an invoice or order reference, to a contract call on Arc. The predeployed `Memo` contract (`0x5294E9927c3306DcBaDb03fe70b92e01cCede505`) wraps the call and emits the metadata as events, so wallets, exchanges, and indexers can reconcile onchain transfers against offchain records without changing the target contract. To send a memo transfer end to end, see [Send USDC with a transaction memo](/arc/tutorials/send-usdc-with-transaction-memo). ## How a memo call works The `Memo` contract wraps a target contract call. It routes the inner call through Arc's `CallFrom` precompile, a system-level contract that executes a call on behalf of the original transaction sender. The [transaction extension contracts](/arc/references/contract-addresses#transaction-extensions) use this precompile to keep the original externally owned account (EOA) wallet as `msg.sender` for the target call. A USDC transfer still sees your wallet as the sender, not the `Memo` contract. The contract exposes a single entry point: ```solidity theme={null} function memo( address target, bytes calldata data, bytes32 memoId, bytes calldata memoData ) external; ``` `target` is the contract to call, `data` is the calldata to forward to it, `memoId` is a caller-supplied identifier for the memo, and `memoData` is the arbitrary memo bytes attached to the subcall. On success, `Memo` emits an ordered audit trail: 1. `BeforeMemo(memoIndex)` before the inner call. 2. The target contract events, such as a USDC `Transfer`. 3. `Memo(sender, target, callDataHash, memoId, memo, memoIndex)` after the inner call. The `Memo` contract supports nested memo calls. `BeforeMemo` events emit when each memo frame starts. `Memo` events unwind from the innermost frame back to the outermost frame. ## Event schema Use the emitted events as your audit trail and reconciliation source: | Event | Field | Type | Description | | :----------- | :------------- | :---------------- | :------------------------------------------------------------------------------------------------------------------------ | | `BeforeMemo` | `memoIndex` | `uint256 indexed` | Sequential memo index reserved before the inner call starts. | | `Memo` | `sender` | `address indexed` | Wallet that called `Memo.memo(...)`. For a direct EOA call, this is the EOA preserved as `msg.sender` in the target call. | | `Memo` | `target` | `address indexed` | Contract called through the memo wrapper, such as the USDC ERC-20 interface. | | `Memo` | `callDataHash` | `bytes32` | `keccak256` hash of the forwarded target calldata. Store the original calldata if you need to reconstruct the exact call. | | `Memo` | `memoId` | `bytes32 indexed` | Identifier that your application defines for lookup and reconciliation. | | `Memo` | `memo` | `bytes` | Memo bytes that your application defines. Encode this value consistently in your application. | | `Memo` | `memoIndex` | `uint256` | Sequential index for the memo frame. Nested memos each receive their own index. | For reconciliation, query `Memo` events by indexed fields such as `memoId`, `sender`, and `target`. If you need to match a memo to a specific inner call, compare `callDataHash` with the hash of the calldata your application created. ## Wallet types The `Memo` contract must be invoked directly by an externally owned account (EOA). Smart contract wallets aren't supported as the direct caller. ### Supported wallets Any wallet that signs and broadcasts a standard EOA transaction works. This includes: * Browser and mobile wallets: MetaMask, Rabby, Coinbase Wallet, Rainbow * Hardware wallets * Server-side signers * Circle developer-controlled wallets and user-controlled wallets configured as EOAs The `CallFrom` precompile preserves the signing EOA as `msg.sender` in the target call, which is what the memo flow depends on. ### Unsupported wallets Smart contract wallets aren't supported as the direct caller. This includes: * ERC-4337 smart accounts, including Circle modular wallets * Circle developer-controlled wallets and user-controlled wallets configured as SCAs * Safe and other multisig contract wallets * Any other account-abstraction setup where the transaction originates from a bundler, entry point, or other intermediary contract When a smart contract wallet calls `Memo.memo(...)`, the `msg.sender` reaching the contract is the wallet contract or entry point, not the user's EOA. The call reverts because sender spoofing isn't allowed. If you need memo metadata from this flow, have the smart contract wallet trigger a separate EOA-signed `Memo.memo(...)` transaction, or attach the metadata in your application layer instead. ## Unsupported patterns and guardrails The transaction memo flow has explicit guardrails: * Submit `Memo.memo(...)` from an EOA. Calls routed through an intermediary contract with a different `msg.sender` revert because sender spoofing is not allowed. * Do not call the `CallFrom` precompile directly from an EOA. Direct calls revert with `unauthorized caller`. * Do not rely on `STATICCALL`. Memo execution changes state by incrementing the memo index, so static execution is rejected. * Do not use `DELEGATECALL` into the `Memo` contract. The delegated context does not have the required authorization for `CallFrom`. * If the child call reverts, the outer transaction reverts. The memo index increment rolls back, and the child return data is wrapped in `MemoFailed(bytes)`. Before you use transaction memos in production, test your memo format and reconciliation query against Arc Testnet with the same wallet and indexing infrastructure you plan to use in your application. # Connect to Arc Source: https://docs.arc.io/arc/references/connect-to-arc Set up your wallet and configure your development environment for Arc Testnet. Connect a wallet to Arc Testnet using one-click setup or manual configuration. ## Wallet setup Use the button below to add Arc Testnet to your wallet automatically. ### Manual setup Arc uses USDC as the native gas token (18 decimals). If your wallet supports **custom gas tokens**, ensure display/decimals are set correctly. Wallets that don't support custom gas tokens still work for signing and sending transactions—balances may display as "ETH" but the underlying token is USDC. See [Gas and fees](/arc/references/gas-and-fees) for details. Open MetaMask → **Settings** → **Networks** → **Add network** → **Add a network manually**. | Field | Value | | :------------------ | :---------------------------- | | **Network name** | Arc Testnet | | **New RPC URL** | `https://rpc.testnet.arc.io` | | **Chain ID** | 5042002 | | **Currency symbol** | USDC | | **Explorer URL** | `https://testnet.arcscan.app` | Click **Save**, then switch to Arc Testnet. Open Rabby → click the **network selector** (top-left) → **Add Custom Network**. | Field | Value | | :----------------- | :---------------------------- | | **Chain Name** | Arc Testnet | | **Chain ID** | 5042002 | | **RPC URL** | `https://rpc.testnet.arc.io` | | **Currency** | USDC | | **Block Explorer** | `https://testnet.arcscan.app` | Click **Confirm**, then select Arc Testnet from the network list. Open Coinbase Wallet → **Settings** → **Networks** → **Add custom network**. | Field | Value | | :------------------ | :---------------------------- | | **Network name** | Arc Testnet | | **RPC URL** | `https://rpc.testnet.arc.io` | | **Chain ID** | 5042002 | | **Currency symbol** | USDC | | **Block explorer** | `https://testnet.arcscan.app` | Click **Save**, then switch to Arc Testnet. Open Rainbow → **Settings** (gear icon) → **Networks** → **Custom Network**. | Field | Value | | :----------------- | :---------------------------- | | **Network name** | Arc Testnet | | **RPC URL** | `https://rpc.testnet.arc.io` | | **Chain ID** | 5042002 | | **Symbol** | USDC | | **Block explorer** | `https://testnet.arcscan.app` | Click **Save**, then switch to Arc Testnet. ## Network details | Parameter | Value | | :-------- | :------------------------------------------------- | | Network | Arc Testnet | | Chain ID | `5042002` | | Currency | USDC | | Explorer | [testnet.arcscan.app](https://testnet.arcscan.app) | | Faucet | [faucet.circle.com](https://faucet.circle.com) | ### RPC endpoints ```text Primary theme={null} https://rpc.testnet.arc.io ``` ```text Alchemy theme={null} https://arc-testnet.g.alchemy.com/v2/YOUR_API_KEY ``` ```text Blockdaemon theme={null} https://rpc.blockdaemon.testnet.arc.io ``` ```text dRPC theme={null} https://rpc.drpc.testnet.arc.io ``` ```text QuickNode theme={null} https://rpc.quicknode.testnet.arc.io ``` ### WebSocket endpoints ```text Primary theme={null} wss://rpc.testnet.arc.io ``` ```text Alchemy theme={null} wss://arc-testnet.g.alchemy.com/v2/YOUR_API_KEY ``` ```text Blockdaemon theme={null} wss://rpc.blockdaemon.testnet.arc.io:443/websocket ``` ```text dRPC theme={null} wss://rpc.drpc.testnet.arc.io ``` ```text QuickNode theme={null} wss://rpc.quicknode.testnet.arc.io ``` ## Frontend wallet libraries Use `wagmi` and `viem` to integrate Arc Testnet into [ConnectKit](https://family.co/docs/connectkit), [Reown AppKit](https://docs.reown.com/appkit/overview), or a bare [WalletConnect](https://docs.walletconnect.com) connector. Don't display native USDC and ERC-20 USDC as separate balance rows. They're the same balance; always show a single USDC balance. ### Arc Testnet chain definition `viem` ships Arc Testnet as a built-in chain. No manual definition is needed. ```typescript theme={null} import { arcTestnet } from "viem/chains"; ``` ### Configure wallet connection Pick the tab that matches your setup. ```bash theme={null} npm install connectkit wagmi viem ``` ```typescript theme={null} import { arcTestnet, mainnet } from "viem/chains"; import { createConfig, http } from "wagmi"; import { getDefaultConfig } from "connectkit"; const config = createConfig( getDefaultConfig({ chains: [arcTestnet, mainnet], transports: { [arcTestnet.id]: http("https://rpc.testnet.arc.io"), [mainnet.id]: http("https://cloudflare-eth.com"), }, walletConnectProjectId: process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID, appName: "Your App Name", }), ); ``` When `mainnet` is omitted from `chains`, ConnectKit falls back to its own `eth.merkle.io` endpoint for ENS name lookups, which blocks cross-origin browser requests. Include `mainnet` with a CORS-safe transport (such as the Cloudflare endpoint shown earlier) to prevent this. ```bash theme={null} npm install @reown/appkit @reown/appkit-adapter-wagmi wagmi viem @tanstack/react-query ``` Import `defineChain` from `@reown/appkit/networks`, not from `viem`. AppKit requires two additional fields (`caipNetworkId` and `chainNamespace`) that `viem`'s version omits; using the wrong import causes a runtime error. ```typescript theme={null} import { defineChain } from "@reown/appkit/networks"; import { WagmiAdapter } from "@reown/appkit-adapter-wagmi"; import { createAppKit } from "@reown/appkit/react"; const projectId = process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID; const arcTestnet = defineChain({ id: 5042002, caipNetworkId: "eip155:5042002", chainNamespace: "eip155", name: "Arc Testnet", nativeCurrency: { decimals: 18, name: "USDC", symbol: "USDC" }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.io"], webSocket: ["wss://rpc.testnet.arc.io"], }, }, blockExplorers: { default: { name: "ArcScan", url: "https://testnet.arcscan.app" }, }, testnet: true, }); const wagmiAdapter = new WagmiAdapter({ networks: [arcTestnet], projectId, }); createAppKit({ adapters: [wagmiAdapter], networks: [arcTestnet], projectId, metadata: { name: "Your App Name", description: "Your App Description", url: "https://yourdomain.com", icons: ["https://yourdomain.com/icon.png"], }, }); ``` Get a project ID at [dashboard.reown.com](https://dashboard.reown.com). The same project ID works for the bare WalletConnect connector. For `wagmi` apps that need WalletConnect sessions without Reown AppKit's full modal UI. ```typescript theme={null} import { arcTestnet } from "viem/chains"; import { createConfig, http } from "wagmi"; import { walletConnect } from "wagmi/connectors"; const config = createConfig({ chains: [arcTestnet], connectors: [ walletConnect({ projectId: process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID, metadata: { name: "Your App", description: "Your App Description", url: "https://yourdomain.com", icons: ["https://yourdomain.com/icon.png"], }, }), ], transports: { [arcTestnet.id]: http("https://rpc.testnet.arc.io"), }, }); ``` Arc Testnet (chain ID 5042002) is not registered in the WalletConnect chain registry. On first connection, WalletConnect issues a `wallet_addEthereumChain` call per user. # Contract addresses Source: https://docs.arc.io/arc/references/contract-addresses Arc Testnet contract addresses for USDC, EURC, USYC, CCTP, Gateway, StableFX, transaction extensions (Memo, Multicall3From), and common Ethereum contracts (CREATE2, Multicall3, Permit2). All addresses on this page are for **Arc Testnet**. Mainnet addresses are not yet available. ## Stablecoins Stablecoins are the foundation of the Arc ecosystem, supporting a growing set of fiat-backed and yield-bearing tokens. These assets provide price stability, onchain yield, and multi-currency support for payments, FX, and financial applications. The ERC-20 functions affect native balance movements. ### USDC USDC is the native EVM asset on Arc and is used for gas fees. An optional ERC-20 interface is also available for developers who need features such as `transferFrom`, `approve`, and allowance management. On other EVM blockchains, protocols typically deploy a WETH-style wrapper to give the native asset an ERC-20 interface; on Arc that step is unnecessary because the native USDC token already satisfies `IERC20` directly. There is no wrapped USDC address on Arc. See [Stablecoin native model](/arc/concepts/stablecoin-native-model) for details on how the native and ERC-20 interfaces share the same underlying balance. | Contract | Address | Notes | | :------- | :----------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------- | | **USDC** | [`0x3600000000000000000000000000000000000000`](https://testnet.arcscan.app/address/0x3600000000000000000000000000000000000000) | Optional ERC-20 interface for interacting with the native USDC balance. Uses 6 decimals. | **Getting testnet USDC:** You can request USDC on Arc Testnet from the [Circle Faucet](https://faucet.circle.com/). USDC is required to pay for gas and interact with contracts on Arc. **Note:** As with any ERC-20 token, always use the `decimals()` function to interpret balances and transfer amounts accurately. On Arc, the **native USDC gas token** uses 18 decimals of precision, while the **USDC ERC-20 interface** uses 6 decimals. Avoid mixing these values directly, as doing so may result in incorrect balance handling. For applications integrating USDC, it's recommended to rely solely on the standard ERC-20 interface for reading balances and sending transfers. ### EURC EURC is the euro-denominated stablecoin issued by Circle and supported natively on Arc for use in payments, FX, and other financial applications. | Contract | Address | Notes | | :------- | :----------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------- | | **EURC** | [`0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a`](https://testnet.arcscan.app/address/0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a) | Main EURC token contract. Uses 6 decimals. | **Getting testnet EURC:** Testnet EURC can be requested from the [Circle Faucet](https://faucet.circle.com/). Select **Arc Testnet** as the network and **EURC** as the token to receive a small test allocation. ### USYC [USYC](https://developers.circle.com/tokenized/usyc/overview) is a yield-bearing token issued by Circle International Bermuda Ltd. and supported on Arc for institutional and DeFi use cases. It represents shares of a tokenized money market fund backed by short-duration U.S. Treasury securities, offering onchain access to regulated, low-risk yield. USYC is only accessible to institutions outside the United States, subject to eligibility restrictions and a \$100,000 USD minimum investment. See [USYC Document Certification Requirements](https://help.circle.com/s/article/Document-certification-requirements-for-USYC-onboarding?language=en_US\&category=USYC) for more information. | Contract | Address | Notes | | :--------------- | :----------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------- | | **USYC** | [`0xe9185F0c5F296Ed1797AaE4238D26CCaBEadb86C`](https://testnet.arcscan.app/address/0xe9185F0c5F296Ed1797AaE4238D26CCaBEadb86C) | The main USYC token contract representing tokenized money market fund shares. Uses 6 decimals. | | **Entitlements** | [`0xCC205224862C7641930c87679E98999d23C26113`](https://testnet.arcscan.app/address/0xCC205224862C7641930c87679E98999d23C26113) | Manages allowlisted access and entitlement controls for permissioned addresses on the Arc Testnet. | | **Teller** | [`0x9fdF14c5B14173D74C08Af27AebFf39240dC105A`](https://testnet.arcscan.app/address/0x9fdF14c5B14173D74C08Af27AebFf39240dC105A) | Contract used to mint and redeem testnet USYC from testnet USDC once your wallet is allowlisted. | **Getting testnet USYC:** 1. Obtain testnet USDC from the [Circle Faucet](https://faucet.circle.com/). 2. Request allowlisting by opening a ticket with [Circle Support](https://support.circle.com/) and include your Arc Testnet wallet address. Requests are typically processed in 24–48 hours. 3. Once approved, call the USYC Teller contract or interact with the [USYC Portal](https://usyc.dev.hashnote.com/) to deposit testnet USDC and receive testnet USYC. For more information on issuance, redemption, and eligibility, see [USYC Overview](https://developers.circle.com/tokenized/usyc/overview). ## Crosschain The following contracts enable crosschain interoperability between Arc and other blockchains through Circle's [Cross-Chain Transfer Protocol](https://developers.circle.com/cctp) (CCTP) and [Gateway](https://developers.circle.com/gateway). CCTP handles crosschain message passing and stablecoin transfers, while Gateway provides chain-abstracted USDC balances for seamless liquidity movement. ### CCTP | Contract | Domain | Address | | :----------------------- | :----- | :----------------------------------------------------------------------------------------------------------------------------- | | **TokenMessengerV2** | 26 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://testnet.arcscan.app/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) | | **MessageTransmitterV2** | 26 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://testnet.arcscan.app/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) | | **TokenMinterV2** | 26 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://testnet.arcscan.app/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) | | **MessageV2** | 26 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://testnet.arcscan.app/address/0xbaC0179bB358A8936169a63408C8481D582390C4) | ### Gateway | Contract | Domain | Address | | :---------------- | :----- | :----------------------------------------------------------------------------------------------------------------------------- | | **GatewayWallet** | 26 | [`0x0077777d7EBA4688BDeF3E311b846F25870A19B9`](https://testnet.arcscan.app/address/0x0077777d7EBA4688BDeF3E311b846F25870A19B9) | | **GatewayMinter** | 26 | [`0x0022222ABE238Cc2C7Bb1f21003F0a260052475B`](https://testnet.arcscan.app/address/0x0022222ABE238Cc2C7Bb1f21003F0a260052475B) | ## Payments and settlement Arc provides payment and settlement contracts that enable foreign exchange and onchain settlement workflows using stablecoins. These components support application-level use cases such as FX execution and escrow-based settlement. ### StableFX [StableFX](https://developers.circle.com/stablefx) is an enterprise-grade stablecoin FX engine that combines Request-for-Quote (RFQ) execution with onchain settlement on Arc. The following is the address for the escrow contract used to settle stablecoin swaps. | Contract | Address | Notes | | ------------ | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | | **FxEscrow** | [`0xd68256f4D69C6BbEcB873D8588AE0Dc6B8E22E10`](https://testnet.arcscan.app/address/0xd68256f4D69C6BbEcB873D8588AE0Dc6B8E22E10) | The escrow contract used by both makers and takers to settle stablecoin swaps. | Before executing FX trades, StableFX must be able to transfer USDC from your wallet. To enable this, you need to grant a USDC allowance to the Permit2 contract. See the Common Ethereum contracts section for the Permit2 address. ## Transaction extensions Arc provides predeployed contracts for attaching memos to transactions and batching calls with sender preservation. Both contracts route subcalls through the CallFrom precompile, which preserves the original `msg.sender` in each subcall. | Contract | Address | Notes | | :----------------- | :----------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------- | | **Memo** | [`0x5294E9927c3306DcBaDb03fe70b92e01cCede505`](https://testnet.arcscan.app/address/0x5294E9927c3306DcBaDb03fe70b92e01cCede505) | Attaches memo metadata to contract calls. Emits `Memo` events with a sequential index. | | **Multicall3From** | [`0x522fAf9A91c41c443c66765030741e4AaCe147D0`](https://testnet.arcscan.app/address/0x522fAf9A91c41c443c66765030741e4AaCe147D0) | Batches multiple calls like Multicall3, but preserves the original `msg.sender` in each subcall. | **Offchain blocklist operators:** If you maintain an offchain blocklist or compliance screening system, you must include the Memo and Multicall3From contract addresses. These contracts preserve `msg.sender` through the CallFrom precompile, meaning the original caller's address appears as the sender in subcalls. Your monitoring should account for transactions routed through these contracts to ensure blocklist enforcement is not bypassed. ## Common Ethereum contracts Arc Testnet includes a set of widely used Ethereum ecosystem contracts for deterministic deployment, batched reads, and standardized token approvals. Although not Circle-managed, these contracts are deployed on Arc to ensure compatibility with common EVM tooling and workflows. | Contract | Address | Notes | | :----------------------------- | :----------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------ | | **CREATE2 Factory (Arachnid)** | [`0x4e59b44847b379578588920cA78FbF26c0B4956C`](https://testnet.arcscan.app/address/0x4e59b44847b379578588920cA78FbF26c0B4956C) | Minimal proxy for deterministic contract deployment using the `CREATE2` opcode. | | **Multicall3** | [`0xcA11bde05977b3631167028862bE2a173976CA11`](https://testnet.arcscan.app/address/0xcA11bde05977b3631167028862bE2a173976CA11) | Aggregates multiple read calls into a single call for efficient data retrieval. | | **Permit2** | [`0x000000000022D473030F116dDEE9F6B43aC78BA3`](https://testnet.arcscan.app/address/0x000000000022D473030F116dDEE9F6B43aC78BA3) | Universal contract for signature-based token approvals. Required for StableFX. | ## Test addresses for restricted transfer behavior To help you exercise the value transfer revert paths described in [EVM differences](/arc/references/evm-differences#value-transfer-rules), Arc Testnet seeds a well-known blocklisted address derived from the standard test mnemonic `test test test test test test test test test test test junk`. Because the mnemonic is public, you can derive its private key locally and sign as the address, for example with Foundry: ```bash theme={null} cast wallet private-key --mnemonic "test test test test test test test test test test test junk" --mnemonic-index 1 ``` | Purpose | Mnemonic index | Address | Behavior | | :-------------- | :------------- | :----------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------ | | **Blocklisted** | 1 | [`0x70997970C51812dc3A010C7d01b50e0d17dc79C8`](https://testnet.arcscan.app/address/0x70997970C51812dc3A010C7d01b50e0d17dc79C8) | A value transfer to or from this address reverts at runtime, including when the address is the beneficiary of a `SELFDESTRUCT`. | Use this address to confirm your contract handles a runtime revert on a value transfer (including as a `SELFDESTRUCT` beneficiary). See [EVM differences](/arc/references/evm-differences#value-transfer-rules) for the full set of rules. # EVM differences Source: https://docs.arc.io/arc/references/evm-differences The complete reference for Arc's protocol-level differences from Ethereum: USDC as native gas, value transfer rules, SELFDESTRUCT semantics, EIP-7708 Transfer events, the fee market, and opcode behavior. Arc is an EVM-compatible Layer-1 blockchain. Solidity, Foundry, Hardhat, Viem, ethers.js, and standard Ethereum wallets work without modification, and you can deploy existing contracts unchanged in most cases. Arc targets the **Osaka** hard fork as its baseline, including features such as EIP-7702 (set-code transactions). Arc also ships select features from Ethereum's upcoming **Amsterdam** hard fork ahead of upstream, notably [EIP-7708](https://eips.ethereum.org/EIPS/eip-7708) (standard `Transfer` logs for native value movements). This page is the complete reference for where Arc's protocol-level behavior diverges from Ethereum. All comparisons are against Ethereum at the **Osaka** hard fork, Arc's baseline. Most differences are transparent to application code, but a few change execution semantics in ways that matter when you port a contract. If you are porting an existing contract, start with the [Porting contracts to Arc checklist](/arc/tutorials/porting-contracts-to-arc). Tools that locally simulate the EVM (such as Foundry's `anvil`) run a standard EVM, not Arc's, so they cannot reproduce Arc-specific behavior. Features that depend on it (the native-coin precompiles, EIP-7708 `Transfer` events, and USDC blocklist enforcement) only surface when you test against an Arc RPC endpoint. ## Integration guidance This page is a complete inventory of Arc's protocol-level divergences from Ethereum. The guides linked here translate those differences into practical guidance for specific integration types: Balance display, transaction history, and USDC fee handling. Deposit detection, withdrawal processing, and CCTP-based liquidity management. Single-asset USDC configuration, deposit detection, and instant settlement for fiat ramp providers. USDC pool implementation, decimal handling in pool math, and contract porting. Indexing EIP-7708 Transfer events and avoiding double-counting from the dual emitter. USDC-denominated gas infrastructure and decimal precision in fee calculation. ## USDC as the native gas token Arc uses USDC as its native token. The single most important thing to understand is that **native USDC and the ERC-20 USDC interface are the same asset**, not two separate tokens that happen to share a name. | Interface | Decimals | Used for | | :--------- | :------- | :----------------------------------------------------- | | **Native** | 18 | Gas accounting, native sends, and `msg.value` | | **ERC-20** | 6 | application-level transfers, approvals, and allowances | The ERC-20 interface lives at the [USDC contract address](/arc/references/contract-addresses#usdc) and provides familiar functions such as `transferFrom`, `approve`, and allowance management. A native send and an ERC-20 `transfer` both move the same underlying balance. On other EVM blockchains, the native token has no ERC-20 interface, so protocols deploy a WETH-style wrapper contract to expose `transfer` and `approve`. On Arc, the built-in ERC-20 interface at `0x3600000000000000000000000000000000000000` already covers those use cases, so no wrapper contract is needed. Because both interfaces operate on one balance, `USDC.balanceOf(addr)` and `addr.balance` are two views of the same value. They use different decimals (6 vs 18), so never compare or mix their raw values without converting first. `eth_getBalance` returns 18-decimal native precision. Using `formatUnits(balance, 6)` instead of `formatUnits(balance, 18)` overstates every displayed balance by a factor of 10¹². This single-asset model has consequences that do not exist on other EVM chains: * **The ERC-20 view truncates, so it is not exact.** The 6-decimal `balanceOf` drops anything less than 1×10⁻⁶ USDC, so a native balance of `0.0000001` USDC reads as `0` and `100.0000001` USDC reads as `100`. A `balanceOf` of `0` does not imply a native balance of `0`. * **A native transfer can revert even with a sufficient balance.** On Ethereum, a transfer with sufficient funds always succeeds. On Arc, transfers to the zero address or to or from a blocklisted address fail regardless of balance. * **Don't treat an ERC-20 allowance as a complete spending control on a contract.** `USDC.approve` bounds `transferFrom` calls only. If the contract you're building also exposes functions that send native USDC, those paths move USDC regardless of any allowance. Account for all transfer paths in your access controls, not just the ERC-20 interface. * **Guard rescue functions against sweeping USDC.** A `recoverToken`-style function that sweeps stranded ERC-20 tokens also drains the contract's native USDC balance, because native USDC and ERC-20 USDC are the same balance. Add an explicit guard to prevent USDC from being swept unintentionally. * **Native USDC and ERC-20 USDC share one underlying balance.** Don't display them as separate rows; always show a single USDC balance. Arc uses USDC for gas. Relayers, paymasters, and any wallet that submits transactions on Arc must hold USDC. For the conceptual model, including EURC and USYC support, see [Stablecoin native model](/arc/concepts/stablecoin-native-model). For how balance changes surface as events, see [USDC system events](/arc/references/usdc-system-events). ## Execution and opcode differences | Behavior | Ethereum (Osaka) | Arc | Developer impact | | :------------------------------------------------- | :--------------------------------------------------- | :--------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------- | | `PREVRANDAO` | Beacon chain RANDAO mix | Always returns `0` | No onchain randomness. Use an oracle or verifiable random function (VRF). | | `SELFDESTRUCT` | EIP-6780 semantics | EIP-6780 plus native value rules; emits a `Transfer` log on success (see [SELFDESTRUCT](#selfdestruct)) | Several patterns that succeed on mainnet revert on Arc. | | Non-zero-value `CALL` to a self-destructed account | Succeeds; value credited | **Reverts** (a transfer to a destructed account is a forbidden burn) | The largest semantic departure. See [SELFDESTRUCT](#selfdestruct). | | `parentBeaconBlockRoot` / EIP-4788 | Beacon-roots contract returns the parent beacon root | Set to the parent execution block hash; the beacon-roots contract is omitted, so reads return empty (`0x`) | Do not treat the beacon-roots oracle as functional or as a randomness source. | | Blob transactions (EIP-4844, type-3) | Supported | Not supported; the mempool rejects type-3 transactions | Do not submit blob transactions. `BLOBHASH` returns `0` and `BLOBBASEFEE` returns `1`. | | Withdrawals (EIP-4895) | May be present | Always empty | `block.withdrawals` is always empty. | [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702) set-code transactions, `CREATE2` (including EIP-7610 residual-storage behavior), and EIP-2935 historical block hashes all behave as on Ethereum. In particular, the EIP-2935 block-hash-history contract is deployed and functional, unlike the EIP-4788 beacon-roots contract noted in the table. ## Value transfer rules On Arc, there are two ways a transfer can fail: sending to the zero address (token burning is not allowed) or sending to or from a blocklisted address. In both cases the transaction is included in a block and gas is consumed. * **Transfers to the zero address are forbidden.** A value-bearing transfer to `0x0` reverts with `"Zero address not allowed"`; a zero-value transfer to `0x0` succeeds. (Mint and burn, the only operations that involve `0x0`, go through the native-coin precompile.) * **Burning is forbidden.** Self-destructing to yourself with a balance, or transferring value to an account that has already self-destructed, reverts. * **Transfers to or from a blocklisted address revert.** Check `receipt.status === 0` after any USDC transfer to detect a blocklist revert and handle it explicitly. * **Sending native value to a contract is not guaranteed to succeed.** A call to a contract that forwards native value can revert for any of the reasons listed here, which breaks a common DeFi assumption. * **Sending to an address with no code (`EXTCODESIZE == 0`) succeeds** and emits a `Transfer` log. Sending value to a precompile address reverts. A liquidity pool that pairs native USDC against the ERC-20 USDC interface (as if they were two assets) is meaningless on Arc, because they are one asset. Don't mix `msg.value` with `USDC.balanceOf()` in pool or LTV math: they use different decimals (18 vs 6) and raw values are off by 10¹². ## `SELFDESTRUCT` `SELFDESTRUCT` is allowed on Arc, including during contract deployment, and follows [EIP-6780](https://eips.ethereum.org/EIPS/eip-6780) (the account is fully deleted only if it was created in the same transaction). A self-destruct **reverts** when its value transfer would violate a native value rule: | Condition | Result | | :------------------------------------------------- | :------- | | Beneficiary is the contract itself, with a balance | Reverts | | Beneficiary is the zero address, with a balance | Reverts | | Source or beneficiary is blocklisted | Reverts | | Beneficiary has already self-destructed | Reverts | | Balance is zero (any beneficiary) | Succeeds | Three behaviors differ from every other EVM chain and deserve attention: **1. Self-destructing a contract that holds USDC moves that USDC out.** On Arc a contract's USDC is its native balance, held in the account, so `SELFDESTRUCT` transfers it to the beneficiary. On other chains the contract's ERC-20 USDC balance lives in the token contract and is unaffected by self-destruct. **2. A non-zero-value call to a self-destructed account reverts.** On Ethereum, sending value to an address that self-destructed earlier in the same transaction succeeds. On Arc it is treated as a transfer to a destructed account, a forbidden burn, and reverts. ```solidity theme={null} // Within one transaction: contractA.selfDestruct(payable(b)); // succeeds; emits Transfer(A, B) // Later in the same transaction, any non-zero-value send to A: (bool ok, ) = address(contractA).call{value: 1}(""); // reverts on Arc ``` **3. A successful self-destruct that moves a balance emits a `Transfer` log.** Unlike Ethereum, the moved native value is recorded as an [EIP-7708](https://eips.ethereum.org/EIPS/eip-7708) `Transfer` log from the system emitter (18 decimals). Index it like any other native movement; see [USDC system events](/arc/references/usdc-system-events). ## Native USDC Transfer events (EIP-7708) On a standard EVM chain, a plain native send emits no log. Arc's [EIP-7708](https://eips.ethereum.org/EIPS/eip-7708) implementation emits a standard ERC-20 `Transfer` log from a system address for **every** native USDC movement: native sends, contract endowments, self-destruct transfers, and the precompile-backed mint, burn, and transfer operations. This gives indexers one universal record of balance changes. The system emitter log uses 18 decimals and is distinct from the ERC-20 USDC contract's own 6-decimal `Transfer`. Two indexing mistakes to avoid: * **Filter on the system emitter, not the ERC-20 contract address.** Plain native USDC sends emit no log at `0x3600…0000`. Filtering the ERC-20 address alone silently misses them. * **Don't count logs from both addresses.** An ERC-20 `transfer()` emits a log from both the system emitter and the ERC-20 contract. Counting both inflates balances. Use the system emitter only. For emitter addresses, the exact log format, and indexing guidance, see [USDC system events](/arc/references/usdc-system-events). ## Fee market and block behavior * **The base fee is paid to the block beneficiary, not burned.** Arc has no EIP-1559 burn. Both the base fee and the priority fee are credited to the block's beneficiary. * **The next block's base fee is published in the parent header's `extra_data`** (an 8-byte big-endian value). Read it there rather than re-deriving it. The fee is computed from an exponentially smoothed view of gas usage and is clamped to bounded minimum and maximum values, so it moves predictably. See [Stable fee design](/arc/concepts/stable-fee-design) and [Gas and fees](/arc/references/gas-and-fees). * **The minimum base fee is 20 Gwei.** Transactions with `maxFeePerGas` lower than 20 Gwei are silently dropped by the mempool. They produce no error receipt and never appear in a block. Set `maxFeePerGas` to at least 20 Gwei before submitting any transaction to Arc. * **Block timestamps are non-decreasing, not strictly increasing.** Timestamps come from the proposer's wall clock at one-second granularity, so sub-second blocks may share a timestamp. Use the block number for ordering, and do not assume `block.timestamp` strictly increases between blocks. * **Finality is deterministic and instant.** Transactions finalize on inclusion; offchain systems can act after a single confirmation. See [Deterministic finality](/arc/concepts/deterministic-finality). # Gas and fees Source: https://docs.arc.io/arc/references/gas-and-fees Runtime parameters for Arc gas pricing, including base fee bounds, EWMA smoothing, and best practices for submitting transactions. Arc denominates all transaction fees in [USDC](/arc/concepts/stablecoin-native-model), the native gas token. The fee market uses an [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) pricing model combined with exponentially weighted moving average (EWMA) smoothing -- a technique that calculates the base fee from a weighted running average of recent block utilization, giving more weight to recent blocks and less to older ones. This produces stable, predictable gas costs. For the mechanism design behind these parameters, see [Stable fee design](/arc/concepts/stable-fee-design). ## Fee parameters | Parameter | Value | Notes | | :----------------------------- | :------------------------------------------------ | :----------------------------------------------------- | | **Gas unit** | USDC (18 decimals) | Native gas accounting precision | | **Pricing model** | EIP-1559 + EWMA smoothing | Replaces per-block recalculation with a moving average | | **Base fee target** | \~\$0.01 per transaction | Design-time target under normal load | | **Minimum base fee (testnet)** | 20 Gwei | Floor enforced by the protocol | | **Maximum base fee** | 20,000 Gwei | Hard ceiling that bounds worst-case cost | | **Gas throughput** | 30M gas/block (\~60M gas/sec at 0.5 s block time) | Protocol-level capacity limit | | **Smoothing method** | EWMA of block utilization | Short spikes do not propagate into sudden fee jumps | The EWMA smoothing window calculates each new base fee as a weighted blend of the previous base fee and the latest block's gas utilization ratio. Because older blocks carry exponentially decreasing weight, short traffic spikes raise the fee only slightly, and the base fee returns to its target quickly once utilization normalizes. The 18-decimal precision in the preceding table applies to Arc's native gas accounting. USDC on Arc also provides a standard [ERC-20 interface with 6 decimals](/arc/references/evm-differences#usdc-as-the-native-gas-token) for application-level transfers. These are not two separate tokens -- they share the same underlying balance. See [Contract addresses](/arc/references/contract-addresses#usdc) for the ERC-20 address. ## Submitting transactions Follow these practices to ensure timely transaction inclusion on Arc. ### Set an adequate max fee Set `maxFeePerGas` to at least **20 Gwei**. Transactions submitted under this floor may remain pending indefinitely or fail outright. ```typescript theme={null} import { ethers } from "ethers"; const provider = new ethers.JsonRpcProvider("https://rpc.testnet.arc.io"); const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider); const tx = await wallet.sendTransaction({ to: recipient, value: ethers.parseUnits("1", 6), // 1 USDC via native send maxFeePerGas: ethers.parseUnits("20", "gwei"), }); ``` Set `maxPriorityFeePerGas` (the EIP-1559 tip) to incentivize sequencer inclusion. A value of **0 Gwei** is accepted, but a small tip (for example, **1 Gwei**) can improve inclusion time during high-utilization periods. ### Fetch the current base fee Query the Arc RPC before submitting to get the latest fee data. Two standard methods are available: | Method | Returns | Use case | | :--------------- | :--------------------------------------------------- | :------------------------------------------------------- | | `eth_gasPrice` | Suggested gas price as a single value | Quick estimation for simple transactions | | `eth_feeHistory` | Base fee and priority fee history over recent blocks | Fine-grained estimation when you need historical context | ```typescript theme={null} import { ethers } from "ethers"; const provider = new ethers.JsonRpcProvider("https://rpc.testnet.arc.io"); // Fetch current gas price // Returns: hex string (e.g., "0x4a817c800" = 20 Gwei) const gasPrice: string = await provider.send("eth_gasPrice", []); // Fetch fee history for the last 5 blocks // Returns: { baseFeePerGas: string[], gasUsedRatio: number[], reward: string[][] } const feeHistory = await provider.send("eth_feeHistory", [ "0x5", // block count "latest", // newest block [25, 50, 75], // percentiles ]); ``` ### Display fees in USDC Because Arc denominates gas in USDC, surface fee estimates to users in dollar terms rather than raw Gwei. This avoids confusion and aligns with the stablecoin-native model. ## Common errors | Error | Cause | Resolution | | :------------------------------------------- | :-------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------- | | `transaction underpriced` | `maxFeePerGas` is lower than the 20 Gwei minimum base fee floor | Increase `maxFeePerGas` to at least `ethers.parseUnits("20", "gwei")` and resubmit | | `intrinsic gas too low` | Gas limit is lower than the intrinsic cost of the transaction | Set the gas limit to at least 21,000 for simple transfers; use `eth_estimateGas` for contract calls | | `insufficient funds for gas * price + value` | The sending account's USDC balance cannot cover both the transfer value and the gas fee | Fund the account with enough USDC to cover the total cost (value + maxFeePerGas x gasLimit) | ## Monitoring View real-time gas metrics and recent averages using the [Arc Gas Tracker](https://testnet.arcscan.app/gas-tracker). The tracker displays current base fee, historical trends, and per-block utilization. The parameters on this page reflect the current Arc Testnet configuration. Values such as the minimum base fee, maximum base fee, and throughput limits may change before mainnet launch. # Node requirements Source: https://docs.arc.io/arc/references/node-requirements System requirements, software, network endpoints, and ports for running an Arc node. For step-by-step setup instructions, see [Run an Arc Node](/arc/tutorials/run-an-arc-node). ## System requirements | Component | Minimum | | :-------- | :---------------------------------- | | OS | Linux (Ubuntu 22.04+ or Debian 12+) | | CPU | Higher clock speed over core count | | Memory | 64 GB+ | | Storage | 1 TB+ NVMe SSD (TLC recommended) | | Network | Stable 24 Mbps+ bandwidth | See [Reth system requirements](https://reth.rs/run/system-requirements/) for more detail on Execution Layer configuration. During sustained high load, such as startup or extended sync when the node is far behind, Execution Layer memory usage surges on some hardware. Systems that meet the listed requirements handle these surges without intervention. If you observe memory growth, [enable backpressure](/arc/tutorials/run-an-arc-node#enable-backpressure-under-memory-pressure) to throttle execution to the speed of disk writes. ### Snapshot download size An Arc node bootstraps from a snapshot; syncing from genesis is not supported. Testnet snapshots are approximately **68 GB compressed for the Execution Layer** and **16 GB compressed for the Consensus Layer**, extracting to roughly 103 GB and 36 GB respectively. On a stable 100 Mbps connection the download takes 10–15 minutes; slower or metered connections can take hours. ## Required binaries Every Arc node ships these three components. Install them with `arcup` (pre-built binaries), build them from the [`arc-node`](https://github.com/circlefin/arc-node) source, or run them in Docker. See [Run an Arc node](/arc/tutorials/run-an-arc-node) for the full walkthrough. | Binary | Description | | :------------------- | :------------------------------------------------------------------------- | | `arc-node-execution` | Execution Layer client (Reth-based). Executes transactions and serves RPC. | | `arc-node-consensus` | Consensus Layer client (Malachite-based). Fetches and verifies blocks. | | `arc-snapshots` | Downloads blockchain snapshots for faster initial sync. | ### Versions | Network | Version | | :---------- | :------ | | Arc Testnet | v0.8.0 | ## Network endpoints Your Consensus Layer connects to relay endpoints to fetch blocks from the network. Specify multiple endpoints for redundancy. | Network | Endpoints | | :---------- | :-------------------------------------------------------------------------------------------------------------------- | | Arc Testnet | `https://rpc.testnet.arc.io`
`https://rpc.drpc.testnet.arc.io`
`https://rpc.blockdaemon.testnet.arc.io` | For developer RPC endpoints (connecting to Arc as an application, not running a node), see [Connect to Arc](/arc/references/connect-to-arc). ## Exposed ports | Port | Protocol | Mode | Description | | :---- | :------- | :------- | :----------------------------------- | | 8545 | HTTP | Both | JSON-RPC API (Execution Layer) | | 8551 | HTTP | RPC only | Engine API authentication | | 9001 | HTTP | Both | Prometheus metrics (Execution Layer) | | 29000 | HTTP | Both | Prometheus metrics (Consensus Layer) | | 31000 | HTTP | Both | Consensus Layer RPC endpoint | The Execution Layer and Consensus Layer communicate through either IPC sockets or RPC. Choose one mode; they are mutually exclusive. **IPC mode** (default): Both processes run on the same host. Lower latency, no authentication required. | Socket path | Purpose | | :------------------ | :----------------------------------------------- | | `/run/arc/reth.ipc` | ETH RPC (Consensus Layer reads chain state) | | `/run/arc/auth.ipc` | Engine API (Consensus Layer drives block import) | **RPC mode**: Processes run on separate hosts. Uses HTTP on ports 8545 and 8551 with JWT authentication. Requires generating a shared JWT secret. See [Run an Arc node: Run on separate hosts](/arc/tutorials/run-an-arc-node#run-on-separate-hosts) for configuration details. ## Recommended flags for public-facing nodes If your node is reachable from the public internet, start the Execution Layer with `--public-api`. This flag hides pending-transaction RPCs (a potential MEV vector) and warns if `--http.api` or `--ws.api` exposes namespaces beyond the safe set (`eth`, `net`, `web3`, `rpc`). ## Pruning Both the Execution Layer and Consensus Layer accept the `--full` flag to enable pruning. When bootstrapping from a pruned snapshot, `--full` is **required** on the first Execution Layer start to reconcile internal database tables that would otherwise fail a consistency check. After the initial startup completes, you may restart without `--full` if you prefer not to prune. EL pruning can increase memory usage on constrained machines. If you observe memory pressure, lower the [backpressure threshold](/arc/tutorials/run-an-arc-node#enable-backpressure-under-memory-pressure). # RPC endpoints Source: https://docs.arc.io/arc/references/rpc-endpoints Testnet RPC endpoints, network parameters, and node provider options for connecting to Arc. Arc exposes standard Ethereum JSON-RPC endpoints -- the same HTTP and WebSocket API that Ethereum nodes use -- for submitting transactions, querying state, and subscribing to events. Connect through Circle's primary endpoint or through a [third-party node provider](#node-providers). For details on Arc's current network phase, see [Deployment model](/arc/concepts/deployment-model). ## Testnet endpoints | Provider | HTTP | WebSocket | | :------------------- | :-------------------------------------------------- | :------------------------------------------------ | | **Primary (Circle)** | `https://rpc.testnet.arc.io` | `wss://rpc.testnet.arc.io` | | **Alchemy** | `https://arc-testnet.g.alchemy.com/v2/YOUR_API_KEY` | `wss://arc-testnet.g.alchemy.com/v2/YOUR_API_KEY` | | **Blockdaemon** | `https://rpc.blockdaemon.testnet.arc.io` | `wss://rpc.blockdaemon.testnet.arc.io/websocket` | | **dRPC** | `https://rpc.drpc.testnet.arc.io` | `wss://rpc.drpc.testnet.arc.io` | | **QuickNode** | `https://rpc.quicknode.testnet.arc.io` | `wss://rpc.quicknode.testnet.arc.io` | ## Network parameters These parameters identify the Arc Testnet on the Ethereum network and are required when adding Arc to a wallet or development framework. | Parameter | Value | | :------------------ | :------------------------------------------------------------------------- | | **Chain ID** | `5042002` | | **Currency symbol** | USDC | | **Block explorer** | [testnet.arcscan.app](https://testnet.arcscan.app) | | **Gas tracker** | [testnet.arcscan.app/gas-tracker](https://testnet.arcscan.app/gas-tracker) | | **Faucet** | [faucet.circle.com](https://faucet.circle.com) | For an overview of network phases, see [Deployment model](/arc/concepts/deployment-model). For wallet configuration instructions using these parameters, see [Connect to Arc](/arc/references/connect-to-arc). ## Node providers The following infrastructure partners offer managed RPC access to Arc. Each provider supports HTTP endpoints, and most support WebSocket connections. See [Node providers](/arc/tools/node-providers) for additional details. | Provider | Description | | :----------------------------------------------------------- | :--------------------------------------------------------------------------- | | [**Alchemy**](https://www.alchemy.com/arc) | Full-stack developer platform with RPC, Data APIs, webhooks, and Wallet APIs | | [**Blockdaemon**](https://www.blockdaemon.com/protocols/arc) | Institutional-grade node infrastructure with secure, compliant access | | [**dRPC**](https://drpc.org/chainlist/arc-testnet-rpc) | Decentralized RPC aggregator with load-balanced, multi-provider routing | | [**QuickNode**](https://www.quicknode.com/chains/arc) | High-performance global endpoints and blockchain APIs | ## Supported methods Arc supports all standard Ethereum JSON-RPC methods. The table below lists commonly used methods by category. | Category | Methods | Notes | | :---------------- | :-------------------------------------------------------------------------------- | :------------- | | **State** | `eth_getBalance`, `eth_getCode`, `eth_getStorageAt`, `eth_call` | | | **Transactions** | `eth_sendRawTransaction`, `eth_getTransactionReceipt`, `eth_getTransactionByHash` | | | **Blocks** | `eth_getBlockByNumber`, `eth_getBlockByHash`, `eth_blockNumber` | | | **Gas** | `eth_gasPrice`, `eth_estimateGas`, `eth_feeHistory` | | | **Subscriptions** | `eth_subscribe`, `eth_unsubscribe` | WebSocket only | For gas fee parameters and best practices for setting `maxFeePerGas`, see [Gas and fees](/arc/references/gas-and-fees). The values on this page apply to the Arc Testnet. Mainnet endpoints and parameters are published separately when available. ## Troubleshooting | Symptom | Cause | Resolution | | :------------------------------ | :---------------------------------------------------- | :------------------------------------------------------------------------------------------------------------ | | `connection refused` or timeout | Incorrect RPC URL or network issue | Verify you are using a URL from the [testnet endpoints](#testnet-endpoints) table. Try an alternate provider. | | `invalid chain id` | Wallet or provider configured with the wrong Chain ID | Set the Chain ID to `5042002`. | | `insufficient funds` | Account has no USDC for gas | Request testnet USDC from the [faucet](https://faucet.circle.com). | # Sample apps Source: https://docs.arc.io/arc/references/sample-applications Open-source reference applications for building on Arc with Circle developer tools, including payment flows, stablecoin FX, DeFi protocols, multichain wallets, and treasury management. Explore sample apps for Arc. Each app is an open-source reference implementation you can clone, run locally, and adapt for your own project. ## Payments and checkout ## Wallets and treasury ## Stablecoin FX ## DeFi # USDC system events Source: https://docs.arc.io/arc/references/usdc-system-events Reference for how Arc emits USDC balance-change events: the native system Transfer log (EIP-7708) and the ERC-20 USDC contract events, with emitter addresses, signatures, and decimals for indexers. For a step-by-step indexing walkthrough, see [Index Arc events](/integrate/infrastructure/indexing-events). For the conceptual model behind USDC's native and ERC-20 interfaces, see [Stablecoin native model](/arc/concepts/stablecoin-native-model). ## Two event streams USDC movements surface as logs from two distinct emitters. The native system emitter logs a `Transfer` for every explicit USDC transfer—native sends, ERC-20 transfers, mints, and burns—at 18-decimal precision. The ERC-20 USDC contract logs its own 6-decimal `Transfer` for ERC-20 interface calls only. Filter by emitter address to tell the two streams apart. | Source | Emitter address | Events | Decimals | | :--------------------------------- | :------------------------------------------- | :--------- | :------- | | **Native USDC** (system, EIP-7708) | `0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE` | `Transfer` | 18 | | **ERC-20 USDC** (NativeFiatToken) | `0x3600000000000000000000000000000000000000` | `Transfer` | 6 | A single ERC-20 `transfer()` emits **two** logs: the ERC-20 contract's own `Transfer` (6 decimals, from `0x3600000000000000000000000000000000000000`) and the native system `Transfer` (18 decimals, from `0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE`). A plain native send emits only the system log. Match on the emitter address so you do not count the same movement twice, and never mix the 6-decimal and 18-decimal values. ## Native USDC system events (EIP-7708) Native USDC movements emit a standard ERC-20 `Transfer` log from a designated system address. This is Arc's implementation of [EIP-7708](https://eips.ethereum.org/EIPS/eip-7708), an Amsterdam-track Ethereum proposal that Arc ships ahead of upstream. Native movements can be indexed the same way as ERC-20 transfers. (For the legacy events that testnet emitted before this behavior was activated, see [Historical events](#historical-events-before-zero5).) The log covers native value transfers (`CALL`), contract creation with an endowment (`CREATE`), `SELFDESTRUCT` balance transfers, and the precompile-driven `mint`, `burn`, and `transfer` operations that back the ERC-20 USDC interface. | Property | Value | | :------- | :------------------------------------------------------------------- | | Emitter | `0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE` | | Event | `Transfer(address indexed from, address indexed to, uint256 value)` | | topic0 | `0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef` | | Decimals | 18 | Mint and burn are expressed as transfers involving the zero address: * **Mint:** `Transfer(0x0, recipient, amount)` * **Burn:** `Transfer(from, 0x0, amount)` The following rules apply to the system `Transfer` log: * The native `Transfer` log is emitted **first**, before any other logs in the transaction. * Zero-value transfers emit no log. * Self-transfers (`from == to`) emit no log. * A native value transfer (`CALL`, `CREATE`, or `SELFDESTRUCT`) to or from the zero address reverts with `"Zero address not allowed"`. Mint and burn are the only paths that produce a `Transfer` involving `0x0`, and they go through the precompile. ## ERC-20 USDC contract events The NativeFiatToken contract at [`0x3600000000000000000000000000000000000000`](/arc/references/contract-addresses#usdc) emits its own standard ERC-20 `Transfer` log (**6 decimals**) for activity on the ERC-20 interface. As on any ERC-20 token, mint and burn surface here as a `Transfer` to and from the zero address. These events are independent of the native system events: an ERC-20 `transfer()` produces a log from both emitters. | Property | Value | | :------- | :------------------------------------------------------------------- | | Emitter | `0x3600000000000000000000000000000000000000` | | Event | `Transfer(address indexed from, address indexed to, uint256 value)` | | topic0 | `0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef` | | Decimals | 6 | ## Historical events (before Zero5) On testnet, before the Zero5 hard fork activated, native USDC movements emitted custom events from the NativeCoinAuthority precompile at `0x1800000000000000000000000000000000000000` (18 decimals) instead of the standard `Transfer` log. These events are **no longer emitted** after activation. Mainnet has used the EIP-7708 `Transfer` log since genesis, so this only affects indexers that backfill pre-activation testnet history. | Event | topic0 | | :-------------------------------------------------------------------------------- | :------------------------------------------------------------------- | | `NativeCoinTransferred(address indexed from, address indexed to, uint256 amount)` | `0x62f084c00a442dcf51cdbb51beed2839bf42a268da8474b0e98f38edb7db5a22` | | `NativeCoinMinted(address indexed recipient, uint256 amount)` | `0xb049859d09b3a7d0189a07db4d4becee1a2aa269023205478b1360ab6fc12114` | | `NativeCoinBurned(address indexed from, uint256 amount)` | `0xaaf1ef013644e67c5cea90217acdf0accd334f8437fc9a89a53cfc9b25fb5c25` | If you backfill history across the hard fork boundary, read `NativeCoin*` from `0x1800000000000000000000000000000000000000` for blocks before activation and `Transfer` from `0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE` at and after activation. For the exact activation time and node version requirements, see the canonical `arc-node` [CHANGELOG.md](https://github.com/circlefin/arc-node/blob/main/CHANGELOG.md) and [BREAKING\_CHANGES.md](https://github.com/circlefin/arc-node/blob/main/BREAKING_CHANGES.md#v071), and the [Run an Arc node](/arc/tutorials/run-an-arc-node) tutorial. ## Out of scope: gas fees and block rewards Gas fees and block rewards are **not** emitted as `Transfer` events and are unaffected by EIP-7708: * **Gas fees** are derived from the receipt (`gasUsed × effectiveGasPrice`). * **Block rewards** are attributed via `block.miner`. ## See also * [Index Arc events](/integrate/infrastructure/indexing-events)—step-by-step indexing walkthrough with code * [Stablecoin native model](/arc/concepts/stablecoin-native-model)—why USDC has native and ERC-20 interfaces * [Process withdrawals](/integrate/exchanges/withdrawals): destination validation * [Contract addresses](/arc/references/contract-addresses)—USDC and other system contract addresses * [EVM compatibility for developers](/arc/references/evm-differences)—interface-level guidance and EVM differences # Account abstraction Source: https://docs.arc.io/arc/tools/account-abstraction Account abstraction providers on Arc, offering SDKs, paymasters, and smart wallet infrastructure for building flexible AA flows. Account abstraction (AA) replaces externally owned accounts (EOAs) with smart contract wallets that support programmable transaction validation, gas sponsorship, and batched operations. Arc supports the ERC-4337 standard, so you can use any compatible bundler, paymaster, or SDK from the providers below. ## Providers ### [Alchemy](https://www.alchemy.com/arc) Wallet APIs, a production bundler, and gas sponsorship so users can transact without holding gas tokens. * [**Wallet APIs**](https://www.alchemy.com/docs/wallets): Create and manage smart wallets with embedded signing and session keys. * [**Gas sponsorship**](https://www.alchemy.com/docs/wallets/transactions/sponsor-gas/sponsor-gas-evm): Sponsor transactions so users can transact without holding USDC for gas. ### [Biconomy](https://www.biconomy.io) Account abstraction toolkit offering modular smart accounts, paymasters, and bundlers as a service to simplify the user experience. ### [Blockradar](https://blockradar.co) Infrastructure and APIs for smart account management and transaction bundling, enabling scalable AA flows with minimal setup. ### [Circle Wallets](https://developers.circle.com/wallets) End-to-end platform for creating and managing secure Arc wallets and cryptographic keys. Supports ERC-20, ERC-721, and ERC-1155 standards. ### [Crossmint](https://www.crossmint.com) Wallet-as-a-service and AA capabilities to onboard users with email or OAuth-based accounts. ### [Dynamic](https://www.dynamic.xyz) Identity and wallet orchestration platform with native ERC-4337 support, enabling passkey wallets and flexible signer management. ### [MetaMask Embedded Wallets](https://docs.metamask.io/embedded-wallets/) Embedded wallet infrastructure with social login and multi-party computation (MPC) key management, plus smart accounts with managed bundlers and paymasters. ### [Para](https://getpara.com) Wallet and authentication suite for fintech and crypto applications, enabling flexible wallet management and transaction signing. ### [Pimlico](https://pimlico.io) Bundler and paymaster infrastructure for ERC-4337 smart accounts, offering sponsored transactions and reliable relay services. ### [Privy](https://www.privy.io) APIs and SDKs for embedded wallets and user authentication. Onboard users with email or social logins while maintaining full control over key management. ### [Thirdweb](https://portal.thirdweb.com/wallets) Full-stack toolkit with built-in AA support, SDKs, and a managed smart wallet layer. ### [Turnkey](https://docs.turnkey.com/reference/aa-wallets) Programmable key infrastructure for embedded wallets, transaction signing, and onchain automation with policy-based controls. ### [Zerodev](https://zerodev.app) Developer SDK for deploying and managing ERC-4337 smart accounts, with built-in session key and bundler support. Arc's account abstraction ecosystem is modular—you can mix SDKs, paymasters, and bundlers from multiple providers to design your smart account architecture. # Compliance Source: https://docs.arc.io/arc/tools/compliance-vendors Compliance providers offering analytics, wallet screening, and monitoring tools for Arc applications. Add regulatory compliance to your Arc application by integrating third-party analytics and screening tools. These vendors provide APIs for anti-money laundering (AML) checks, wallet risk scoring, sanctions screening, and real-time transaction monitoring. ## Providers ### [Chainalysis](https://www.chainalysis.com) Blockchain data platform offering transaction monitoring, address screening, and investigation tools to support AML, sanctions, and fraud-detection programs. ### [Elliptic](https://www.elliptic.co) Blockchain analytics and transaction monitoring APIs to identify illicit activity, assess risk exposure, and ensure compliance with AML and sanctions requirements. ### [TRM Labs](https://www.trmlabs.com) Risk intelligence, wallet screening, and real-time monitoring tools to detect fraud, money laundering, and other suspicious behavior across Arc transactions. # Data indexers Source: https://docs.arc.io/arc/tools/data-indexers Data indexers for querying Arc blockchain data through APIs, sub-graphs, and real-time streams. Data indexers make it easy to query and analyze onchain data from Arc. They provide APIs and SDKs for tracking smart contract events, balances, and historical state changes without running your own indexing infrastructure. ## Providers ### [Alchemy](https://www.alchemy.com/arc) REST API infrastructure for querying Arc token balances, transfers, and transaction history, plus webhooks to stream onchain events in real time. * [**Data APIs**](https://www.alchemy.com/docs/reference/data-overview): Query token balances, transfers, and transaction history through a unified API. * [**Webhooks**](https://www.alchemy.com/docs/reference/notify-api-quickstart): Stream onchain events to your backend in real time. ### [Envio](https://envio.dev) Developer-first indexing framework for event-driven data and GraphQL APIs on Arc. * [**HyperIndex**](https://envio.dev/#hyperindex): Build production-ready APIs from Arc data in minutes. * Stream live blockchain events with minimal latency. ### [Goldsky](https://goldsky.com) Managed subgraph and data pipeline platform for Arc contracts. * [**Subgraphs**](https://goldsky.com/products/subgraphs): Autoscaling query engine with 99.9%+ uptime and up to 6x faster performance. * [**Mirror**](https://goldsky.com/products/mirror): Stream onchain data to your database with sub-second latency. ### [Pinax](https://pinax.network) Blockchain data infrastructure for querying real-time and historical Arc data. * **RPC**: Access Arc through RPC infrastructure with archive support for current and historical chain state. * **Firehose**: Stream Arc block data, including transactions, logs, traces, and state changes. * **Substreams**: Build parallelized data streams and indexes from Arc blockchain data. ### [The Graph](https://thegraph.com) Decentralized indexing protocol for querying Arc's onchain data through APIs. * [**Subgraphs**](https://thegraph.com/docs/en/developing/creating-a-subgraph/): Query smart contract data through multiple independent indexers for redundancy. * [**Graph Explorer**](https://thegraph.com/explorer): Discover and reuse subgraphs published by other developers. ### [Thirdweb](https://thirdweb.com) Open-source blockchain data tooling. * [**Insight**](https://insight.thirdweb.com/reference): Retrieve Arc blockchain data, enrich it with metadata, and transform it using custom logic. ### [Zerion](https://zerion.io/api) Wallet data platform for querying portfolio, transaction, and DeFi position data on Arc. * [**Zerion API**](https://developers.zerion.io/quickstart): Query balances, portfolios, DeFi positions, transactions, PnL, and NFTs through a single API. * [**Transaction Subscriptions**](https://developers.zerion.io/recipes/wallet-activity-alerts): Subscribe to wallet activity and receive real-time notifications when monitored wallet addresses transact. # Node providers Source: https://docs.arc.io/arc/tools/node-providers Node providers for reliable RPC access, transaction submission, and data queries on Arc. Connect to the Arc network through third-party RPC infrastructure partners listed below. Each provider offers HTTP and WebSocket endpoints for submitting transactions, querying blockchain data, and subscribing to events. You can also use Arc's public endpoints directly. | Connection type | Public endpoint | | :-------------- | :--------------------------- | | HTTP RPC | `https://rpc.testnet.arc.io` | | WebSocket | `wss://rpc.testnet.arc.io` | | Chain ID | `5042002` | ## Providers ### [Alchemy](https://www.alchemy.com/arc) Institutional-grade developer platform powering RPC, Data APIs, real-time webhooks, and Wallet APIs for Arc. ### [Blockdaemon](https://www.blockdaemon.com/protocols/arc) Institutional-grade node provider offering secure and compliant infrastructure for Arc and other EVM chains. ### [dRPC](https://drpc.org/chainlist/arc-testnet-rpc) Decentralized RPC aggregator providing high-speed, load-balanced access to Arc nodes through a multi-provider architecture. ### [QuickNode](https://www.quicknode.com/chains/arc) High-performance blockchain infrastructure offering global endpoints and APIs for developers. You can connect directly to Arc's public RPC endpoint or through any of these infrastructure partners using your preferred SDK or web3 client. You can also [run your own node](/arc/concepts/running-a-node) for independent verification and direct RPC access without third-party dependencies. # Oracles Source: https://docs.arc.io/arc/tools/oracles Oracle providers for bringing external market data and offchain signals into Arc smart contracts. Connect your Arc smart contracts to real-world data using the oracle providers listed below. They offer price feeds and related infrastructure for DeFi, trading, lending, and other financial applications. If your application also needs to query historical onchain data, see [Data indexers](/arc/tools/data-indexers). ## Providers ### [Chainlink](https://chain.link) Decentralized oracle network for bringing market data and other offchain information onchain. Chainlink Data Feeds aggregate multiple data sources and publish secure, widely used feeds for lending, trading, stablecoins, and tokenized assets. * [**Data Feeds**](https://docs.chain.link/data-feeds): Access decentralized price feeds for smart contract integrations. * [**Data Streams**](https://docs.chain.link/data-streams): Retrieve low-latency market data delivered through a pull-based model. * [**Feed Explorer**](https://data.chain.link): Browse available feeds and contract addresses across supported networks. * [**Contract Addresses**](https://docs.chain.link/data-feeds/price-feeds/addresses): Find deployed price feed contract addresses across supported networks. ### [Chronicle](https://chroniclelabs.org) Onchain data infrastructure for high-integrity price feeds and tokenized asset verification. Chronicle originated as the oracle for Sky (MakerDAO) and provides DeFi price feeds alongside proof-of-asset attestations for lending, stablecoins, tokenized treasuries, and other RWAs. * [**DeFi Price Feeds**](https://chroniclelabs.org/dashboard/oracles): Access high-integrity feeds purpose-built for lending, stablecoins, and other onchain financial applications. * [**Proof of Asset**](https://chroniclelabs.org/dashboard/proofofasset): Provide cryptographic attestations of reserves and collateral for tokenized treasuries, stablecoins, RWAs, and other assets where verifiable backing is essential. * [**Docs**](https://docs.chroniclelabs.org/): Get started with Chronicle's oracle integration guides. ### [Pyth](https://www.pyth.network) Real-time, first-party market data oracle for onchain applications. Pyth provides price feeds across crypto, equities, FX, metals, and more, with pull and push delivery models for different latency and cost requirements. * [**Price Feeds**](https://docs.pyth.network/price-feeds): Integrate real-time price data into Arc smart contracts. * [**EVM Contract Addresses**](https://docs.pyth.network/price-feeds/core/contract-addresses/evm): Find deployed contract addresses for EVM-compatible chains. ### [RedStone](https://www.redstone.finance) Modular oracle network for secure, real-time price feeds and specialized market data. RedStone supports push, pull, and hybrid delivery models, with coverage across crypto assets, LSTs, LRTs, RWAs, tokenized funds, FX, and other custom data feeds for DeFi and institutional applications. * [**Docs**](https://docs.redstone.finance/docs/introduction/): Learn about RedStone's oracle architecture and supported integration models. * [**Pull Model**](https://docs.redstone.finance/docs/dapps/redstone-pull/): Inject signed oracle data directly into user transactions for low-latency, gas-efficient integrations. * [**Push Feeds**](https://app.redstone.finance/push-feeds): View deployed push feed contract addresses. * [**Pull Feeds**](https://app.redstone.finance/pull-feeds): Browse available pull feed configurations and supported assets. * [**Price Feeds**](https://www.redstone.finance/price-feeds): Browse RedStone's supported asset coverage and feed types. ### [Stork](https://www.stork.network) Ultra-low-latency oracle protocol for real-time market data. Stork provides fast, pull-based delivery with cryptographic verifiability for DeFi applications that require sub-second pricing. * [**Docs**](https://docs.stork.network/): Get started with Stork's oracle integration guides. * [**EVM Contract Addresses**](https://docs.stork.network/resources/contract-addresses/evm#arc): Find Stork's deployed contracts on Arc. # Send batch USDC transfers Source: https://docs.arc.io/arc/tutorials/batch-usdc-transfers Send multiple USDC transfers in one Arc transaction with Multicall3From and sender-preserving subcalls. Use the `Multicall3From` contract to batch multiple USDC transfers into one Arc transaction. This tutorial shows the full flow with viem, ethers.js, Python, and curl. You will configure a client, encode two ERC-20 `transfer(...)` subcalls, submit them through `Multicall3From.aggregate3(...)`, and verify the resulting `Transfer` events. To learn how `Multicall3From` preserves your wallet as the sender, see [Batched transactions](/arc/concepts/batched-transactions). ## Prerequisites Before you begin, ensure that you've: * Installed [Node.js v22+](https://nodejs.org/) for the TypeScript examples, or Python 3.10+ for the Python example. * Created an [Arc Testnet wallet](/arc/references/connect-to-arc). * Funded the wallet with testnet USDC from the [Circle Faucet](https://faucet.circle.com/). * Chosen two recipient addresses on Arc Testnet. ## Step 1. Set up the project Create a new project and install the dependencies for the client library you want to use: ```bash Node.js theme={null} mkdir arc-batched-transfers cd arc-batched-transfers npm init -y npm pkg set type=module npm install dotenv ethers tsx typescript viem ``` ```bash Python theme={null} mkdir arc-batched-transfers cd arc-batched-transfers python3 -m venv .venv source .venv/bin/activate pip install web3 python-dotenv ``` Create an `.env` file: ```bash Shell theme={null} touch .env ``` Add your configuration: ```text .env theme={null} PRIVATE_KEY=YOUR_PRIVATE_KEY RECIPIENT_ONE_ADDRESS=RECIPIENT_ONE_ADDRESS RECIPIENT_TWO_ADDRESS=RECIPIENT_TWO_ADDRESS RPC_URL=https://rpc.testnet.arc.io ``` Replace `YOUR_PRIVATE_KEY` with the `0x`-prefixed private key for the funded wallet. Replace each recipient value with an Arc Testnet address. ## Step 2. Review the contract address and ABI Arc Testnet uses the following predeployed contracts for this tutorial: | Contract | Address | | :--------------- | :----------------------------------------------------------------------------------------------------------------------------- | | `Multicall3From` | [`0x522fAf9A91c41c443c66765030741e4AaCe147D0`](https://testnet.arcscan.app/address/0x522fAf9A91c41c443c66765030741e4AaCe147D0) | | `USDC` | [`0x3600000000000000000000000000000000000000`](https://testnet.arcscan.app/address/0x3600000000000000000000000000000000000000) | For the full address list, see [contract addresses](/arc/references/contract-addresses#transaction-extensions). Create `multicall3from-abi.json` in your project: ```json multicall3from-abi.json theme={null} [ { "type": "function", "name": "aggregate3", "stateMutability": "nonpayable", "inputs": [ { "name": "calls", "type": "tuple[]", "components": [ { "name": "target", "type": "address" }, { "name": "allowFailure", "type": "bool" }, { "name": "callData", "type": "bytes" } ] } ], "outputs": [ { "name": "returnData", "type": "tuple[]", "components": [ { "name": "success", "type": "bool" }, { "name": "returnData", "type": "bytes" } ] } ] } ] ``` The script you build in the next steps loads this ABI file from the project root. ## Step 3. Configure the client connection Create the script file for your client library. The first chunk reads the wallet and recipient configuration from `.env`, sets the contract addresses, loads the `Multicall3From` ABI, and creates the clients that read chain state and submit transactions. Create `viem-batch.ts`: ```typescript TypeScript theme={null} import "dotenv/config"; import { readFileSync } from "node:fs"; import { type Address, createPublicClient, createWalletClient, defineChain, encodeFunctionData, erc20Abi, getAddress, http, parseEventLogs, parseUnits, } from "viem"; import { privateKeyToAccount } from "viem/accounts"; const rpcUrl = process.env.RPC_URL ?? "https://rpc.testnet.arc.io"; const privateKey = process.env.PRIVATE_KEY as `0x${string}`; const recipients = [ getAddress(process.env.RECIPIENT_ONE_ADDRESS as Address), getAddress(process.env.RECIPIENT_TWO_ADDRESS as Address), ]; const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USDC", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: [rpcUrl] } }, blockExplorers: { default: { name: "ArcScan", url: "https://testnet.arcscan.app" }, }, testnet: true, }); const multicall3FromAddress = "0x522fAf9A91c41c443c66765030741e4AaCe147D0"; const usdcAddress = "0x3600000000000000000000000000000000000000"; const multicall3FromAbi = JSON.parse( readFileSync("multicall3from-abi.json", "utf8"), ); const account = privateKeyToAccount(privateKey); const publicClient = createPublicClient({ chain: arcTestnet, transport: http(rpcUrl), }); const walletClient = createWalletClient({ account, chain: arcTestnet, transport: http(rpcUrl), }); ``` Create `ethers-batch.ts`: ```typescript TypeScript theme={null} import "dotenv/config"; import { readFileSync } from "node:fs"; import { ethers } from "ethers"; const rpcUrl = process.env.RPC_URL ?? "https://rpc.testnet.arc.io"; const privateKey = process.env.PRIVATE_KEY as string; const recipients = [ ethers.getAddress(process.env.RECIPIENT_ONE_ADDRESS as string), ethers.getAddress(process.env.RECIPIENT_TWO_ADDRESS as string), ]; const multicall3FromAddress = "0x522fAf9A91c41c443c66765030741e4AaCe147D0"; const usdcAddress = "0x3600000000000000000000000000000000000000"; const explorerUrl = "https://testnet.arcscan.app"; const multicall3FromAbi = JSON.parse( readFileSync("multicall3from-abi.json", "utf8"), ); const erc20Abi = [ "function transfer(address to, uint256 amount) returns (bool)", "event Transfer(address indexed from, address indexed to, uint256 value)", ]; const provider = new ethers.JsonRpcProvider(rpcUrl, { chainId: 5042002, name: "arc-testnet", }); const wallet = new ethers.Wallet(privateKey, provider); const multicall3FromInterface = new ethers.Interface(multicall3FromAbi); const erc20Interface = new ethers.Interface(erc20Abi); ``` Create `python-batch.py`: ```python Python theme={null} import json import os from dotenv import load_dotenv from web3 import Web3 from web3.logs import DISCARD load_dotenv() rpc_url = os.getenv("RPC_URL", "https://rpc.testnet.arc.io") private_key = os.environ["PRIVATE_KEY"] recipients = [ Web3.to_checksum_address(os.environ["RECIPIENT_ONE_ADDRESS"]), Web3.to_checksum_address(os.environ["RECIPIENT_TWO_ADDRESS"]), ] multicall3_from_address = Web3.to_checksum_address( "0x522fAf9A91c41c443c66765030741e4AaCe147D0" ) usdc_address = Web3.to_checksum_address("0x3600000000000000000000000000000000000000") explorer_url = "https://testnet.arcscan.app" with open("multicall3from-abi.json", encoding="utf-8") as abi_file: multicall3_from_abi = json.load(abi_file) erc20_abi = [ { "type": "function", "name": "transfer", "stateMutability": "nonpayable", "inputs": [ {"name": "to", "type": "address"}, {"name": "amount", "type": "uint256"}, ], "outputs": [{"name": "", "type": "bool"}], }, { "type": "event", "name": "Transfer", "anonymous": False, "inputs": [ {"name": "from", "type": "address", "indexed": True}, {"name": "to", "type": "address", "indexed": True}, {"name": "value", "type": "uint256", "indexed": False}, ], }, ] w3 = Web3(Web3.HTTPProvider(rpc_url)) account = w3.eth.account.from_key(private_key) multicall3_from = w3.eth.contract( address=multicall3_from_address, abi=multicall3_from_abi, ) usdc = w3.eth.contract(address=usdc_address, abi=erc20_abi) ``` The public clients read chain state, the wallet or account objects sign transactions, and the contract interfaces encode the `Multicall3From` and ERC-20 calls. ## Step 4. Encode the transfer calls Add a chunk that sets the USDC amount and builds one ERC-20 `transfer(...)` subcall for each recipient. Each subcall targets the USDC contract, sets `allowFailure` to `false`, and includes the encoded transfer calldata. ```typescript TypeScript theme={null} const amount = parseUnits("1", 6); const calls = recipients.map((recipient) => ({ target: usdcAddress, allowFailure: false, callData: encodeFunctionData({ abi: erc20Abi, functionName: "transfer", args: [recipient, amount], }), })); ``` ```typescript TypeScript theme={null} const amount = ethers.parseUnits("1", 6); const calls = recipients.map((recipient) => ({ target: usdcAddress, allowFailure: false, callData: erc20Interface.encodeFunctionData("transfer", [recipient, amount]), })); const aggregateData = multicall3FromInterface.encodeFunctionData("aggregate3", [ calls, ]); ``` ```python Python theme={null} amount = 1_000_000 calls = [] for recipient in recipients: transfer_data = usdc.functions.transfer( recipient, amount, )._encode_transaction_data() calls.append( ( usdc_address, False, bytes.fromhex(transfer_data[2:]), ) ) ``` `1000000` is `1` USDC in base units because the USDC ERC-20 interface uses 6 decimals. ## Step 5. Confirm `Multicall3From` is deployed Before sending a transaction, check that the configured `Multicall3From` address has deployed bytecode. ```typescript TypeScript theme={null} const multicall3FromCode = await publicClient.getCode({ address: multicall3FromAddress, }); if (!multicall3FromCode || multicall3FromCode === "0x") { throw new Error(`Multicall3From is not deployed at ${multicall3FromAddress}`); } ``` ```typescript TypeScript theme={null} const multicall3FromCode = await provider.getCode(multicall3FromAddress); if (multicall3FromCode === "0x") { throw new Error(`Multicall3From is not deployed at ${multicall3FromAddress}`); } ``` ```python Python theme={null} multicall3_from_code = w3.eth.get_code(multicall3_from_address) if multicall3_from_code == b"": raise RuntimeError( f"Multicall3From is not deployed at {multicall3_from_address}" ) ``` If the bytecode check returns empty code, confirm that your `RPC_URL` points to Arc Testnet and that the address matches the table in Step 2. ## Step 6. Simulate and send the batch Simulate the `aggregate3(...)` call before sending the transaction. The simulation confirms that each encoded subcall can succeed from your wallet. ```typescript TypeScript theme={null} const simulation = await publicClient.simulateContract({ account, address: multicall3FromAddress, abi: multicall3FromAbi, functionName: "aggregate3", args: [calls], }); const simulatedResults = simulation.result as { success: boolean; returnData: `0x${string}`; }[]; if (!simulatedResults.every((result) => result.success)) { throw new Error( `Expected all simulated subcalls to succeed: ${JSON.stringify( simulatedResults, )}`, ); } const hash = await walletClient.writeContract(simulation.request); const receipt = await publicClient.waitForTransactionReceipt({ hash }); if (receipt.status !== "success") { throw new Error(`Batched transaction reverted: ${hash}`); } ``` ```typescript TypeScript theme={null} const [simulatedResults] = multicall3FromInterface.decodeFunctionResult( "aggregate3", await provider.call({ from: wallet.address, to: multicall3FromAddress, data: aggregateData, }), ) as unknown as [{ success: boolean; returnData: string }[]]; if (!simulatedResults.every((result) => result.success)) { throw new Error( `Expected all simulated subcalls to succeed: ${JSON.stringify( simulatedResults, )}`, ); } const tx = await wallet.sendTransaction({ to: multicall3FromAddress, data: aggregateData, }); const receipt = await tx.wait(); if (!receipt) { throw new Error("Transaction was not mined"); } if (receipt.status !== 1) { throw new Error(`Batched transaction reverted: ${tx.hash}`); } ``` ```python Python theme={null} simulated_results = multicall3_from.functions.aggregate3(calls).call( {"from": account.address} ) if not all(result[0] for result in simulated_results): raise RuntimeError(f"Expected all simulated subcalls to succeed: {simulated_results}") latest_block = w3.eth.get_block("latest") priority_fee = w3.to_wei(1, "gwei") base_fee = latest_block["baseFeePerGas"] transaction = multicall3_from.functions.aggregate3(calls).build_transaction( { "from": account.address, "nonce": w3.eth.get_transaction_count(account.address), "chainId": w3.eth.chain_id, "maxPriorityFeePerGas": priority_fee, "maxFeePerGas": (base_fee * 2) + priority_fee, } ) signed = account.sign_transaction(transaction) tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction) receipt = w3.eth.wait_for_transaction_receipt(tx_hash) if receipt["status"] != 1: raise RuntimeError(f"Batched transaction reverted: {Web3.to_hex(tx_hash)}") ``` The receipt is reused in the next step to check the emitted USDC `Transfer` events. ## Step 7. Verify the transfer events Decode the USDC logs from the receipt and confirm that each expected transfer is present. ```typescript TypeScript theme={null} const transferLogs = parseEventLogs({ abi: erc20Abi, eventName: "Transfer", logs: receipt.logs.filter( (log) => log.address.toLowerCase() === usdcAddress.toLowerCase(), ), }); for (const recipient of recipients) { const transfer = transferLogs.find( (log) => getAddress(log.args.from) === account.address && getAddress(log.args.to) === recipient && log.args.value === amount, ); if (!transfer) { throw new Error(`Missing expected USDC Transfer log to ${recipient}`); } } console.log( "Transaction:", `${arcTestnet.blockExplorers.default.url}/tx/${hash}`, ); console.log("Block:", receipt.blockNumber.toString()); console.log("Sender:", account.address); console.log( "Transfers:", recipients.map((recipient) => ({ to: recipient, amount: amount.toString(), })), ); ``` ```typescript TypeScript theme={null} const transferEvents: ethers.LogDescription[] = []; for (const log of receipt.logs) { if (log.address.toLowerCase() !== usdcAddress.toLowerCase()) continue; const parsed = erc20Interface.parseLog(log); if (parsed?.name === "Transfer") transferEvents.push(parsed); } for (const recipient of recipients) { const transfer = transferEvents.find( (event) => event.args.from.toLowerCase() === wallet.address.toLowerCase() && event.args.to.toLowerCase() === recipient.toLowerCase() && event.args.value === amount, ); if (!transfer) { throw new Error(`Missing expected USDC Transfer log to ${recipient}`); } } console.log("Transaction:", `${explorerUrl}/tx/${tx.hash}`); console.log("Block:", receipt.blockNumber); console.log("Sender:", wallet.address); console.log( "Transfers:", recipients.map((recipient) => ({ to: recipient, amount: amount.toString(), })), ); ``` ```python Python theme={null} transfer_events = usdc.events.Transfer().process_receipt(receipt, errors=DISCARD) for recipient in recipients: transfer = next( ( event for event in transfer_events if Web3.to_checksum_address(event["args"]["from"]) == account.address and Web3.to_checksum_address(event["args"]["to"]) == recipient and event["args"]["value"] == amount ), None, ) if transfer is None: raise RuntimeError(f"Missing expected USDC Transfer log to {recipient}") print("Transaction:", f"{explorer_url}/tx/{Web3.to_hex(tx_hash)}") print("Block:", receipt["blockNumber"]) print("Sender:", account.address) print( "Transfers:", [{"to": recipient, "amount": str(amount)} for recipient in recipients], ) ``` The `from` value in each expected `Transfer` event is your wallet address. That check confirms that the target USDC contract saw your wallet as the sender for each subcall. ## Step 8. Run the script Run the command for the script you created: ```bash viem theme={null} npx tsx --env-file=.env viem-batch.ts ``` ```bash ethers.js theme={null} npx tsx --env-file=.env ethers-batch.ts ``` ```bash Python theme={null} python python-batch.py ``` Successful output includes the transaction URL, block number, sender, and both recipient transfers: ```text theme={null} Transaction: https://testnet.arcscan.app/tx/0x... Block: 123456 Sender: 0xYourWallet... Transfers: [ { to: "0xRecipientOne...", amount: "1000000" }, { to: "0xRecipientTwo...", amount: "1000000" } ] ``` ## Step 9. Check JSON-RPC directly with curl Use curl for read-only JSON-RPC checks, such as verifying deployed bytecode or reading a transaction receipt. Use a client library such as viem, ethers.js, or web3.py to sign and submit the transaction itself. ```bash eth_getCode theme={null} MULTICALL3FROM_ADDRESS=0x522fAf9A91c41c443c66765030741e4AaCe147D0 RPC_URL=https://rpc.testnet.arc.io curl --request POST "$RPC_URL" \ --header "content-type: application/json" \ --data '{ "jsonrpc": "2.0", "method": "eth_getCode", "params": ["'"$MULTICALL3FROM_ADDRESS"'", "latest"], "id": 1 }' ``` ```bash eth_getTransactionReceipt theme={null} TRANSACTION_HASH=0xYOUR_TRANSACTION_HASH RPC_URL=https://rpc.testnet.arc.io curl --request POST "$RPC_URL" \ --header "content-type: application/json" \ --data '{ "jsonrpc": "2.0", "method": "eth_getTransactionReceipt", "params": ["'"$TRANSACTION_HASH"'"], "id": 1 }' ``` # Create your first ERC-8183 job Source: https://docs.arc.io/arc/tutorials/create-your-first-erc-8183-job Create an ERC-8183 job, fund escrow with USDC, submit a deliverable hash, and complete settlement on Arc Testnet. This quickstart guides you through the ERC-8183 job lifecycle on Arc Testnet. You'll create developer-controlled smart contract account wallets, create a job, fund escrow with USDC, submit a deliverable hash, and complete the job as the evaluator. Select the tab that matches your preferred setup. ## ERC-8183 contract on Arc testnet | Contract | Address | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | AgenticCommerce reference implementation | [`0x0747EEf0706327138c69792bF28Cd525089e4583`](https://testnet.arcscan.app/address/0x0747EEf0706327138c69792bF28Cd525089e4583) | ## Prerequisites Before you begin, make sure you have: 1. A [Circle Developer Console](https://console.circle.com) account 2. An API key created in the Console: **Keys → Create a key → API key → Standard Key** 3. Your entity secret generated and registered using the [Circle Console](https://console.circle.com/wallets/dev/configurator/entity-secret) or the [SDK](https://developers.circle.com/wallets/dev-controlled/register-entity-secret#create-an-entity-secret-using-the-sdk) Circle wallets use three credentials. An [**API key**](https://developers.circle.com/api-reference/keys) authenticates your requests to Circle APIs. An [**entity secret**](https://developers.circle.com/wallets/dev-controlled/entity-secret-management) authorizes each wallet operation. A [**wallet set ID**](https://developers.circle.com/wallets/dev-controlled/create-your-first-wallet) groups your wallets. ## Step 1. Set up your project Create a project directory, install dependencies, and configure your environment. ### 1.1. Create the project and install dependencies ```shell Node.js theme={null} mkdir erc8183-quickstart cd erc8183-quickstart npm init -y npm pkg set type=module npm pkg set scripts.start="tsx --env-file=.env index.ts" npm install @circle-fin/developer-controlled-wallets viem npm install --save-dev tsx typescript @types/node ``` ```shell Python theme={null} mkdir erc8183-quickstart cd erc8183-quickstart python3 -m venv .venv source .venv/bin/activate pip install circle-developer-controlled-wallets web3 python-dotenv ``` ### 1.2. Configure TypeScript (optional) This step is optional. It helps prevent missing types in your IDE or editor. 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 Create a `.env` file in the project directory and add your Circle credentials: ```text theme={null} CIRCLE_API_KEY=YOUR_API_KEY CIRCLE_ENTITY_SECRET=YOUR_ENTITY_SECRET ``` * `CIRCLE_API_KEY` is your Circle Developer API key. * `CIRCLE_ENTITY_SECRET` is your registered Entity Secret. The `npm run start` command loads variables from `.env` using Node.js native env-file support. The `python index.py` command loads the same `.env` file using `python-dotenv`. Prefer editing `.env` files in your IDE or editor so credentials are not leaked to your shell history. ## Step 2. Create developer-controlled wallets In this step, you create two Arc Testnet dev-controlled wallets for the ERC-8183 flow: a client wallet and a provider wallet. In this quickstart, the client also acts as the evaluator. If you already have two Arc Testnet funded dev-controlled wallets for this flow, skip to [Step 4](#step-4-create-the-job). The Step 2 through 9 sections explain the flow in smaller pieces. Not every step includes a code snippet, and the snippets are not cumulative. To run the full workflow end to end, use the [complete script](#full-job-lifecycle-script) at the end of this tutorial. ```typescript index.ts theme={null} const walletSet = await circleClient.createWalletSet({ name: "ERC8183 Job Wallets", }); const walletsResponse = await circleClient.createWallets({ blockchains: ["ARC-TESTNET"], count: 2, walletSetId: walletSet.data?.walletSet?.id ?? "", accountType: "SCA", }); const clientWallet = walletsResponse.data?.wallets?.[0]!; const providerWallet = walletsResponse.data?.wallets?.[1]!; console.log(`Client: ${clientWallet.address} (${clientWallet.id})`); console.log(`Provider: ${providerWallet.address} (${providerWallet.id})`); console.log(`Evaluator: ${clientWallet.address} (${clientWallet.id})`); ``` ```python index.py theme={null} from circle.web3 import developer_controlled_wallets, utils import os from dotenv import load_dotenv load_dotenv() circle_client = utils.init_developer_controlled_wallets_client( api_key=os.getenv("CIRCLE_API_KEY"), entity_secret=os.getenv("CIRCLE_ENTITY_SECRET"), ) wallet_sets_api = developer_controlled_wallets.WalletSetsApi(circle_client) wallets_api = developer_controlled_wallets.WalletsApi(circle_client) wallet_set = wallet_sets_api.create_wallet_set( developer_controlled_wallets.CreateWalletSetRequest.from_dict({ "name": "ERC8183 Job Wallets", }) ) wallets_response = wallets_api.create_wallet( developer_controlled_wallets.CreateWalletRequest.from_dict({ "blockchains": ["ARC-TESTNET"], "count": 2, "walletSetId": wallet_set.data.wallet_set.actual_instance.id, "accountType": "SCA", }) ) client_wallet = wallets_response.data.wallets[0].actual_instance provider_wallet = wallets_response.data.wallets[1].actual_instance print(f"Client: {client_wallet.address} ({client_wallet.id})") print(f"Provider: {provider_wallet.address} ({provider_wallet.id})") print(f"Evaluator: {client_wallet.address} ({client_wallet.id})") ``` ## Step 3. Fund the client wallet The script will pause to allow you to fund the client wallet with Arc Testnet USDC from one of these faucets: * [Circle Faucet](https://faucet.circle.com) * [Circle Console Faucet](https://console.circle.com/faucet) You only fund the client wallet as the script transfers starter USDC to the provider wallet automatically before the ERC-8183 flow begins. The public faucet is rate-limited, so this quickstart avoids requiring a second faucet request for the provider wallet. ## Step 4. Create the job Call `createJob(provider, evaluator, expiredAt, description, hook)` on the deployed ERC-8183 reference implementation. This creates the job in the `Open` state. This quickstart uses `address(0)` for `hook` so the flow stays on the default non-hooked path. ```typescript index.ts theme={null} const createJobTx = await circleClient.createContractExecutionTransaction({ walletAddress: clientWallet.address!, blockchain: "ARC-TESTNET", contractAddress: AGENTIC_COMMERCE_CONTRACT, abiFunctionSignature: "createJob(address,address,uint256,string,address)", abiParameters: [ providerWallet.address!, clientWallet.address!, expiredAt.toString(), "ERC-8183 demo job on Arc Testnet", "0x0000000000000000000000000000000000000000", ], fee: { type: "level", config: { feeLevel: "MEDIUM" } }, }); ``` ```python index.py theme={null} create_job_request = ( developer_controlled_wallets.CreateContractExecutionTransactionForDeveloperRequest.from_dict( { "walletAddress": client_wallet.address, "blockchain": "ARC-TESTNET", "contractAddress": AGENTIC_COMMERCE_CONTRACT, "abiFunctionSignature": "createJob(address,address,uint256,string,address)", "abiParameters": [ provider_wallet.address, client_wallet.address, str(expired_at), "ERC-8183 demo job on Arc Testnet", "0x0000000000000000000000000000000000000000", ], "feeLevel": "MEDIUM", } ) ) ``` ## Step 5. Set the budget In this deployed contract, the provider sets the job price by calling `setBudget(jobId, amount, optParams)`. ```typescript index.ts theme={null} const setBudgetTx = await circleClient.createContractExecutionTransaction({ walletAddress: providerWallet.address!, blockchain: "ARC-TESTNET", contractAddress: AGENTIC_COMMERCE_CONTRACT, abiFunctionSignature: "setBudget(uint256,uint256,bytes)", abiParameters: [jobId.toString(), JOB_BUDGET.toString(), "0x"], fee: { type: "level", config: { feeLevel: "MEDIUM" } }, }); ``` ```python index.py theme={null} set_budget_request = ( developer_controlled_wallets.CreateContractExecutionTransactionForDeveloperRequest.from_dict( { "walletAddress": provider_wallet.address, "blockchain": "ARC-TESTNET", "contractAddress": AGENTIC_COMMERCE_CONTRACT, "abiFunctionSignature": "setBudget(uint256,uint256,bytes)", "abiParameters": [str(job_id), JOB_BUDGET, "0x"], "feeLevel": "MEDIUM", } ) ) ``` ## Step 6. Approve USDC and fund escrow Before the client can fund the job, the USDC contract must approve the ERC-8183 contract to transfer the escrow amount. Then the client calls `fund(jobId, optParams)` to move the job into the `Funded` state. ```typescript index.ts theme={null} const approveTx = await circleClient.createContractExecutionTransaction({ walletAddress: clientWallet.address!, blockchain: "ARC-TESTNET", contractAddress: "0x3600000000000000000000000000000000000000", abiFunctionSignature: "approve(address,uint256)", abiParameters: [AGENTIC_COMMERCE_CONTRACT, JOB_BUDGET.toString()], fee: { type: "level", config: { feeLevel: "MEDIUM" } }, }); const fundTx = await circleClient.createContractExecutionTransaction({ walletAddress: clientWallet.address!, blockchain: "ARC-TESTNET", contractAddress: AGENTIC_COMMERCE_CONTRACT, abiFunctionSignature: "fund(uint256,bytes)", abiParameters: [jobId.toString(), "0x"], fee: { type: "level", config: { feeLevel: "MEDIUM" } }, }); ``` ```python index.py theme={null} approve_request = ( developer_controlled_wallets.CreateContractExecutionTransactionForDeveloperRequest.from_dict( { "walletAddress": client_wallet.address, "blockchain": "ARC-TESTNET", "contractAddress": "0x3600000000000000000000000000000000000000", "abiFunctionSignature": "approve(address,uint256)", "abiParameters": [AGENTIC_COMMERCE_CONTRACT, JOB_BUDGET], "feeLevel": "MEDIUM", } ) ) fund_request = ( developer_controlled_wallets.CreateContractExecutionTransactionForDeveloperRequest.from_dict( { "walletAddress": client_wallet.address, "blockchain": "ARC-TESTNET", "contractAddress": AGENTIC_COMMERCE_CONTRACT, "abiFunctionSignature": "fund(uint256,bytes)", "abiParameters": [str(job_id), "0x"], "feeLevel": "MEDIUM", } ) ) ``` ## Step 7. Submit the deliverable The provider submits a `bytes32` deliverable hash, moving the job into the `Submitted` state. ```typescript index.ts theme={null} const deliverableHash = keccak256(toHex("arc-erc8183-demo-deliverable")); const submitTx = await circleClient.createContractExecutionTransaction({ walletAddress: providerWallet.address!, blockchain: "ARC-TESTNET", contractAddress: AGENTIC_COMMERCE_CONTRACT, abiFunctionSignature: "submit(uint256,bytes32,bytes)", abiParameters: [jobId.toString(), deliverableHash, "0x"], fee: { type: "level", config: { feeLevel: "MEDIUM" } }, }); ``` ```python index.py theme={null} deliverable_hash = Web3.to_hex( Web3.keccak(text="arc-erc8183-demo-deliverable") ) submit_request = ( developer_controlled_wallets.CreateContractExecutionTransactionForDeveloperRequest.from_dict( { "walletAddress": provider_wallet.address, "blockchain": "ARC-TESTNET", "contractAddress": AGENTIC_COMMERCE_CONTRACT, "abiFunctionSignature": "submit(uint256,bytes32,bytes)", "abiParameters": [str(job_id), deliverable_hash, "0x"], "feeLevel": "MEDIUM", } ) ) ``` ## Step 8. Complete the job The evaluator completes the job by calling `complete(jobId, reason, optParams)`. In this quickstart, the client is also the evaluator. ```typescript index.ts theme={null} const reasonHash = keccak256(toHex("deliverable-approved")); const completeTx = await circleClient.createContractExecutionTransaction({ walletAddress: clientWallet.address!, blockchain: "ARC-TESTNET", contractAddress: AGENTIC_COMMERCE_CONTRACT, abiFunctionSignature: "complete(uint256,bytes32,bytes)", abiParameters: [jobId.toString(), reasonHash, "0x"], fee: { type: "level", config: { feeLevel: "MEDIUM" } }, }); ``` ```python index.py theme={null} reason_hash = Web3.to_hex(Web3.keccak(text="deliverable-approved")) complete_request = ( developer_controlled_wallets.CreateContractExecutionTransactionForDeveloperRequest.from_dict( { "walletAddress": client_wallet.address, "blockchain": "ARC-TESTNET", "contractAddress": AGENTIC_COMMERCE_CONTRACT, "abiFunctionSignature": "complete(uint256,bytes32,bytes)", "abiParameters": [str(job_id), reason_hash, "0x"], "feeLevel": "MEDIUM", } ) ) ``` ## Step 9. Check the final job state Read the job back from the contract to confirm it reached `Completed`. This reference implementation does not return the deliverable in `getJob()`, so the script prints the submitted deliverable hash from local flow state instead. ```typescript index.ts theme={null} const job = await publicClient.readContract({ address: AGENTIC_COMMERCE_CONTRACT, abi: agenticCommerceAbi, functionName: "getJob", args: [jobId], }); console.log(`Job ID: ${jobId}`); console.log(`Status: ${STATUS_NAMES[Number(job.status)]}`); console.log(`Budget: ${formatUnits(job.budget, 6)} USDC`); console.log(`Hook: ${job.hook}`); console.log(`Deliverable hash submitted: ${deliverableHash}`); ``` ```python index.py theme={null} contract = web3.eth.contract( address=Web3.to_checksum_address(AGENTIC_COMMERCE_CONTRACT), abi=agentic_commerce_abi, ) job = contract.functions.getJob(job_id).call() print(f"Job ID: {job_id}") print(f"Status: {STATUS_NAMES[int(job[7])]}") print(f"Budget: {Web3.from_wei(job[5], 'mwei')} USDC") print(f"Hook: {job[8]}") print(f"Deliverable hash submitted: {deliverable_hash}") ``` ## Full job lifecycle script The following complete scripts combine all the preceding steps into a single runnable file. ```typescript index.ts expandable theme={null} import { createInterface } from "node:readline/promises"; import { setTimeout as delay } from "node:timers/promises"; import { stdin as input, stdout as output } from "node:process"; import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets"; import { createPublicClient, decodeEventLog, formatUnits, http, keccak256, parseUnits, toHex, type Address, type Hex, } from "viem"; import { arcTestnet } from "viem/chains"; // To bootstrap provider wallet during setup (see Step 3) const PROVIDER_STARTER_BALANCE = "1"; const AGENTIC_COMMERCE_CONTRACT = "0x0747EEf0706327138c69792bF28Cd525089e4583" as Address; const JOB_BUDGET = parseUnits("5", 6); // 5 USDC (ERC-20, 6 decimals) const circleClient = initiateDeveloperControlledWalletsClient({ apiKey: process.env.CIRCLE_API_KEY!, entitySecret: process.env.CIRCLE_ENTITY_SECRET!, }); const publicClient = createPublicClient({ chain: arcTestnet, transport: http(), }); const agenticCommerceAbi = [ { type: "function", name: "createJob", stateMutability: "nonpayable", inputs: [ { name: "provider", type: "address" }, { name: "evaluator", type: "address" }, { name: "expiredAt", type: "uint256" }, { name: "description", type: "string" }, { name: "hook", type: "address" }, ], outputs: [{ name: "jobId", type: "uint256" }], }, { type: "function", name: "setBudget", stateMutability: "nonpayable", inputs: [ { name: "jobId", type: "uint256" }, { name: "amount", type: "uint256" }, { name: "optParams", type: "bytes" }, ], outputs: [], }, { type: "function", name: "fund", stateMutability: "nonpayable", inputs: [ { name: "jobId", type: "uint256" }, { name: "optParams", type: "bytes" }, ], outputs: [], }, { type: "function", name: "submit", stateMutability: "nonpayable", inputs: [ { name: "jobId", type: "uint256" }, { name: "deliverable", type: "bytes32" }, { name: "optParams", type: "bytes" }, ], outputs: [], }, { type: "function", name: "complete", stateMutability: "nonpayable", inputs: [ { name: "jobId", type: "uint256" }, { name: "reason", type: "bytes32" }, { name: "optParams", type: "bytes" }, ], outputs: [], }, { type: "function", name: "getJob", stateMutability: "view", inputs: [{ name: "jobId", type: "uint256" }], outputs: [ { type: "tuple", components: [ { name: "id", type: "uint256" }, { name: "client", type: "address" }, { name: "provider", type: "address" }, { name: "evaluator", type: "address" }, { name: "description", type: "string" }, { name: "budget", type: "uint256" }, { name: "expiredAt", type: "uint256" }, { name: "status", type: "uint8" }, { name: "hook", type: "address" }, ], }, ], }, { type: "event", name: "JobCreated", inputs: [ { indexed: true, name: "jobId", type: "uint256" }, { indexed: true, name: "client", type: "address" }, { indexed: true, name: "provider", type: "address" }, { indexed: false, name: "evaluator", type: "address" }, { indexed: false, name: "expiredAt", type: "uint256" }, { indexed: false, name: "hook", type: "address" }, ], anonymous: false, }, ] as const; const STATUS_NAMES = [ "Open", "Funded", "Submitted", "Completed", "Rejected", "Expired", ]; function extractJobId(txHash: Hex) { return publicClient .getTransactionReceipt({ hash: txHash }) .then((receipt) => { for (const log of receipt.logs) { try { const decoded = decodeEventLog({ abi: agenticCommerceAbi, data: log.data, topics: log.topics, }); if (decoded.eventName === "JobCreated") { return decoded.args.jobId; } } catch { continue; } } throw new Error("Could not parse JobCreated event"); }); } async function waitForTransaction(txId: string, label: string) { process.stdout.write(` Waiting for ${label}`); for (let i = 0; i < 60; i++) { await delay(2000); const tx = await circleClient.getTransaction({ id: txId }); const data = tx.data?.transaction; if (data?.state === "COMPLETE" && data.txHash) { const txHash = data.txHash; console.log( ` ✓\n Tx: ${arcTestnet.blockExplorers.default.url}/tx/${txHash}`, ); return txHash as Hex; } if (data?.state === "FAILED") { throw new Error(`${label} failed onchain`); } process.stdout.write("."); } throw new Error(`${label} timed out`); } async function printBalances( title: string, wallets: Array<{ label: string; id?: string; address?: string | null }>, ) { console.log(`\n${title}:`); for (const wallet of wallets) { const balances = await circleClient.getWalletTokenBalance({ id: wallet.id!, }); const usdc = balances.data?.tokenBalances?.find( (b) => b.token?.symbol === "USDC", ); console.log(` ${wallet.label}: ${wallet.address}`); console.log(` USDC: ${usdc?.amount ?? "0"}`); } } async function main() { console.log("── Step 1: Create wallets ──"); const walletSet = await circleClient.createWalletSet({ name: "ERC8183 Job Wallets", }); const walletsResponse = await circleClient.createWallets({ blockchains: ["ARC-TESTNET"], count: 2, walletSetId: walletSet.data?.walletSet?.id ?? "", accountType: "SCA", }); const clientWallet = walletsResponse.data?.wallets?.[0]!; const providerWallet = walletsResponse.data?.wallets?.[1]!; console.log("\n── Step 2: Fund the client wallet ──"); console.log(" Fund this wallet with Arc Testnet USDC:"); console.log(` Client: ${clientWallet.address}`); console.log(` Wallet ID: ${clientWallet.id}`); console.log(" Public faucet: https://faucet.circle.com"); console.log(" Console faucet: https://console.circle.com/faucet"); console.log("\n This script will fund the provider wallet automatically."); const rl = createInterface({ input, output }); await rl.question("\nPress Enter after the client wallet is funded... "); rl.close(); console.log("\n── Step 3: Transfer starter USDC to provider ──"); const transferTx = await circleClient.createTransaction({ walletAddress: clientWallet.address!, blockchain: "ARC-TESTNET", tokenAddress: "0x3600000000000000000000000000000000000000", destinationAddress: providerWallet.address!, amount: [PROVIDER_STARTER_BALANCE], fee: { type: "level", config: { feeLevel: "MEDIUM" } }, }); await waitForTransaction( transferTx.data?.id!, "transfer starter USDC to provider", ); console.log("\n── Step 4: Check balances ──"); await printBalances("Balances", [ { label: "Client", ...clientWallet }, { label: "Provider", ...providerWallet }, ]); const now = await publicClient.getBlock(); const expiredAt = now.timestamp + 3600n; console.log("\n── Step 5: Create job - createJob() ──"); const createJobTx = await circleClient.createContractExecutionTransaction({ walletAddress: clientWallet.address!, blockchain: "ARC-TESTNET", contractAddress: AGENTIC_COMMERCE_CONTRACT, abiFunctionSignature: "createJob(address,address,uint256,string,address)", abiParameters: [ providerWallet.address!, clientWallet.address!, expiredAt.toString(), "ERC-8183 demo job on Arc Testnet", "0x0000000000000000000000000000000000000000", ], fee: { type: "level", config: { feeLevel: "MEDIUM" } }, }); const createJobTxHash = await waitForTransaction( createJobTx.data?.id!, "create job", ); const jobId = await extractJobId(createJobTxHash); console.log(` Job ID: ${jobId}`); console.log("\n── Step 6: Set budget - setBudget() ──"); const setBudgetTx = await circleClient.createContractExecutionTransaction({ walletAddress: providerWallet.address!, blockchain: "ARC-TESTNET", contractAddress: AGENTIC_COMMERCE_CONTRACT, abiFunctionSignature: "setBudget(uint256,uint256,bytes)", abiParameters: [jobId.toString(), JOB_BUDGET.toString(), "0x"], fee: { type: "level", config: { feeLevel: "MEDIUM" } }, }); await waitForTransaction(setBudgetTx.data?.id!, "set budget"); console.log("\n── Step 7: Approve USDC - approve() ──"); const approveTx = await circleClient.createContractExecutionTransaction({ walletAddress: clientWallet.address!, blockchain: "ARC-TESTNET", contractAddress: "0x3600000000000000000000000000000000000000", abiFunctionSignature: "approve(address,uint256)", abiParameters: [AGENTIC_COMMERCE_CONTRACT, JOB_BUDGET.toString()], fee: { type: "level", config: { feeLevel: "MEDIUM" } }, }); await waitForTransaction(approveTx.data?.id!, "approve USDC"); console.log("\n── Step 8: Fund escrow - fund() ──"); const fundTx = await circleClient.createContractExecutionTransaction({ walletAddress: clientWallet.address!, blockchain: "ARC-TESTNET", contractAddress: AGENTIC_COMMERCE_CONTRACT, abiFunctionSignature: "fund(uint256,bytes)", abiParameters: [jobId.toString(), "0x"], fee: { type: "level", config: { feeLevel: "MEDIUM" } }, }); await waitForTransaction(fundTx.data?.id!, "fund escrow"); console.log("\n── Step 9: Submit deliverable - submit() ──"); const deliverableHash = keccak256(toHex("arc-erc8183-demo-deliverable")); const submitTx = await circleClient.createContractExecutionTransaction({ walletAddress: providerWallet.address!, blockchain: "ARC-TESTNET", contractAddress: AGENTIC_COMMERCE_CONTRACT, abiFunctionSignature: "submit(uint256,bytes32,bytes)", abiParameters: [jobId.toString(), deliverableHash, "0x"], fee: { type: "level", config: { feeLevel: "MEDIUM" } }, }); await waitForTransaction(submitTx.data?.id!, "submit deliverable"); console.log("\n── Step 10: Complete job - complete() ──"); const reasonHash = keccak256(toHex("deliverable-approved")); const completeTx = await circleClient.createContractExecutionTransaction({ walletAddress: clientWallet.address!, blockchain: "ARC-TESTNET", contractAddress: AGENTIC_COMMERCE_CONTRACT, abiFunctionSignature: "complete(uint256,bytes32,bytes)", abiParameters: [jobId.toString(), reasonHash, "0x"], fee: { type: "level", config: { feeLevel: "MEDIUM" } }, }); await waitForTransaction(completeTx.data?.id!, "complete job"); console.log("\n── Step 11: Check final job state ──"); const job = await publicClient.readContract({ address: AGENTIC_COMMERCE_CONTRACT, abi: agenticCommerceAbi, functionName: "getJob", args: [jobId], }); console.log(` Job ID: ${jobId}`); console.log(` Status: ${STATUS_NAMES[Number(job.status)]}`); console.log(` Budget: ${formatUnits(job.budget, 6)} USDC`); console.log(` Hook: ${job.hook}`); console.log(` Deliverable hash submitted: ${deliverableHash}`); console.log("\n── Step 12: Check final balances ──"); await printBalances("Balances", [ { label: "Client", ...clientWallet }, { label: "Provider", ...providerWallet }, ]); } main().catch((error) => { console.error("\nError:", error.message || error); process.exit(1); }); ``` ```python index.py expandable theme={null} import os import sys import time from circle.web3 import developer_controlled_wallets, utils from dotenv import load_dotenv from web3 import Web3 load_dotenv() # Bootstrap the provider wallet from the client wallet to avoid a second faucet request. PROVIDER_STARTER_BALANCE = "1" AGENTIC_COMMERCE_CONTRACT = "0x0747EEf0706327138c69792bF28Cd525089e4583" JOB_BUDGET = "5000000" # 5 USDC (ERC-20, 6 decimals) circle_client = utils.init_developer_controlled_wallets_client( api_key=os.getenv("CIRCLE_API_KEY"), entity_secret=os.getenv("CIRCLE_ENTITY_SECRET"), ) wallet_sets_api = developer_controlled_wallets.WalletSetsApi(circle_client) wallets_api = developer_controlled_wallets.WalletsApi(circle_client) transactions_api = developer_controlled_wallets.TransactionsApi(circle_client) web3 = Web3(Web3.HTTPProvider("https://rpc.testnet.arc.io")) agentic_commerce_abi = [ { "type": "function", "name": "getJob", "stateMutability": "view", "inputs": [{"name": "jobId", "type": "uint256"}], "outputs": [ { "type": "tuple", "components": [ {"name": "id", "type": "uint256"}, {"name": "client", "type": "address"}, {"name": "provider", "type": "address"}, {"name": "evaluator", "type": "address"}, {"name": "description", "type": "string"}, {"name": "budget", "type": "uint256"}, {"name": "expiredAt", "type": "uint256"}, {"name": "status", "type": "uint8"}, {"name": "hook", "type": "address"}, ], } ], }, { "type": "event", "name": "JobCreated", "inputs": [ {"indexed": True, "name": "jobId", "type": "uint256"}, {"indexed": True, "name": "client", "type": "address"}, {"indexed": True, "name": "provider", "type": "address"}, {"indexed": False, "name": "evaluator", "type": "address"}, {"indexed": False, "name": "expiredAt", "type": "uint256"}, {"indexed": False, "name": "hook", "type": "address"}, ], "anonymous": False, }, ] STATUS_NAMES = [ "Open", "Funded", "Submitted", "Completed", "Rejected", "Expired", ] def extract_job_id(tx_hash: str) -> int: contract = web3.eth.contract( address=Web3.to_checksum_address(AGENTIC_COMMERCE_CONTRACT), abi=agentic_commerce_abi, ) receipt = web3.eth.get_transaction_receipt(tx_hash) logs = contract.events.JobCreated().process_receipt(receipt) if not logs: raise RuntimeError("Could not parse JobCreated event") return int(logs[0]["args"]["jobId"]) def wait_for_transaction(tx_id: str, label: str) -> str: sys.stdout.write(f" Waiting for {label}") sys.stdout.flush() for _ in range(60): time.sleep(2) tx = transactions_api.get_transaction(id=tx_id) transaction = tx.data.transaction if transaction.state == "COMPLETE" and transaction.tx_hash: tx_hash = transaction.tx_hash print(f" ✓\n Tx: https://testnet.arcscan.app/tx/{tx_hash}") return tx_hash if transaction.state == "FAILED": raise RuntimeError(f"{label} failed onchain") sys.stdout.write(".") sys.stdout.flush() raise RuntimeError(f"{label} timed out") def print_balances(title: str, wallets: list[dict[str, str]]) -> None: print(f"\n{title}:") for wallet in wallets: balances = wallets_api.list_wallet_balance(id=wallet["id"]) usdc_amount = "0" for entry in balances.data.token_balances or []: balance = getattr(entry, "actual_instance", entry) token = getattr(balance, "token", None) token = getattr(token, "actual_instance", token) if token and getattr(token, "symbol", None) == "USDC": usdc_amount = getattr(balance, "amount", "0") break print(f" {wallet['label']}: {wallet['address']}") print(f" USDC: {usdc_amount}") def main() -> None: print("── Step 1: Create wallets ──") wallet_set = wallet_sets_api.create_wallet_set( developer_controlled_wallets.CreateWalletSetRequest.from_dict({ "name": "ERC8183 Job Wallets", }) ) wallets_response = wallets_api.create_wallet( developer_controlled_wallets.CreateWalletRequest.from_dict({ "blockchains": ["ARC-TESTNET"], "count": 2, "walletSetId": wallet_set.data.wallet_set.actual_instance.id, "accountType": "SCA", }) ) client_wallet = wallets_response.data.wallets[0].actual_instance provider_wallet = wallets_response.data.wallets[1].actual_instance print("\n── Step 2: Fund the client wallet ──") print(" Fund this wallet with Arc Testnet USDC:") print(f" Client: {client_wallet.address}") print(f" Wallet ID: {client_wallet.id}") print(" Public faucet: https://faucet.circle.com") print(" Console faucet: https://console.circle.com/faucet") print("\n This script will fund the provider wallet automatically.") input("\nPress Enter after the client wallet is funded... ") print("\n── Step 3: Transfer starter USDC to provider ──") transfer_request = ( developer_controlled_wallets.CreateTransferTransactionForDeveloperRequest.from_dict( { "walletAddress": client_wallet.address, "blockchain": "ARC-TESTNET", "tokenAddress": "0x3600000000000000000000000000000000000000", "destinationAddress": provider_wallet.address, "amounts": [PROVIDER_STARTER_BALANCE], "feeLevel": "MEDIUM", } ) ) transfer_response = transactions_api.create_developer_transaction_transfer( create_transfer_transaction_for_developer_request=transfer_request ) wait_for_transaction( transfer_response.data.id, "transfer starter USDC to provider", ) print("\n── Step 4: Check balances ──") print_balances( "Balances", [ {"label": "Client", "address": client_wallet.address, "id": client_wallet.id}, {"label": "Provider", "address": provider_wallet.address, "id": provider_wallet.id}, ], ) expired_at = web3.eth.get_block("latest")["timestamp"] + 3600 print("\n── Step 5: Create job - createJob() ──") create_job_request = ( developer_controlled_wallets.CreateContractExecutionTransactionForDeveloperRequest.from_dict( { "walletAddress": client_wallet.address, "blockchain": "ARC-TESTNET", "contractAddress": AGENTIC_COMMERCE_CONTRACT, "abiFunctionSignature": "createJob(address,address,uint256,string,address)", "abiParameters": [ provider_wallet.address, client_wallet.address, str(expired_at), "ERC-8183 demo job on Arc Testnet", "0x0000000000000000000000000000000000000000", ], "feeLevel": "MEDIUM", } ) ) create_job_response = transactions_api.create_developer_transaction_contract_execution( create_job_request ) create_job_tx_hash = wait_for_transaction( create_job_response.data.id, "create job", ) job_id = extract_job_id(create_job_tx_hash) print(f" Job ID: {job_id}") print("\n── Step 6: Set budget - setBudget() ──") set_budget_request = ( developer_controlled_wallets.CreateContractExecutionTransactionForDeveloperRequest.from_dict( { "walletAddress": provider_wallet.address, "blockchain": "ARC-TESTNET", "contractAddress": AGENTIC_COMMERCE_CONTRACT, "abiFunctionSignature": "setBudget(uint256,uint256,bytes)", "abiParameters": [str(job_id), JOB_BUDGET, "0x"], "feeLevel": "MEDIUM", } ) ) set_budget_response = transactions_api.create_developer_transaction_contract_execution( set_budget_request ) wait_for_transaction(set_budget_response.data.id, "set budget") print("\n── Step 7: Approve USDC - approve() ──") approve_request = ( developer_controlled_wallets.CreateContractExecutionTransactionForDeveloperRequest.from_dict( { "walletAddress": client_wallet.address, "blockchain": "ARC-TESTNET", "contractAddress": "0x3600000000000000000000000000000000000000", "abiFunctionSignature": "approve(address,uint256)", "abiParameters": [AGENTIC_COMMERCE_CONTRACT, JOB_BUDGET], "feeLevel": "MEDIUM", } ) ) approve_response = transactions_api.create_developer_transaction_contract_execution( approve_request ) wait_for_transaction(approve_response.data.id, "approve USDC") print("\n── Step 8: Fund escrow - fund() ──") fund_request = ( developer_controlled_wallets.CreateContractExecutionTransactionForDeveloperRequest.from_dict( { "walletAddress": client_wallet.address, "blockchain": "ARC-TESTNET", "contractAddress": AGENTIC_COMMERCE_CONTRACT, "abiFunctionSignature": "fund(uint256,bytes)", "abiParameters": [str(job_id), "0x"], "feeLevel": "MEDIUM", } ) ) fund_response = transactions_api.create_developer_transaction_contract_execution( fund_request ) wait_for_transaction(fund_response.data.id, "fund escrow") print("\n── Step 9: Submit deliverable - submit() ──") deliverable_hash = Web3.to_hex( Web3.keccak(text="arc-erc8183-demo-deliverable") ) submit_request = ( developer_controlled_wallets.CreateContractExecutionTransactionForDeveloperRequest.from_dict( { "walletAddress": provider_wallet.address, "blockchain": "ARC-TESTNET", "contractAddress": AGENTIC_COMMERCE_CONTRACT, "abiFunctionSignature": "submit(uint256,bytes32,bytes)", "abiParameters": [str(job_id), deliverable_hash, "0x"], "feeLevel": "MEDIUM", } ) ) submit_response = transactions_api.create_developer_transaction_contract_execution( submit_request ) wait_for_transaction(submit_response.data.id, "submit deliverable") print("\n── Step 10: Complete job - complete() ──") reason_hash = Web3.to_hex(Web3.keccak(text="deliverable-approved")) complete_request = ( developer_controlled_wallets.CreateContractExecutionTransactionForDeveloperRequest.from_dict( { "walletAddress": client_wallet.address, "blockchain": "ARC-TESTNET", "contractAddress": AGENTIC_COMMERCE_CONTRACT, "abiFunctionSignature": "complete(uint256,bytes32,bytes)", "abiParameters": [str(job_id), reason_hash, "0x"], "feeLevel": "MEDIUM", } ) ) complete_response = transactions_api.create_developer_transaction_contract_execution( complete_request ) wait_for_transaction(complete_response.data.id, "complete job") print("\n── Step 11: Check final job state ──") contract = web3.eth.contract( address=Web3.to_checksum_address(AGENTIC_COMMERCE_CONTRACT), abi=agentic_commerce_abi, ) job = contract.functions.getJob(job_id).call() print(f" Job ID: {job_id}") print(f" Status: {STATUS_NAMES[int(job[7])]}") print(f" Budget: {Web3.from_wei(job[5], 'mwei')} USDC") print(f" Hook: {job[8]}") print(f" Deliverable hash submitted: {deliverable_hash}") print("\n── Step 12: Check final balances ──") print_balances( "Balances", [ {"label": "Client", "address": client_wallet.address, "id": client_wallet.id}, {"label": "Provider", "address": provider_wallet.address, "id": provider_wallet.id}, ], ) if __name__ == "__main__": try: main() except Exception as error: print(f"\nError: {error}") sys.exit(1) ``` Run the script: ```shell Node.js theme={null} npm run start ``` ```shell Python theme={null} python index.py ``` ## Prerequisites Before you begin, make sure you have: 1. Installed [Node.js v22+](https://nodejs.org/) 2. Two self-managed EVM wallets for Arc Testnet * Testnet USDC in both wallets to pay for gas and for the client wallet to fund escrow ## Step 1. Set up your project Create a project directory, install dependencies, and configure your environment. ### 1.1. Create the project and install dependencies ```shell theme={null} mkdir erc8183-quickstart cd erc8183-quickstart 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 tsx typescript @types/node ``` ### 1.2. Configure TypeScript (optional) This step is optional. It helps prevent missing types in your IDE or editor. 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 Create a `.env` file in the project directory and add the two private keys used for the flow: ```text theme={null} CLIENT_PRIVATE_KEY=0xYOUR_CLIENT_PRIVATE_KEY PROVIDER_PRIVATE_KEY=0xYOUR_PROVIDER_PRIVATE_KEY ``` * `CLIENT_PRIVATE_KEY` is the `0x`-prefixed private key for the Arc Testnet wallet that creates the job, approves USDC, funds escrow, and completes the job as the evaluator. * `PROVIDER_PRIVATE_KEY` is the `0x`-prefixed private key for the Arc Testnet wallet that sets the budget and submits the deliverable. The `npm run start` command loads variables from `.env` using Node.js native env-file support. Prefer editing `.env` files in your IDE or editor so credentials are not leaked to your shell history. ## Step 2. Prepare your wallets In this step, you prepare two self-managed Arc Testnet wallets for the ERC-8183 flow. One wallet acts as the client and evaluator, and the other acts as the provider. If you already have two funded Arc Testnet wallets for this flow, skip to [Step 3](#step-3-create-the-job). The Step 3 through 8 code snippets explain the flow in smaller pieces. They are not cumulative and will not run if pasted together. To run the full workflow end to end, use the [complete script](#full-job-lifecycle-script-2) at the end of this tab. ### 2.1. Create or fund your wallets Create two self-managed EVM wallets if you do not already have them. For example, you can generate throwaway wallets with Foundry: ```shell theme={null} cast wallet new --json ``` Run it twice, once for the client wallet and once for the provider wallet, then fund both wallets with Arc Testnet USDC so they can submit transactions. Fund the client wallet with Arc Testnet USDC so it can escrow the job budget. ### 2.2. Confirm wallet roles * the client wallet creates the job, approves USDC, funds escrow, and completes the job as the evaluator * the provider wallet sets the budget and submits the deliverable ## Step 3. Create the job Call `createJob(provider, evaluator, expiredAt, description, hook)` on the deployed ERC-8183 reference implementation. This creates the job in the `Open` state. This quickstart uses `address(0)` for `hook` so the flow stays on the default non-hooked path. ```typescript index.ts theme={null} const block = await publicClient.getBlock(); const expiredAt = block.timestamp + 3600n; const createJobHash = await clientWalletClient.writeContract({ address: AGENTIC_COMMERCE_CONTRACT, abi: agenticCommerceAbi, functionName: "createJob", args: [ providerAccount.address, clientAccount.address, expiredAt, "ERC-8183 demo job on Arc Testnet", "0x0000000000000000000000000000000000000000", ], }); ``` ## Step 4. Set the budget In this deployed contract, the provider sets the job price by calling `setBudget(jobId, amount, optParams)`. ```typescript index.ts theme={null} const setBudgetHash = await providerWalletClient.writeContract({ address: AGENTIC_COMMERCE_CONTRACT, abi: agenticCommerceAbi, functionName: "setBudget", args: [jobId, JOB_BUDGET, "0x"], }); ``` ## Step 5. Approve USDC and fund escrow Before the client can fund the job, the USDC contract must approve the ERC-8183 contract to transfer the escrow amount. Then the client calls `fund(jobId, optParams)` to move the job into the `Funded` state. ```typescript index.ts theme={null} const approveHash = await clientWalletClient.writeContract({ address: USDC_CONTRACT, abi: erc20Abi, functionName: "approve", args: [AGENTIC_COMMERCE_CONTRACT, JOB_BUDGET], }); const fundHash = await clientWalletClient.writeContract({ address: AGENTIC_COMMERCE_CONTRACT, abi: agenticCommerceAbi, functionName: "fund", args: [jobId, "0x"], }); ``` ## Step 6. Submit the deliverable The provider submits a `bytes32` deliverable hash, moving the job into the `Submitted` state. ```typescript index.ts theme={null} const deliverableHash = keccak256(toHex("arc-erc8183-demo-deliverable")); const submitHash = await providerWalletClient.writeContract({ address: AGENTIC_COMMERCE_CONTRACT, abi: agenticCommerceAbi, functionName: "submit", args: [jobId, deliverableHash, "0x"], }); ``` ## Step 7. Complete the job The evaluator completes the job by calling `complete(jobId, reason, optParams)`. In this quickstart, the client is also the evaluator. ```typescript index.ts theme={null} const reasonHash = keccak256(toHex("work-delivered-and-approved")); const completeHash = await clientWalletClient.writeContract({ address: AGENTIC_COMMERCE_CONTRACT, abi: agenticCommerceAbi, functionName: "complete", args: [jobId, reasonHash, "0x"], }); ``` ## Step 8. Check the final job state Read the job back from the contract to confirm it reached `Completed`. This reference implementation does not return the deliverable in `getJob()`, so the script prints the submitted deliverable hash from local flow state instead. ```typescript index.ts theme={null} const job = await publicClient.readContract({ address: AGENTIC_COMMERCE_CONTRACT, abi: agenticCommerceAbi, functionName: "getJob", args: [jobId], }); console.log(`Job ID: ${job.id}`); console.log(`Status: ${STATUS_NAMES[Number(job.status)]}`); console.log(`Budget: ${formatUnits(job.budget, 6)} USDC`); console.log(`Hook: ${job.hook}`); console.log(`Deliverable hash submitted: ${deliverableHash}`); ``` This complete script combines the preceding steps into a single runnable file. ## Full job lifecycle script ```typescript index.ts expandable theme={null} import { createPublicClient, createWalletClient, decodeEventLog, formatUnits, http, keccak256, toHex, } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { arcTestnet } from "viem/chains"; const AGENTIC_COMMERCE_CONTRACT = "0x0747EEf0706327138c69792bF28Cd525089e4583"; const USDC_CONTRACT = "0x3600000000000000000000000000000000000000"; const JOB_BUDGET = 1_000_000n; const clientAccount = privateKeyToAccount( process.env.CLIENT_PRIVATE_KEY as `0x${string}`, ); const providerAccount = privateKeyToAccount( process.env.PROVIDER_PRIVATE_KEY as `0x${string}`, ); const publicClient = createPublicClient({ chain: arcTestnet, transport: http(), }); const clientWalletClient = createWalletClient({ account: clientAccount, chain: arcTestnet, transport: http(), }); const providerWalletClient = createWalletClient({ account: providerAccount, chain: arcTestnet, transport: http(), }); const agenticCommerceAbi = [ { name: "createJob", type: "function", stateMutability: "nonpayable", inputs: [ { name: "provider", type: "address" }, { name: "evaluator", type: "address" }, { name: "expiredAt", type: "uint256" }, { name: "description", type: "string" }, { name: "hook", type: "address" }, ], outputs: [{ name: "jobId", type: "uint256" }], }, { name: "setBudget", type: "function", stateMutability: "nonpayable", inputs: [ { name: "jobId", type: "uint256" }, { name: "amount", type: "uint256" }, { name: "optParams", type: "bytes" }, ], outputs: [], }, { name: "fund", type: "function", stateMutability: "nonpayable", inputs: [ { name: "jobId", type: "uint256" }, { name: "optParams", type: "bytes" }, ], outputs: [], }, { name: "submit", type: "function", stateMutability: "nonpayable", inputs: [ { name: "jobId", type: "uint256" }, { name: "deliverable", type: "bytes32" }, { name: "optParams", type: "bytes" }, ], outputs: [], }, { name: "complete", type: "function", stateMutability: "nonpayable", inputs: [ { name: "jobId", type: "uint256" }, { name: "reason", type: "bytes32" }, { name: "optParams", type: "bytes" }, ], outputs: [], }, { name: "getJob", type: "function", stateMutability: "view", inputs: [{ name: "jobId", type: "uint256" }], outputs: [ { type: "tuple", components: [ { name: "id", type: "uint256" }, { name: "client", type: "address" }, { name: "provider", type: "address" }, { name: "evaluator", type: "address" }, { name: "description", type: "string" }, { name: "budget", type: "uint256" }, { name: "expiredAt", type: "uint256" }, { name: "status", type: "uint8" }, { name: "hook", type: "address" }, ], }, ], }, { name: "JobCreated", type: "event", anonymous: false, inputs: [ { indexed: true, name: "jobId", type: "uint256" }, { indexed: true, name: "client", type: "address" }, { indexed: true, name: "provider", type: "address" }, { indexed: false, name: "evaluator", type: "address" }, { indexed: false, name: "expiredAt", type: "uint256" }, { indexed: false, name: "hook", type: "address" }, ], }, ] as const; const erc20Abi = [ { name: "approve", type: "function", stateMutability: "nonpayable", inputs: [ { name: "spender", type: "address" }, { name: "amount", type: "uint256" }, ], outputs: [{ name: "", type: "bool" }], }, { name: "balanceOf", type: "function", stateMutability: "view", inputs: [{ name: "account", type: "address" }], outputs: [{ name: "", type: "uint256" }], }, ] as const; const STATUS_NAMES = [ "Open", "Funded", "Submitted", "Completed", "Rejected", "Expired", ]; async function waitForTransaction(hash: `0x${string}`, label: string) { process.stdout.write(` Waiting for ${label}`); const receipt = await publicClient.waitForTransactionReceipt({ hash }); console.log(` ✓\n Tx: ${arcTestnet.blockExplorers.default.url}/tx/${hash}`); return receipt; } async function printBalances(title: string) { console.log(`\n${title}:`); const clientBalance = await publicClient.readContract({ address: USDC_CONTRACT, abi: erc20Abi, functionName: "balanceOf", args: [clientAccount.address], }); const providerBalance = await publicClient.readContract({ address: USDC_CONTRACT, abi: erc20Abi, functionName: "balanceOf", args: [providerAccount.address], }); console.log(` Client: ${clientAccount.address}`); console.log(` USDC: ${formatUnits(clientBalance, 6)}`); console.log(` Provider: ${providerAccount.address}`); console.log(` USDC: ${formatUnits(providerBalance, 6)}`); } async function main(): Promise { console.log("── Step 1: Prepare accounts ──"); console.log(` Client: ${clientAccount.address}`); console.log(` Provider: ${providerAccount.address}`); console.log(` Evaluator: ${clientAccount.address}`); console.log("\n── Step 2: Check balances ──"); await printBalances("Balances"); const block = await publicClient.getBlock(); const expiredAt = block.timestamp + 3600n; console.log("\n── Step 3: Create job - createJob() ──"); const createJobHash = await clientWalletClient.writeContract({ address: AGENTIC_COMMERCE_CONTRACT, abi: agenticCommerceAbi, functionName: "createJob", args: [ providerAccount.address, clientAccount.address, expiredAt, "ERC-8183 demo job on Arc Testnet", "0x0000000000000000000000000000000000000000", ], }); const createJobReceipt = await waitForTransaction( createJobHash, "create job", ); let jobId: bigint | undefined; for (const log of createJobReceipt.logs) { try { const decoded = decodeEventLog({ abi: agenticCommerceAbi, data: log.data, topics: log.topics, }); if (decoded.eventName === "JobCreated") { jobId = decoded.args.jobId; break; } } catch { continue; } } if (jobId == null) { throw new Error("Could not parse JobCreated event"); } console.log(` Job ID: ${jobId}`); console.log("\n── Step 4: Set budget - setBudget() ──"); const setBudgetHash = await providerWalletClient.writeContract({ address: AGENTIC_COMMERCE_CONTRACT, abi: agenticCommerceAbi, functionName: "setBudget", args: [jobId, JOB_BUDGET, "0x"], }); await waitForTransaction(setBudgetHash, "set budget"); console.log("\n── Step 5: Approve USDC - approve() ──"); const approveHash = await clientWalletClient.writeContract({ address: USDC_CONTRACT, abi: erc20Abi, functionName: "approve", args: [AGENTIC_COMMERCE_CONTRACT, JOB_BUDGET], }); await waitForTransaction(approveHash, "approve USDC"); console.log("\n── Step 6: Fund escrow - fund() ──"); const fundHash = await clientWalletClient.writeContract({ address: AGENTIC_COMMERCE_CONTRACT, abi: agenticCommerceAbi, functionName: "fund", args: [jobId, "0x"], }); await waitForTransaction(fundHash, "fund escrow"); console.log("\n── Step 7: Submit deliverable - submit() ──"); const deliverableHash = keccak256(toHex("arc-erc8183-demo-deliverable")); const submitHash = await providerWalletClient.writeContract({ address: AGENTIC_COMMERCE_CONTRACT, abi: agenticCommerceAbi, functionName: "submit", args: [jobId, deliverableHash, "0x"], }); await waitForTransaction(submitHash, "submit deliverable"); console.log("\n── Step 8: Complete job - complete() ──"); const reasonHash = keccak256(toHex("work-delivered-and-approved")); const completeHash = await clientWalletClient.writeContract({ address: AGENTIC_COMMERCE_CONTRACT, abi: agenticCommerceAbi, functionName: "complete", args: [jobId, reasonHash, "0x"], }); await waitForTransaction(completeHash, "complete job"); console.log("\n── Step 9: Check final job state ──"); const job = await publicClient.readContract({ address: AGENTIC_COMMERCE_CONTRACT, abi: agenticCommerceAbi, functionName: "getJob", args: [jobId], }); console.log(` Job ID: ${job.id}`); console.log(` Status: ${STATUS_NAMES[Number(job.status)]}`); console.log(` Budget: ${formatUnits(job.budget, 6)} USDC`); console.log(` Hook: ${job.hook}`); console.log(` Deliverable hash submitted: ${deliverableHash}`); console.log("\n── Step 10: Check final balances ──"); await printBalances("Balances"); } void main().catch((error) => { console.error("\nError:", error); process.exit(1); }); ``` Run the script: ```shell theme={null} npm run start ``` ## Verify the result If the flow succeeds, the output should show: * a created job ID * a completed final status * the client balance reduced by the funded escrow amount * the provider balance increased after completion If platform or evaluator fees are configured on the deployed contract, the provider receives the net amount after fees rather than the full job budget. You can also inspect the transaction links in the terminal output on [Arcscan Testnet](https://testnet.arcscan.app). ## Summary After completing this quickstart, you've successfully: * Set up a project for running an ERC-8183 job flow on Arc Testnet * Prepared client and provider wallets for the client, provider, and evaluator roles * Walked through an example ERC-8183 job lifecycle * Confirmed balances and job state in the script output and reviewed transactions on Arcscan Testnet # Deploy contracts Source: https://docs.arc.io/arc/tutorials/deploy-contracts Deploy pre-audited smart contract templates on Arc with Circle Contracts. This tutorial guides you through deploying smart contracts on Arc Testnet with [Circle Contracts](https://developers.circle.com/contracts/scp-templates-overview). You'll create a [Circle Dev-Controlled SCA Wallet](https://developers.circle.com/wallets/dev-controlled), then deploy pre-audited contract templates (ERC-20, ERC-721, ERC-1155, Airdrop). With SCA wallets, [Circle Gas Station](https://developers.circle.com/wallets/gas-station) automatically sponsors your transaction fees on Arc Testnet. These pre-audited templates represent building blocks: ERC-20 for money and liquidity, ERC-721 for identity and unique rights, ERC-1155 for scalable financial instruments, and Airdrops for distributing incentives. To learn more about available templates, visit the [Templates Overview](https://developers.circle.com/contracts/scp-templates-overview) to review all templates and their options. ## Prerequisites To complete this tutorial, you need: 1. [Node.js v22+](https://nodejs.org/) installed 2. **Circle Developer Account** - [Sign up](https://console.circle.com/) on the Developer Console 3. **API Key** - Create in the Console: **Keys → Create a key → API key → Standard Key** 4. **Entity secret**: Generated and registered using the [Circle Console](https://console.circle.com/wallets/dev/configurator/entity-secret) or the [SDK](https://developers.circle.com/wallets/dev-controlled/register-entity-secret#create-an-entity-secret-using-the-sdk) Circle wallets use three credentials. An [**API key**](https://developers.circle.com/api-reference/keys) authenticates your requests to Circle APIs. An [**entity secret**](https://developers.circle.com/wallets/dev-controlled/entity-secret-management) authorizes each wallet operation. A [**wallet set ID**](https://developers.circle.com/wallets/dev-controlled/create-your-first-wallet) groups your wallets. ## Step 1. Set up your project Before deploying any template, you need a working project and a funded dev-controlled wallet on Arc Testnet. Complete the steps in this section once. Then reuse the same wallet and credentials across all template deployments that follow. ### 1.1. Create the project and install dependencies Create a new directory. Navigate to it and start a new project with default settings. ```shell Node.js theme={null} mkdir hello-arc cd hello-arc npm init -y npm pkg set type=module # Add run scripts for wallet creation and contract deployment npm pkg set scripts.create-wallet="tsx --env-file=.env create-wallet.ts" npm pkg set scripts.deploy-erc20="tsx --env-file=.env deploy-erc20.ts" npm pkg set scripts.deploy-erc721="tsx --env-file=.env deploy-erc721.ts" npm pkg set scripts.deploy-erc1155="tsx --env-file=.env deploy-erc1155.ts" npm pkg set scripts.deploy-airdrop="tsx --env-file=.env deploy-airdrop.ts" ``` ```shell Python theme={null} mkdir hello-arc cd hello-arc python3 -m venv .venv source .venv/bin/activate ``` In the project directory, install the [Circle Dev-Controlled Wallets SDK](https://developers.circle.com/wallets/dev-controlled) and the [Circle Contracts SDK](https://developers.circle.com/sdks). Dev-Controlled Wallets are Circle-managed wallets that your app controls using APIs. You can deploy contracts and submit transactions without managing private keys directly. You can also call the [Circle Wallets API](https://developers.circle.com/api-reference/wallets/) and [Circle Contracts API](https://developers.circle.com/api-reference/contracts/) directly if you can't use the SDKs in your project. ```shell Node.js theme={null} npm install @circle-fin/developer-controlled-wallets @circle-fin/smart-contract-platform npm install --save-dev tsx typescript @types/node ``` ```shell Python theme={null} pip install circle-smart-contract-platform circle-developer-controlled-wallets ``` ### 1.2. Configure TypeScript (optional) Create a `tsconfig.json` file: ```shell Node.js theme={null} npx tsc --init ``` Then, edit the `tsconfig.json` file: ```shell Node.js theme={null} cat <<'EOF' > tsconfig.json { "compilerOptions": { "target": "ESNext", "module": "ESNext", "moduleResolution": "bundler", "strict": true, "types": ["node"] } } EOF ``` ### 1.3. Set environment variables Create a `.env` file in the project directory with your Circle credentials. Replace these placeholders with your own credentials: ```text .env theme={null} CIRCLE_API_KEY=YOUR_API_KEY CIRCLE_ENTITY_SECRET=YOUR_ENTITY_SECRET CIRCLE_WEB3_API_KEY=YOUR_API_KEY ``` * `CIRCLE_API_KEY` is your Circle Developer API key for Wallets and Contracts API requests. * `CIRCLE_ENTITY_SECRET` is your registered entity secret used to authorize developer-controlled wallet operations. * `CIRCLE_WEB3_API_KEY` is the Python SDK compatibility variable and should use the same value as `CIRCLE_API_KEY`. The npm run commands in this tutorial load variables from `.env` using Node.js native env-file support. Prefer editing `.env` files in your IDE or editor so credentials are not leaked to your shell history. This tutorial adds runtime values such as wallet IDs, transaction IDs, and contract IDs later in the flow. Keep those derived values aligned with the script outputs as you progress through the deployment steps. ## Step 2. Set up your wallet In this step, you create a dev-controlled wallet and fund it for contract deployment on Arc Testnet. If you already have a funded Arc Testnet dev-controlled wallet, skip to [the contract templates section](#deploy-an-erc-20-contract). ### 2.1. Create a wallet Import the Wallets SDK and start the client with your API key and Entity Secret. Dev-controlled wallets are created in a [wallet set](https://developers.circle.com/wallets/dev-controlled/create-your-first-wallet#1-create-a-wallet-set). The wallet set is the source from which wallet keys are derived. ```ts create-wallet.ts theme={null} import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets"; const client = initiateDeveloperControlledWalletsClient({ apiKey: process.env.CIRCLE_API_KEY, entitySecret: process.env.CIRCLE_ENTITY_SECRET, }); // Create a wallet set const walletSetResponse = await client.createWalletSet({ name: "Wallet Set 1", }); // Create a wallet on Arc Testnet const walletsResponse = await client.createWallets({ blockchains: ["ARC-TESTNET"], count: 1, walletSetId: walletSetResponse.data?.walletSet?.id ?? "", accountType: "SCA", }); console.log(JSON.stringify(walletsResponse.data, null, 2)); ``` ```python create_wallet.py theme={null} from circle.web3 import utils, developer_controlled_wallets import os import json client = utils.init_developer_controlled_wallets_client( api_key=os.getenv("CIRCLE_API_KEY"), entity_secret=os.getenv("CIRCLE_ENTITY_SECRET") ) wallet_sets_api = developer_controlled_wallets.WalletSetsApi(client) wallets_api = developer_controlled_wallets.WalletsApi(client) # Create a wallet set wallet_set = wallet_sets_api.create_wallet_set( developer_controlled_wallets.CreateWalletSetRequest.from_dict({ "name": "Wallet Set 1" }) ) # Create a wallet on Arc testnet wallet = wallets_api.create_wallet( developer_controlled_wallets.CreateWalletRequest.from_dict({ "blockchains": ["ARC-TESTNET"], "count": 1, "walletSetId": wallet_set.data.wallet_set.actual_instance.id, "accountType": "SCA" }) ) print(json.dumps(wallet.data.to_dict(), indent=2)) ``` **Run the script:** ```shell Node.js theme={null} npm run create-wallet ``` ```shell Python theme={null} python create_wallet.py ``` **Response:** If you're calling the API directly, you'll need two requests. One to [create the wallet set](https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/create-wallet-set). One to [create the wallet](https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/create-wallet). Be sure to replace the [Entity Secret ciphertext](https://developers.circle.com/wallets/dev-controlled/entity-secret-management#what-is-an-entity-secret-ciphertext) and the idempotency key in your request. If you're using the SDKs, this is handled for you. You should now have a newly created dev-controlled wallet. The API response will look similar to the following: ```json theme={null} { "wallets": [ { "id": "45692c3e-2ffa-5c5b-a99c-61366939114c", "state": "LIVE", "walletSetId": "ee58db40-22b4-55cb-9ce6-3444cb6efd2f", "custodyType": "DEVELOPER", "address": "0xbcf83d3b112cbf43b19904e376dd8dee01fe2758", "blockchain": "ARC-TESTNET", "accountType": "SCA", "updateDate": "2026-01-20T09:39:16Z", "createDate": "2026-01-20T09:39:16Z", "scaCore": "circle_6900_singleowner_v3" } ] } ``` **Why SCA wallets?** Smart Contract Accounts (SCA) on Arc Testnet work with [Gas Station](https://developers.circle.com/wallets/gas-station) to automatically sponsor transaction fees. Learn more about [Gas Station policies and setup](https://developers.circle.com/wallets/gas-station). *** ## Deploy an ERC-20 contract ERC-20 is the standard for fungible tokens. Use this template for tokenized assets, treasury instruments, governance tokens, or programmable money. ### Step 3: Prepare for deployment #### 3.1. Get your wallet information Retrieve your wallet ID from Step 2. Ensure: * Wallet custody type is **Dev-Controlled** * Blockchain is **Arc Testnet** * Account type is **SCA** (Smart Contract Account, recommended for Gas Station compatibility) Note your wallet's address for subsequent steps. #### 3.2. Understand deployment parameters | Parameter | Description | | :----------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `idempotencyKey` | A unique value to prevent duplicate requests. | | `name` | The offchain contract name (visible in Circle Console only). Use `MyTokenContract`. | | `walletId` | The ID of the wallet deploying the contract. Use your dev-controlled wallet ID. | | `templateId` | The template identifier. Use `a1b74add-23e0-4712-88d1-6b3009e85a86` for ERC-20. See [Templates](https://developers.circle.com/contracts/scp-templates-overview). | | `blockchain` | The network to deploy onto. Use `ARC-TESTNET`. | | `entitySecretCiphertext` | The re-encrypted entity secret. See [Entity Secret Management](https://developers.circle.com/wallets/dev-controlled/entity-secret-management). | | `feeLevel` | The fee level for transaction processing. Use `MEDIUM`. | | `templateParameters` | The onchain initialization parameters (see the following section). | #### 3.3. Template parameters **Required Parameters:** | Parameter | Type | Description | | :--------------------- | :----- | :---------------------------------------------------------------------------------- | | `name` | String | The onchain contract name. Use `MyToken`. | | `defaultAdmin` | String | The address with administrator permissions. Use your Dev-Controlled Wallet address. | | `primarySaleRecipient` | String | The address that receives proceeds from first-time sales. Use your wallet address. | **Optional Parameters:** | Parameter | Type | Description | | :--------------------- | :--------- | :--------------------------------------------------------------------------------------------------------- | | `symbol` | String | The token symbol (for example, `MTK`). | | `platformFeeRecipient` | String | The address that receives platform fees from sales. Set this when implementing platform fee revenue share. | | `platformFeePercent` | Float | The platform fee percentage as decimal (for example, `0.1` for 10%). Requires `platformFeeRecipient`. | | `contractUri` | String | The URL for the contract metadata. | | `trustedForwarders` | Strings\[] | A list of addresses that can forward ERC2771 meta-transactions to this contract. | ### Step 4: Deploy the smart contract Deploy by making a request to [`POST /templates/{id}/deploy`](https://developers.circle.com/api-reference/contracts/smart-contract-platform/deploy-contract-template): ```ts deploy-erc20.ts theme={null} import { initiateSmartContractPlatformClient } from "@circle-fin/smart-contract-platform"; const circleContractSdk = initiateSmartContractPlatformClient({ apiKey: process.env.CIRCLE_API_KEY, entitySecret: process.env.CIRCLE_ENTITY_SECRET, }); const response = await circleContractSdk.deployContractTemplate({ id: "a1b74add-23e0-4712-88d1-6b3009e85a86", blockchain: "ARC-TESTNET", name: "MyTokenContract", walletId: process.env.WALLET_ID, templateParameters: { name: "MyToken", symbol: "MTK", defaultAdmin: process.env.WALLET_ADDRESS, primarySaleRecipient: process.env.WALLET_ADDRESS, }, fee: { type: "level", config: { feeLevel: "MEDIUM", }, }, }); console.log(JSON.stringify(response.data, null, 2)); ``` ```python deploy_erc20.py theme={null} from circle.web3 import utils, smart_contract_platform import os import json scpClient = utils.init_smart_contract_platform_client( api_key=os.getenv("CIRCLE_API_KEY"), entity_secret=os.getenv("CIRCLE_ENTITY_SECRET") ) api_instance = smart_contract_platform.TemplatesApi(scpClient) request = smart_contract_platform.TemplateContractDeploymentRequest.from_dict({ "blockchain": "ARC-TESTNET", "name": "MyTokenContract", "walletId": os.getenv("WALLET_ID"), "templateParameters": { "name": "MyToken", "symbol": "MTK", "defaultAdmin": os.getenv("WALLET_ADDRESS"), "primarySaleRecipient": os.getenv("WALLET_ADDRESS"), }, "feeLevel": "MEDIUM" }) response = api_instance.deploy_contract_template("a1b74add-23e0-4712-88d1-6b3009e85a86", request) print(json.dumps(response.data.to_dict(), indent=2)) ``` ```shell cURL theme={null} curl --request POST \ --url https://api.circle.com/v1/w3s/templates/a1b74add-23e0-4712-88d1-6b3009e85a86/deploy \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data ' { "idempotencyKey": "", "entitySecretCiphertext": "", "blockchain": "ARC-TESTNET", "walletId": "", "name": "MyTokenContract", "templateParameters": { "name": "MyToken", "symbol": "MTK", "defaultAdmin": "", "primarySaleRecipient": "" }, "feeLevel": "MEDIUM" } ' ``` **Run the script:** ```shell Node.js theme={null} npm run deploy-erc20 ``` ```shell Python theme={null} python deploy_erc20.py ``` **Response:** ```json theme={null} { "contractIds": ["019c053d-1ed1-772b-91a8-6970003dad8d"], "transactionId": "5b6185b2-f9a1-5645-9db2-ca5d9a330794" } ``` A successful response indicates deployment has been **initiated**, not completed. Use the `transactionId` to check the deployment status in the next step. #### 4.1. Check deployment status You can check the status of the deployment from the [Circle Developer Console](https://console.circle.com/smart-contracts/contracts) or by calling [`GET /transactions/{id}`](https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/get-transaction). After running the deployment script, copy the `transactionId` from the response and update your `.env` file with `TRANSACTION_ID={your-transaction-id}`. Then run the check-transaction script to verify deployment status. ```ts check-transaction.ts theme={null} import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets"; const circleDeveloperSdk = initiateDeveloperControlledWalletsClient({ apiKey: process.env.CIRCLE_API_KEY, entitySecret: process.env.CIRCLE_ENTITY_SECRET, }); const transactionResponse = await circleDeveloperSdk.getTransaction({ id: process.env.TRANSACTION_ID!, }); console.log(JSON.stringify(transactionResponse.data, null, 2)); ``` ```python check_transaction.py theme={null} from circle.web3 import utils, developer_controlled_wallets from pathlib import Path from dotenv import load_dotenv import os import json # Load environment variables env_path = Path(__file__).resolve().parent / ".env" load_dotenv(env_path) client = utils.init_developer_controlled_wallets_client( api_key=os.getenv("CIRCLE_API_KEY"), entity_secret=os.getenv("CIRCLE_ENTITY_SECRET") ) api_instance = developer_controlled_wallets.TransactionsApi(client) transaction_response = api_instance.get_transaction( id=os.getenv("TRANSACTION_ID") ) print(json.dumps(transaction_response.data.to_dict(), indent=2, default=str)) ``` **Run the script:** ```shell Node.js theme={null} npm pkg set scripts.check-transaction="tsx --env-file=.env check-transaction.ts" npm run check-transaction ``` ```shell Python theme={null} python check_transaction.py ``` Transaction status may show PENDING immediately after deployment. Wait 10-30 seconds and re-run check-transaction to see COMPLETE status. **Response:** ```json theme={null} { "transaction": { "id": "601a0815-f749-41d8-b193-22cadd2a8977", "blockchain": "ARC-TESTNET", "walletId": "45692c3e-2ffa-5c5b-a99c-61366939114c", "sourceAddress": "0xbcf83d3b112cbf43b19904e376dd8dee01fe2758", "contractAddress": "0x281156899E5BD6fEcf1C0831Ee24894eEEAea2f8", "transactionType": "OUTBOUND", "custodyType": "DEVELOPER", "state": "COMPLETE", "amounts": [], "nfts": null, "txHash": "0x3bfbab5d5ce0d1a5d682cbc742d3940cf59db0369d173b71ba2a3b8f43bfbcb1", "blockHash": "0x7d12148f9331556b31f84f58a41b7ff16eaaa47940f9e86733037d7ab74d858e", "blockHeight": 23686153, "userOpHash": "0x66befac1a371fcdddf1566215e4677127e111dff9253f306f7096fed8642a208", "networkFee": "0.044628774800664", "firstConfirmDate": "2026-01-26T08:59:56Z", "operation": "CONTRACT_EXECUTION", "feeLevel": "MEDIUM", "estimatedFee": { "gasLimit": "500797", "networkFee": "0.16506442157883425", "baseFee": "160", "priorityFee": "9.60345525", "maxFee": "329.60345525" }, "refId": "", "abiFunctionSignature": "mintTo(address,uint256)", "abiParameters": [ "0xbcf83d3b112cbf43b19904e376dd8dee01fe2758", "1000000000000000000" ], "createDate": "2026-01-26T08:59:54Z", "updateDate": "2026-01-26T08:59:56Z" } } ``` #### 4.2. Get the contract address After deployment completes, retrieve the contract address using [`GET /contracts/{id}`](https://developers.circle.com/api-reference/contracts/smart-contract-platform/get-contract). After deployment completes, copy the `contractIds[0]` from the deployment response and update your `.env` file with `CONTRACT_ID={your-contract-id}`. Then run the get-contract script to retrieve the contract address. ```ts get-contract.ts theme={null} import { initiateSmartContractPlatformClient } from "@circle-fin/smart-contract-platform"; const circleContractSdk = initiateSmartContractPlatformClient({ apiKey: process.env.CIRCLE_API_KEY, entitySecret: process.env.CIRCLE_ENTITY_SECRET, }); const contractResponse = await circleContractSdk.getContract({ id: process.env.CONTRACT_ID!, }); console.log(JSON.stringify(contractResponse.data, null, 2)); ``` ```python get_contract.py theme={null} from circle.web3 import utils, smart_contract_platform from pathlib import Path from dotenv import load_dotenv import os import json # Load environment variables env_path = Path(__file__).resolve().parent / ".env" load_dotenv(env_path) scpClient = utils.init_smart_contract_platform_client( api_key=os.getenv("CIRCLE_API_KEY"), entity_secret=os.getenv("CIRCLE_ENTITY_SECRET") ) api_instance = smart_contract_platform.ViewUpdateApi(scpClient) contract_response = api_instance.get_contract( id=os.getenv("CONTRACT_ID") ) print(json.dumps(contract_response.data.to_dict(), indent=2, default=str)) ``` **Run the script:** ```shell Node.js theme={null} npm pkg set scripts.get-contract="tsx --env-file=.env get-contract.ts" npm run get-contract ``` ```shell Python theme={null} python get_contract.py ``` **Response:** ```json theme={null} { "contract": { "id": "b7c35372-ce69-4ccd-bfaa-504c14634f0d", "contractAddress": "0x1234567890abcdef1234567890abcdef12345678", "blockchain": "ARC-TESTNET", "status": "COMPLETE" } } ``` Once your contract is deployed, you can interact with it from your application. You'll be able to view the contract both in the [Circle Developer Console](https://console.circle.com/smart-contracts/contracts) and on the [Arc Testnet Explorer](https://testnet.arcscan.app/). **Initial Supply:** The contract starts with 0 token supply at deployment. Use the `mintTo` function to create tokens and assign them to addresses as needed. *** ## Deploy an ERC-721 contract ERC-721 is the standard for unique digital assets. Use this template for ownership certificates, tokenized assets, unique financial instruments, or distinct asset representation. ### Step 3: Prepare for deployment #### 3.1. Get your wallet information Retrieve your wallet ID from Step 2. Ensure: * Wallet custody type is **Dev-Controlled** * Blockchain is **Arc Testnet** * Account type is **SCA** (Smart Contract Account, recommended for Gas Station compatibility) Note your wallet's address for subsequent steps. #### 3.2. Understand deployment parameters | Parameter | Description | | :----------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `idempotencyKey` | A unique value to prevent duplicate requests. | | `name` | The offchain contract name (visible in Circle Console only). Use `MyTokenContract`. | | `walletId` | The ID of the wallet deploying the contract. Use your dev-controlled wallet ID. | | `templateId` | The template identifier. Use `76b83278-50e2-4006-8b63-5b1a2a814533` for ERC-721. See [Templates](https://developers.circle.com/contracts/scp-templates-overview). | | `blockchain` | The network to deploy onto. Use `ARC-TESTNET`. | | `entitySecretCiphertext` | The re-encrypted entity secret. See [Entity Secret Management](https://developers.circle.com/wallets/dev-controlled/entity-secret-management). | | `feeLevel` | The fee level for transaction processing. Use `MEDIUM`. | | `templateParameters` | The onchain initialization parameters (see the following section). | #### 3.3. Template parameters **Required Parameters:** | Parameter | Type | Description | | :--------------------- | :----- | :---------------------------------------------------------------------------------- | | `name` | String | The onchain contract name. Use `MyToken`. | | `defaultAdmin` | String | The address with administrator permissions. Use your Dev-Controlled Wallet address. | | `primarySaleRecipient` | String | The address for first-time sale proceeds. Use your Dev-Controlled Wallet address. | | `royaltyRecipient` | String | The address for secondary sale royalties. Use your Dev-Controlled Wallet address. | | `royaltyPercent` | Float | The royalty share as a decimal (for example, `0.01` for 1%). Use `0.01`. | **Optional Parameters:** | Parameter | Type | Description | | :--------------------- | :--------- | :--------------------------------------------------------------------------------------------------------- | | `symbol` | String | The token symbol (for example, `MTK`). | | `platformFeeRecipient` | String | The address that receives platform fees from sales. Set this when implementing platform fee revenue share. | | `platformFeePercent` | Float | The platform fee percentage as decimal (for example, `0.1` for 10%). Requires `platformFeeRecipient`. | | `contractUri` | String | The URL for the contract metadata. | | `trustedForwarders` | Strings\[] | A list of addresses that can forward ERC2771 meta-transactions to this contract. | ### Step 4: Deploy the smart contract Deploy by making a request to [`POST /templates/{id}/deploy`](https://developers.circle.com/api-reference/contracts/smart-contract-platform/deploy-contract-template): ```ts deploy-erc721.ts theme={null} import { initiateSmartContractPlatformClient } from "@circle-fin/smart-contract-platform"; const circleContractSdk = initiateSmartContractPlatformClient({ apiKey: process.env.CIRCLE_API_KEY, entitySecret: process.env.CIRCLE_ENTITY_SECRET, }); const response = await circleContractSdk.deployContractTemplate({ id: "76b83278-50e2-4006-8b63-5b1a2a814533", blockchain: "ARC-TESTNET", name: "MyTokenContract", walletId: process.env.WALLET_ID, templateParameters: { name: "MyToken", symbol: "MTK", defaultAdmin: process.env.WALLET_ADDRESS, primarySaleRecipient: process.env.WALLET_ADDRESS, royaltyRecipient: process.env.WALLET_ADDRESS, royaltyPercent: 0.01, }, fee: { type: "level", config: { feeLevel: "MEDIUM", }, }, }); console.log(JSON.stringify(response.data, null, 2)); ``` ```python deploy_erc721.py theme={null} from circle.web3 import utils, smart_contract_platform import os import json scpClient = utils.init_smart_contract_platform_client( api_key=os.getenv("CIRCLE_API_KEY"), entity_secret=os.getenv("CIRCLE_ENTITY_SECRET") ) api_instance = smart_contract_platform.TemplatesApi(scpClient) request = smart_contract_platform.TemplateContractDeploymentRequest.from_dict({ "blockchain": "ARC-TESTNET", "name": "MyTokenContract", "walletId": os.getenv("WALLET_ID"), "templateParameters": { "name": "MyToken", "symbol": "MTK", "defaultAdmin": os.getenv("WALLET_ADDRESS"), "primarySaleRecipient": os.getenv("WALLET_ADDRESS"), "royaltyRecipient": os.getenv("WALLET_ADDRESS"), "royaltyPercent": "0.01", }, "feeLevel": "MEDIUM" }) request.template_parameters["royaltyPercent"] = 0.01 response = api_instance.deploy_contract_template("76b83278-50e2-4006-8b63-5b1a2a814533", request) print(json.dumps(response.data.to_dict(), indent=2)) ``` ```shell cURL theme={null} curl --request POST \ --url https://api.circle.com/v1/w3s/templates/76b83278-50e2-4006-8b63-5b1a2a814533/deploy \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data ' { "idempotencyKey": "", "entitySecretCiphertext": "", "blockchain": "ARC-TESTNET", "walletId": "", "name": "MyTokenContract", "templateParameters": { "name": "MyToken", "symbol": "MTK", "defaultAdmin": "", "primarySaleRecipient": "", "royaltyRecipient": "", "royaltyPercent": 0.01 }, "feeLevel": "MEDIUM" } ' ``` **Run the script:** ```shell Node.js theme={null} npm run deploy-erc721 ``` ```shell Python theme={null} python deploy_erc721.py ``` **Response:** ```json theme={null} { "contractIds": ["019c053d-1ed1-772b-91a8-6970003dad8d"], "transactionId": "5b6185b2-f9a1-5645-9db2-ca5d9a330794" } ``` A successful response indicates deployment has been **initiated**, not completed. Use the `transactionId` to check the deployment status in the next step. #### 4.1. Check deployment status Verify deployment with [`GET /transactions/{id}`](https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/get-transaction). After running the deployment script, copy the `transactionId` from the response and update your `.env` file with `TRANSACTION_ID={your-transaction-id}`. Then run the check-transaction script to verify deployment status. ```ts check-transaction.ts theme={null} import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets"; const circleDeveloperSdk = initiateDeveloperControlledWalletsClient({ apiKey: process.env.CIRCLE_API_KEY, entitySecret: process.env.CIRCLE_ENTITY_SECRET, }); const transactionResponse = await circleDeveloperSdk.getTransaction({ id: process.env.TRANSACTION_ID!, }); console.log(JSON.stringify(transactionResponse.data, null, 2)); ``` ```python check_transaction.py theme={null} from circle.web3 import utils, developer_controlled_wallets from pathlib import Path from dotenv import load_dotenv import os import json # Load environment variables env_path = Path(__file__).resolve().parent / ".env" load_dotenv(env_path) client = utils.init_developer_controlled_wallets_client( api_key=os.getenv("CIRCLE_API_KEY"), entity_secret=os.getenv("CIRCLE_ENTITY_SECRET") ) api_instance = developer_controlled_wallets.TransactionsApi(client) transaction_response = api_instance.get_transaction( id=os.getenv("TRANSACTION_ID") ) print(json.dumps(transaction_response.data.to_dict(), indent=2, default=str)) ``` **Run the script:** ```shell Node.js theme={null} npm run check-transaction ``` ```shell Python theme={null} python check_transaction.py ``` Transaction status may show PENDING immediately after deployment. Wait 10-30 seconds and re-run check-transaction to see COMPLETE status. **Response:** ```json theme={null} { "transaction": { "id": "601a0815-f749-41d8-b193-22cadd2a8977", "blockchain": "ARC-TESTNET", "walletId": "45692c3e-2ffa-5c5b-a99c-61366939114c", "sourceAddress": "0xbcf83d3b112cbf43b19904e376dd8dee01fe2758", "contractAddress": "0x281156899E5BD6fEcf1C0831Ee24894eEEAea2f8", "transactionType": "OUTBOUND", "custodyType": "DEVELOPER", "state": "COMPLETE", "amounts": [], "nfts": null, "txHash": "0x3bfbab5d5ce0d1a5d682cbc742d3940cf59db0369d173b71ba2a3b8f43bfbcb1", "blockHash": "0x7d12148f9331556b31f84f58a41b7ff16eaaa47940f9e86733037d7ab74d858e", "blockHeight": 23686153, "userOpHash": "0x66befac1a371fcdddf1566215e4677127e111dff9253f306f7096fed8642a208", "networkFee": "0.044628774800664", "firstConfirmDate": "2026-01-26T08:59:56Z", "operation": "CONTRACT_EXECUTION", "feeLevel": "MEDIUM", "estimatedFee": { "gasLimit": "500797", "networkFee": "0.16506442157883425", "baseFee": "160", "priorityFee": "9.60345525", "maxFee": "329.60345525" }, "refId": "", "abiFunctionSignature": "mintTo(address,uint256)", "abiParameters": [ "0xbcf83d3b112cbf43b19904e376dd8dee01fe2758", "1000000000000000000" ], "createDate": "2026-01-26T08:59:54Z", "updateDate": "2026-01-26T08:59:56Z" } } ``` #### 4.2. Get the contract address After deployment completes, retrieve the contract address using [`GET /contracts/{id}`](https://developers.circle.com/api-reference/contracts/smart-contract-platform/get-contract). After deployment completes, copy the `contractIds[0]` from the deployment response and update your `.env` file with `CONTRACT_ID={your-contract-id}`. Then run the get-contract script to retrieve the contract address. ```ts get-contract.ts theme={null} import { initiateSmartContractPlatformClient } from "@circle-fin/smart-contract-platform"; const circleContractSdk = initiateSmartContractPlatformClient({ apiKey: process.env.CIRCLE_API_KEY, entitySecret: process.env.CIRCLE_ENTITY_SECRET, }); const contractResponse = await circleContractSdk.getContract({ id: process.env.CONTRACT_ID!, }); console.log(JSON.stringify(contractResponse.data, null, 2)); ``` ```python get_contract.py theme={null} from circle.web3 import utils, smart_contract_platform from pathlib import Path from dotenv import load_dotenv import os import json # Load environment variables env_path = Path(__file__).resolve().parent / ".env" load_dotenv(env_path) scpClient = utils.init_smart_contract_platform_client( api_key=os.getenv("CIRCLE_API_KEY"), entity_secret=os.getenv("CIRCLE_ENTITY_SECRET") ) api_instance = smart_contract_platform.ViewUpdateApi(scpClient) contract_response = api_instance.get_contract( id=os.getenv("CONTRACT_ID") ) print(json.dumps(contract_response.data.to_dict(), indent=2, default=str)) ``` **Run the script:** ```shell Node.js theme={null} npm run get-contract ``` ```shell Python theme={null} python get_contract.py ``` **Response:** ```json theme={null} { "contract": { "id": "b7c35372-ce69-4ccd-bfaa-504c14634f0d", "contractAddress": "0x1234567890abcdef1234567890abcdef12345678", "blockchain": "ARC-TESTNET", "status": "COMPLETE" } } ``` *** ## Deploy an ERC-1155 contract ERC-1155 is the standard for multi-asset token management. Use this template for structured products, tiered assets, batch settlements, or managing diverse asset portfolios. ### Step 3: Prepare for deployment #### 3.1. Get your wallet information Retrieve your wallet ID from Step 2. Ensure: * Wallet custody type is **Dev-Controlled** * Blockchain is **Arc Testnet** * Account type is **SCA** (Smart Contract Account, recommended for Gas Station compatibility) Note your wallet's address for subsequent steps. #### 3.2. Understand deployment parameters | Parameter | Description | | :----------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `idempotencyKey` | A unique value to prevent duplicate requests. | | `name` | The offchain contract name (visible in Circle Console only). Use `MyMultiTokenContract`. | | `walletId` | The ID of the wallet deploying the contract. Use your dev-controlled wallet ID. | | `templateId` | The template identifier. Use `aea21da6-0aa2-4971-9a1a-5098842b1248` for ERC-1155. See [Templates](https://developers.circle.com/contracts/scp-templates-overview). | | `blockchain` | The network to deploy onto. Use `ARC-TESTNET`. | | `entitySecretCiphertext` | The re-encrypted entity secret. See [Entity Secret Management](https://developers.circle.com/wallets/dev-controlled/entity-secret-management). | | `feeLevel` | The fee level for transaction processing. Use `MEDIUM`. | | `templateParameters` | The onchain initialization parameters (see the following section). | #### 3.3. Template parameters **Required Parameters:** | Parameter | Type | Description | | :--------------------- | :----- | :---------------------------------------------------------------------------------- | | `name` | String | The onchain contract name. Use `MyMultiToken`. | | `defaultAdmin` | String | The address with administrator permissions. Use your Dev-Controlled Wallet address. | | `primarySaleRecipient` | String | The address for first-time sale proceeds. Use your Dev-Controlled Wallet address. | | `royaltyRecipient` | String | The address for secondary sale royalties. Use your Dev-Controlled Wallet address. | | `royaltyPercent` | Float | The royalty share as a decimal (for example, `0.01` for 1%). Use `0.01`. | **Optional Parameters:** | Parameter | Type | Description | | :--------------------- | :--------- | :--------------------------------------------------------------------------------------------------------- | | `symbol` | String | The token symbol (for example, `MMTK`). | | `platformFeeRecipient` | String | The address that receives platform fees from sales. Set this when implementing platform fee revenue share. | | `platformFeePercent` | Float | The platform fee percentage as decimal (for example, `0.1` for 10%). Requires `platformFeeRecipient`. | | `contractUri` | String | The URL for the contract metadata. | | `trustedForwarders` | Strings\[] | A list of addresses that can forward ERC2771 meta-transactions to this contract. | ### Step 4: Deploy the smart contract Deploy by making a request to [`POST /templates/{id}/deploy`](https://developers.circle.com/api-reference/contracts/smart-contract-platform/deploy-contract-template): ```ts deploy-erc1155.ts theme={null} import { initiateSmartContractPlatformClient } from "@circle-fin/smart-contract-platform"; const circleContractSdk = initiateSmartContractPlatformClient({ apiKey: process.env.CIRCLE_API_KEY, entitySecret: process.env.CIRCLE_ENTITY_SECRET, }); const response = await circleContractSdk.deployContractTemplate({ id: "aea21da6-0aa2-4971-9a1a-5098842b1248", blockchain: "ARC-TESTNET", name: "MyMultiTokenContract", walletId: process.env.WALLET_ID, templateParameters: { name: "MyMultiToken", symbol: "MMTK", defaultAdmin: process.env.WALLET_ADDRESS, primarySaleRecipient: process.env.WALLET_ADDRESS, royaltyRecipient: process.env.WALLET_ADDRESS, royaltyPercent: 0.01, }, fee: { type: "level", config: { feeLevel: "MEDIUM", }, }, }); console.log(JSON.stringify(response.data, null, 2)); ``` ```python deploy_erc1155.py theme={null} from circle.web3 import utils, smart_contract_platform import os import json scpClient = utils.init_smart_contract_platform_client( api_key=os.getenv("CIRCLE_API_KEY"), entity_secret=os.getenv("CIRCLE_ENTITY_SECRET") ) api_instance = smart_contract_platform.TemplatesApi(scpClient) request = smart_contract_platform.TemplateContractDeploymentRequest.from_dict({ "blockchain": "ARC-TESTNET", "name": "MyMultiTokenContract", "walletId": os.getenv("WALLET_ID"), "templateParameters": { "name": "MyMultiToken", "symbol": "MMTK", "defaultAdmin": os.getenv("WALLET_ADDRESS"), "primarySaleRecipient": os.getenv("WALLET_ADDRESS"), "royaltyRecipient": os.getenv("WALLET_ADDRESS"), "royaltyPercent": "0.01", }, "feeLevel": "MEDIUM" }) request.template_parameters["royaltyPercent"] = 0.01 response = api_instance.deploy_contract_template("aea21da6-0aa2-4971-9a1a-5098842b1248", request) print(json.dumps(response.data.to_dict(), indent=2)) ``` ```shell cURL theme={null} curl --request POST \ --url https://api.circle.com/v1/w3s/templates/aea21da6-0aa2-4971-9a1a-5098842b1248/deploy \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data ' { "idempotencyKey": "", "entitySecretCiphertext": "", "blockchain": "ARC-TESTNET", "walletId": "", "name": "MyMultiTokenContract", "templateParameters": { "name": "MyMultiToken", "symbol": "MMTK", "defaultAdmin": "", "primarySaleRecipient": "", "royaltyRecipient": "", "royaltyPercent": 0.01 }, "feeLevel": "MEDIUM" } ' ``` **Run the script:** ```shell Node.js theme={null} npm run deploy-erc1155 ``` ```shell Python theme={null} python deploy_erc1155.py ``` **Response:** ```json theme={null} { "contractIds": ["019c053d-1ed1-772b-91a8-6970003dad8d"], "transactionId": "5b6185b2-f9a1-5645-9db2-ca5d9a330794" } ``` A successful response indicates deployment has been **initiated**, not completed. Use the `transactionId` to check the deployment status in the next step. #### 4.1. Check deployment status Verify deployment with [`GET /transactions/{id}`](https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/get-transaction). After running the deployment script, copy the `transactionId` from the response and update your `.env` file with `TRANSACTION_ID={your-transaction-id}`. Then run the check-transaction script to verify deployment status. ```ts check-transaction.ts theme={null} import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets"; const circleDeveloperSdk = initiateDeveloperControlledWalletsClient({ apiKey: process.env.CIRCLE_API_KEY, entitySecret: process.env.CIRCLE_ENTITY_SECRET, }); const transactionResponse = await circleDeveloperSdk.getTransaction({ id: process.env.TRANSACTION_ID!, }); console.log(JSON.stringify(transactionResponse.data, null, 2)); ``` ```python check_transaction.py theme={null} from circle.web3 import utils, developer_controlled_wallets from pathlib import Path from dotenv import load_dotenv import os import json # Load environment variables env_path = Path(__file__).resolve().parent / ".env" load_dotenv(env_path) client = utils.init_developer_controlled_wallets_client( api_key=os.getenv("CIRCLE_API_KEY"), entity_secret=os.getenv("CIRCLE_ENTITY_SECRET") ) api_instance = developer_controlled_wallets.TransactionsApi(client) transaction_response = api_instance.get_transaction( id=os.getenv("TRANSACTION_ID") ) print(json.dumps(transaction_response.data.to_dict(), indent=2, default=str)) ``` **Run the script:** ```shell Node.js theme={null} npm run check-transaction ``` ```shell Python theme={null} python check_transaction.py ``` Transaction status may show PENDING immediately after deployment. Wait 10-30 seconds and re-run check-transaction to see COMPLETE status. **Response:** ```json theme={null} { "transaction": { "id": "601a0815-f749-41d8-b193-22cadd2a8977", "blockchain": "ARC-TESTNET", "walletId": "45692c3e-2ffa-5c5b-a99c-61366939114c", "sourceAddress": "0xbcf83d3b112cbf43b19904e376dd8dee01fe2758", "contractAddress": "0x281156899E5BD6fEcf1C0831Ee24894eEEAea2f8", "transactionType": "OUTBOUND", "custodyType": "DEVELOPER", "state": "COMPLETE", "amounts": [], "nfts": null, "txHash": "0x3bfbab5d5ce0d1a5d682cbc742d3940cf59db0369d173b71ba2a3b8f43bfbcb1", "blockHash": "0x7d12148f9331556b31f84f58a41b7ff16eaaa47940f9e86733037d7ab74d858e", "blockHeight": 23686153, "userOpHash": "0x66befac1a371fcdddf1566215e4677127e111dff9253f306f7096fed8642a208", "networkFee": "0.044628774800664", "firstConfirmDate": "2026-01-26T08:59:56Z", "operation": "CONTRACT_EXECUTION", "feeLevel": "MEDIUM", "estimatedFee": { "gasLimit": "500797", "networkFee": "0.16506442157883425", "baseFee": "160", "priorityFee": "9.60345525", "maxFee": "329.60345525" }, "refId": "", "abiFunctionSignature": "mintTo(address,uint256)", "abiParameters": [ "0xbcf83d3b112cbf43b19904e376dd8dee01fe2758", "1000000000000000000" ], "createDate": "2026-01-26T08:59:54Z", "updateDate": "2026-01-26T08:59:56Z" } } ``` #### 4.2. Get the contract address After deployment completes, retrieve the contract address using [`GET /contracts/{id}`](https://developers.circle.com/api-reference/contracts/smart-contract-platform/get-contract). After deployment completes, copy the `contractIds[0]` from the deployment response and update your `.env` file with `CONTRACT_ID={your-contract-id}`. Then run the get-contract script to retrieve the contract address. ```ts get-contract.ts theme={null} import { initiateSmartContractPlatformClient } from "@circle-fin/smart-contract-platform"; const circleContractSdk = initiateSmartContractPlatformClient({ apiKey: process.env.CIRCLE_API_KEY, entitySecret: process.env.CIRCLE_ENTITY_SECRET, }); const contractResponse = await circleContractSdk.getContract({ id: process.env.CONTRACT_ID!, }); console.log(JSON.stringify(contractResponse.data, null, 2)); ``` ```python get_contract.py theme={null} from circle.web3 import utils, smart_contract_platform from pathlib import Path from dotenv import load_dotenv import os import json # Load environment variables env_path = Path(__file__).resolve().parent / ".env" load_dotenv(env_path) scpClient = utils.init_smart_contract_platform_client( api_key=os.getenv("CIRCLE_API_KEY"), entity_secret=os.getenv("CIRCLE_ENTITY_SECRET") ) api_instance = smart_contract_platform.ViewUpdateApi(scpClient) contract_response = api_instance.get_contract( id=os.getenv("CONTRACT_ID") ) print(json.dumps(contract_response.data.to_dict(), indent=2, default=str)) ``` **Run the script:** ```shell Node.js theme={null} npm run get-contract ``` ```shell Python theme={null} python get_contract.py ``` **Response:** ```json theme={null} { "contract": { "id": "b7c35372-ce69-4ccd-bfaa-504c14634f0d", "contractAddress": "0x1234567890abcdef1234567890abcdef12345678", "blockchain": "ARC-TESTNET", "status": "COMPLETE" } } ``` *** ## Deploy an airdrop contract The Airdrop template enables mass token distribution to many recipients. Use this template for treasury distributions, stakeholder settlements, operational payments, or programmatic capital allocation. ### Step 3: Prepare for deployment #### 3.1. Get your wallet information Retrieve your wallet ID from Step 2. Ensure: * Wallet custody type is **Dev-Controlled** * Blockchain is **Arc Testnet** * Account type is **SCA** (Smart Contract Account, recommended for Gas Station compatibility) Note your wallet's address for subsequent steps. #### 3.2. Understand deployment parameters | Parameter | Description | | :----------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `idempotencyKey` | A unique value to prevent duplicate requests. | | `name` | The offchain contract name (visible in Circle Console only). Use `MyAirdropContract`. | | `walletId` | The ID of the wallet deploying the contract. Use your dev-controlled wallet ID. | | `templateId` | The template identifier. Use `13e322f2-18dc-4f57-8eed-4bddfc50f85e` for Airdrop. See [Templates](https://developers.circle.com/contracts/scp-templates-overview). | | `blockchain` | The network to deploy onto. Use `ARC-TESTNET`. | | `entitySecretCiphertext` | The re-encrypted entity secret. See [Entity Secret Management](https://developers.circle.com/wallets/dev-controlled/entity-secret-management). | | `feeLevel` | The fee level for transaction processing. Use `MEDIUM`. | | `templateParameters` | The onchain initialization parameters (see the following section). | #### 3.3. Template parameters **Required Parameters:** | Parameter | Type | Description | | :------------- | :----- | :---------------------------------------------------------------------------------- | | `defaultAdmin` | String | The address with administrator permissions. Use your Dev-Controlled Wallet address. | **Optional Parameters:** | Parameter | Type | Description | | :------------ | :----- | :--------------------------------- | | `contractURI` | String | The URL for the contract metadata. | ### Step 4: Deploy the smart contract Deploy by making a request to [`POST /templates/{id}/deploy`](https://developers.circle.com/api-reference/contracts/smart-contract-platform/deploy-contract-template): ```ts deploy-airdrop.ts theme={null} import { initiateSmartContractPlatformClient } from "@circle-fin/smart-contract-platform"; const circleContractSdk = initiateSmartContractPlatformClient({ apiKey: process.env.CIRCLE_API_KEY, entitySecret: process.env.CIRCLE_ENTITY_SECRET, }); const response = await circleContractSdk.deployContractTemplate({ id: "13e322f2-18dc-4f57-8eed-4bddfc50f85e", blockchain: "ARC-TESTNET", name: "MyAirdropContract", walletId: process.env.WALLET_ID, templateParameters: { defaultAdmin: process.env.WALLET_ADDRESS, }, fee: { type: "level", config: { feeLevel: "MEDIUM", }, }, }); console.log(JSON.stringify(response.data, null, 2)); ``` ```python deploy_airdrop.py theme={null} from circle.web3 import utils, smart_contract_platform import os import json scpClient = utils.init_smart_contract_platform_client( api_key=os.getenv("CIRCLE_API_KEY"), entity_secret=os.getenv("CIRCLE_ENTITY_SECRET") ) api_instance = smart_contract_platform.TemplatesApi(scpClient) request = smart_contract_platform.TemplateContractDeploymentRequest.from_dict({ "blockchain": "ARC-TESTNET", "name": "MyAirdropContract", "walletId": os.getenv("WALLET_ID"), "templateParameters": { "defaultAdmin": os.getenv("WALLET_ADDRESS"), }, "feeLevel": "MEDIUM" }) response = api_instance.deploy_contract_template("13e322f2-18dc-4f57-8eed-4bddfc50f85e", request) print(json.dumps(response.data.to_dict(), indent=2)) ``` ```shell cURL theme={null} curl --request POST \ --url https://api.circle.com/v1/w3s/templates/13e322f2-18dc-4f57-8eed-4bddfc50f85e/deploy \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data ' { "idempotencyKey": "", "entitySecretCiphertext": "", "blockchain": "ARC-TESTNET", "walletId": "", "name": "MyAirdropContract", "templateParameters": { "defaultAdmin": "" }, "feeLevel": "MEDIUM" } ' ``` **Run the script:** ```shell Node.js theme={null} npm run deploy-airdrop ``` ```shell Python theme={null} python deploy_airdrop.py ``` **Response:** ```json theme={null} { "contractIds": ["019c053d-1ed1-772b-91a8-6970003dad8d"], "transactionId": "5b6185b2-f9a1-5645-9db2-ca5d9a330794" } ``` A successful response indicates deployment has been **initiated**, not completed. Use the `transactionId` to check the deployment status in the next step. #### 4.1. Check deployment status Verify deployment with [`GET /transactions/{id}`](https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/get-transaction). After running the deployment script, copy the `transactionId` from the response and update your `.env` file with `TRANSACTION_ID={your-transaction-id}`. Then run the check-transaction script to verify deployment status. ```ts check-transaction.ts theme={null} import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets"; const circleDeveloperSdk = initiateDeveloperControlledWalletsClient({ apiKey: process.env.CIRCLE_API_KEY, entitySecret: process.env.CIRCLE_ENTITY_SECRET, }); const transactionResponse = await circleDeveloperSdk.getTransaction({ id: process.env.TRANSACTION_ID!, }); console.log(JSON.stringify(transactionResponse.data, null, 2)); ``` ```python check_transaction.py theme={null} from circle.web3 import utils, developer_controlled_wallets from pathlib import Path from dotenv import load_dotenv import os import json # Load environment variables env_path = Path(__file__).resolve().parent / ".env" load_dotenv(env_path) client = utils.init_developer_controlled_wallets_client( api_key=os.getenv("CIRCLE_API_KEY"), entity_secret=os.getenv("CIRCLE_ENTITY_SECRET") ) api_instance = developer_controlled_wallets.TransactionsApi(client) transaction_response = api_instance.get_transaction( id=os.getenv("TRANSACTION_ID") ) print(json.dumps(transaction_response.data.to_dict(), indent=2, default=str)) ``` **Run the script:** ```shell Node.js theme={null} npm run check-transaction ``` ```shell Python theme={null} python check_transaction.py ``` Transaction status may show PENDING immediately after deployment. Wait 10-30 seconds and re-run check-transaction to see COMPLETE status. **Response:** ```json theme={null} { "transaction": { "id": "601a0815-f749-41d8-b193-22cadd2a8977", "blockchain": "ARC-TESTNET", "walletId": "45692c3e-2ffa-5c5b-a99c-61366939114c", "sourceAddress": "0xbcf83d3b112cbf43b19904e376dd8dee01fe2758", "contractAddress": "0x281156899E5BD6fEcf1C0831Ee24894eEEAea2f8", "transactionType": "OUTBOUND", "custodyType": "DEVELOPER", "state": "COMPLETE", "amounts": [], "nfts": null, "txHash": "0x3bfbab5d5ce0d1a5d682cbc742d3940cf59db0369d173b71ba2a3b8f43bfbcb1", "blockHash": "0x7d12148f9331556b31f84f58a41b7ff16eaaa47940f9e86733037d7ab74d858e", "blockHeight": 23686153, "userOpHash": "0x66befac1a371fcdddf1566215e4677127e111dff9253f306f7096fed8642a208", "networkFee": "0.044628774800664", "firstConfirmDate": "2026-01-26T08:59:56Z", "operation": "CONTRACT_EXECUTION", "feeLevel": "MEDIUM", "estimatedFee": { "gasLimit": "500797", "networkFee": "0.16506442157883425", "baseFee": "160", "priorityFee": "9.60345525", "maxFee": "329.60345525" }, "refId": "", "abiFunctionSignature": "mintTo(address,uint256)", "abiParameters": [ "0xbcf83d3b112cbf43b19904e376dd8dee01fe2758", "1000000000000000000" ], "createDate": "2026-01-26T08:59:54Z", "updateDate": "2026-01-26T08:59:56Z" } } ``` #### 4.2. Get the contract address After deployment completes, retrieve the contract address using [`GET /contracts/{id}`](https://developers.circle.com/api-reference/contracts/smart-contract-platform/get-contract). After deployment completes, copy the `contractIds[0]` from the deployment response and update your `.env` file with `CONTRACT_ID={your-contract-id}`. Then run the get-contract script to retrieve the contract address. ```ts get-contract.ts theme={null} import { initiateSmartContractPlatformClient } from "@circle-fin/smart-contract-platform"; const circleContractSdk = initiateSmartContractPlatformClient({ apiKey: process.env.CIRCLE_API_KEY, entitySecret: process.env.CIRCLE_ENTITY_SECRET, }); const contractResponse = await circleContractSdk.getContract({ id: process.env.CONTRACT_ID!, }); console.log(JSON.stringify(contractResponse.data, null, 2)); ``` ```python get_contract.py theme={null} from circle.web3 import utils, smart_contract_platform from pathlib import Path from dotenv import load_dotenv import os import json # Load environment variables env_path = Path(__file__).resolve().parent / ".env" load_dotenv(env_path) scpClient = utils.init_smart_contract_platform_client( api_key=os.getenv("CIRCLE_API_KEY"), entity_secret=os.getenv("CIRCLE_ENTITY_SECRET") ) api_instance = smart_contract_platform.ViewUpdateApi(scpClient) contract_response = api_instance.get_contract( id=os.getenv("CONTRACT_ID") ) print(json.dumps(contract_response.data.to_dict(), indent=2, default=str)) ``` **Run the script:** ```shell Node.js theme={null} npm run get-contract ``` ```shell Python theme={null} python get_contract.py ``` **Response:** ```json theme={null} { "contract": { "id": "b7c35372-ce69-4ccd-bfaa-504c14634f0d", "contractAddress": "0x1234567890abcdef1234567890abcdef12345678", "blockchain": "ARC-TESTNET", "status": "COMPLETE" } } ``` *** *** ## Summary After completing this tutorial, you've successfully: * Created a dev-controlled wallet on Arc Testnet * Funded your wallet with testnet USDC * Deployed a smart contract using Contract Templates * Retrieved your contract address # Deploying a node as a service Source: https://docs.arc.io/arc/tutorials/deploy-node-as-service Configure your Arc node to run as systemd services that auto-restart and survive reboots. **v0.8.0 upgrade required.** Testnet operators must upgrade before timestamp `1788447600` (2026-09-03 15:00:00 UTC), when Zero8 activates. Mainnet operators must upgrade before timestamp `1789052400` (2026-09-10 15:00:00 UTC), when Zero7/Zero8 activate. Earlier versions are not supported after those timestamps. This release includes breaking changes. See [CHANGELOG.md](https://github.com/circlefin/arc-node/blob/main/CHANGELOG.md#v080) and [BREAKING\_CHANGES.md](https://github.com/circlefin/arc-node/blob/main/BREAKING_CHANGES.md#v080) for migration details before upgrading. Configure your Arc node's Execution Layer and Consensus Layer as systemd services so they auto-restart on failure and survive reboots. ## Prerequisites Before you begin, ensure that you've: * Installed Arc and completed the [Run an Arc node](/arc/tutorials/run-an-arc-node) tutorial with both layers syncing * Prepared a Linux machine with systemd * Obtained root or `sudo` access The unit files in the following steps reference `$USER` and `$HOME`. The shell expands these to your current username and home directory **before** the file is written. After running each `sudo tee` command, review the generated unit with `sudo cat /etc/systemd/system/arc-execution.service` to confirm the paths are correct. ## Step 1: Create the Execution Layer unit file Write the systemd unit file to `/etc/systemd/system/arc-execution.service`: ```shell theme={null} sudo tee /etc/systemd/system/arc-execution.service > /dev/null < The service files assume binaries are installed at `/usr/local/bin/`. If you installed with `arcup`, the binaries live in `~/.arc/bin/`. Update the `ExecStart` paths accordingly, or symlink the binaries into `/usr/local/bin/`. If you built from source with `cargo install`, binaries default to `~/.cargo/bin/`. Add `--root /usr/local` to `cargo install` to place them under `/usr/local/bin/` instead. The `RuntimeDirectory=arc` directive automatically creates `/run/arc` owned by the `User=` account specified in the service file when the service starts. The `--metrics` flag enables the Prometheus endpoint on port 9001. The `--full` flag is required on the first start when bootstrapping from a pruned snapshot. Remove it on subsequent starts if you prefer to run without pruning. When `--full` is used, EL pruning runs on a 128-block interval (changed from 5000 in v0.7.3); pass `--prune.block-interval=5000` explicitly to retain the previous schedule. ## Step 2: Create the Consensus Layer unit file Write the systemd unit file to `/etc/systemd/system/arc-consensus.service`. The `After` and `Requires` directives ensure this service starts only after the Execution Layer is running. ```shell theme={null} sudo tee /etc/systemd/system/arc-consensus.service > /dev/null < /etc/systemd/system/arc-execution.service. Created symlink /etc/systemd/system/multi-user.target.wants/arc-consensus.service -> /etc/systemd/system/arc-consensus.service. ``` ## Step 4: Verify the services are running Check the status of both services: ```shell theme={null} sudo systemctl status arc-execution sudo systemctl status arc-consensus ``` Each command prints output similar to: ```text theme={null} ● arc-execution.service - Arc Node - Execution Layer Loaded: loaded (/etc/systemd/system/arc-execution.service; enabled) Active: active (running) since ... ``` Both services display `Active: active (running)` in their output. If a service shows `failed` or `activating`, check its logs: ```shell theme={null} sudo journalctl -u arc-execution --no-pager -n 50 sudo journalctl -u arc-consensus --no-pager -n 50 ``` **Common issues:** * **Incorrect file paths in `ExecStart`:** Verify that the binary paths in the unit files match your installation directory. * **Permission errors on data directories:** Confirm that the `User` specified in the unit file owns `$HOME/.arc/`. * **Consensus Layer connection error:** The Execution Layer may not be ready yet. Wait for it to reach `active (running)`, then run `sudo systemctl restart arc-consensus`. To confirm sync progress, view logs, and verify the Prometheus metrics endpoints on ports `9001` (Execution Layer) and `29000` (Consensus Layer), see [Monitor a node](/arc/tutorials/monitor-a-node). To set up local Prometheus and Grafana dashboards on the same host, see [Set up Prometheus and Grafana for an Arc node](/arc/tutorials/set-up-node-monitoring). **v0.7.3 is outdated.** Testnet operators must upgrade to v0.8.0 before timestamp `1788447600` (2026-09-03 15:00:00 UTC), and mainnet operators before timestamp `1789052400` (2026-09-10 15:00:00 UTC). Use the v0.8.0 view for current setup instructions and migration details. Configure your Arc node's Execution Layer and Consensus Layer as systemd services so they auto-restart on failure and survive reboots. ## Prerequisites * You have installed Arc and completed the [Run an Arc node](/arc/tutorials/run-an-arc-node) tutorial with both layers syncing * You have a Linux machine with systemd * You have root or `sudo` access The unit files in the following steps reference `$USER` and `$HOME`. The shell expands these to your current username and home directory **before** the file is written. After running each `sudo tee` command, review the generated unit with `sudo cat /etc/systemd/system/arc-execution.service` to confirm the paths are correct. ## Step 1: Create the Execution Layer unit file Write the systemd unit file to `/etc/systemd/system/arc-execution.service`: ```shell theme={null} sudo tee /etc/systemd/system/arc-execution.service > /dev/null < The service files assume binaries are installed at `/usr/local/bin/`. If you installed with `arcup`, the binaries live in `~/.arc/bin/`. Update the `ExecStart` paths accordingly, or symlink the binaries into `/usr/local/bin/`. If you built from source with `cargo install`, binaries default to `~/.cargo/bin/`. Add `--root /usr/local` to `cargo install` to place them under `/usr/local/bin/` instead. The `RuntimeDirectory=arc` directive automatically creates `/run/arc` owned by the `User=` account specified in the service file when the service starts. The `--metrics` flag enables the Prometheus endpoint on port 9001. The `--full` flag is required on the first start when bootstrapping from a pruned snapshot. Remove it on subsequent starts if you prefer to run without pruning. ## Step 2: Create the Consensus Layer unit file Write the systemd unit file to `/etc/systemd/system/arc-consensus.service`. The `After` and `Requires` directives ensure this service starts only after the Execution Layer is running. ```shell theme={null} sudo tee /etc/systemd/system/arc-consensus.service > /dev/null < /etc/systemd/system/arc-execution.service. Created symlink /etc/systemd/system/multi-user.target.wants/arc-consensus.service -> /etc/systemd/system/arc-consensus.service. ``` ## Step 4: Verify the services are running Check the status of both services: ```shell theme={null} sudo systemctl status arc-execution sudo systemctl status arc-consensus ``` Each command prints output similar to: ```text theme={null} ● arc-execution.service - Arc Node - Execution Layer Loaded: loaded (/etc/systemd/system/arc-execution.service; enabled) Active: active (running) since ... ``` Both services display `Active: active (running)` in their output. If a service shows `failed` or `activating`, check its logs: ```shell theme={null} sudo journalctl -u arc-execution --no-pager -n 50 sudo journalctl -u arc-consensus --no-pager -n 50 ``` **Common issues:** * **Incorrect file paths in `ExecStart`:** Verify that the binary paths in the unit files match your installation directory. * **Permission errors on data directories:** Confirm that the `User` specified in the unit file owns `$HOME/.arc/`. * **Consensus Layer connection error:** The Execution Layer may not be ready yet. Wait for it to reach `active (running)`, then run `sudo systemctl restart arc-consensus`. To confirm sync progress, view logs, and verify the Prometheus metrics endpoints on ports `9001` (Execution Layer) and `29000` (Consensus Layer), see [Monitor a node](/arc/tutorials/monitor-a-node). To set up local Prometheus and Grafana dashboards on the same host, see [Set up Prometheus and Grafana for an Arc node](/arc/tutorials/set-up-node-monitoring). **v0.7.2: Arc Testnet hardfork activates 2026-06-18 14:00:00 UTC.** The Zero7 hardfork activates at Unix timestamp `1781791200`. Testnet nodes must be on v0.7.2 by that timestamp; earlier versions stop syncing after activation. See [Run an Arc node](/arc/tutorials/run-an-arc-node) for those defaults, or [CHANGELOG.md](https://github.com/circlefin/arc-node/blob/main/CHANGELOG.md) and [BREAKING\_CHANGES.md](https://github.com/circlefin/arc-node/blob/main/BREAKING_CHANGES.md#v072). Configure your Arc node's Execution Layer and Consensus Layer as systemd services so they auto-restart on failure and survive reboots. ## Prerequisites * You have installed Arc and completed the [Run an Arc node](/arc/tutorials/run-an-arc-node) tutorial with both layers syncing * You have a Linux machine with systemd * You have root or `sudo` access The unit files below reference `$USER` and `$HOME`. The shell expands these to your current username and home directory **before** the file is written. After running each `sudo tee` command, review the generated unit with `sudo cat /etc/systemd/system/arc-execution.service` to confirm the paths are correct. ## Step 1: Create the Execution Layer unit file Write the systemd unit file to `/etc/systemd/system/arc-execution.service`: ```shell theme={null} sudo tee /etc/systemd/system/arc-execution.service > /dev/null < The service files assume binaries are installed at `/usr/local/bin/`. If you installed with `arcup`, the binaries live in `~/.arc/bin/`. Update the `ExecStart` paths accordingly, or symlink the binaries into `/usr/local/bin/`. If you built from source with `cargo install`, binaries default to `~/.cargo/bin/`. Add `--root /usr/local` to `cargo install` to place them under `/usr/local/bin/` instead. The `RuntimeDirectory=arc` directive automatically creates `/run/arc` owned by the `User=` account specified in the service file when the service starts. The `--metrics` flag enables the Prometheus endpoint on port 9001. The `--full` flag is required on the first start when bootstrapping from a pruned snapshot. Remove it on subsequent starts if you prefer to run without pruning. ## Step 2: Create the Consensus Layer unit file Write the systemd unit file to `/etc/systemd/system/arc-consensus.service`. The `After` and `Requires` directives ensure this service starts only after the Execution Layer is running. ```shell theme={null} sudo tee /etc/systemd/system/arc-consensus.service > /dev/null < /etc/systemd/system/arc-execution.service. Created symlink /etc/systemd/system/multi-user.target.wants/arc-consensus.service -> /etc/systemd/system/arc-consensus.service. ``` ## Step 4: Verify the services are running Check the status of both services: ```shell theme={null} sudo systemctl status arc-execution sudo systemctl status arc-consensus ``` Each command prints output similar to: ```text theme={null} ● arc-execution.service - Arc Node - Execution Layer Loaded: loaded (/etc/systemd/system/arc-execution.service; enabled) Active: active (running) since ... ``` Both services display `Active: active (running)` in their output. If a service shows `failed` or `activating`, check its logs: ```shell theme={null} sudo journalctl -u arc-execution --no-pager -n 50 sudo journalctl -u arc-consensus --no-pager -n 50 ``` **Common issues:** * **Incorrect file paths in `ExecStart`:** Verify that the binary paths in the unit files match your installation directory. * **Permission errors on data directories:** Confirm that the `User` specified in the unit file owns `$HOME/.arc/`. * **Consensus Layer connection error:** The Execution Layer may not be ready yet. Wait for it to reach `active (running)`, then run `sudo systemctl restart arc-consensus`. To confirm sync progress, view logs, and verify the Prometheus metrics endpoints on ports `9001` (Execution Layer) and `29000` (Consensus Layer), see [Monitor a node](/arc/tutorials/monitor-a-node). To set up local Prometheus and Grafana dashboards on the same host, see [Set up Prometheus and Grafana for an Arc node](/arc/tutorials/set-up-node-monitoring). **v0.7.1: Arc Testnet hardfork activates 2026-05-27 15:08:37 UTC.** The Zero5 and Zero6 hardforks activate at Unix timestamp `1779894517`. Testnet nodes must be on v0.7.1 by that timestamp; earlier versions stop syncing after activation. The systemd unit files below are unchanged from v0.7.0; v0.7.1 only tightens EL RPC connection defaults (`--rpc.max-connections` and `--rpc.max-subscriptions-per-connection`). See [Run an Arc node](/arc/tutorials/run-an-arc-node) for those defaults, or [CHANGELOG.md](https://github.com/circlefin/arc-node/blob/main/CHANGELOG.md) and [BREAKING\_CHANGES.md](https://github.com/circlefin/arc-node/blob/main/BREAKING_CHANGES.md#v071) for the full upgrade notes. Configure your Arc node's Execution Layer and Consensus Layer as systemd services so they auto-restart on failure and survive reboots. ## Prerequisites * You have installed Arc and completed the [Run an Arc node](/arc/tutorials/run-an-arc-node) tutorial with both layers syncing * You have a Linux machine with systemd * You have root or `sudo` access The unit files below reference `$USER` and `$HOME`. The shell expands these to your current username and home directory **before** the file is written. After running each `sudo tee` command, review the generated unit with `sudo cat /etc/systemd/system/arc-execution.service` to confirm the paths are correct. ## Step 1: Create the Execution Layer unit file Write the systemd unit file to `/etc/systemd/system/arc-execution.service`: ```shell theme={null} sudo tee /etc/systemd/system/arc-execution.service > /dev/null < The service files assume binaries are installed at `/usr/local/bin/`. If you installed with `arcup`, the binaries live in `~/.arc/bin/`. Update the `ExecStart` paths accordingly, or symlink the binaries into `/usr/local/bin/`. If you built from source with `cargo install`, binaries default to `~/.cargo/bin/`. Add `--root /usr/local` to `cargo install` to place them under `/usr/local/bin/` instead. The `RuntimeDirectory=arc` directive automatically creates `/run/arc` owned by the `User=` account specified in the service file when the service starts. The `--metrics` flag enables the Prometheus endpoint on port 9001. The `--full` flag is required on the first start when bootstrapping from a pruned snapshot. Remove it on subsequent starts if you prefer to run without pruning. ## Step 2: Create the Consensus Layer unit file Write the systemd unit file to `/etc/systemd/system/arc-consensus.service`. The `After` and `Requires` directives ensure this service starts only after the Execution Layer is running. ```shell theme={null} sudo tee /etc/systemd/system/arc-consensus.service > /dev/null < /etc/systemd/system/arc-execution.service. Created symlink /etc/systemd/system/multi-user.target.wants/arc-consensus.service -> /etc/systemd/system/arc-consensus.service. ``` ## Step 4: Verify the services are running Check the status of both services: ```shell theme={null} sudo systemctl status arc-execution sudo systemctl status arc-consensus ``` Each command prints output similar to: ```text theme={null} ● arc-execution.service - Arc Node - Execution Layer Loaded: loaded (/etc/systemd/system/arc-execution.service; enabled) Active: active (running) since ... ``` Both services display `Active: active (running)` in their output. If a service shows `failed` or `activating`, check its logs: ```shell theme={null} sudo journalctl -u arc-execution --no-pager -n 50 sudo journalctl -u arc-consensus --no-pager -n 50 ``` **Common issues:** * **Incorrect file paths in `ExecStart`:** Verify that the binary paths in the unit files match your installation directory. * **Permission errors on data directories:** Confirm that the `User` specified in the unit file owns `$HOME/.arc/`. * **Consensus Layer connection error:** The Execution Layer may not be ready yet. Wait for it to reach `active (running)`, then run `sudo systemctl restart arc-consensus`. To confirm sync progress, view logs, and verify the Prometheus metrics endpoints on ports `9001` (Execution Layer) and `29000` (Consensus Layer), see [Monitor a node](/arc/tutorials/monitor-a-node). To set up local Prometheus and Grafana dashboards on the same host, see [Set up Prometheus and Grafana for an Arc node](/arc/tutorials/set-up-node-monitoring). **Upgrade required by 2026-05-27 15:08:37 UTC.** The Zero5 and Zero6 hardforks activate at Unix timestamp `1779894517`. The v0.7.0 systemd unit files below stop syncing on Arc Testnet after activation. Switch the view at the top of this page to **v0.7.1 (latest)** and follow those instructions before the deadline. See [BREAKING\_CHANGES.md](https://github.com/circlefin/arc-node/blob/main/BREAKING_CHANGES.md#v071) for migration details. Configure your Arc node's Execution Layer and Consensus Layer as systemd services so they auto-restart on failure and survive reboots. ## Prerequisites * You have installed Arc and completed the [Run an Arc node](/arc/tutorials/run-an-arc-node) tutorial with both layers syncing * You have a Linux machine with systemd * You have root or `sudo` access The unit files below reference `$USER` and `$HOME`. The shell expands these to your current username and home directory **before** the file is written. After running each `sudo tee` command, review the generated unit with `sudo cat /etc/systemd/system/arc-execution.service` to confirm the paths are correct. ## Step 1: Create the Execution Layer unit file Write the systemd unit file to `/etc/systemd/system/arc-execution.service`: ```shell theme={null} sudo tee /etc/systemd/system/arc-execution.service > /dev/null < The service files assume binaries are installed at `/usr/local/bin/`. If you installed with `arcup`, the binaries live in `~/.arc/bin/`. Update the `ExecStart` paths accordingly, or symlink the binaries into `/usr/local/bin/`. If you built from source with `cargo install`, binaries default to `~/.cargo/bin/`. Add `--root /usr/local` to `cargo install` to place them under `/usr/local/bin/` instead. The `RuntimeDirectory=arc` directive automatically creates `/run/arc` owned by the `User=` account specified in the service file when the service starts. The `--metrics` flag enables the Prometheus endpoint on port 9001. The `--full` flag is required on the first start when bootstrapping from a pruned snapshot. Remove it on subsequent starts if you prefer to run without pruning. ## Step 2: Create the Consensus Layer unit file Write the systemd unit file to `/etc/systemd/system/arc-consensus.service`. The `After` and `Requires` directives ensure this service starts only after the Execution Layer is running. ```shell theme={null} sudo tee /etc/systemd/system/arc-consensus.service > /dev/null < /etc/systemd/system/arc-execution.service. Created symlink /etc/systemd/system/multi-user.target.wants/arc-consensus.service -> /etc/systemd/system/arc-consensus.service. ``` ## Step 4: Verify the services are running Check the status of both services: ```shell theme={null} sudo systemctl status arc-execution sudo systemctl status arc-consensus ``` Each command prints output similar to: ```text theme={null} ● arc-execution.service - Arc Node - Execution Layer Loaded: loaded (/etc/systemd/system/arc-execution.service; enabled) Active: active (running) since ... ``` Both services display `Active: active (running)` in their output. If a service shows `failed` or `activating`, check its logs: ```shell theme={null} sudo journalctl -u arc-execution --no-pager -n 50 sudo journalctl -u arc-consensus --no-pager -n 50 ``` **Common issues:** * **Incorrect file paths in `ExecStart`:** Verify that the binary paths in the unit files match your installation directory. * **Permission errors on data directories:** Confirm that the `User` specified in the unit file owns `$HOME/.arc/`. * **Consensus Layer connection error:** The Execution Layer may not be ready yet. Wait for it to reach `active (running)`, then run `sudo systemctl restart arc-consensus`. To confirm sync progress, view logs, and verify the Prometheus metrics endpoints on ports `9001` (Execution Layer) and `29000` (Consensus Layer), see [Monitor a node](/arc/tutorials/monitor-a-node). To set up local Prometheus and Grafana dashboards on the same host, see [Set up Prometheus and Grafana for an Arc node](/arc/tutorials/set-up-node-monitoring). **Upgrade required by 2026-05-27 15:08:37 UTC.** The Zero5 and Zero6 hardforks activate at Unix timestamp `1779894517`. v0.6.0 is two releases behind and stops syncing on Arc Testnet after activation. Switch the view at the top of this page to **v0.7.1 (latest)** and follow those instructions before the deadline. See [BREAKING\_CHANGES.md](https://github.com/circlefin/arc-node/blob/main/BREAKING_CHANGES.md#v071) for migration details. Configure your Arc node's Execution Layer and Consensus Layer as systemd services so they auto-restart on failure and survive reboots. ## Prerequisites * You have installed Arc and completed the [Run an Arc node](/arc/tutorials/run-an-arc-node) tutorial with both layers syncing * You have a Linux machine with systemd * You have root or `sudo` access The unit files below reference `$USER` and `$HOME`. The shell expands these to your current username and home directory **before** the file is written. After running each `sudo tee` command, review the generated unit with `sudo cat /etc/systemd/system/arc-execution.service` to confirm the paths are correct. ## Step 1: Create the Execution Layer unit file Write the systemd unit file to `/etc/systemd/system/arc-execution.service`: ```shell theme={null} sudo tee /etc/systemd/system/arc-execution.service > /dev/null < The service files assume binaries are installed at `/usr/local/bin/`. If you built from source with `cargo install`, binaries default to `~/.cargo/bin/`. Add `--root /usr/local` to `cargo install` to place them under `/usr/local/bin/` instead. The `RuntimeDirectory=arc` directive automatically creates `/run/arc` owned by the `User=` account specified in the service file when the service starts. The `--metrics` flag enables the Prometheus endpoint on port 9001. ## Step 2: Create the Consensus Layer unit file Write the systemd unit file to `/etc/systemd/system/arc-consensus.service`. The `After` and `Requires` directives ensure this service starts only after the Execution Layer is running. ```shell theme={null} sudo tee /etc/systemd/system/arc-consensus.service > /dev/null < /etc/systemd/system/arc-execution.service. Created symlink /etc/systemd/system/multi-user.target.wants/arc-consensus.service -> /etc/systemd/system/arc-consensus.service. ``` ## Step 4: Verify the services are running Check the status of both services: ```shell theme={null} sudo systemctl status arc-execution sudo systemctl status arc-consensus ``` Each command prints output similar to: ```text theme={null} ● arc-execution.service - Arc Node - Execution Layer Loaded: loaded (/etc/systemd/system/arc-execution.service; enabled) Active: active (running) since ... ``` Both services display `Active: active (running)` in their output. If a service shows `failed` or `activating`, check its logs: ```shell theme={null} sudo journalctl -u arc-execution --no-pager -n 50 sudo journalctl -u arc-consensus --no-pager -n 50 ``` **Common issues:** * **Incorrect file paths in `ExecStart`:** Verify that the binary paths in the unit files match your installation directory. * **Permission errors on data directories:** Confirm that the `User` specified in the unit file owns `$HOME/.arc/`. * **Consensus Layer connection error:** The Execution Layer may not be ready yet. Wait for it to reach `active (running)`, then run `sudo systemctl restart arc-consensus`. To confirm sync progress, view logs, and verify the Prometheus metrics endpoints on ports `9001` (Execution Layer) and `29000` (Consensus Layer), see [Monitor a node](/arc/tutorials/monitor-a-node). To set up local Prometheus and Grafana dashboards on the same host, see [Set up Prometheus and Grafana for an Arc node](/arc/tutorials/set-up-node-monitoring). # Deploy on Arc Source: https://docs.arc.io/arc/tutorials/deploy-on-arc Learn how to deploy, test, and interact with a Solidity smart contract on the Arc Testnet. Arc is currently in its testnet phase. During this period, the network may experience instability or unplanned downtime. **Note:** Throughout this page, all references to Arc refer specifically to the Arc Testnet. In this tutorial, you'll use [Foundry](https://getfoundry.sh/) to deploy and interact with the default `Counter` contract on the Arc Testnet. `forge init` scaffolds a working Solidity project—contract, tests, and deployment script—so you can ship to Arc without writing any new contract code. By the end, you'll have configured Foundry for Arc, deployed `Counter`, and called it from the command line with `cast`. ## Prerequisites Before you begin, ensure that you've: * Confirmed access to a Unix-like shell (macOS, Linux, or Windows with [WSL](https://learn.microsoft.com/en-us/windows/wsl/install)) * Installed [`curl`](https://curl.se/) (used by the Foundry installer) * Installed a code editor such as [VS Code](https://code.visualstudio.com/) ## Step 1. Set up Foundry Install Foundry's command-line tools (`forge`, `cast`, `anvil`, `chisel`): ```shell theme={null} curl -L https://foundry.paradigm.xyz | bash ``` After the installer finishes, run the `source` command shown in the installer output, or open a new terminal session. Then install Foundry's tools: ```shell theme={null} foundryup ``` Navigate to the directory where you want to create your project, then initialize a new Solidity project: ```shell theme={null} forge init hello-arc && cd hello-arc ``` This generates `src/Counter.sol`, a matching test file at `test/Counter.t.sol`, and a deployment script at `script/Counter.s.sol`. ## Step 2. Configure Foundry for Arc Create a `.env` file in the project root with the Arc Testnet RPC URL: ```ini theme={null} ARC_TESTNET_RPC_URL="https://rpc.testnet.arc.io" ``` Load the variables into your shell: ```shell theme={null} source .env ``` Never commit your `.env` file to version control. Store private keys and sensitive variables securely. ## Step 3. Test the contract Run the included tests to compile the contract and verify it works locally: ```shell theme={null} forge test ``` The `Counter` tests pass, confirming compilation and local correctness. ## Step 4. Deploy to Arc Testnet ### 4.1. Create and fund a wallet Generate a new keypair with `cast`: ```shell theme={null} cast wallet new ``` The command returns an address and private key: ```text theme={null} Successfully created new keypair. Address: 0xB815A0c4bC23930119324d4359dB65e27A846A2d Private key: 0xcc1b30a6af68ea9a9917f1dd••••••••••••••••••••••••••••••••••••••97c5 ``` Keep your private key secure. Never share it or commit it to source control. Use environment variables or a secrets manager for any non-test deployment. Add the private key to your `.env` file and reload: ```ini theme={null} PRIVATE_KEY="0x..." ``` ```shell theme={null} source .env ``` Arc uses USDC for gas. Request testnet USDC from the [Circle Faucet](https://faucet.circle.com) to fund your deployment wallet. Visit the [Circle Faucet](https://faucet.circle.com), select **Arc Testnet**, paste your wallet address, and request testnet USDC. Arc uses USDC as its native gas token—transaction fees are paid in USDC instead of a separate cryptocurrency—so this funds the wallet for deployment. Testnet USDC is for testing purposes only. It has no real-world value and must not be used in production. ### 4.2. Deploy the contract Deploy `Counter` to Arc Testnet: ```shell theme={null} forge create src/Counter.sol:Counter \ --rpc-url $ARC_TESTNET_RPC_URL \ --private-key $PRIVATE_KEY \ --broadcast ``` After deployment completes, you'll see output similar to: ```text theme={null} Deployer: 0xB815A0c4bC23930119324d4359dB65e27A846A2d Deployed to: 0x32368037b14819C9e5Dbe96b3d67C59b8c65c4BF Transaction hash: 0xeba0fcb5e528d586db0aeb2465a8fad0299330a9773ca62818a1827560a67346 ``` Save the `Deployed to` address from the output to your `.env` file and reload: ```ini theme={null} COUNTER_ADDRESS="0x..." ``` ```shell theme={null} source .env ``` ### 4.3. Verify the contract on Arc Testnet Explorer Arc Testnet Explorer runs Blockscout, so you can publish your contract's source code with `forge verify-contract` and Foundry's Blockscout verifier. Verified contracts show a **Contract** tab on the explorer with source code, ABI, and a read/write UI. Run the verification command from your Foundry project root, using the same compiler settings you used to deploy: ```shell theme={null} forge verify-contract $COUNTER_ADDRESS src/Counter.sol:Counter \ --chain-id 5042002 \ --verifier blockscout \ --verifier-url https://testnet.arcscan.app/api/ ``` If your contract's constructor takes arguments, ABI-encode them with `cast abi-encode` and pass the result with `--constructor-args`. For example: ```shell theme={null} forge verify-contract $CONTRACT_ADDRESS src/MyToken.sol:MyToken \ --chain-id 5042002 \ --verifier blockscout \ --verifier-url https://testnet.arcscan.app/api/ \ --constructor-args $(cast abi-encode "constructor(string,string)" "MyToken" "MTK") ``` You can also submit source code manually from the [contract verification page](https://testnet.arcscan.app/contract-verification) on the explorer if you didn't deploy with Foundry. After verification succeeds, open the deployed address on [testnet.arcscan.app](https://testnet.arcscan.app) to confirm the **Contract** tab now shows the verified source and lets you call functions directly from the UI. ## Step 5. Interact with your contract Confirm the deployment on the [Arc Testnet Explorer](https://testnet.arcscan.app) by pasting the transaction hash from the previous step. Read the current counter value with `cast call`: ```shell theme={null} cast call $COUNTER_ADDRESS "number()(uint256)" \ --rpc-url $ARC_TESTNET_RPC_URL ``` A freshly deployed `Counter` returns `0`. Increment it onchain with `cast send`: ```shell theme={null} cast send $COUNTER_ADDRESS "increment()" \ --rpc-url $ARC_TESTNET_RPC_URL \ --private-key $PRIVATE_KEY ``` Re-run the `cast call` command. The returned value is now `1`. You now have a working deployment pipeline on Arc Testnet. To deploy production-ready tokens or NFTs without writing Solidity, see [Deploy contracts](/arc/tutorials/deploy-contracts). # Interact with contracts Source: https://docs.arc.io/arc/tutorials/interact-with-contracts Execute contract functions on Arc Testnet to mint tokens, transfer assets, and perform contract operations. This tutorial guides you through interacting with smart contracts deployed on Arc Testnet. You'll learn how to execute contract functions like minting tokens, transferring assets, and performing contract-specific operations for ERC-20, ERC-721, ERC-1155, and Airdrop contracts. ## Prerequisites Complete the [Deploy contracts](/arc/tutorials/deploy-contracts) tutorial first. You'll need a deployed contract. ## Step 1. Update your project In this step, you update the project you created in the Deploy contracts tutorial with the additional environment variable and npm scripts needed for contract interactions. ### 1.1. Set environment variables Add this new variable to your existing `.env` file (from the Deploy contracts tutorial): ```text .env theme={null} RECIPIENT_WALLET_ADDRESS=YOUR_RECIPIENT_ADDRESS ``` * `RECIPIENT_WALLET_ADDRESS` is the wallet address that receives transferred tokens during the interaction examples. Your `.env` file should already have `CIRCLE_API_KEY`, `CIRCLE_ENTITY_SECRET`, `WALLET_ID`, `WALLET_ADDRESS`, and `CONTRACT_ADDRESS` from the Deploy contracts tutorial. You're only adding 1 new variable here. The npm run commands in this tutorial load variables from `.env` using Node.js native env-file support. Prefer editing `.env` files in your IDE or editor so credentials are not leaked to your shell history. ### 1.2. Add npm scripts Add run scripts for contract interactions to your `package.json`: ```shell theme={null} npm pkg set scripts.interact-erc20="tsx --env-file=.env interact-erc20.ts" npm pkg set scripts.interact-erc721="tsx --env-file=.env interact-erc721.ts" npm pkg set scripts.interact-erc1155="tsx --env-file=.env interact-erc1155.ts" npm pkg set scripts.interact-airdrop="tsx --env-file=.env interact-airdrop.ts" ``` ## Step 2. Interact with contracts Select the contract type you want to interact with from the tabs below. ## Interact with ERC-20 contracts ERC-20 tokens support standard fungible token operations. You'll learn to mint new tokens and transfer them between addresses. ### Mint tokens Use the `mintTo` function to mint tokens. The wallet must have `MINTER_ROLE`. ```ts Node.js theme={null} import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets"; const circleDeveloperSdk = initiateDeveloperControlledWalletsClient({ apiKey: process.env.CIRCLE_API_KEY, entitySecret: process.env.CIRCLE_ENTITY_SECRET, }); const mintResponse = await circleDeveloperSdk.createContractExecutionTransaction({ walletId: process.env.WALLET_ID, abiFunctionSignature: "mintTo(address,uint256)", abiParameters: [ process.env.WALLET_ADDRESS, "1000000000000000000", // 1 token with 18 decimals ], contractAddress: process.env.CONTRACT_ADDRESS, fee: { type: "level", config: { feeLevel: "MEDIUM", }, }, }); console.log(JSON.stringify(mintResponse.data, null, 2)); ``` ```python Python theme={null} from circle.web3 import utils, developer_controlled_wallets import os import json client = utils.init_developer_controlled_wallets_client( api_key=os.getenv("CIRCLE_API_KEY"), entity_secret=os.getenv("CIRCLE_ENTITY_SECRET") ) api_instance = developer_controlled_wallets.TransactionsApi(client) mint_request = developer_controlled_wallets.CreateContractExecutionTransactionForDeveloperRequest.from_dict({ "walletId": os.getenv("WALLET_ID"), "abiFunctionSignature": "mintTo(address,uint256)", "abiParameters": [ os.getenv("WALLET_ADDRESS"), "1000000000000000000" ], "contractAddress": os.getenv("CONTRACT_ADDRESS"), "feeLevel": "MEDIUM", }) mint_response = api_instance.create_developer_transaction_contract_execution(mint_request) print(json.dumps(mint_response.data.to_dict(), indent=2)) ``` ```shell cURL theme={null} curl --request POST \ --url 'https://api.circle.com/v1/w3s/developer/transactions/contractExecution' \ --header 'authorization: Bearer ' \ --header 'accept: application/json' \ --header 'content-type: application/json' \ --data '{ "idempotencyKey": "", "entitySecretCiphertext": "", "walletId": "", "abiFunctionSignature": "mintTo(address,uint256)", "abiParameters": [ "", "1000000000000000000" ], "contractAddress": "", "feeLevel": "MEDIUM" }' ``` **Response:** ```json theme={null} { "id": "601a0815-f749-41d8-b193-22cadd2a8977", "state": "INITIATED" } ``` **Token decimals**: ERC-20 tokens typically use 18 decimals. To mint 1 token, use `1000000000000000000` (1 × 10^18). ### Transfer tokens Use the `transfer` function to send tokens to another address. ```ts Node.js theme={null} const transferResponse = await circleDeveloperSdk.createContractExecutionTransaction({ walletId: process.env.WALLET_ID, abiFunctionSignature: "transfer(address,uint256)", abiParameters: [ process.env.RECIPIENT_WALLET_ADDRESS, "1000000000000000000", // 1 token with 18 decimals ], contractAddress: process.env.CONTRACT_ADDRESS, fee: { type: "level", config: { feeLevel: "MEDIUM", }, }, }); console.log(JSON.stringify(transferResponse.data, null, 2)); ``` ```python Python theme={null} transfer_request = developer_controlled_wallets.CreateContractExecutionTransactionForDeveloperRequest.from_dict({ "walletId": os.getenv("WALLET_ID"), "abiFunctionSignature": "transfer(address,uint256)", "abiParameters": [ os.getenv("RECIPIENT_WALLET_ADDRESS"), "1000000000000000000" ], "contractAddress": os.getenv("CONTRACT_ADDRESS"), "feeLevel": "MEDIUM", }) transfer_response = api_instance.create_developer_transaction_contract_execution(transfer_request) print(json.dumps(transfer_response.data.to_dict(), indent=2)) ``` ```shell cURL theme={null} curl --request POST \ --url 'https://api.circle.com/v1/w3s/developer/transactions/contractExecution' \ --header 'authorization: Bearer ' \ --header 'accept: application/json' \ --header 'content-type: application/json' \ --data '{ "idempotencyKey": "", "entitySecretCiphertext": "", "walletId": "", "abiFunctionSignature": "transfer(address,uint256)", "abiParameters": [ "", "1000000000000000000" ], "contractAddress": "", "feeLevel": "MEDIUM" }' ``` **Response:** ```json theme={null} { "id": "601a0815-f749-41d8-b193-22cadd2a8977", "state": "INITIATED" } ``` ### Full ERC-20 interaction script Here's the full script combining mint and transfer operations: ```ts interact-erc20.ts theme={null} import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets"; const circleDeveloperSdk = initiateDeveloperControlledWalletsClient({ apiKey: process.env.CIRCLE_API_KEY, entitySecret: process.env.CIRCLE_ENTITY_SECRET, }); async function main() { // Mint tokens const mintResponse = await circleDeveloperSdk.createContractExecutionTransaction({ walletId: process.env.WALLET_ID, abiFunctionSignature: "mintTo(address,uint256)", abiParameters: [ process.env.WALLET_ADDRESS, "1000000000000000000", // 1 token with 18 decimals ], contractAddress: process.env.CONTRACT_ADDRESS, fee: { type: "level", config: { feeLevel: "MEDIUM", }, }, }); console.log(JSON.stringify(mintResponse.data, null, 2)); // Transfer tokens const transferResponse = await circleDeveloperSdk.createContractExecutionTransaction({ walletId: process.env.WALLET_ID, abiFunctionSignature: "transfer(address,uint256)", abiParameters: [ process.env.RECIPIENT_WALLET_ADDRESS, "1000000000000000000", // 1 token with 18 decimals ], contractAddress: process.env.CONTRACT_ADDRESS, fee: { type: "level", config: { feeLevel: "MEDIUM", }, }, }); console.log(JSON.stringify(transferResponse.data, null, 2)); } main(); ``` ```python interact_erc20.py theme={null} from circle.web3 import utils, developer_controlled_wallets import os import json client = utils.init_developer_controlled_wallets_client( api_key=os.getenv("CIRCLE_API_KEY"), entity_secret=os.getenv("CIRCLE_ENTITY_SECRET") ) api_instance = developer_controlled_wallets.TransactionsApi(client) # Mint tokens mint_request = developer_controlled_wallets.CreateContractExecutionTransactionForDeveloperRequest.from_dict({ "walletId": os.getenv("WALLET_ID"), "abiFunctionSignature": "mintTo(address,uint256)", "abiParameters": [ os.getenv("WALLET_ADDRESS"), "1000000000000000000" ], "contractAddress": os.getenv("CONTRACT_ADDRESS"), "feeLevel": "MEDIUM", }) mint_response = api_instance.create_developer_transaction_contract_execution(mint_request) print(json.dumps(mint_response.data.to_dict(), indent=2)) # Transfer tokens transfer_request = developer_controlled_wallets.CreateContractExecutionTransactionForDeveloperRequest.from_dict({ "walletId": os.getenv("WALLET_ID"), "abiFunctionSignature": "transfer(address,uint256)", "abiParameters": [ os.getenv("RECIPIENT_WALLET_ADDRESS"), "1000000000000000000" ], "contractAddress": os.getenv("CONTRACT_ADDRESS"), "feeLevel": "MEDIUM", }) transfer_response = api_instance.create_developer_transaction_contract_execution(transfer_request) print(json.dumps(transfer_response.data.to_dict(), indent=2)) ``` **Run the script:** ```shell Node.js theme={null} npm run interact-erc20 ``` ```shell Python theme={null} python interact_erc20.py ``` ## Interact with ERC-721 contracts ERC-721 tokens are unique tokens. Each token has a unique ID and can have associated metadata stored on IPFS or other storage. ### Mint tokens Use the `mintTo` function to mint tokens. The wallet must have `MINTER_ROLE`. ```ts Node.js theme={null} import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets"; const circleDeveloperSdk = initiateDeveloperControlledWalletsClient({ apiKey: process.env.CIRCLE_API_KEY, entitySecret: process.env.CIRCLE_ENTITY_SECRET, }); const mintResponse = await circleDeveloperSdk.createContractExecutionTransaction({ walletId: process.env.WALLET_ID, abiFunctionSignature: "mintTo(address,string)", abiParameters: [ process.env.WALLET_ADDRESS, "ipfs://bafkreibdi6623n3xpf7ymk62ckb4bo75o3qemwkpfvp5i25j66itxvsoei", ], contractAddress: process.env.CONTRACT_ADDRESS, fee: { type: "level", config: { feeLevel: "MEDIUM", }, }, }); console.log(JSON.stringify(mintResponse.data, null, 2)); ``` ```python Python theme={null} from circle.web3 import utils, developer_controlled_wallets import os import json client = utils.init_developer_controlled_wallets_client( api_key=os.getenv("CIRCLE_API_KEY"), entity_secret=os.getenv("CIRCLE_ENTITY_SECRET") ) api_instance = developer_controlled_wallets.TransactionsApi(client) mint_request = developer_controlled_wallets.CreateContractExecutionTransactionForDeveloperRequest.from_dict({ "walletId": os.getenv("WALLET_ID"), "abiFunctionSignature": "mintTo(address,string)", "abiParameters": [ os.getenv("WALLET_ADDRESS"), "ipfs://bafkreibdi6623n3xpf7ymk62ckb4bo75o3qemwkpfvp5i25j66itxvsoei" ], "contractAddress": os.getenv("CONTRACT_ADDRESS"), "feeLevel": "MEDIUM", }) mint_response = api_instance.create_developer_transaction_contract_execution(mint_request) print(json.dumps(mint_response.data.to_dict(), indent=2)) ``` ```shell cURL theme={null} curl --request POST \ --url 'https://api.circle.com/v1/w3s/developer/transactions/contractExecution' \ --header 'authorization: Bearer ' \ --header 'accept: application/json' \ --header 'content-type: application/json' \ --data '{ "idempotencyKey": "", "entitySecretCiphertext": "", "walletId": "", "abiFunctionSignature": "mintTo(address,string)", "abiParameters": [ "", "ipfs://bafkreibdi6623n3xpf7ymk62ckb4bo75o3qemwkpfvp5i25j66itxvsoei" ], "contractAddress": "", "feeLevel": "MEDIUM" }' ``` **Response:** ```json theme={null} { "id": "601a0815-f749-41d8-b193-22cadd2a8977", "state": "INITIATED" } ``` **Metadata URI**: The second parameter is the token metadata URI. It typically points to an IPFS hash containing the token's metadata (name, description, image, etc.). You can use the example IPFS URI from the code sample for testing. ### Transfer tokens Use the `transferFrom` or `safeTransferFrom` function to transfer tokens between addresses. ```ts Node.js theme={null} const transferResponse = await circleDeveloperSdk.createContractExecutionTransaction({ walletId: process.env.WALLET_ID, abiFunctionSignature: "safeTransferFrom(address,address,uint256)", abiParameters: [ "", "", "1", // Token ID ], contractAddress: process.env.CONTRACT_ADDRESS, fee: { type: "level", config: { feeLevel: "MEDIUM", }, }, }); console.log(JSON.stringify(transferResponse.data, null, 2)); ``` ```python Python theme={null} transfer_request = developer_controlled_wallets.CreateContractExecutionTransactionForDeveloperRequest.from_dict({ "walletId": os.getenv("WALLET_ID"), "abiFunctionSignature": "safeTransferFrom(address,address,uint256)", "abiParameters": [ "", "", "1" ], "contractAddress": os.getenv("CONTRACT_ADDRESS"), "feeLevel": "MEDIUM", }) transfer_response = api_instance.create_developer_transaction_contract_execution(transfer_request) print(json.dumps(transfer_response.data.to_dict(), indent=2)) ``` ```shell cURL theme={null} curl --request POST \ --url 'https://api.circle.com/v1/w3s/developer/transactions/contractExecution' \ --header 'authorization: Bearer ' \ --header 'accept: application/json' \ --header 'content-type: application/json' \ --data '{ "idempotencyKey": "", "entitySecretCiphertext": "", "walletId": "", "abiFunctionSignature": "safeTransferFrom(address,address,uint256)", "abiParameters": [ "", "", "1" ], "contractAddress": "", "feeLevel": "MEDIUM" }' ``` ### Full ERC-721 interaction script Here's the full script combining mint and transfer operations: ```ts interact-erc721.ts theme={null} import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets"; const circleDeveloperSdk = initiateDeveloperControlledWalletsClient({ apiKey: process.env.CIRCLE_API_KEY, entitySecret: process.env.CIRCLE_ENTITY_SECRET, }); async function main() { // Mint token const mintResponse = await circleDeveloperSdk.createContractExecutionTransaction({ walletId: process.env.WALLET_ID, abiFunctionSignature: "mintTo(address,string)", abiParameters: [ process.env.WALLET_ADDRESS, "ipfs://bafkreibdi6623n3xpf7ymk62ckb4bo75o3qemwkpfvp5i25j66itxvsoei", ], contractAddress: process.env.CONTRACT_ADDRESS, fee: { type: "level", config: { feeLevel: "MEDIUM", }, }, }); console.log(JSON.stringify(mintResponse.data, null, 2)); // Transfer token (token ID 1) const transferResponse = await circleDeveloperSdk.createContractExecutionTransaction({ walletId: process.env.WALLET_ID, abiFunctionSignature: "safeTransferFrom(address,address,uint256)", abiParameters: [ process.env.WALLET_ADDRESS, process.env.RECIPIENT_WALLET_ADDRESS, "1", // Token ID ], contractAddress: process.env.CONTRACT_ADDRESS, fee: { type: "level", config: { feeLevel: "MEDIUM", }, }, }); console.log(JSON.stringify(transferResponse.data, null, 2)); } main(); ``` ```python interact_erc721.py theme={null} from circle.web3 import utils, developer_controlled_wallets import os import json client = utils.init_developer_controlled_wallets_client( api_key=os.getenv("CIRCLE_API_KEY"), entity_secret=os.getenv("CIRCLE_ENTITY_SECRET") ) api_instance = developer_controlled_wallets.TransactionsApi(client) # Mint token mint_request = developer_controlled_wallets.CreateContractExecutionTransactionForDeveloperRequest.from_dict({ "walletId": os.getenv("WALLET_ID"), "abiFunctionSignature": "mintTo(address,string)", "abiParameters": [ os.getenv("WALLET_ADDRESS"), "ipfs://bafkreibdi6623n3xpf7ymk62ckb4bo75o3qemwkpfvp5i25j66itxvsoei" ], "contractAddress": os.getenv("CONTRACT_ADDRESS"), "feeLevel": "MEDIUM", }) mint_response = api_instance.create_developer_transaction_contract_execution(mint_request) print(json.dumps(mint_response.data.to_dict(), indent=2)) # Transfer token (token ID 1) transfer_request = developer_controlled_wallets.CreateContractExecutionTransactionForDeveloperRequest.from_dict({ "walletId": os.getenv("WALLET_ID"), "abiFunctionSignature": "safeTransferFrom(address,address,uint256)", "abiParameters": [ os.getenv("WALLET_ADDRESS"), os.getenv("RECIPIENT_WALLET_ADDRESS"), "1" ], "contractAddress": os.getenv("CONTRACT_ADDRESS"), "feeLevel": "MEDIUM", }) transfer_response = api_instance.create_developer_transaction_contract_execution(transfer_request) print(json.dumps(transfer_response.data.to_dict(), indent=2)) ``` **Run the script:** ```shell Node.js theme={null} npm run interact-erc721 ``` ```shell Python theme={null} python interact_erc721.py ``` **Response:** ```json theme={null} { "id": "601a0815-f749-41d8-b193-22cadd2a8977", "state": "INITIATED" } ``` ## Interact with ERC-1155 contracts ERC-1155 contracts support multiple token types in a single contract. Each token has a unique ID and can be fungible or non-fungible. ### Mint tokens Use the `mintTo` function to mint tokens. The wallet must have `MINTER_ROLE`. The first mint requires the maximum uint256 value to create token ID 0. For subsequent mints, always use `0` which creates the next token ID. ```ts Node.js theme={null} import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets"; const circleDeveloperSdk = initiateDeveloperControlledWalletsClient({ apiKey: process.env.CIRCLE_API_KEY, entitySecret: process.env.CIRCLE_ENTITY_SECRET, }); const mintResponse = await circleDeveloperSdk.createContractExecutionTransaction({ walletId: process.env.WALLET_ID, abiFunctionSignature: "mintTo(address,uint256,string,uint256)", abiParameters: [ process.env.WALLET_ADDRESS, "115792089237316195423570985008687907853269984665640564039457584007913129639935", // Max uint256 = ID 0 "ipfs://bafkreibdi6623n3xpf7ymk62ckb4bo75o3qemwkpfvp5i25j66itxvsoei", "1", // Amount ], contractAddress: process.env.CONTRACT_ADDRESS, fee: { type: "level", config: { feeLevel: "MEDIUM", }, }, }); console.log(JSON.stringify(mintResponse.data, null, 2)); ``` ```python Python theme={null} from circle.web3 import utils, developer_controlled_wallets import os import json client = utils.init_developer_controlled_wallets_client( api_key=os.getenv("CIRCLE_API_KEY"), entity_secret=os.getenv("CIRCLE_ENTITY_SECRET") ) api_instance = developer_controlled_wallets.TransactionsApi(client) mint_request = developer_controlled_wallets.CreateContractExecutionTransactionForDeveloperRequest.from_dict({ "walletId": os.getenv("WALLET_ID"), "abiFunctionSignature": "mintTo(address,uint256,string,uint256)", "abiParameters": [ os.getenv("WALLET_ADDRESS"), "115792089237316195423570985008687907853269984665640564039457584007913129639935", "ipfs://bafkreibdi6623n3xpf7ymk62ckb4bo75o3qemwkpfvp5i25j66itxvsoei", "1" ], "contractAddress": os.getenv("CONTRACT_ADDRESS"), "feeLevel": "MEDIUM", }) mint_response = api_instance.create_developer_transaction_contract_execution(mint_request) print(json.dumps(mint_response.data.to_dict(), indent=2)) ``` ```shell cURL theme={null} curl --request POST \ --url 'https://api.circle.com/v1/w3s/developer/transactions/contractExecution' \ --header 'authorization: Bearer ' \ --header 'accept: application/json' \ --header 'content-type: application/json' \ --data '{ "idempotencyKey": "", "entitySecretCiphertext": "", "walletId": "", "abiFunctionSignature": "mintTo(address,uint256,string,uint256)", "abiParameters": [ "", "115792089237316195423570985008687907853269984665640564039457584007913129639935", "ipfs://bafkreibdi6623n3xpf7ymk62ckb4bo75o3qemwkpfvp5i25j66itxvsoei", "1" ], "contractAddress": "", "feeLevel": "MEDIUM" }' ``` **Response:** ```json theme={null} { "id": "601a0815-f749-41d8-b193-22cadd2a8977", "state": "INITIATED" } ``` **ERC-1155 Token ID Creation**: The first mint of each token ID requires passing the maximum uint256 value (`2^256 - 1` or `115792089237316195423570985008687907853269984665640564039457584007913129639935`) to create token ID 0 in the contract. For all subsequent mints, use `0` which creates the next sequential token ID (1, 2, 3, etc.). This is an ERC-1155 standard requirement for lazy minting, where token IDs are created on demand rather than pre-initialized. ### Batch transfer tokens Use the `safeBatchTransferFrom` function to transfer multiple token types in a single transaction. ```ts Node.js theme={null} const transferResponse = await circleDeveloperSdk.createContractExecutionTransaction({ walletId: process.env.WALLET_ID, abiFunctionSignature: "safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)", abiParameters: [ "", "", ["0"], // Token IDs ["1"], // Amounts "0x", // Empty bytes ], contractAddress: process.env.CONTRACT_ADDRESS, fee: { type: "level", config: { feeLevel: "MEDIUM", }, }, }); console.log(JSON.stringify(transferResponse.data, null, 2)); ``` ```python Python theme={null} transfer_request = developer_controlled_wallets.CreateContractExecutionTransactionForDeveloperRequest.from_dict({ "walletId": os.getenv("WALLET_ID"), "abiFunctionSignature": "safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)", "abiParameters": [ "", "", ["0"], ["1"], "0x" ], "contractAddress": os.getenv("CONTRACT_ADDRESS"), "feeLevel": "MEDIUM", }) transfer_response = api_instance.create_developer_transaction_contract_execution(transfer_request) print(json.dumps(transfer_response.data.to_dict(), indent=2)) ``` ```shell cURL theme={null} curl --request POST \ --url 'https://api.circle.com/v1/w3s/developer/transactions/contractExecution' \ --header 'authorization: Bearer ' \ --header 'accept: application/json' \ --header 'content-type: application/json' \ --data '{ "idempotencyKey": "", "entitySecretCiphertext": "", "walletId": "", "abiFunctionSignature": "safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)", "abiParameters": [ "", "", ["0"], ["1"], "0x" ], "contractAddress": "", "feeLevel": "MEDIUM" }' ``` ### Full ERC-1155 interaction script Here's the full script combining mint and batch transfer operations: ```ts interact-erc1155.ts theme={null} import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets"; const circleDeveloperSdk = initiateDeveloperControlledWalletsClient({ apiKey: process.env.CIRCLE_API_KEY, entitySecret: process.env.CIRCLE_ENTITY_SECRET, }); async function main() { // Mint tokens (token ID 0) const mintResponse = await circleDeveloperSdk.createContractExecutionTransaction({ walletId: process.env.WALLET_ID, abiFunctionSignature: "mintTo(address,uint256,string,uint256)", abiParameters: [ process.env.WALLET_ADDRESS, "115792089237316195423570985008687907853269984665640564039457584007913129639935", // Max uint256 = ID 0 "ipfs://bafkreibdi6623n3xpf7ymk62ckb4bo75o3qemwkpfvp5i25j66itxvsoei", "1", // Amount ], contractAddress: process.env.CONTRACT_ADDRESS, fee: { type: "level", config: { feeLevel: "MEDIUM", }, }, }); console.log(JSON.stringify(mintResponse.data, null, 2)); // Batch transfer tokens const transferResponse = await circleDeveloperSdk.createContractExecutionTransaction({ walletId: process.env.WALLET_ID, abiFunctionSignature: "safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)", abiParameters: [ process.env.WALLET_ADDRESS, process.env.RECIPIENT_WALLET_ADDRESS, ["0"], // Token IDs ["1"], // Amounts "0x", // Empty bytes ], contractAddress: process.env.CONTRACT_ADDRESS, fee: { type: "level", config: { feeLevel: "MEDIUM", }, }, }); console.log(JSON.stringify(transferResponse.data, null, 2)); } main(); ``` ```python interact_erc1155.py theme={null} from circle.web3 import utils, developer_controlled_wallets import os import json client = utils.init_developer_controlled_wallets_client( api_key=os.getenv("CIRCLE_API_KEY"), entity_secret=os.getenv("CIRCLE_ENTITY_SECRET") ) api_instance = developer_controlled_wallets.TransactionsApi(client) # Mint tokens (token ID 0) mint_request = developer_controlled_wallets.CreateContractExecutionTransactionForDeveloperRequest.from_dict({ "walletId": os.getenv("WALLET_ID"), "abiFunctionSignature": "mintTo(address,uint256,string,uint256)", "abiParameters": [ os.getenv("WALLET_ADDRESS"), "115792089237316195423570985008687907853269984665640564039457584007913129639935", "ipfs://bafkreibdi6623n3xpf7ymk62ckb4bo75o3qemwkpfvp5i25j66itxvsoei", "1" ], "contractAddress": os.getenv("CONTRACT_ADDRESS"), "feeLevel": "MEDIUM", }) mint_response = api_instance.create_developer_transaction_contract_execution(mint_request) print(json.dumps(mint_response.data.to_dict(), indent=2)) # Batch transfer tokens transfer_request = developer_controlled_wallets.CreateContractExecutionTransactionForDeveloperRequest.from_dict({ "walletId": os.getenv("WALLET_ID"), "abiFunctionSignature": "safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)", "abiParameters": [ os.getenv("WALLET_ADDRESS"), os.getenv("RECIPIENT_WALLET_ADDRESS"), ["0"], ["1"], "0x" ], "contractAddress": os.getenv("CONTRACT_ADDRESS"), "feeLevel": "MEDIUM", }) transfer_response = api_instance.create_developer_transaction_contract_execution(transfer_request) print(json.dumps(transfer_response.data.to_dict(), indent=2)) ``` **Run the script:** ```shell Node.js theme={null} npm run interact-erc1155 ``` ```shell Python theme={null} python interact_erc1155.py ``` **Response:** ```json theme={null} { "id": "601a0815-f749-41d8-b193-22cadd2a8977", "state": "INITIATED" } ``` ## Execute airdrop operations The Airdrop contract enables mass token distribution to multiple recipients. ### Prerequisites Before executing an airdrop, you need: 1. **A token contract address** - Deploy one using the [ERC-20](/arc/tutorials/deploy-contracts#erc-20), [ERC-721](/arc/tutorials/deploy-contracts#erc-721), or [ERC-1155](/arc/tutorials/deploy-contracts#erc-1155) templates, or use an existing token 2. **Token balance** - Your wallet must hold enough tokens to distribute 3. **Token approval** - Call the `approve` or `setApprovalForAll` function on your token contract to allow the airdrop contract to transfer tokens ### Execute an ERC-20 airdrop Use the `airdropERC20` function to distribute ERC-20 tokens to multiple recipients. ```ts Node.js theme={null} import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets"; const circleDeveloperSdk = initiateDeveloperControlledWalletsClient({ apiKey: process.env.CIRCLE_API_KEY, entitySecret: process.env.CIRCLE_ENTITY_SECRET, }); const airdropResponse = await circleDeveloperSdk.createContractExecutionTransaction({ walletId: process.env.WALLET_ID, abiFunctionSignature: "airdropERC20(address,(address,uint256)[])", abiParameters: [ "", // ERC-20 token contract address [ ["", "1000000000000000000"], ["", "2000000000000000000"], ], ], contractAddress: process.env.CONTRACT_ADDRESS, fee: { type: "level", config: { feeLevel: "MEDIUM", }, }, }); console.log(JSON.stringify(airdropResponse.data, null, 2)); ``` ```python Python theme={null} from circle.web3 import utils, developer_controlled_wallets import os import json client = utils.init_developer_controlled_wallets_client( api_key=os.getenv("CIRCLE_API_KEY"), entity_secret=os.getenv("CIRCLE_ENTITY_SECRET") ) api_instance = developer_controlled_wallets.TransactionsApi(client) airdrop_request = developer_controlled_wallets.CreateContractExecutionTransactionForDeveloperRequest.from_dict({ "walletId": os.getenv("WALLET_ID"), "abiFunctionSignature": "airdropERC20(address,(address,uint256)[])", "abiParameters": [ "", [ ["", "1000000000000000000"], ["", "2000000000000000000"] ] ], "contractAddress": os.getenv("CONTRACT_ADDRESS"), "feeLevel": "MEDIUM", }) airdrop_response = api_instance.create_developer_transaction_contract_execution(airdrop_request) print(json.dumps(airdrop_response.data.to_dict(), indent=2)) ``` ```shell cURL theme={null} curl --request POST \ --url 'https://api.circle.com/v1/w3s/developer/transactions/contractExecution' \ --header 'authorization: Bearer ' \ --header 'accept: application/json' \ --header 'content-type: application/json' \ --data '{ "idempotencyKey": "", "entitySecretCiphertext": "", "walletId": "", "abiFunctionSignature": "airdropERC20(address,(address,uint256)[])", "abiParameters": [ "", [ ["", "1000000000000000000"], ["", "2000000000000000000"] ] ], "contractAddress": "", "feeLevel": "MEDIUM" }' ``` **Response:** ```json theme={null} { "id": "601a0815-f749-41d8-b193-22cadd2a8977", "state": "INITIATED" } ``` **Token contract**: The first parameter is the address of the ERC-20 token contract you want to airdrop. You must deploy this contract first using the [Deploy contracts](/arc/tutorials/deploy-contracts) tutorial. ### Execute an ERC-721 airdrop Use the `airdropERC721` function to distribute tokens to multiple recipients. ```ts Node.js theme={null} const airdropResponse = await circleDeveloperSdk.createContractExecutionTransaction({ walletId: process.env.WALLET_ID, abiFunctionSignature: "airdropERC721(address,(address,uint256)[])", abiParameters: [ "", // ERC-721 token contract address [ ["", "1"], // Token ID 1 ["", "2"], // Token ID 2 ], ], contractAddress: process.env.CONTRACT_ADDRESS, fee: { type: "level", config: { feeLevel: "MEDIUM", }, }, }); console.log(JSON.stringify(airdropResponse.data, null, 2)); ``` ```python Python theme={null} airdrop_request = developer_controlled_wallets.CreateContractExecutionTransactionForDeveloperRequest.from_dict({ "walletId": os.getenv("WALLET_ID"), "abiFunctionSignature": "airdropERC721(address,(address,uint256)[])", "abiParameters": [ "", [ ["", "1"], ["", "2"] ] ], "contractAddress": os.getenv("CONTRACT_ADDRESS"), "feeLevel": "MEDIUM", }) airdrop_response = api_instance.create_developer_transaction_contract_execution(airdrop_request) print(json.dumps(airdrop_response.data.to_dict(), indent=2)) ``` ```shell cURL theme={null} curl --request POST \ --url 'https://api.circle.com/v1/w3s/developer/transactions/contractExecution' \ --header 'authorization: Bearer ' \ --header 'accept: application/json' \ --header 'content-type: application/json' \ --data '{ "idempotencyKey": "", "entitySecretCiphertext": "", "walletId": "", "abiFunctionSignature": "airdropERC721(address,(address,uint256)[])", "abiParameters": [ "", [ ["", "1"], ["", "2"] ] ], "contractAddress": "", "feeLevel": "MEDIUM" }' ``` **Response:** ```json theme={null} { "id": "601a0815-f749-41d8-b193-22cadd2a8977", "state": "INITIATED" } ``` ### Execute an ERC-1155 airdrop Use the `airdropERC1155` function to distribute ERC-1155 tokens to multiple recipients. ```ts Node.js theme={null} const airdropResponse = await circleDeveloperSdk.createContractExecutionTransaction({ walletId: process.env.WALLET_ID, abiFunctionSignature: "airdropERC1155(address,(address,uint256,uint256)[])", abiParameters: [ "", // ERC-1155 token contract address [ ["", "0", "10"], // Token ID 0, amount 10 ["", "1", "5"], // Token ID 1, amount 5 ], ], contractAddress: process.env.CONTRACT_ADDRESS, fee: { type: "level", config: { feeLevel: "MEDIUM", }, }, }); console.log(JSON.stringify(airdropResponse.data, null, 2)); ``` ```python Python theme={null} airdrop_request = developer_controlled_wallets.CreateContractExecutionTransactionForDeveloperRequest.from_dict({ "walletId": os.getenv("WALLET_ID"), "abiFunctionSignature": "airdropERC1155(address,(address,uint256,uint256)[])", "abiParameters": [ "", [ ["", "0", "10"], ["", "1", "5"] ] ], "contractAddress": os.getenv("CONTRACT_ADDRESS"), "feeLevel": "MEDIUM", }) airdrop_response = api_instance.create_developer_transaction_contract_execution(airdrop_request) print(json.dumps(airdrop_response.data.to_dict(), indent=2)) ``` ```shell cURL theme={null} curl --request POST \ --url 'https://api.circle.com/v1/w3s/developer/transactions/contractExecution' \ --header 'authorization: Bearer ' \ --header 'accept: application/json' \ --header 'content-type: application/json' \ --data '{ "idempotencyKey": "", "entitySecretCiphertext": "", "walletId": "", "abiFunctionSignature": "airdropERC1155(address,(address,uint256,uint256)[])", "abiParameters": [ "", [ ["", "0", "10"], ["", "1", "5"] ] ], "contractAddress": "", "feeLevel": "MEDIUM" }' ``` ### Full airdrop interaction script Here's the full script for executing an ERC-20 airdrop. You can adapt it for ERC-721 or ERC-1155 by changing the function signature and parameters as shown in the examples previously: ```ts interact-airdrop.ts theme={null} import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets"; const circleDeveloperSdk = initiateDeveloperControlledWalletsClient({ apiKey: process.env.CIRCLE_API_KEY, entitySecret: process.env.CIRCLE_ENTITY_SECRET, }); async function main() { // Execute ERC-20 airdrop const airdropResponse = await circleDeveloperSdk.createContractExecutionTransaction({ walletId: process.env.WALLET_ID, abiFunctionSignature: "airdropERC20(address,(address,uint256)[])", abiParameters: [ process.env.TOKEN_CONTRACT_ADDRESS, // ERC-20 token contract address [ [process.env.RECIPIENT_ADDRESS_1, "1000000000000000000"], [process.env.RECIPIENT_ADDRESS_2, "2000000000000000000"], ], ], contractAddress: process.env.CONTRACT_ADDRESS, fee: { type: "level", config: { feeLevel: "MEDIUM", }, }, }); console.log(JSON.stringify(airdropResponse.data, null, 2)); // For ERC-721 airdrop, use: // abiFunctionSignature: "airdropERC721(address,(address,uint256)[])" // abiParameters: [tokenAddress, [[recipient1, tokenId1], [recipient2, tokenId2]]] // For ERC-1155 airdrop, use: // abiFunctionSignature: "airdropERC1155(address,(address,uint256,uint256)[])" // abiParameters: [tokenAddress, [[recipient1, tokenId, amount], [recipient2, tokenId, amount]]] } main(); ``` ```python interact_airdrop.py theme={null} from circle.web3 import utils, developer_controlled_wallets import os import json client = utils.init_developer_controlled_wallets_client( api_key=os.getenv("CIRCLE_API_KEY"), entity_secret=os.getenv("CIRCLE_ENTITY_SECRET") ) api_instance = developer_controlled_wallets.TransactionsApi(client) # Execute ERC-20 airdrop airdrop_request = developer_controlled_wallets.CreateContractExecutionTransactionForDeveloperRequest.from_dict({ "walletId": os.getenv("WALLET_ID"), "abiFunctionSignature": "airdropERC20(address,(address,uint256)[])", "abiParameters": [ os.getenv("TOKEN_CONTRACT_ADDRESS"), [ [os.getenv("RECIPIENT_ADDRESS_1"), "1000000000000000000"], [os.getenv("RECIPIENT_ADDRESS_2"), "2000000000000000000"] ] ], "contractAddress": os.getenv("CONTRACT_ADDRESS"), "feeLevel": "MEDIUM", }) airdrop_response = api_instance.create_developer_transaction_contract_execution(airdrop_request) print(json.dumps(airdrop_response.data.to_dict(), indent=2)) # For ERC-721 airdrop, use: # abiFunctionSignature: "airdropERC721(address,(address,uint256)[])" # abiParameters: [token_address, [[recipient1, token_id1], [recipient2, token_id2]]] # For ERC-1155 airdrop, use: # abiFunctionSignature: "airdropERC1155(address,(address,uint256,uint256)[])" # abiParameters: [token_address, [[recipient1, token_id, amount], [recipient2, token_id, amount]]] ``` **Run the script:** ```shell Node.js theme={null} npm run interact-airdrop ``` ```shell Python theme={null} python interact_airdrop.py ``` **Response:** ```json theme={null} { "id": "601a0815-f749-41d8-b193-22cadd2a8977", "state": "INITIATED" } ``` *** ## Summary After completing this tutorial, you've learned how to: * Execute contract functions using the Circle SDKs * Mint and transfer tokens for your deployed contracts * Perform contract-specific operations based on token type # Monitor a node Source: https://docs.arc.io/arc/tutorials/monitor-a-node Verify your Arc node is syncing, view logs, and confirm Prometheus metrics endpoints are reachable. Verify that your Arc node is syncing, diagnose issues from logs, and confirm that the Prometheus metrics endpoints are reachable on the host. For a full Prometheus and Grafana setup on the same host, see [Set up Prometheus and Grafana for an Arc node](/arc/tutorials/set-up-node-monitoring). ## Prerequisites * You have a running Arc node that meets the [node requirements](/arc/references/node-requirements), set up either from the [Run an Arc node](/arc/tutorials/run-an-arc-node) tutorial or [deployed as a systemd service](/arc/tutorials/deploy-node-as-service) * You have installed [Foundry](https://book.getfoundry.sh/getting-started/installation), which provides the `cast` command ## Step 1. Check service status If your node runs as systemd services, check the status of both processes: ```shell theme={null} sudo systemctl status arc-execution sudo systemctl status arc-consensus ``` Both services display `Active: active (running)` in their output. If either service has failed, review the logs in Step 3. ## Step 2. Check block height Query the local RPC endpoint to confirm the node is syncing: ```shell theme={null} cast block-number --rpc-url http://localhost:8545 ``` Run this command several times over a few seconds. The block number increases steadily, confirming the node is syncing: ```text theme={null} # First run: 1234567 # Second run (a few seconds later): 1234572 ``` To view the latest block details: ```shell theme={null} cast block --rpc-url http://localhost:8545 ``` Example output: ```text theme={null} baseFeePerGas 7 difficulty 0 gasLimit 30000000 gasUsed 21000 hash 0xabc123... number 1234572 timestamp 1711234567 transactions: [0xdef456...] ``` ## Step 3. View the logs Stream real-time logs for each service: ```shell theme={null} # Execution Layer logs sudo journalctl -u arc-execution -f # Consensus Layer logs sudo journalctl -u arc-consensus -f ``` If your node is not running as a systemd service, check the terminal output where each process is running. **What to look for:** * **Healthy:** Log entries showing new blocks being imported, increasing block heights * **Unhealthy:** Repeated connection errors to relay endpoints, IPC socket failures, or no new blocks for an extended period ## Step 4. Verify the metrics endpoints Both layers expose Prometheus-compatible metrics endpoints on the host. Note that the Execution Layer serves metrics at the root path, not at `/metrics`: | Endpoint | Description | | :------------------------------- | :---------------------- | | `http://localhost:9001/` | Execution Layer metrics | | `http://localhost:29000/metrics` | Consensus Layer metrics | The metrics endpoints are only available if the `--metrics` flag was passed when starting the node. The [Deploy a node as a service](/arc/tutorials/deploy-node-as-service) guide includes this flag in both unit files. Confirm the endpoints are reachable: ```shell theme={null} curl -s http://localhost:9001/ | head curl -s http://localhost:29000/metrics | head ``` Both commands return Prometheus-formatted text metrics. Example output: ```text theme={null} # HELP reth_sync_stage_checkpoint Stage checkpoint block number # TYPE reth_sync_stage_checkpoint gauge reth_sync_stage_checkpoint{stage="Headers"} 1234567 ``` If either returns an empty response or connection error, confirm the `--metrics` flag is set in your startup command. To scrape these endpoints with a local Prometheus + Grafana stack and load the pre-built Arc dashboards, see [Set up Prometheus and Grafana for an Arc node](/arc/tutorials/set-up-node-monitoring). # Monitor contract events Source: https://docs.arc.io/arc/tutorials/monitor-contract-events Track onchain activity by monitoring contract events. Track contract events and get event logs with the Circle Contracts API. ## Prerequisites You need a deployed contract to monitor. If you completed the [Deploy contracts](/arc/tutorials/deploy-contracts) tutorial, you can continue with that contract. If your contract was deployed elsewhere, import it in [Step 3](#step-3-import-a-contract-optional). Test against Arc Testnet only. Local EVM simulators run a standard EVM. They don't replicate EIP-7708 Transfer events, the USDC precompile, or blocklist enforcement. ## Step 1. Update your project If you haven't already, add run scripts for monitoring contract events to your `package.json`: ```shell theme={null} npm pkg set scripts.webhook="tsx webhook-receiver.ts" npm pkg set scripts.import-contract="tsx --env-file=.env import-contract.ts" npm pkg set scripts.create-monitor="tsx --env-file=.env create-monitor.ts" npm pkg set scripts.get-event-logs="tsx --env-file=.env get-event-logs.ts" ``` If you completed the Deploy contracts tutorial, your project already has the required SDKs installed. The npm scripts previously listed work with your existing setup. ## Step 2. Set up your webhook Event monitors send real-time updates to your webhook endpoint when events happen. 1. Visit [webhook.site](https://webhook.site) 2. Copy your unique webhook URL (for example, `https://webhook.site/your-uuid`) 1. Install `ngrok` from [ngrok.com](https://ngrok.com) 2. Create a webhook receiver script: ```ts webhook-receiver.ts theme={null} import express, { Request, Response } from "express"; const app = express(); app.use(express.json()); app.post("/webhook", (req: Request, res: Response) => { console.log("Received webhook:"); console.log(JSON.stringify(req.body, null, 2)); res.status(200).json({ received: true }); }); const PORT = 3000; app.listen(PORT, () => { console.log(`Webhook receiver listening on port ${PORT}`); console.log(`Endpoint: http://localhost:${PORT}/webhook`); }); ``` ```python webhook_receiver.py theme={null} from flask import Flask, request, jsonify import json app = Flask(__name__) @app.route("/webhook", methods=["POST"]) def webhook(): data = request.get_json() print("Received webhook:") print(json.dumps(data, indent=2)) return jsonify({"received": True}), 200 if __name__ == "__main__": PORT = 3000 print(f"Webhook receiver listening on port {PORT}") print(f"Endpoint: http://localhost:{PORT}/webhook") app.run(port=PORT) ``` 3. Start the webhook receiver: ```shell Node.js theme={null} npm run webhook ``` ```shell Python theme={null} python webhook_receiver.py ``` 4. In a separate terminal, start `ngrok`: ```shell theme={null} ngrok http 3000 ``` 5. Copy the HTTPS forwarding URL (for example, `https://abc123.ngrok-free.app/webhook`) If using `ngrok` for local testing, you can optionally set `WEBHOOK_URL` in your `.env` file to store your `ngrok` forwarding URL. ## Step 3. Register your webhook in Console Register your webhook URL in the Developer Console: 1. Go to [Developer Console](https://console.circle.com) 2. Navigate to **Webhooks** (left sidebar) 3. Click **Add a webhook** 4. Enter your webhook URL (from Step 1) and create the webhook Register your webhook before creating event monitors. This allows Circle to send notifications to your endpoint. ## Step 4. Import a contract (optional) If your contract was deployed elsewhere and is not yet available in the Developer Console, import it first. If you deployed a contract using Circle Contracts, including the [Deploy contracts](/arc/tutorials/deploy-contracts) tutorial, skip this step. Your contract is already available in the Console. ```ts import-contract.ts theme={null} import { initiateSmartContractPlatformClient } from "@circle-fin/smart-contract-platform"; const contractClient = initiateSmartContractPlatformClient({ apiKey: process.env.CIRCLE_API_KEY, entitySecret: process.env.CIRCLE_ENTITY_SECRET, }); async function importContract() { try { const response = await contractClient.importContract({ blockchain: "ARC-TESTNET", address: process.env.CONTRACT_ADDRESS, name: "MyContract", }); console.log(JSON.stringify(response.data, null, 2)); } catch (error) { console.error("Error importing contract:", error.message); throw error; } } importContract(); ``` ```python import_contract.py theme={null} from circle.web3 import utils, smart_contract_platform import os import json client = utils.init_smart_contract_platform_client( api_key=os.getenv("CIRCLE_API_KEY"), entity_secret=os.getenv("CIRCLE_ENTITY_SECRET") ) def import_contract(): try: contracts_api = smart_contract_platform.ContractsApi(client) import_request = smart_contract_platform.ImportContractRequest( blockchain="ARC-TESTNET", address=os.getenv("CONTRACT_ADDRESS"), name="MyContract", ) response = contracts_api.import_contract(import_contract_request=import_request) print(json.dumps(response.data.to_dict(), indent=2)) except Exception as error: print(f"Error importing contract: {error}") raise error import_contract() ``` **Run the script:** ```shell Node.js theme={null} npm run import-contract ``` ```shell Python theme={null} python import_contract.py ``` If the contract is already imported, you'll see an error: `contract already exists`. This means the contract is already available in the Console and you can proceed to create an event monitor. ## Step 5. Create an event monitor Event monitors track specific contract events. They send updates to your webhook endpoint. This example monitors `Transfer` events: ```ts create-monitor.ts theme={null} import { initiateSmartContractPlatformClient } from "@circle-fin/smart-contract-platform"; const contractClient = initiateSmartContractPlatformClient({ apiKey: process.env.CIRCLE_API_KEY, entitySecret: process.env.CIRCLE_ENTITY_SECRET, }); async function createEventMonitor() { try { const response = await contractClient.createEventMonitor({ blockchain: "ARC-TESTNET", contractAddress: process.env.CONTRACT_ADDRESS, eventSignature: "Transfer(address,address,uint256)", }); console.log(JSON.stringify(response.data, null, 2)); } catch (error) { console.error("Error creating event monitor:", error.message); throw error; } } createEventMonitor(); ``` ```python create_monitor.py theme={null} from circle.web3 import utils, smart_contract_platform import os import json client = utils.init_smart_contract_platform_client( api_key=os.getenv("CIRCLE_API_KEY"), entity_secret=os.getenv("CIRCLE_ENTITY_SECRET") ) def create_event_monitor(): try: event_monitors_api = smart_contract_platform.EventMonitorsApi(client) monitor_request = smart_contract_platform.CreateEventMonitorRequest( blockchain="ARC-TESTNET", contract_address=os.getenv("CONTRACT_ADDRESS"), event_signature="Transfer(address,address,uint256)", ) response = event_monitors_api.create_event_monitor( create_event_monitor_request=monitor_request ) print(json.dumps(response.data.to_dict(), indent=2)) except Exception as error: print(f"Error creating event monitor: {error}") raise create_event_monitor() ``` **Run the script:** ```shell Node.js theme={null} npm run create-monitor ``` ```shell Python theme={null} python create_monitor.py ``` **Response:** ```json theme={null} { "eventMonitor": { "id": "019bf984-b4da-7026-a3d2-674ce371a933", "contractName": "TestERC20Token", "contractId": "019bf8be-7be5-7a3e-89cc-05bcd7413f20", "contractAddress": "0x281156899E5BD6fEcf1C0831Ee24894eEEAea2f8", "blockchain": "ARC-TESTNET", "eventSignature": "Transfer(address,address,uint256)", "eventSignatureHash": "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "isEnabled": true, "createDate": "2026-01-26T08:56:22.490638Z", "updateDate": "2026-01-26T08:56:22.490638Z" } } ``` If you're monitoring USDC transfers, the `contractAddress` filter alone isn't sufficient: * **Set up a monitor for the system emitter to capture native sends.** Plain native USDC sends emit from `0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE`, not from the ERC-20 USDC contract or any user-deployed contract. A monitor filtered on the ERC-20 contract address silently misses native sends. * **Don't count logs from both the system emitter and the ERC-20 contract.** An ERC-20 `transfer()` emits a log from both. Use the system emitter only to avoid counting the same transfer twice. ## Step 6. Receive webhook notifications When events occur, Circle sends updates to your endpoint. Here is what a `Transfer` event looks like: ```json theme={null} { "subscriptionId": "f0332621-a117-4b7b-bdf0-5c61a4681826", "notificationId": "5c5eea9f-398f-426f-a4a5-1bdc28b36d2c", "notificationType": "contracts.eventLog", "notification": { "contractAddress": "0x4abcffb90897fe7ce86ed689d1178076544a021b", "blockchain": "ARC-TESTNET", "txHash": "0xe15d6dbb50178f60930b8a3e3e775f3c022505ea2e351b6c2c2985d2405c8ebc", "userOpHash": "0x78c3e8185ff9abfc7197a8432d9b79566123616c136001e609102c97e732e55e", "blockHash": "0x0ad6bf57a110d42620defbcb9af98d6223f060de588ed96ae495ddeaf3565c8d", "blockHeight": 22807198, "eventSignature": "Transfer(address,address,uint256)", "eventSignatureHash": "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x000000000000000000000000bcf83d3b112cbf43b19904e376dd8dee01fe2758" ], "data": "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000", "firstConfirmDate": "2026-01-21T06:53:12Z" }, "timestamp": "2026-01-21T06:53:13.194467201Z", "version": 2 } ``` **Key fields:** * `notificationType`: Always `"contracts.eventLog"` for event monitor webhooks * `notification.eventSignature`: The event that was emitted * `notification.contractAddress`: Address of the contract that emitted the event * `notification.blockchain`: The blockchain network (for example, `ARC-TESTNET`) * `notification.txHash`: Transaction hash where the event occurred * `notification.userOpHash`: User operation hash (for ERC-4337 smart contract accounts). When present, `tx.from` at the RPC level is the bundler. Attributing from it identifies the bundler, not the user. Check the Transfer event's indexed topics for the token sender rather than using `tx.from`. * `notification.blockHash`: Hash of the block containing the transaction * `notification.blockHeight`: Block number where the event occurred * `notification.eventSignatureHash`: Keccak256 hash of the event signature * `notification.topics`: Indexed event parameters (for example, `from` and `to` addresses) * `notification.data`: Non-indexed event parameters (for example, token amount) * `notification.firstConfirmDate`: Timestamp when the event was first confirmed * `timestamp`: Timestamp when the webhook was sent * `version`: Webhook payload version You can verify webhook delivery status in the [Developer Console](https://console.circle.com) under Contracts → Monitoring. ## Step 7. Retrieve event logs You can also query event logs with the API. This is useful for past events or if you prefer polling. **Webhooks vs Polling**: Webhooks send real-time updates (push). Polling needs periodic API calls (pull). Use webhooks for production and polling for testing or past queries. ```ts get-event-logs.ts theme={null} import { initiateSmartContractPlatformClient } from "@circle-fin/smart-contract-platform"; const contractClient = initiateSmartContractPlatformClient({ apiKey: process.env.CIRCLE_API_KEY, entitySecret: process.env.CIRCLE_ENTITY_SECRET, }); async function getEventLogs() { try { const response = await contractClient.listEventLogs({ contractAddress: process.env.CONTRACT_ADDRESS, blockchain: "ARC-TESTNET", pageSize: 10, }); console.log(JSON.stringify(response.data, null, 2)); } catch (error) { console.error("Error fetching event logs:", error.message); throw error; } } getEventLogs(); ``` ```python get_event_logs.py theme={null} from circle.web3 import utils, smart_contract_platform import os import json client = utils.init_smart_contract_platform_client( api_key=os.getenv("CIRCLE_API_KEY"), entity_secret=os.getenv("CIRCLE_ENTITY_SECRET") ) event_monitors_api = smart_contract_platform.EventMonitorsApi(client) def get_event_logs(): try: response = event_monitors_api.list_event_logs( contract_address=os.getenv("CONTRACT_ADDRESS"), blockchain="ARC-TESTNET", page_size=10 ) print(json.dumps(response.data.to_dict(), indent=2, default=str)) except Exception as error: print(f"Error fetching event logs: {error}") raise get_event_logs() ``` **Run the script:** ```shell Node.js theme={null} npm run get-event-logs ``` ```shell Python theme={null} python get_event_logs.py ``` Replace `CONTRACT_ADDRESS` with your contract address. You can get this address when you deploy the contract, or by listing your contracts with `listContracts()`. **Response:** ```json theme={null} { "eventLogs": [ { "id": "019bf987-f901-7145-9e95-55f177b05b24", "subscriptionId": "019bf984-b4da-7026-a3d2-674ce371a933", "contractId": "019bf8be-7be5-7a3e-89cc-05bcd7413f20", "contractName": "TestERC20Token", "blockchain": "ARC-TESTNET", "txHash": "0x3bfbab5d5ce0d1a5d682cbc742d3940cf59db0369d173b71ba2a3b8f43bfbcb1", "logIndex": "50", "blockHash": "0x7d12148f9331556b31f84f58a41b7ff16eaaa47940f9e86733037d7ab74d858e", "blockHeight": 23686153, "contractAddress": "0x281156899E5BD6fEcf1C0831Ee24894eEEAea2f8", "eventSignature": "Transfer(address,address,uint256)", "eventSignatureHash": "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x000000000000000000000000bcf83d3b112cbf43b19904e376dd8dee01fe2758" ], "data": "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000", "decodedTopics": null, "decodedData": null, "userOpHash": "0x66befac1a371fcdddf1566215e4677127e111dff9253f306f7096fed8642a208", "firstConfirmDate": "2026-01-26T08:59:55Z", "createDate": "2026-01-26T08:59:56.545962Z", "updateDate": "2026-01-26T08:59:56.545962Z" } ] } ``` You can view, update, and delete event monitors with the Circle Contracts API. See the [API Reference](https://developers.circle.com/api-reference/contracts/smart-contract-platform/get-event-monitors) for details on managing your monitors. ## Summary After completing this tutorial, you've successfully: * Set up webhook endpoints using webhook.site or `ngrok` * Registered webhooks in the Developer Console * Created event monitors for specific contract events * Received real-time webhook updates for contract events * Retrieved past event logs with the Circle SDK # Port a contract to Arc Source: https://docs.arc.io/arc/tutorials/porting-contracts-to-arc How to adapt an existing EVM contract for Arc, where USDC is the native gas token, and verify it against Arc's value transfer rules before you deploy. Most Ethereum contracts deploy and run on Arc without changes. The cases that need attention almost all come from one fact: **on Arc the native gas token is USDC, and native USDC and the ERC-20 USDC interface are the same asset.** This guide walks through the checks to perform on an existing contract and how to verify it on Arc Testnet before you ship. For the protocol-level detail behind each check, see [EVM differences](/arc/references/evm-differences). ## Before you start * Connect your tooling to Arc Testnet. See [Connect to Arc](/arc/references/connect-to-arc). * Have your contract source and its test suite ready. * Get testnet USDC from the [Circle Faucet](https://faucet.circle.com/) to pay for gas and fund test transfers. ## Steps Search your contract for places that read `balanceOf` or `address.balance` and for any logic that compares or combines the two. * Convert before comparing: the ERC-20 view uses 6 decimals and the native view uses 18 for the same balance. * Treat the ERC-20 `balanceOf` as inexact. It truncates anything below 1×10⁻⁶ USDC, so `0.0000001` USDC reads as `0` and `100.0000001` USDC reads as `100`. A `balanceOf` of `0` does not mean the native balance is `0`. Find every native send and every path that forwards value. * Handle reverts: a native transfer can revert even with a sufficient balance, for example a transfer to the zero address, a transfer to or from a blocklisted address, or any transfer that would burn value. Gas is consumed on a revert. Check `receipt.status === 0` to detect it. Review allowances and any "sweep" logic. * **Don't treat an ERC-20 allowance as a complete spending control on a contract.** `USDC.approve` bounds `transferFrom` calls only. If the contract you're building also exposes functions that send native USDC, those paths move USDC regardless of any allowance. Account for all transfer paths in your access controls, not just the ERC-20 interface. * If a contract is meant to hold USDC only as an ERC-20 token and not to act on native value, don't let it sweep its native balance. Native USDC and the ERC-20 USDC interface are the same asset, so a native sweep also moves the users' ERC-20 USDC balance. * Don't pair "native" against the ERC-20 USDC interface in a liquidity pool. Both legs are the same asset, so the pairing is meaningless. If you are porting a DEX, AMM, or router (for example, Uniswap V2, Uniswap V3, or a fork), there is no wrapped native token on Arc and no `WUSDC`/`WETH` equivalent is needed. * Use the ERC-20 USDC contract directly as the pair token. Arc's ERC-20 USDC interface at [`0x3600000000000000000000000000000000000000`](/arc/references/contract-addresses#usdc) is the canonical token. Treat it like any other ERC-20 in your pool, pair, and router code. * Do not deploy a `WUSDC` wrapper contract. Wrapping the native asset on Arc fragments liquidity and can create user confusion. The ERC-20 interface already exposes `transfer`, `approve`, and `transferFrom` over the same underlying native balance. * Replace `WETH`-style code paths. Remove `deposit()` / `withdraw()` wrap-and-unwrap calls, and replace any `WETH` address constant in your router or periphery contracts with the ERC-20 USDC address above. Routes that accepted raw native value via `msg.value` can either keep accepting native USDC, or be simplified to ERC-20-only flows since both interfaces move the same balance. Mind the decimals when you mix paths: `msg.value` is denominated in 18-decimal native USDC, while the ERC-20 USDC interface uses 6 decimals. * Don't alias the EIP-7528 native-asset sentinel that DeFi SDKs use in routing tables to Arc's ERC-20 USDC address (`0x3600000000000000000000000000000000000000`). Aliasing it conflates native value transfers with ERC-20 transfers. * Do not bridge or deploy `USDC.e` / `wUSDC` variants. All USDC on Arc arrives via CCTP as the native asset. See [Bridges](/integrate/infrastructure/bridges) for the bridging rules. If your contract uses `SELFDESTRUCT`, confirm it doesn't depend on burning, on sending value to a destructed account, or on retaining USDC afterward. * A contract's USDC is its native balance, so self-destructing transfers that USDC to the beneficiary. On other chains the ERC-20 USDC balance would remain in the token contract. * Self-destructing to yourself with a balance, to the zero address with a balance, to a blocklisted address, or to an already self-destructed account all revert. * A non-zero-value call to a contract after it self-destructs reverts on Arc, even though it succeeds on Ethereum. See [SELFDESTRUCT](/arc/references/evm-differences#selfdestruct) for the exact conditions and a worked example. Replace any use of `block.prevrandao` with a VRF or randomness oracle. On Arc, `PREVRANDAO` always returns `0`. Contracts that use it for lottery, shuffle, or relay logic will always receive `0`. Run your test suite against Arc Testnet, not a local EVM simulator. Tools like Foundry's `anvil` run a standard EVM and cannot reproduce Arc's precompiles, EIP-7708 `Transfer` events, or USDC blocklist enforcement. Confirm your contract handles a blocklist revert gracefully using the seeded blocklisted test address on the [contract addresses](/arc/references/contract-addresses#test-addresses-for-restricted-transfer-behavior) page. A value transfer to or from it reverts at runtime, including when it is the beneficiary of a `SELFDESTRUCT`. Once these checks pass against Arc Testnet, your contract is ready to deploy. For the full set of protocol-level rules, see [EVM differences](/arc/references/evm-differences). # Register your first AI agent Source: https://docs.arc.io/arc/tutorials/register-your-first-ai-agent Register AI agents with onchain identity, build reputation, and verify credentials using ERC-8004 on Arc Testnet. This quickstart guides you through registering an AI agent using the [ERC-8004](https://eips.ethereum.org/EIPS/eip-8004) standard on Arc Testnet. You'll create developer-controlled wallets, register your agent's identity, record reputation events, and verify credentials. Select the tab that matches your preferred setup. ## ERC-8004 contracts on Arc testnet | Contract | Address | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------ | | IdentityRegistry | [`0x8004A818BFB912233c491871b3d84c89A494BD9e`](https://testnet.arcscan.app/address/0x8004A818BFB912233c491871b3d84c89A494BD9e) | | ReputationRegistry | [`0x8004B663056A597Dffe9eCcC1965A193B7388713`](https://testnet.arcscan.app/address/0x8004B663056A597Dffe9eCcC1965A193B7388713) | | ValidationRegistry | [`0x8004Cb1BF31DAf7788923b405b754f57acEB4272`](https://testnet.arcscan.app/address/0x8004Cb1BF31DAf7788923b405b754f57acEB4272) | ## Prerequisites Before you begin, make sure you have: 1. A [Circle Developer Console](https://console.circle.com) account 2. An API key created in the Console: **Keys → Create a key → API key → Standard Key** 3. Your entity secret generated and registered using the [Circle Console](https://console.circle.com/wallets/dev/configurator/entity-secret) or the [SDK](https://developers.circle.com/wallets/dev-controlled/register-entity-secret#create-an-entity-secret-using-the-sdk) Circle wallets use three credentials. An [**API key**](https://developers.circle.com/api-reference/keys) authenticates your requests to Circle APIs. An [**entity secret**](https://developers.circle.com/wallets/dev-controlled/entity-secret-management) authorizes each wallet operation. A [**wallet set ID**](https://developers.circle.com/wallets/dev-controlled/create-your-first-wallet) groups your wallets. ## Step 1. Set up your project Create a project directory, install dependencies, and configure your environment. ### 1.1. Create the project and install dependencies ```shell Node.js theme={null} mkdir erc8004-quickstart cd erc8004-quickstart npm init -y npm pkg set type=module npm pkg set scripts.start="tsx --env-file=.env index.ts" npm install @circle-fin/developer-controlled-wallets viem npm install --save-dev tsx typescript @types/node ``` ```shell Python theme={null} mkdir erc8004-quickstart cd erc8004-quickstart python3 -m venv .venv source .venv/bin/activate pip install circle-developer-controlled-wallets web3 python-dotenv ``` ### 1.2. Configure TypeScript (optional) This step is optional. It helps prevent missing types in your IDE or editor. 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 Create a `.env` file in the project directory and add your Circle credentials: ```text theme={null} CIRCLE_API_KEY=YOUR_API_KEY CIRCLE_ENTITY_SECRET=YOUR_ENTITY_SECRET ``` Where `YOUR_API_KEY` is your Circle Developer API key and `YOUR_ENTITY_SECRET` is your registered Entity Secret. The `npm run start` command loads these variables from `.env` using Node.js native env-file support. Prefer editing `.env` files in your IDE or editor so credentials are not leaked to your shell history. ## Step 2. Create developer-controlled wallets In this step, you create two Arc Testnet dev-controlled wallets for the ERC-8004 flow. One wallet owns the agent and the other records reputation. If you already have two Arc Testnet dev-controlled wallets for this flow, skip to [Step 3](#step-3-prepare-agent-metadata). Per ERC-8004, agent owners cannot record reputation for their own agents to prevent self-dealing. The Step 2 through 7 code snippets explain the flow in smaller pieces. They are not cumulative and will not run if pasted together. To run the full workflow end to end, use the [complete script](#full-agent-registration-script) at the end of this tutorial. ```typescript index.ts theme={null} import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets"; const circleClient = initiateDeveloperControlledWalletsClient({ apiKey: process.env.CIRCLE_API_KEY!, entitySecret: process.env.CIRCLE_ENTITY_SECRET!, }); const walletSet = await circleClient.createWalletSet({ name: "ERC8004 Agent Wallets", }); const walletsResponse = await circleClient.createWallets({ blockchains: ["ARC-TESTNET"], count: 2, walletSetId: walletSet.data?.walletSet?.id ?? "", accountType: "SCA", }); const ownerWallet = walletsResponse.data?.wallets?.[0]!; const validatorWallet = walletsResponse.data?.wallets?.[1]!; console.log(`Owner: ${ownerWallet.address}`); console.log(`Validator: ${validatorWallet.address}`); ``` ```python index.py theme={null} from circle.web3 import utils, developer_controlled_wallets import os from dotenv import load_dotenv load_dotenv() circle_client = utils.init_developer_controlled_wallets_client( api_key=os.getenv("CIRCLE_API_KEY"), entity_secret=os.getenv("CIRCLE_ENTITY_SECRET"), ) wallet_sets_api = developer_controlled_wallets.WalletSetsApi(circle_client) wallets_api = developer_controlled_wallets.WalletsApi(circle_client) wallet_set = wallet_sets_api.create_wallet_set( developer_controlled_wallets.CreateWalletSetRequest.from_dict({ "name": "ERC8004 Agent Wallets", }) ) wallet_set_id = wallet_set.data.wallet_set.actual_instance.id wallets_response = wallets_api.create_wallet( developer_controlled_wallets.CreateWalletRequest.from_dict({ "blockchains": ["ARC-TESTNET"], "count": 2, "walletSetId": wallet_set_id, "accountType": "SCA", }) ) owner_wallet = wallets_response.data.wallets[0].actual_instance validator_wallet = wallets_response.data.wallets[1].actual_instance print(f"Owner: {owner_wallet.address}") print(f"Validator: {validator_wallet.address}") ``` ## Step 3. Prepare agent metadata Create a JSON file with metadata for your agent. The following structure is an example you can adapt for your use case. ERC-8004 registration stores a metadata URI, but the JSON fields at that URI are application-defined unless your integration follows a separate metadata convention. ```json agent-metadata.json theme={null} { "name": "DeFi Arbitrage Agent v1.0", "description": "Autonomous trading agent for cross-DEX arbitrage on Arc", "image": "ipfs://QmAgentAvatarHash...", "agent_type": "trading", "capabilities": [ "arbitrage_detection", "liquidity_monitoring", "automated_execution" ], "version": "1.0.0" } ``` Upload to IPFS using [Pinata](https://pinata.cloud), [NFT.Storage](https://nft.storage), [Web3.Storage](https://web3.storage) or your preferred IPFS tool. You'll receive an IPFS URI like `ipfs://QmYourHash...`. For this quickstart, you can skip uploading and use the example URI: `ipfs://bafkreibdi6623n3xpf7ymk62ckb4bo75o3qemwkpfvp5i25j66itxvsoei` ## Step 4. Register your agent identity Call `register(metadataURI)` on the IdentityRegistry to mint an identity NFT for your agent. ```typescript index.ts theme={null} const IDENTITY_REGISTRY = "0x8004A818BFB912233c491871b3d84c89A494BD9e"; const METADATA_URI = process.env.METADATA_URI || "ipfs://bafkreibdi6623n3xpf7ymk62ckb4bo75o3qemwkpfvp5i25j66itxvsoei"; const registerTx = await circleClient.createContractExecutionTransaction({ walletAddress: ownerWallet.address!, blockchain: "ARC-TESTNET", contractAddress: IDENTITY_REGISTRY, abiFunctionSignature: "register(string)", abiParameters: [METADATA_URI], fee: { type: "level", config: { feeLevel: "MEDIUM" } }, }); // Poll until confirmed let txHash: string | undefined; for (let i = 0; i < 30; i++) { await new Promise((r) => setTimeout(r, 2000)); const { data } = await circleClient.getTransaction({ id: registerTx.data?.id!, }); if (data?.transaction?.state === "COMPLETE") { txHash = data.transaction.txHash; break; } if (data?.transaction?.state === "FAILED") throw new Error("Registration failed"); } console.log(`Registered: https://testnet.arcscan.app/tx/${txHash}`); ``` ```python index.py theme={null} import time IDENTITY_REGISTRY = "0x8004A818BFB912233c491871b3d84c89A494BD9e" METADATA_URI = os.getenv("METADATA_URI") or \ "ipfs://bafkreibdi6623n3xpf7ymk62ckb4bo75o3qemwkpfvp5i25j66itxvsoei" transactions_api = developer_controlled_wallets.TransactionsApi(circle_client) request = developer_controlled_wallets \ .CreateContractExecutionTransactionForDeveloperRequest.from_dict({ "walletAddress": owner_wallet.address, "blockchain": "ARC-TESTNET", "contractAddress": IDENTITY_REGISTRY, "abiFunctionSignature": "register(string)", "abiParameters": [METADATA_URI], "feeLevel": "MEDIUM", }) response = transactions_api.create_developer_transaction_contract_execution(request) # Poll until confirmed tx_hash = None for _ in range(30): time.sleep(2) tx = transactions_api.get_transaction(id=response.data.id) if tx.data.transaction.state == "COMPLETE": tx_hash = tx.data.transaction.tx_hash break if tx.data.transaction.state == "FAILED": raise Exception("Registration failed") print(f"Registered: https://testnet.arcscan.app/tx/{tx_hash}") ``` With Circle Gas Station, your application sponsors the transaction fees. On Arc, gas is approximately 0.006 `USDC-TESTNET` per transaction. ## Step 5. Retrieve your agent ID Query the `Transfer` event from the IdentityRegistry to find the token ID minted for your agent. ```typescript index.ts theme={null} import { createPublicClient, http, parseAbiItem, getContract } from "viem"; import { arcTestnet } from "viem/chains"; const publicClient = createPublicClient({ chain: arcTestnet, transport: http(), }); const latestBlock = await publicClient.getBlockNumber(); const blockRange = 10000n; // RPC limit: eth_getLogs is often capped at 10,000 blocks const fromBlock = latestBlock > blockRange ? latestBlock - blockRange : 0n; const transferLogs = await publicClient.getLogs({ address: IDENTITY_REGISTRY, event: parseAbiItem( "event Transfer(address indexed from, address indexed to, uint256 indexed tokenId)", ), args: { to: ownerWallet.address as `0x${string}` }, fromBlock, toBlock: latestBlock, }); if (transferLogs.length === 0) { throw new Error("No Transfer events found — registration may have failed"); } const agentId = transferLogs[transferLogs.length - 1].args.tokenId!.toString(); const identityContract = getContract({ address: IDENTITY_REGISTRY, abi: [ { name: "ownerOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }], }, { name: "tokenURI", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "string" }], }, ], client: publicClient, }); const owner = await identityContract.read.ownerOf([BigInt(agentId)]); const tokenURI = await identityContract.read.tokenURI([BigInt(agentId)]); console.log(`Agent ID: ${agentId}`); console.log(`Owner: ${owner}`); console.log(`Metadata: ${tokenURI}`); ``` ```python index.py theme={null} from web3 import Web3 RPC_URL = "https://rpc.testnet.arc.io/" w3 = Web3(Web3.HTTPProvider(RPC_URL)) identity_abi = [ { "anonymous": False, "inputs": [ {"indexed": True, "name": "from", "type": "address"}, {"indexed": True, "name": "to", "type": "address"}, {"indexed": True, "name": "tokenId", "type": "uint256"}, ], "name": "Transfer", "type": "event", }, { "inputs": [{"name": "tokenId", "type": "uint256"}], "name": "ownerOf", "outputs": [{"name": "", "type": "address"}], "stateMutability": "view", "type": "function", }, { "inputs": [{"name": "tokenId", "type": "uint256"}], "name": "tokenURI", "outputs": [{"name": "", "type": "string"}], "stateMutability": "view", "type": "function", }, ] identity_contract = w3.eth.contract(address=IDENTITY_REGISTRY, abi=identity_abi) latest_block = w3.eth.block_number from_block = max(0, latest_block - 10000) events = identity_contract.events.Transfer.create_filter( from_block=from_block, to_block=latest_block, argument_filters={"to": owner_wallet.address}, ).get_all_entries() agent_id = events[-1]["args"]["tokenId"] on_chain_owner = identity_contract.functions.ownerOf(agent_id).call() token_uri = identity_contract.functions.tokenURI(agent_id).call() print(f"Agent ID: {agent_id}") print(f"Owner: {on_chain_owner}") print(f"Metadata: {token_uri}") ``` Your AI agent now has a unique onchain identity. ## Step 6. Record agent reputation Build your agent's reputation by recording feedback. Use the **validator wallet**—per ERC-8004, agent owners cannot record reputation for their own agents. ```typescript index.ts theme={null} import { keccak256, toHex } from "viem"; const REPUTATION_REGISTRY = "0x8004B663056A597Dffe9eCcC1965A193B7388713"; const tag = "successful_trade"; const feedbackHash = keccak256(toHex(tag)); const reputationTx = await circleClient.createContractExecutionTransaction({ walletAddress: validatorWallet.address!, blockchain: "ARC-TESTNET", contractAddress: REPUTATION_REGISTRY, abiFunctionSignature: "giveFeedback(uint256,int128,uint8,string,string,string,string,bytes32)", abiParameters: [agentId, "95", "0", tag, "", "", "", feedbackHash], fee: { type: "level", config: { feeLevel: "MEDIUM" } }, }); // Poll until confirmed (same pattern as Step 4) ``` ```python index.py theme={null} REPUTATION_REGISTRY = "0x8004B663056A597Dffe9eCcC1965A193B7388713" tag = "successful_trade" feedback_hash = "0x" + w3.keccak(text=tag).hex() request = developer_controlled_wallets \ .CreateContractExecutionTransactionForDeveloperRequest.from_dict({ "walletAddress": validator_wallet.address, "blockchain": "ARC-TESTNET", "contractAddress": REPUTATION_REGISTRY, "abiFunctionSignature": "giveFeedback(uint256,int128,uint8,string,string,string,string,bytes32)", "abiParameters": [ str(agent_id), "95", "0", tag, "", "", "", feedback_hash ], "feeLevel": "MEDIUM", }) response = transactions_api.create_developer_transaction_contract_execution(request) # Poll until confirmed (same pattern as Step 4) ``` **Production scoring**: This quickstart hardcodes `score: 95` for demonstration. In production, calculate scores dynamically based on agent behavior. For example, `score = loanRepaidOnTime ? 100 : 20` for lending protocols, or `score = slippagePct < 1 ? 95 : 60` for trading platforms. The ReputationRegistry stores attestations from external observers who witnessed the agent's actions. Your application logic calculates scores based on outcomes, then records them onchain. ## Step 7. Request and verify validation The ERC-8004 ValidationRegistry uses a two-step request/response flow. The **agent owner** requests validation from a validator, then the **validator** submits a response. ```typescript index.ts theme={null} const VALIDATION_REGISTRY = "0x8004Cb1BF31DAf7788923b405b754f57acEB4272"; const requestURI = "ipfs://bafkreiexamplevalidationrequest"; const requestHash = keccak256( toHex(`kyc_verification_request_agent_${agentId}`), ); // Owner requests validation const validationReqTx = await circleClient.createContractExecutionTransaction({ walletAddress: ownerWallet.address!, blockchain: "ARC-TESTNET", contractAddress: VALIDATION_REGISTRY, abiFunctionSignature: "validationRequest(address,uint256,string,bytes32)", abiParameters: [validatorWallet.address!, agentId, requestURI, requestHash], fee: { type: "level", config: { feeLevel: "MEDIUM" } }, }); // Poll until confirmed (same pattern as Step 4) // Validator responds (100 = passed, 0 = failed) const validationResTx = await circleClient.createContractExecutionTransaction({ walletAddress: validatorWallet.address!, blockchain: "ARC-TESTNET", contractAddress: VALIDATION_REGISTRY, abiFunctionSignature: "validationResponse(bytes32,uint8,string,bytes32,string)", abiParameters: [ requestHash, "100", "", "0x" + "0".repeat(64), "kyc_verified", ], fee: { type: "level", config: { feeLevel: "MEDIUM" } }, }); // Poll until confirmed, then verify: const validationContract = getContract({ address: VALIDATION_REGISTRY, abi: [ { name: "getValidationStatus", type: "function", stateMutability: "view", inputs: [{ name: "requestHash", type: "bytes32" }], outputs: [ { name: "validatorAddress", type: "address" }, { name: "agentId", type: "uint256" }, { name: "response", type: "uint8" }, { name: "responseHash", type: "bytes32" }, { name: "tag", type: "string" }, { name: "lastUpdate", type: "uint256" }, ], }, ], client: publicClient, }); type ValidationStatus = readonly [ `0x${string}`, bigint, number, `0x${string}`, string, bigint, ]; const [valAddr, , response, , tag] = (await validationContract.read.getValidationStatus([ requestHash, ])) as ValidationStatus; console.log(`Validator: ${valAddr}`); console.log(`Response: ${response} (100 = passed)`); console.log(`Tag: ${tag}`); ``` ```python index.py theme={null} VALIDATION_REGISTRY = "0x8004Cb1BF31DAf7788923b405b754f57acEB4272" request_uri = "ipfs://bafkreiexamplevalidationrequest" request_hash = "0x" + w3.keccak(text=f"kyc_verification_request_agent_{agent_id}").hex() # Owner requests validation request = developer_controlled_wallets \ .CreateContractExecutionTransactionForDeveloperRequest.from_dict({ "walletAddress": owner_wallet.address, "blockchain": "ARC-TESTNET", "contractAddress": VALIDATION_REGISTRY, "abiFunctionSignature": "validationRequest(address,uint256,string,bytes32)", "abiParameters": [validator_wallet.address, str(agent_id), request_uri, request_hash], "feeLevel": "MEDIUM", }) response = transactions_api.create_developer_transaction_contract_execution(request) # Poll until confirmed (same pattern as Step 4) # Validator responds (100 = passed, 0 = failed) request = developer_controlled_wallets \ .CreateContractExecutionTransactionForDeveloperRequest.from_dict({ "walletAddress": validator_wallet.address, "blockchain": "ARC-TESTNET", "contractAddress": VALIDATION_REGISTRY, "abiFunctionSignature": "validationResponse(bytes32,uint8,string,bytes32,string)", "abiParameters": [request_hash, "100", "", "0x" + "0" * 64, "kyc_verified"], "feeLevel": "MEDIUM", }) response = transactions_api.create_developer_transaction_contract_execution(request) # Poll until confirmed, then verify: validation_abi = [{ "inputs": [{"name": "requestHash", "type": "bytes32"}], "name": "getValidationStatus", "outputs": [ {"name": "validatorAddress", "type": "address"}, {"name": "agentId", "type": "uint256"}, {"name": "response", "type": "uint8"}, {"name": "responseHash", "type": "bytes32"}, {"name": "tag", "type": "string"}, {"name": "lastUpdate", "type": "uint256"}, ], "stateMutability": "view", "type": "function", }] validation_contract = w3.eth.contract(address=VALIDATION_REGISTRY, abi=validation_abi) val_addr, _, val_response, _, val_tag, _ = \ validation_contract.functions.getValidationStatus( bytes.fromhex(request_hash[2:]) ).call() print(f"Validator: {val_addr}") print(f"Response: {val_response} (100 = passed)") print(f"Tag: {val_tag}") ``` ## Full agent registration script The following complete script combines all the preceding steps into a single runnable file. ```typescript index.ts expandable theme={null} import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets"; import { createPublicClient, http, parseAbiItem, getContract, keccak256, toHex, } from "viem"; import { arcTestnet } from "viem/chains"; const IDENTITY_REGISTRY = "0x8004A818BFB912233c491871b3d84c89A494BD9e"; const REPUTATION_REGISTRY = "0x8004B663056A597Dffe9eCcC1965A193B7388713"; const VALIDATION_REGISTRY = "0x8004Cb1BF31DAf7788923b405b754f57acEB4272"; const METADATA_URI = process.env.METADATA_URI || "ipfs://bafkreibdi6623n3xpf7ymk62ckb4bo75o3qemwkpfvp5i25j66itxvsoei"; const circleClient = initiateDeveloperControlledWalletsClient({ apiKey: process.env.CIRCLE_API_KEY!, entitySecret: process.env.CIRCLE_ENTITY_SECRET!, }); const publicClient = createPublicClient({ chain: arcTestnet, transport: http(), }); // Helper functions async function waitForTransaction(txId: string, label: string) { process.stdout.write(` Waiting for ${label}`); for (let i = 0; i < 30; i++) { await new Promise((r) => setTimeout(r, 2000)); const { data } = await circleClient.getTransaction({ id: txId }); if (data?.transaction?.state === "COMPLETE") { const txHash = data.transaction.txHash; console.log(` ✓\n Tx: https://testnet.arcscan.app/tx/${txHash}`); return txHash; } if (data?.transaction?.state === "FAILED") { throw new Error(`${label} failed onchain`); } process.stdout.write("."); } throw new Error(`${label} timed out`); } // Main invocation async function main() { console.log("\n── Step 1: Create wallets ──"); const walletSet = await circleClient.createWalletSet({ name: "ERC8004 Agent Wallets", }); const walletsResponse = await circleClient.createWallets({ blockchains: ["ARC-TESTNET"], count: 2, walletSetId: walletSet.data?.walletSet?.id ?? "", accountType: "SCA", }); const ownerWallet = walletsResponse.data?.wallets?.[0]!; const validatorWallet = walletsResponse.data?.wallets?.[1]!; console.log(` Owner: ${ownerWallet.address} (${ownerWallet.id})`); console.log( ` Validator: ${validatorWallet.address} (${validatorWallet.id})`, ); console.log("\n── Step 2: Register agent identity ──"); console.log(` Metadata URI: ${METADATA_URI}`); const registerTx = await circleClient.createContractExecutionTransaction({ walletAddress: ownerWallet.address!, blockchain: "ARC-TESTNET", contractAddress: IDENTITY_REGISTRY, abiFunctionSignature: "register(string)", abiParameters: [METADATA_URI], fee: { type: "level", config: { feeLevel: "MEDIUM" } }, }); await waitForTransaction(registerTx.data?.id!, "registration"); console.log("\n── Step 3: Retrieve agent ID ──"); const latestBlock = await publicClient.getBlockNumber(); const blockRange = 10000n; // RPC limit: eth_getLogs is often capped at 10,000 blocks const fromBlock = latestBlock > blockRange ? latestBlock - blockRange : 0n; const transferLogs = await publicClient.getLogs({ address: IDENTITY_REGISTRY, event: parseAbiItem( "event Transfer(address indexed from, address indexed to, uint256 indexed tokenId)", ), args: { to: ownerWallet.address as `0x${string}` }, fromBlock, toBlock: latestBlock, }); if (transferLogs.length === 0) { throw new Error("No Transfer events found — registration may have failed"); } const agentId = transferLogs[transferLogs.length - 1].args.tokenId!.toString(); const identityContract = getContract({ address: IDENTITY_REGISTRY, abi: [ { name: "ownerOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }], }, { name: "tokenURI", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "string" }], }, ], client: publicClient, }); const owner = await identityContract.read.ownerOf([BigInt(agentId)]); const tokenURI = await identityContract.read.tokenURI([BigInt(agentId)]); console.log(` Agent ID: ${agentId}`); console.log(` Owner: ${owner}`); console.log(` Metadata URI: ${tokenURI}`); console.log("\n── Step 4: Record reputation ──"); const tag = "successful_trade"; const feedbackHash = keccak256(toHex(tag)); const reputationTx = await circleClient.createContractExecutionTransaction({ walletAddress: validatorWallet.address!, blockchain: "ARC-TESTNET", contractAddress: REPUTATION_REGISTRY, abiFunctionSignature: "giveFeedback(uint256,int128,uint8,string,string,string,string,bytes32)", abiParameters: [agentId, "95", "0", tag, "", "", "", feedbackHash], fee: { type: "level", config: { feeLevel: "MEDIUM" } }, }); await waitForTransaction(reputationTx.data?.id!, "reputation"); console.log("\n── Step 5: Verify reputation ──"); const reputationLogs = await publicClient.getLogs({ address: REPUTATION_REGISTRY, fromBlock: latestBlock - 1000n, toBlock: "latest", }); console.log(` Found ${reputationLogs.length} feedback event(s)`); // Owner requests; validator responds per ERC-8004 console.log("\n── Step 6: Request validation ──"); const requestURI = "ipfs://bafkreiexamplevalidationrequest"; const requestHash = keccak256( toHex(`kyc_verification_request_agent_${agentId}`), ); const validationReqTx = await circleClient.createContractExecutionTransaction( { walletAddress: ownerWallet.address!, blockchain: "ARC-TESTNET", contractAddress: VALIDATION_REGISTRY, abiFunctionSignature: "validationRequest(address,uint256,string,bytes32)", abiParameters: [ validatorWallet.address!, agentId, requestURI, requestHash, ], fee: { type: "level", config: { feeLevel: "MEDIUM" } }, }, ); await waitForTransaction(validationReqTx.data?.id!, "validation request"); // Validator responds; 100 = passed, 0 = failed console.log("\n── Step 7: Validation response ──"); const validationResTx = await circleClient.createContractExecutionTransaction( { walletAddress: validatorWallet.address!, blockchain: "ARC-TESTNET", contractAddress: VALIDATION_REGISTRY, abiFunctionSignature: "validationResponse(bytes32,uint8,string,bytes32,string)", abiParameters: [ requestHash, "100", "", "0x" + "0".repeat(64), "kyc_verified", ], fee: { type: "level", config: { feeLevel: "MEDIUM" } }, }, ); await waitForTransaction(validationResTx.data?.id!, "validation response"); console.log("\n── Step 8: Check validation ──"); const validationContract = getContract({ address: VALIDATION_REGISTRY, abi: [ { name: "getValidationStatus", type: "function", stateMutability: "view", inputs: [{ name: "requestHash", type: "bytes32" }], outputs: [ { name: "validatorAddress", type: "address" }, { name: "agentId", type: "uint256" }, { name: "response", type: "uint8" }, { name: "responseHash", type: "bytes32" }, { name: "tag", type: "string" }, { name: "lastUpdate", type: "uint256" }, ], }, ], client: publicClient, }); type ValidationStatus = readonly [ `0x${string}`, bigint, number, `0x${string}`, string, bigint, ]; const [valAddr, , valResponse, , valTag] = (await validationContract.read.getValidationStatus([ requestHash, ])) as ValidationStatus; console.log(` Validator: ${valAddr}`); console.log(` Response: ${valResponse} (100 = passed)`); console.log(` Tag: ${valTag}`); console.log("\n── Complete ──"); console.log(" ✓ Identity registered"); console.log(" ✓ Reputation recorded"); console.log(" ✓ Validation requested and verified"); console.log( `\n Explorer: https://testnet.arcscan.app/address/${ownerWallet.address}\n`, ); } main().catch((error) => { console.error("\nError:", error.message ?? error); process.exit(1); }); ``` ```python index.py expandable theme={null} from circle.web3 import utils, developer_controlled_wallets from web3 import Web3 import os import time from dotenv import load_dotenv load_dotenv() IDENTITY_REGISTRY = "0x8004A818BFB912233c491871b3d84c89A494BD9e" REPUTATION_REGISTRY = "0x8004B663056A597Dffe9eCcC1965A193B7388713" VALIDATION_REGISTRY = "0x8004Cb1BF31DAf7788923b405b754f57acEB4272" RPC_URL = "https://rpc.testnet.arc.io/" METADATA_URI = os.getenv("METADATA_URI") or \ "ipfs://bafkreibdi6623n3xpf7ymk62ckb4bo75o3qemwkpfvp5i25j66itxvsoei" circle_client = utils.init_developer_controlled_wallets_client( api_key=os.getenv("CIRCLE_API_KEY"), entity_secret=os.getenv("CIRCLE_ENTITY_SECRET"), ) wallet_sets_api = developer_controlled_wallets.WalletSetsApi(circle_client) wallets_api = developer_controlled_wallets.WalletsApi(circle_client) transactions_api = developer_controlled_wallets.TransactionsApi(circle_client) w3 = Web3(Web3.HTTPProvider(RPC_URL)) def wait_for_transaction(tx_id: str, label: str) -> str: print(f" Waiting for {label}", end="", flush=True) for _ in range(30): time.sleep(2) tx = transactions_api.get_transaction(id=tx_id) state = tx.data.transaction.state if state == "COMPLETE": tx_hash = tx.data.transaction.tx_hash print(f" ✓\n Tx: https://testnet.arcscan.app/tx/{tx_hash}") return tx_hash if state == "FAILED": raise Exception(f"{label} failed onchain") print(".", end="", flush=True) raise Exception(f"{label} timed out") def send_contract_tx(wallet_address, address, sig, params, label): request = developer_controlled_wallets \ .CreateContractExecutionTransactionForDeveloperRequest.from_dict({ "walletAddress": wallet_address, "blockchain": "ARC-TESTNET", "contractAddress": address, "abiFunctionSignature": sig, "abiParameters": params, "feeLevel": "MEDIUM", }) response = transactions_api.create_developer_transaction_contract_execution(request) return wait_for_transaction(response.data.id, label) def main(): # Step 1: Create wallets print("\n── Step 1: Create wallets ──") wallet_set = wallet_sets_api.create_wallet_set( developer_controlled_wallets.CreateWalletSetRequest.from_dict({ "name": "ERC8004 Agent Wallets", }) ) wallet_set_id = wallet_set.data.wallet_set.actual_instance.id wallets_response = wallets_api.create_wallet( developer_controlled_wallets.CreateWalletRequest.from_dict({ "blockchains": ["ARC-TESTNET"], "count": 2, "walletSetId": wallet_set_id, "accountType": "SCA", }) ) owner_wallet = wallets_response.data.wallets[0].actual_instance validator_wallet = wallets_response.data.wallets[1].actual_instance print(f" Owner: {owner_wallet.address} ({owner_wallet.id})") print(f" Validator: {validator_wallet.address} ({validator_wallet.id})") # Step 2: Register agent identity print("\n── Step 2: Register agent identity ──") print(f" Metadata URI: {METADATA_URI}") send_contract_tx( owner_wallet.address, IDENTITY_REGISTRY, "register(string)", [METADATA_URI], "registration", ) # Step 3: Retrieve agent ID print("\n── Step 3: Retrieve agent ID ──") identity_abi = [ { "anonymous": False, "inputs": [ {"indexed": True, "name": "from", "type": "address"}, {"indexed": True, "name": "to", "type": "address"}, {"indexed": True, "name": "tokenId", "type": "uint256"}, ], "name": "Transfer", "type": "event", }, { "inputs": [{"name": "tokenId", "type": "uint256"}], "name": "ownerOf", "outputs": [{"name": "", "type": "address"}], "stateMutability": "view", "type": "function", }, { "inputs": [{"name": "tokenId", "type": "uint256"}], "name": "tokenURI", "outputs": [{"name": "", "type": "string"}], "stateMutability": "view", "type": "function", }, ] identity_contract = w3.eth.contract(address=IDENTITY_REGISTRY, abi=identity_abi) latest_block = w3.eth.block_number from_block = max(0, latest_block - 10000) events = identity_contract.events.Transfer.create_filter( from_block=from_block, to_block=latest_block, argument_filters={"to": owner_wallet.address}, ).get_all_entries() if not events: raise Exception("No Transfer events found — registration may have failed") agent_id = events[-1]["args"]["tokenId"] on_chain_owner = identity_contract.functions.ownerOf(agent_id).call() token_uri = identity_contract.functions.tokenURI(agent_id).call() print(f" Agent ID: {agent_id}") print(f" Owner: {on_chain_owner}") print(f" Metadata URI: {token_uri}") # Step 4: Record reputation print("\n── Step 4: Record reputation ──") tag = "successful_trade" feedback_hash = "0x" + w3.keccak(text=tag).hex() send_contract_tx( validator_wallet.address, REPUTATION_REGISTRY, "giveFeedback(uint256,int128,uint8,string,string,string,string,bytes32)", [str(agent_id), "95", "0", tag, "", "", "", feedback_hash], "reputation", ) # Step 5: Verify reputation print("\n── Step 5: Verify reputation ──") reputation_logs = w3.eth.get_logs({ "address": REPUTATION_REGISTRY, "fromBlock": max(0, latest_block - 1000), "toBlock": "latest", }) print(f" Found {len(reputation_logs)} feedback event(s)") # Step 6: Request validation (owner requests; validator responds per ERC-8004) print("\n── Step 6: Request validation ──") request_uri = "ipfs://bafkreiexamplevalidationrequest" request_hash = "0x" + w3.keccak(text=f"kyc_verification_request_agent_{agent_id}").hex() send_contract_tx( owner_wallet.address, VALIDATION_REGISTRY, "validationRequest(address,uint256,string,bytes32)", [validator_wallet.address, str(agent_id), request_uri, request_hash], "validation request", ) # Step 7: Validation response (validator responds; 100 = passed, 0 = failed) print("\n── Step 7: Validation response ──") send_contract_tx( validator_wallet.address, VALIDATION_REGISTRY, "validationResponse(bytes32,uint8,string,bytes32,string)", [request_hash, "100", "", "0x" + "0" * 64, "kyc_verified"], "validation response", ) # Step 8: Check validation status print("\n── Step 8: Check validation ──") validation_abi = [{ "inputs": [{"name": "requestHash", "type": "bytes32"}], "name": "getValidationStatus", "outputs": [ {"name": "validatorAddress", "type": "address"}, {"name": "agentId", "type": "uint256"}, {"name": "response", "type": "uint8"}, {"name": "responseHash", "type": "bytes32"}, {"name": "tag", "type": "string"}, {"name": "lastUpdate", "type": "uint256"}, ], "stateMutability": "view", "type": "function", }] validation_contract = w3.eth.contract( address=VALIDATION_REGISTRY, abi=validation_abi ) val_addr, _, val_response, _, val_tag, _ = \ validation_contract.functions.getValidationStatus( bytes.fromhex(request_hash[2:]) ).call() print(f" Validator: {val_addr}") print(f" Response: {val_response} (100 = passed)") print(f" Tag: {val_tag}") print("\n── Complete ──") print(" ✓ Identity registered") print(" ✓ Reputation recorded") print(" ✓ Validation requested and verified") print(f"\n Explorer: https://testnet.arcscan.app/address/{owner_wallet.address}\n") if __name__ == "__main__": try: main() except Exception as error: print(f"\nError: {error}") exit(1) ``` Save it, then run: ```shell Node.js theme={null} npm run start ``` ```shell Python theme={null} python index.py ``` If you followed the Python workflow, run `deactivate` when you're done to exit the virtual environment. ## Prerequisites Before you begin, make sure you have: 1. Installed [Node.js v22+](https://nodejs.org/) 2. Two self-managed EVM wallets for Arc Testnet * Testnet USDC in both wallets to pay for gas ## Step 1. Set up your project Create a project directory, install dependencies, and configure your environment. ### 1.1. Create the project and install dependencies ```shell theme={null} mkdir erc8004-quickstart cd erc8004-quickstart 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 tsx typescript @types/node ``` ### 1.2. Configure TypeScript (optional) This step is optional. It helps prevent missing types in your IDE or editor. 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 Create a `.env` file in the project directory: ```text .env theme={null} OWNER_PRIVATE_KEY=0xYOUR_OWNER_PRIVATE_KEY VALIDATOR_PRIVATE_KEY=0xYOUR_VALIDATOR_PRIVATE_KEY ``` * `OWNER_PRIVATE_KEY` is the `0x`-prefixed private key for the Arc Testnet wallet that owns the agent and requests validation. * `VALIDATOR_PRIVATE_KEY` is the `0x`-prefixed private key for the Arc Testnet wallet that records reputation and submits the validation response. The `npm run start` command loads variables from `.env` using Node.js native env-file support. Prefer editing `.env` files in your IDE or editor so credentials are not leaked to your shell history. ## Step 2. Prepare your wallets In this step, you prepare two self-managed Arc Testnet wallets for the ERC-8004 flow. One wallet owns the agent and the other records reputation. If you already have two funded Arc Testnet wallets for this flow, skip to [Step 3](#step-3-prepare-agent-metadata-2). Per ERC-8004, agent owners cannot record reputation for their own agents to prevent self-dealing. The Step 2 through 7 code snippets explain the flow in smaller pieces. They are not cumulative and will not run if pasted together. To run the full workflow end to end, use the [complete script](#full-agent-registration-script-2) at the end of this tutorial. ### 2.1. Create or fund your wallets Create two self-managed EVM wallets if you do not already have them. For example, you can generate throwaway wallets with Foundry: ```shell theme={null} cast wallet new --json ``` Run it twice, once for the owner wallet and once for the validator wallet, then fund both wallets with Arc Testnet USDC so they can submit transactions. ### 2.2. Confirm wallet roles * the owner wallet registers the agent identity and requests validation * the validator wallet records reputation and submits the validation response ## Step 3. Prepare agent metadata Create a JSON file with metadata for your agent. The following structure is an example you can adapt for your use case. ERC-8004 registration stores a metadata URI, but the JSON fields at that URI are application-defined unless your integration follows a separate metadata convention. ```json agent-metadata.json theme={null} { "name": "DeFi Arbitrage Agent v1.0", "description": "Autonomous trading agent for cross-DEX arbitrage on Arc", "image": "ipfs://QmAgentAvatarHash...", "agent_type": "trading", "capabilities": [ "arbitrage_detection", "liquidity_monitoring", "automated_execution" ], "version": "1.0.0" } ``` Upload to IPFS using [Pinata](https://pinata.cloud), [NFT.Storage](https://nft.storage), [Web3.Storage](https://web3.storage) or your preferred IPFS tool. You'll receive an IPFS URI like `ipfs://QmYourHash...`. For this quickstart, you can skip uploading and use the example URI: `ipfs://bafkreibdi6623n3xpf7ymk62ckb4bo75o3qemwkpfvp5i25j66itxvsoei` ## Step 4. Register your agent identity Call `register(metadataURI)` on the IdentityRegistry to mint an identity NFT for your agent. ```typescript index.ts theme={null} import { createPublicClient, createWalletClient, http, getContract, } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { arcTestnet } from "viem/chains"; const IDENTITY_REGISTRY = "0x8004A818BFB912233c491871b3d84c89A494BD9e"; const METADATA_URI = process.env.METADATA_URI || "ipfs://bafkreibdi6623n3xpf7ymk62ckb4bo75o3qemwkpfvp5i25j66itxvsoei"; const ownerAccount = privateKeyToAccount( process.env.OWNER_PRIVATE_KEY as `0x${string}`, ); const publicClient = createPublicClient({ chain: arcTestnet, transport: http(), }); const ownerWalletClient = createWalletClient({ account: ownerAccount, chain: arcTestnet, transport: http(), }); const identityContract = getContract({ address: IDENTITY_REGISTRY, abi: [ { name: "register", type: "function", stateMutability: "nonpayable", inputs: [{ name: "metadataURI", type: "string" }], outputs: [], }, ], client: { public: publicClient, wallet: ownerWalletClient }, }); const registerTx = await identityContract.write.register([METADATA_URI], { account: ownerAccount, }); await publicClient.waitForTransactionReceipt({ hash: registerTx }); console.log(`Registered: https://testnet.arcscan.app/tx/${registerTx}`); ``` ## Step 5. Retrieve your agent ID Query the `Transfer` event from the IdentityRegistry to find the token ID minted for your agent. ```typescript index.ts theme={null} import { parseAbiItem } from "viem"; const latestBlock = await publicClient.getBlockNumber(); const blockRange = 10000n; const fromBlock = latestBlock > blockRange ? latestBlock - blockRange : 0n; const transferLogs = await publicClient.getLogs({ address: IDENTITY_REGISTRY, event: parseAbiItem( "event Transfer(address indexed from, address indexed to, uint256 indexed tokenId)", ), args: { to: ownerAccount.address }, fromBlock, toBlock: latestBlock, }); if (transferLogs.length === 0) { throw new Error("No Transfer events found — registration may have failed"); } const agentId = transferLogs[transferLogs.length - 1].args.tokenId!; const identityReadContract = getContract({ address: IDENTITY_REGISTRY, abi: [ { name: "ownerOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }], }, { name: "tokenURI", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "string" }], }, ], client: publicClient, }); const owner = await identityReadContract.read.ownerOf([agentId]); const tokenURI = await identityReadContract.read.tokenURI([agentId]); console.log(`Agent ID: ${agentId}`); console.log(`Owner: ${owner}`); console.log(`Metadata URI: ${tokenURI}`); ``` Your AI agent now has a unique onchain identity. ## Step 6. Record agent reputation Build your agent's reputation by recording feedback. Use the **validator wallet**—per ERC-8004, agent owners cannot record reputation for their own agents. ```typescript index.ts theme={null} import { keccak256, toHex } from "viem"; const REPUTATION_REGISTRY = "0x8004B663056A597Dffe9eCcC1965A193B7388713"; const validatorAccount = privateKeyToAccount( process.env.VALIDATOR_PRIVATE_KEY as `0x${string}`, ); const validatorWalletClient = createWalletClient({ account: validatorAccount, chain: arcTestnet, transport: http(), }); const tag = "successful_trade"; const feedbackHash = keccak256(toHex(tag)); const reputationContract = getContract({ address: REPUTATION_REGISTRY, abi: [ { name: "giveFeedback", type: "function", stateMutability: "nonpayable", inputs: [ { name: "agentId", type: "uint256" }, { name: "score", type: "int128" }, { name: "feedbackType", type: "uint8" }, { name: "tag", type: "string" }, { name: "metadataURI", type: "string" }, { name: "evidenceURI", type: "string" }, { name: "comment", type: "string" }, { name: "feedbackHash", type: "bytes32" }, ], outputs: [], }, ], client: { public: publicClient, wallet: validatorWalletClient }, }); const reputationTx = await reputationContract.write.giveFeedback( [agentId, 95n, 0, tag, "", "", "", feedbackHash], { account: validatorAccount }, ); await publicClient.waitForTransactionReceipt({ hash: reputationTx }); console.log(`Reputation: https://testnet.arcscan.app/tx/${reputationTx}`); ``` **Production scoring**: This quickstart hardcodes `score: 95` for demonstration. In production, calculate scores dynamically based on agent behavior. For example, `score = loanRepaidOnTime ? 100 : 20` for lending protocols, or `score = slippagePct < 1 ? 95 : 60` for trading platforms. The ReputationRegistry stores attestations from external observers who witnessed the agent's actions. Your application logic calculates scores based on outcomes, then records them onchain. ## Step 7. Request and verify validation The ERC-8004 ValidationRegistry uses a two-step request/response flow. The **agent owner** requests validation from a validator, then the **validator** submits a response. ```typescript index.ts theme={null} const VALIDATION_REGISTRY = "0x8004Cb1BF31DAf7788923b405b754f57acEB4272"; const requestURI = "ipfs://bafkreiexamplevalidationrequest"; const requestHash = keccak256( toHex(`kyc_verification_request_agent_${agentId}`), ); const validationContract = getContract({ address: VALIDATION_REGISTRY, abi: [ { name: "validationRequest", type: "function", stateMutability: "nonpayable", inputs: [ { name: "validator", type: "address" }, { name: "agentId", type: "uint256" }, { name: "requestURI", type: "string" }, { name: "requestHash", type: "bytes32" }, ], outputs: [], }, { name: "validationResponse", type: "function", stateMutability: "nonpayable", inputs: [ { name: "requestHash", type: "bytes32" }, { name: "response", type: "uint8" }, { name: "responseURI", type: "string" }, { name: "responseHash", type: "bytes32" }, { name: "tag", type: "string" }, ], outputs: [], }, { name: "getValidationStatus", type: "function", stateMutability: "view", inputs: [{ name: "requestHash", type: "bytes32" }], outputs: [ { name: "validatorAddress", type: "address" }, { name: "agentId", type: "uint256" }, { name: "response", type: "uint8" }, { name: "responseHash", type: "bytes32" }, { name: "tag", type: "string" }, { name: "lastUpdate", type: "uint256" }, ], }, ], client: publicClient, }); const validationRequestTx = await ownerWalletClient.writeContract({ address: VALIDATION_REGISTRY, abi: validationContract.abi, functionName: "validationRequest", args: [validatorAccount.address, agentId, requestURI, requestHash], account: ownerAccount, }); await publicClient.waitForTransactionReceipt({ hash: validationRequestTx }); const validationResponseTx = await validatorWalletClient.writeContract({ address: VALIDATION_REGISTRY, abi: validationContract.abi, functionName: "validationResponse", args: [requestHash, 100, "", `0x${"0".repeat(64)}`, "kyc_verified"], account: validatorAccount, }); await publicClient.waitForTransactionReceipt({ hash: validationResponseTx }); type ValidationStatus = readonly [ `0x${string}`, bigint, number, `0x${string}`, string, bigint, ]; const [valAddr, , response, , tag] = (await validationContract.read.getValidationStatus([ requestHash, ])) as ValidationStatus; console.log(`Validator: ${valAddr}`); console.log(`Response: ${response} (100 = passed)`); console.log(`Tag: ${tag}`); ``` ## Full agent registration script The following complete script combines all the preceding steps into a single runnable file. ```typescript index.ts expandable theme={null} import { createPublicClient, createWalletClient, getContract, http, keccak256, parseAbiItem, toHex, } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { arcTestnet } from "viem/chains"; const IDENTITY_REGISTRY = "0x8004A818BFB912233c491871b3d84c89A494BD9e"; const REPUTATION_REGISTRY = "0x8004B663056A597Dffe9eCcC1965A193B7388713"; const VALIDATION_REGISTRY = "0x8004Cb1BF31DAf7788923b405b754f57acEB4272"; const METADATA_URI = process.env.METADATA_URI || "ipfs://bafkreibdi6623n3xpf7ymk62ckb4bo75o3qemwkpfvp5i25j66itxvsoei"; const ownerAccount = privateKeyToAccount( process.env.OWNER_PRIVATE_KEY as `0x${string}`, ); const validatorAccount = privateKeyToAccount( process.env.VALIDATOR_PRIVATE_KEY as `0x${string}`, ); const publicClient = createPublicClient({ chain: arcTestnet, transport: http(), }); const ownerWalletClient = createWalletClient({ account: ownerAccount, chain: arcTestnet, transport: http(), }); const validatorWalletClient = createWalletClient({ account: validatorAccount, chain: arcTestnet, transport: http(), }); const identityAbi = [ { name: "register", type: "function", stateMutability: "nonpayable", inputs: [{ name: "metadataURI", type: "string" }], outputs: [], }, { name: "ownerOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }], }, { name: "tokenURI", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "string" }], }, ] as const; const reputationAbi = [ { name: "giveFeedback", type: "function", stateMutability: "nonpayable", inputs: [ { name: "agentId", type: "uint256" }, { name: "score", type: "int128" }, { name: "feedbackType", type: "uint8" }, { name: "tag", type: "string" }, { name: "metadataURI", type: "string" }, { name: "evidenceURI", type: "string" }, { name: "comment", type: "string" }, { name: "feedbackHash", type: "bytes32" }, ], outputs: [], }, ] as const; const validationAbi = [ { name: "validationRequest", type: "function", stateMutability: "nonpayable", inputs: [ { name: "validator", type: "address" }, { name: "agentId", type: "uint256" }, { name: "requestURI", type: "string" }, { name: "requestHash", type: "bytes32" }, ], outputs: [], }, { name: "validationResponse", type: "function", stateMutability: "nonpayable", inputs: [ { name: "requestHash", type: "bytes32" }, { name: "response", type: "uint8" }, { name: "responseURI", type: "string" }, { name: "responseHash", type: "bytes32" }, { name: "tag", type: "string" }, ], outputs: [], }, { name: "getValidationStatus", type: "function", stateMutability: "view", inputs: [{ name: "requestHash", type: "bytes32" }], outputs: [ { name: "validatorAddress", type: "address" }, { name: "agentId", type: "uint256" }, { name: "response", type: "uint8" }, { name: "responseHash", type: "bytes32" }, { name: "tag", type: "string" }, { name: "lastUpdate", type: "uint256" }, ], }, ] as const; type ValidationStatus = readonly [ `0x${string}`, bigint, number, `0x${string}`, string, bigint, ]; async function waitForReceipt(hash: `0x${string}`, label: string) { console.log(` Waiting for ${label}: ${hash}`); const receipt = await publicClient.waitForTransactionReceipt({ hash }); console.log(` ${label} confirmed in block ${receipt.blockNumber}`); console.log(` Explorer: https://testnet.arcscan.app/tx/${hash}`); return receipt; } async function main() { console.log("\n── Step 1: Prepare wallets ──"); console.log(` Owner: ${ownerAccount.address}`); console.log(` Validator: ${validatorAccount.address}`); console.log("\n── Step 2: Register agent identity ──"); console.log(` Metadata URI: ${METADATA_URI}`); const registerTx = await ownerWalletClient.writeContract({ address: IDENTITY_REGISTRY, abi: identityAbi, functionName: "register", args: [METADATA_URI], account: ownerAccount, }); const receipt = await waitForReceipt(registerTx, "Registration"); console.log("\n── Step 3: Retrieve agent ID ──"); const transferLogs = await publicClient.getLogs({ address: IDENTITY_REGISTRY, event: parseAbiItem( "event Transfer(address indexed from, address indexed to, uint256 indexed tokenId)", ), args: { to: ownerAccount.address }, fromBlock: receipt.blockNumber, toBlock: receipt.blockNumber, }); if (transferLogs.length === 0) { throw new Error("No Transfer events found in the registration block"); } const agentId = transferLogs[transferLogs.length - 1].args.tokenId; if (agentId == null) { throw new Error("Registration event did not include a tokenId"); } const identityContract = getContract({ address: IDENTITY_REGISTRY, abi: identityAbi, client: publicClient, }); const owner = await identityContract.read.ownerOf([agentId]); const tokenURI = await identityContract.read.tokenURI([agentId]); console.log(` Agent ID: ${agentId}`); console.log(` Owner: ${owner}`); console.log(` Metadata URI: ${tokenURI}`); console.log("\n── Step 4: Record reputation ──"); const tag = "successful_trade"; const feedbackHash = keccak256(toHex(tag)); const reputationContract = getContract({ address: REPUTATION_REGISTRY, abi: reputationAbi, client: { public: publicClient, wallet: validatorWalletClient }, }); const reputationTx = await reputationContract.write.giveFeedback( [agentId, 95n, 0, tag, "", "", "", feedbackHash], { account: validatorAccount }, ); const reputationReceipt = await waitForReceipt(reputationTx, "Reputation"); console.log("\n── Step 5: Verify reputation ──"); const fromBlock = reputationReceipt.blockNumber > 1000n ? reputationReceipt.blockNumber - 1000n : 0n; const reputationLogs = await publicClient.getLogs({ address: REPUTATION_REGISTRY, fromBlock, toBlock: "latest", }); console.log(` Found ${reputationLogs.length} feedback event(s)`); console.log("\n── Step 6: Request validation ──"); const requestURI = "ipfs://bafkreiexamplevalidationrequest"; const requestHash = keccak256( toHex(`kyc_verification_request_agent_${agentId}`), ); const validationRequestContract = getContract({ address: VALIDATION_REGISTRY, abi: validationAbi, client: { public: publicClient, wallet: ownerWalletClient }, }); const validationRequestTx = await validationRequestContract.write.validationRequest( [validatorAccount.address, agentId, requestURI, requestHash], { account: ownerAccount }, ); await waitForReceipt(validationRequestTx, "Validation request"); console.log("\n── Step 7: Validation response ──"); const validationResponseContract = getContract({ address: VALIDATION_REGISTRY, abi: validationAbi, client: { public: publicClient, wallet: validatorWalletClient }, }); const validationResponseTx = await validationResponseContract.write.validationResponse( [ requestHash, 100, "", `0x${"0".repeat(64)}` as `0x${string}`, "kyc_verified", ], { account: validatorAccount }, ); await waitForReceipt(validationResponseTx, "Validation response"); console.log("\n── Step 8: Check validation ──"); const validationReadContract = getContract({ address: VALIDATION_REGISTRY, abi: validationAbi, client: publicClient, }); const [valAddr, , response, , validationTag] = (await validationReadContract.read.getValidationStatus([ requestHash, ])) as ValidationStatus; console.log(` Validator: ${valAddr}`); console.log(` Response: ${response} (100 = passed)`); console.log(` Tag: ${validationTag}`); console.log("\n── Complete ──"); console.log(" ✓ Identity registered"); console.log(" ✓ Reputation recorded"); console.log(" ✓ Validation requested and verified"); } main().catch((error) => { console.error("\nError:", error.message ?? error); process.exit(1); }); ``` Save it, then run: ```shell theme={null} npm run start ``` ## Summary After completing this quickstart, you've successfully: * Created or prepared two Arc Testnet wallets for the ERC-8004 flow * Registered an AI agent with a unique onchain identity (ERC-721 token) * Recorded reputation feedback from an external validator * Requested validation from a validator and verified the response onchain # Run an Arc node Source: https://docs.arc.io/arc/tutorials/run-an-arc-node Set up and run an Arc node to independently verify the blockchain and serve a local RPC endpoint. Choose between pre-built binaries, building from source, or Docker Compose. **v0.8.0 upgrade required.** Testnet operators must upgrade before timestamp `1788447600` (2026-09-03 15:00:00 UTC), when Zero8 activates. Mainnet operators must upgrade before timestamp `1789052400` (2026-09-10 15:00:00 UTC), when Zero7/Zero8 activate. Earlier versions are not supported after those timestamps. This release includes breaking changes. See [CHANGELOG.md](https://github.com/circlefin/arc-node/blob/main/CHANGELOG.md#v080) and [BREAKING\_CHANGES.md](https://github.com/circlefin/arc-node/blob/main/BREAKING_CHANGES.md#v080) for migration details before upgrading. An Arc node syncs blocks from the network and serves a local JSON-RPC endpoint so you can independently verify every block and transaction. It runs two processes: the Execution Layer (EL), which executes transactions and maintains state, and the Consensus Layer (CL), which fetches finalized blocks from relay endpoints (trusted HTTP servers that distribute signed block data), verifies their signatures, and passes them to the EL. For a deeper explanation of how these components fit together, see [Running a node](/arc/concepts/running-a-node). Arc is currently in its testnet phase. During this period, the network may experience instability or unplanned downtime. Throughout this page, all references to Arc refer specifically to the Arc Testnet. Choose the install path that fits your environment. Install pre-built Arc binaries using the `arcup` script. This is the fastest path and requires no Rust toolchain or build dependencies. ## Prerequisites Before you begin, ensure that you've: * Reviewed the [node requirements](/arc/references/node-requirements) for hardware, software, and network endpoints * Prepared a Linux or macOS machine that meets the minimum system requirements * Installed [Foundry](https://book.getfoundry.sh/getting-started/installation) (optional, provides the `cast` command used to verify your node) ## Step 1: Install with `arcup` `arcup` installs the Arc binaries under `$ARC_HOME` (default `~/.arc`). Export `ARC_HOME` first if you want to install somewhere else; otherwise the default applies for both the installer and the environment file: ```shell theme={null} export ARC_HOME="${ARC_HOME:-$HOME/.arc}" curl -L https://raw.githubusercontent.com/circlefin/arc-node/main/arcup/install | bash ``` The installer places `arc-node-execution`, `arc-node-consensus`, `arc-snapshots`, and `arcup` itself into `$ARC_HOME/bin`. Load the produced environment file so the binaries are on your `PATH`: ```shell theme={null} source "$ARC_HOME/env" ``` Verify the installation: ```shell theme={null} arc-snapshots --version arc-node-execution --version arc-node-consensus --version ``` Each command prints a version string. To update the binaries later, run `arcup`. ## Step 2: Set environment variables The remaining steps use a small set of variables to keep paths consistent. Write them to a file and source it: ```shell theme={null} cat << "EOF" > ~/.arc_env ARC_HOME="${ARC_HOME:-$HOME/.arc}" # Linux runtime directory: ARC_RUN="/run/arc" # macOS runtime directory (uncomment if on macOS): #ARC_RUN="$ARC_HOME/run" ARC_EXECUTION=$ARC_HOME/execution ARC_CONSENSUS=$ARC_HOME/consensus EOF source ~/.arc_env ``` ## Step 3: Create data directories Create the data directories for the EL and CL, plus the runtime directory used for IPC sockets: ```shell theme={null} mkdir -p "$ARC_EXECUTION" "$ARC_CONSENSUS" sudo install -d -o $USER "$ARC_RUN" ``` On macOS, replace the second command with `mkdir -p "$ARC_RUN"` after uncommenting the macOS line in `~/.arc_env`. When running as a systemd service, the `RuntimeDirectory=arc` directive creates `/run/arc` automatically. You can skip the second command in that case. ## Step 4: Download blockchain snapshots Download snapshots so the node starts syncing from a recent block height rather than from genesis: ```shell theme={null} arc-snapshots download \ --chain=arc-testnet \ --execution-path "$ARC_EXECUTION" \ --consensus-path "$ARC_CONSENSUS" ``` This command fetches the latest snapshot URLs from `https://snapshots.arc.network`, downloads the snapshots, and extracts them into `$ARC_EXECUTION` and `$ARC_CONSENSUS`. Snapshots are large. Testnet snapshots are approximately **68 GB EL + 16 GB CL compressed** (around 103 GB and 36 GB extracted). The download takes 10–15 minutes on a stable 100 Mbps connection; slower or metered connections can take hours. Ensure you have at least 150 GB of free disk space, a stable network connection, and available CPU. The terminal stops producing output during extraction. This is expected. Starting with v0.8.0, Circle provides snapshots in Reth V2 storage format. A fresh install from these snapshots does not require the V1→V2 data migration described in the [Migrate to V2 storage](#migrate-to-v2-storage) section. ## Step 5: Initialize the consensus layer Generate the CL private key file used for network identity. This is a one-time setup step: ```shell theme={null} arc-node-consensus init --home "$ARC_CONSENSUS" ``` ## Step 6: Start the execution layer Start the Execution Layer. This creates the IPC sockets that the Consensus Layer connects to. ```shell theme={null} arc-node-execution node \ --chain arc-testnet \ --datadir "$ARC_EXECUTION" \ --full \ --disable-discovery \ --ipcpath "$ARC_RUN/reth.ipc" \ --auth-ipc \ --auth-ipc.path "$ARC_RUN/auth.ipc" \ --http \ --http.addr 127.0.0.1 \ --http.port 8545 \ --http.api eth,net,web3,txpool,trace,debug \ --metrics 127.0.0.1:9001 \ --enable-arc-rpc \ --rpc.forwarder https://rpc.testnet.arc.io/ ``` The EL starts and waits for blocks. Log output shows that the IPC sockets exist at `$ARC_RUN/reth.ipc` and `$ARC_RUN/auth.ipc`. | Flag | Required | Description | | :--------------------------------------- | :---------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--chain arc-testnet` | Yes | Selects the Arc Testnet genesis configuration bundled in the binary | | `--datadir` | Yes | Path to the EL data directory | | `--full` | First start | Required on the first start from a pruned snapshot; optional after | | `--disable-discovery` | Yes | Disables P2P peer discovery (Arc uses relay endpoints instead) | | `--ipcpath` / `--auth-ipc` | Yes (IPC mode) | Paths for the IPC sockets the CL connects to | | `--http` / `--http.addr` | Yes | Enables the JSON-RPC endpoint on the specified address and port | | `--http.api` | Recommended | RPC namespaces to expose | | `--metrics` | Recommended | Enables Prometheus metrics on the specified address and port | | `--enable-arc-rpc` | Yes | Enables Arc-specific RPC methods | | `--rpc.forwarder` | Recommended | Routes submitted transactions to an RPC node for broadcast | | `--public-api` | Recommended for public-facing nodes | Hides pending-tx RPCs (a potential MEV vector) and warns if `--http.api` or `--ws.api` exposes namespaces beyond `eth`, `net`, `web3`, `rpc` | | `--rpc.max-connections` | Recommended for public-facing nodes | Maximum concurrent RPC connections. Default `250` (lowered from `500` in v0.7.1). Raise only if clients see `MaxConnections` errors | | `--rpc.max-subscriptions-per-connection` | Recommended for public-facing nodes | Maximum subscriptions per single WebSocket connection. Default `32` (lowered from `1024` in v0.7.1). Raise only if clients see `TooManySubscriptions` errors | On high-traffic public endpoints, raise `--rpc.max-connections` (default 250) and `--rpc.max-subscriptions-per-connection` (default 32) if clients see `MaxConnections` or `TooManySubscriptions` errors. The defaults bound WebSocket log-fanout memory growth and should only be raised, not lowered. The `--full` flag is required on the first start when bootstrapping from a pruned snapshot. It reconciles internal database tables that would otherwise fail a consistency check. After the initial startup completes, restart without `--full` if you prefer not to prune. When `--full` is used, EL pruning runs on a 128-block interval (changed from 5000 in v0.7.3); pass `--prune.block-interval=5000` explicitly to retain the previous schedule. `--chain arc-testnet` uses the bundled genesis configuration; replace with `--chain /path/to/genesis.json` if you have a custom genesis file. See [reth node](https://reth.rs/cli/reth/node/) for additional flags. ## Step 7: Start the consensus layer Open a separate terminal and start the Consensus Layer. The CL connects to the EL through the IPC sockets and begins fetching blocks from the network. ```shell theme={null} arc-node-consensus start \ --home "$ARC_CONSENSUS" \ --full \ --eth-socket "$ARC_RUN/reth.ipc" \ --execution-socket "$ARC_RUN/auth.ipc" \ --rpc.addr 127.0.0.1:31000 \ --follow \ --follow.endpoint https://rpc.testnet.arc.io,wss=rpc.testnet.arc.io \ --follow.endpoint https://rpc.drpc.testnet.arc.io,wss=rpc.drpc.testnet.arc.io \ --follow.endpoint https://rpc.blockdaemon.testnet.arc.io,wss=rpc.blockdaemon.testnet.arc.io/websocket \ --execution-persistence-backpressure \ --execution-persistence-backpressure-threshold=16 \ --metrics 127.0.0.1:29000 ``` | Flag | Required | Description | | :----------------------------------------------- | :------------- | :--------------------------------------------------------------------------------------------- | | `--home` | Yes | Path to the CL data directory (contains keys and state) | | `--full` | Recommended | Enables CL pruning to bound disk growth | | `--eth-socket` | Yes (IPC mode) | Path to the Execution Layer ETH RPC IPC socket | | `--execution-socket` | Yes (IPC mode) | Path to the Execution Layer Engine API IPC socket | | `--rpc.addr` | Recommended | CL RPC listen address and port | | `--follow` | Yes | Enables block-following mode through relay endpoints | | `--follow.endpoint` | Yes | Relay endpoint URLs. Specify multiple for redundancy. Format: `https://host,wss=host` | | `--execution-persistence-backpressure` | Recommended | Throttles execution to match disk-write speed, bounding EL memory growth | | `--execution-persistence-backpressure-threshold` | Recommended | Threshold for backpressure. Default `16` (must be `> 0`). Lower if you observe memory pressure | | `--metrics` | Recommended | Enables Prometheus metrics on the specified address and port | ## Step 8: Verify the node is syncing After both layers start, wait roughly 30 seconds, then query the local JSON-RPC endpoint for the latest block height: ```shell theme={null} curl -s -X POST http://localhost:8545 \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 1 }' ``` The `result` field is a hexadecimal block number that increases over time. If it stays at `0x0`, check the CL logs for errors. If you have [Foundry](https://book.getfoundry.sh/getting-started/installation) installed, you can run the same check with `cast`: ```shell theme={null} cast block-number --rpc-url http://localhost:8545 ``` Run it several times over a few seconds. The block number increases as the node syncs. If the block number stays at `0x0` or the node does not start as expected, check these common causes: * **IPC socket files missing.** The EL writes sockets at `$ARC_RUN/reth.ipc` and `$ARC_RUN/auth.ipc` on startup. If they are missing after 30 seconds, the EL did not start. Review its terminal output for a panic or configuration error. * **CL cannot connect to the EL.** Start the EL first. If the CL was launched before the EL was ready, restart the CL. * **Snapshot extraction incomplete.** `arc-snapshots download` runs silently during extraction. If it was interrupted, `$ARC_EXECUTION` or `$ARC_CONSENSUS` may be partially populated. Re-run the download to overwrite the existing files. If you clear the directories first, note that `$ARC_CONSENSUS` also holds the CL private key written by `arc-node-consensus init`. Re-run that command after clearing, or your node loses its network identity. * **Path mismatch between EL and CL.** `$ARC_RUN` must resolve to the same directory in both shells. Re-source `~/.arc_env` in any terminal that lost its environment. Once your node is syncing steadily, you can [deploy it as a systemd service](/arc/tutorials/deploy-node-as-service) for production use, or [set up Prometheus and Grafana](/arc/tutorials/set-up-node-monitoring) to view the pre-built dashboards. Build Arc binaries from the [`arc-node`](https://github.com/circlefin/arc-node) repository. Use this path to audit the build, run on an unsupported platform, or pin to a specific commit. ## Prerequisites Before you begin, ensure that you've: * Reviewed the [node requirements](/arc/references/node-requirements) for hardware, software, and network endpoints * Prepared a Linux, macOS, or Windows machine that meets the minimum system requirements * Installed [Foundry](https://book.getfoundry.sh/getting-started/installation) (optional, provides the `cast` command used to verify your node) ## Step 1: Install build dependencies Install [Rust](https://rust-lang.org/tools/install/) if you don't have it: ```shell theme={null} curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source ~/.cargo/env ``` Install the platform packages required to build the EL database bindings: * **Ubuntu:** `sudo apt-get install libclang-dev pkg-config build-essential` * **macOS:** `brew install llvm pkg-config`, then `export LIBCLANG_PATH="$(brew --prefix llvm)/lib"` * **Windows:** `choco install llvm` or `winget install LLVM.LLVM` ## Step 2: Clone the repository Clone the [`arc-node`](https://github.com/circlefin/arc-node) repository and check out the version for your target network. See [node requirements](/arc/references/node-requirements#versions) for the current version: ```shell theme={null} git clone https://github.com/circlefin/arc-node.git cd arc-node git checkout v0.8.0 git submodule update --init --recursive ``` ## Step 3: Build and install The following commands build three Arc node binaries: `arc-node-execution`, `arc-node-consensus`, and `arc-snapshots`: ```shell theme={null} cargo install --path crates/node cargo install --path crates/malachite-app cargo install --path crates/snapshots ``` `cargo install` places compiled binaries into `~/.cargo/bin`, which is added to `PATH` by loading `~/.cargo/env`. To install elsewhere, add `--root $BASE_DIR` to each command (for instance, `--root /usr/local` places them under `/usr/local/bin`). Verify the installation: ```shell theme={null} arc-snapshots --version arc-node-execution --version arc-node-consensus --version ``` ## Step 4: Set environment variables The remaining steps use a small set of variables to keep paths consistent. Write them to a file and source it: ```shell theme={null} cat << "EOF" > ~/.arc_env ARC_HOME="${ARC_HOME:-$HOME/.arc}" # Linux runtime directory: ARC_RUN="/run/arc" # macOS runtime directory (uncomment if on macOS): #ARC_RUN="$ARC_HOME/run" ARC_EXECUTION=$ARC_HOME/execution ARC_CONSENSUS=$ARC_HOME/consensus EOF source ~/.arc_env ``` ## Step 5: Create data directories Create the data directories for the EL and CL, plus the runtime directory used for IPC sockets: ```shell theme={null} mkdir -p "$ARC_EXECUTION" "$ARC_CONSENSUS" sudo install -d -o $USER "$ARC_RUN" ``` On macOS, replace the second command with `mkdir -p "$ARC_RUN"` after uncommenting the macOS line in `~/.arc_env`. When running as a systemd service, the `RuntimeDirectory=arc` directive creates `/run/arc` automatically. You can skip the second command in that case. ## Step 6: Download blockchain snapshots Download snapshots so the node starts syncing from a recent block height rather than from genesis: ```shell theme={null} arc-snapshots download \ --chain=arc-testnet \ --execution-path "$ARC_EXECUTION" \ --consensus-path "$ARC_CONSENSUS" ``` This command fetches the latest snapshot URLs from `https://snapshots.arc.network`, downloads the snapshots, and extracts them into `$ARC_EXECUTION` and `$ARC_CONSENSUS`. Snapshots are large. Testnet snapshots are approximately **68 GB EL + 16 GB CL compressed** (around 103 GB and 36 GB extracted). The download takes 10–15 minutes on a stable 100 Mbps connection; slower or metered connections can take hours. Ensure you have at least 150 GB of free disk space, a stable network connection, and available CPU. The terminal stops producing output during extraction. This is expected. Starting with v0.8.0, Circle provides snapshots in Reth V2 storage format. A fresh install from these snapshots does not require the V1→V2 data migration described in the [Migrate to V2 storage](#migrate-to-v2-storage) section. ## Step 7: Initialize the consensus layer Generate the CL private key file used for network identity. This is a one-time setup step: ```shell theme={null} arc-node-consensus init --home "$ARC_CONSENSUS" ``` ## Step 8: Start the execution layer Start the Execution Layer. This creates the IPC sockets that the Consensus Layer connects to. ```shell theme={null} arc-node-execution node \ --chain arc-testnet \ --datadir "$ARC_EXECUTION" \ --full \ --disable-discovery \ --ipcpath "$ARC_RUN/reth.ipc" \ --auth-ipc \ --auth-ipc.path "$ARC_RUN/auth.ipc" \ --http \ --http.addr 127.0.0.1 \ --http.port 8545 \ --http.api eth,net,web3,txpool,trace,debug \ --metrics 127.0.0.1:9001 \ --enable-arc-rpc \ --rpc.forwarder https://rpc.testnet.arc.io/ ``` The EL starts and waits for blocks. Log output shows that the IPC sockets exist at `$ARC_RUN/reth.ipc` and `$ARC_RUN/auth.ipc`. | Flag | Required | Description | | :--------------------------------------- | :---------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--chain arc-testnet` | Yes | Selects the Arc Testnet genesis configuration bundled in the binary | | `--datadir` | Yes | Path to the EL data directory | | `--full` | First start | Required on the first start from a pruned snapshot; optional after | | `--disable-discovery` | Yes | Disables P2P peer discovery (Arc uses relay endpoints instead) | | `--ipcpath` / `--auth-ipc` | Yes (IPC mode) | Paths for the IPC sockets the CL connects to | | `--http` / `--http.addr` | Yes | Enables the JSON-RPC endpoint on the specified address and port | | `--http.api` | Recommended | RPC namespaces to expose | | `--metrics` | Recommended | Enables Prometheus metrics on the specified address and port | | `--enable-arc-rpc` | Yes | Enables Arc-specific RPC methods | | `--rpc.forwarder` | Recommended | Routes submitted transactions to an RPC node for broadcast | | `--public-api` | Recommended for public-facing nodes | Hides pending-tx RPCs (a potential MEV vector) and warns if `--http.api` or `--ws.api` exposes namespaces beyond `eth`, `net`, `web3`, `rpc` | | `--rpc.max-connections` | Recommended for public-facing nodes | Maximum concurrent RPC connections. Default `250` (lowered from `500` in v0.7.1). Raise only if clients see `MaxConnections` errors | | `--rpc.max-subscriptions-per-connection` | Recommended for public-facing nodes | Maximum subscriptions per single WebSocket connection. Default `32` (lowered from `1024` in v0.7.1). Raise only if clients see `TooManySubscriptions` errors | On high-traffic public endpoints, raise `--rpc.max-connections` (default 250) and `--rpc.max-subscriptions-per-connection` (default 32) if clients see `MaxConnections` or `TooManySubscriptions` errors. The defaults bound WebSocket log-fanout memory growth and should only be raised, not lowered. The `--full` flag is required on the first start when bootstrapping from a pruned snapshot. It reconciles internal database tables that would otherwise fail a consistency check. After the initial startup completes, restart without `--full` if you prefer not to prune. When `--full` is used, EL pruning runs on a 128-block interval (changed from 5000 in v0.7.3); pass `--prune.block-interval=5000` explicitly to retain the previous schedule. `--chain arc-testnet` uses the bundled genesis configuration; replace with `--chain /path/to/genesis.json` if you have a custom genesis file. See [reth node](https://reth.rs/cli/reth/node/) for additional flags. ## Step 9: Start the consensus layer Open a separate terminal and start the Consensus Layer. The CL connects to the EL through the IPC sockets and begins fetching blocks from the network. ```shell theme={null} arc-node-consensus start \ --home "$ARC_CONSENSUS" \ --full \ --eth-socket "$ARC_RUN/reth.ipc" \ --execution-socket "$ARC_RUN/auth.ipc" \ --rpc.addr 127.0.0.1:31000 \ --follow \ --follow.endpoint https://rpc.testnet.arc.io,wss=rpc.testnet.arc.io \ --follow.endpoint https://rpc.drpc.testnet.arc.io,wss=rpc.drpc.testnet.arc.io \ --follow.endpoint https://rpc.blockdaemon.testnet.arc.io,wss=rpc.blockdaemon.testnet.arc.io/websocket \ --execution-persistence-backpressure \ --execution-persistence-backpressure-threshold=16 \ --metrics 127.0.0.1:29000 ``` | Flag | Required | Description | | :----------------------------------------------- | :------------- | :--------------------------------------------------------------------------------------------- | | `--home` | Yes | Path to the CL data directory (contains keys and state) | | `--full` | Recommended | Enables CL pruning to bound disk growth | | `--eth-socket` | Yes (IPC mode) | Path to the Execution Layer ETH RPC IPC socket | | `--execution-socket` | Yes (IPC mode) | Path to the Execution Layer Engine API IPC socket | | `--rpc.addr` | Recommended | CL RPC listen address and port | | `--follow` | Yes | Enables block-following mode through relay endpoints | | `--follow.endpoint` | Yes | Relay endpoint URLs. Specify multiple for redundancy. Format: `https://host,wss=host` | | `--execution-persistence-backpressure` | Recommended | Throttles execution to match disk-write speed, bounding EL memory growth | | `--execution-persistence-backpressure-threshold` | Recommended | Threshold for backpressure. Default `16` (must be `> 0`). Lower if you observe memory pressure | | `--metrics` | Recommended | Enables Prometheus metrics on the specified address and port | ## Step 10: Verify the node is syncing After both layers start, wait roughly 30 seconds, then query the local JSON-RPC endpoint for the latest block height: ```shell theme={null} curl -s -X POST http://localhost:8545 \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 1 }' ``` The `result` field is a hexadecimal block number that increases over time. If it stays at `0x0`, check the CL logs for errors. If you have [Foundry](https://book.getfoundry.sh/getting-started/installation) installed, you can run the same check with `cast`: ```shell theme={null} cast block-number --rpc-url http://localhost:8545 ``` Run it several times over a few seconds. The block number increases as the node syncs. If the block number stays at `0x0` or the node does not start as expected, check these common causes: * **IPC socket files missing.** The EL writes sockets at `$ARC_RUN/reth.ipc` and `$ARC_RUN/auth.ipc` on startup. If they are missing after 30 seconds, the EL did not start. Review its terminal output for a panic or configuration error. * **CL cannot connect to the EL.** Start the EL first. If the CL was launched before the EL was ready, restart the CL. * **Snapshot extraction incomplete.** `arc-snapshots download` runs silently during extraction. If it was interrupted, `$ARC_EXECUTION` or `$ARC_CONSENSUS` may be partially populated. Re-run the download to overwrite the existing files. If you clear the directories first, note that `$ARC_CONSENSUS` also holds the CL private key written by `arc-node-consensus init`. Re-run that command after clearing, or your node loses its network identity. * **Path mismatch between EL and CL.** `$ARC_RUN` must resolve to the same directory in both shells. Re-source `~/.arc_env` in any terminal that lost its environment. Once your node is syncing steadily, you can [deploy it as a systemd service](/arc/tutorials/deploy-node-as-service) for production use, or [set up Prometheus and Grafana](/arc/tutorials/set-up-node-monitoring) to view the pre-built dashboards. Run the Arc node from pre-built Docker images using Docker Compose. Init containers handle snapshot download and CL initialization on the first run. ## Prerequisites Before you begin, ensure that you've: * Reviewed the [node requirements](/arc/references/node-requirements) for hardware, software, and network endpoints * Prepared a Linux machine that meets the minimum system requirements * Installed [Docker Engine 24+ with BuildKit](https://docs.docker.com/engine/install/) and [Docker Compose v2](https://docs.docker.com/compose/install/) ## Step 1: Set environment variables The Compose file reads images from environment variables. Set the version, data directory, and image references. See [node requirements](/arc/references/node-requirements#versions) for the current version: ```shell theme={null} export ARC_VERSION=0.8.0 export ARC_HOME=~/.arc export ARC_EXECUTION_IMAGE=docker.cloudsmith.io/circle/arc-network/arc-execution:$ARC_VERSION export ARC_CONSENSUS_IMAGE=docker.cloudsmith.io/circle/arc-network/arc-consensus:$ARC_VERSION ``` To build images locally instead of pulling from Cloudsmith, see [Installation: Docker](https://github.com/circlefin/arc-node/blob/main/docs/installation.md#docker) in the `arc-node` repository. ## Step 2: Prepare the data directory Create `$ARC_HOME` on the host before starting Docker Compose. If you skip this, Docker creates it as `root` and the snapshot init container fails with permission errors: ```shell theme={null} mkdir -p "${ARC_HOME:-$HOME/.arc}" ``` ## Step 3: Download the Compose file Fetch the `docker-compose.yml` for your release into a working directory: ```shell theme={null} curl -O https://raw.githubusercontent.com/circlefin/arc-node/v${ARC_VERSION}/deployments/docker-compose.yml ``` ## Step 4: Start the node From the directory containing `docker-compose.yml`: ```shell theme={null} docker compose up -d ``` On the first run, init containers automatically: 1. Download the latest testnet snapshots (approximately 84 GB compressed; see [snapshot download size](/arc/references/node-requirements#snapshot-download-size)) 2. Initialize the consensus layer private key 3. Prepare the shared IPC socket volume Subsequent runs detect that initialization is already complete and start immediately. The init container runs as `root` so it can set file ownership for the EL and CL services (UID 999). No manual `chown` is needed. ## Step 5: Verify the node is syncing On the first run, wait for the snapshot init container to finish: ```shell theme={null} docker compose logs -f arc-snapshots ``` Once the EL and CL containers are up, wait roughly 30 seconds, then query the latest block height: ```shell theme={null} curl -s -X POST http://localhost:8545 \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 1 }' ``` The `result` field is a hexadecimal block number that increases over time. If it stays at `0x0`, check the logs: ```shell theme={null} docker compose logs -f ``` The containers expose Prometheus metrics on the host at `localhost:9001/` (Execution Layer, root path) and `localhost:29000/metrics` (Consensus Layer). To scrape these endpoints with a local stack, see [Set up Prometheus and Grafana for an Arc node](/arc/tutorials/set-up-node-monitoring). ## Step 6: Stop and reset To stop the node while preserving data: ```shell theme={null} docker compose down ``` Node data persists in `~/.arc/` (or the path set by `$ARC_HOME`). To remove all data and start fresh: ```shell theme={null} docker compose down -v rm -rf ~/.arc ``` `docker compose down -v` removes the named sockets volume, and `rm -rf ~/.arc` permanently deletes the consensus layer private key, which is your node's network identity and can't be recovered. ## Run on separate hosts The preceding tabs describe running the Execution Layer and Consensus Layer on the same host, communicating through IPC sockets. To run them on separate hosts, swap IPC for authenticated RPC. This section applies to the binary install paths (`arcup` and source build); the Docker path is single-host by default. The RPC/HTTP transport between the Consensus Layer and Execution Layer is deprecated in v0.8.0 and will be removed in v0.9.0. IPC is the recommended transport for single-host deployments. Multi-host deployments should plan to migrate before v0.9.0. ### Generate a JWT secret The EL and CL authenticate to each other with a shared JWT secret. Generate it once, then securely copy it to both hosts: ```shell theme={null} openssl rand -hex 32 | tr -d "\n" > "$ARC_HOME/jwtsecret" chmod 600 "$ARC_HOME/jwtsecret" ``` ### Execution layer flags for RPC mode Remove the IPC flags (`--ipcpath`, `--auth-ipc`, `--auth-ipc.path`) from the EL command and add: ```shell theme={null} --authrpc.addr 0.0.0.0 \ --authrpc.port 8551 \ --authrpc.jwtsecret "$ARC_HOME/jwtsecret" ``` `--authrpc.addr 0.0.0.0` exposes port 8551 on every network interface. Restrict access with a firewall rule or private network—the Engine API controls block production and must not be reachable from the public internet. ### Consensus layer flags for RPC mode Remove `--eth-socket` and `--execution-socket` from the CL command, and add: ```shell theme={null} --eth-rpc-endpoint http://:8545 \ --execution-endpoint http://:8551 \ --execution-jwt "$ARC_HOME/jwtsecret" ``` Replace `` with the IP address or `hostname` of the EL host. IPC and RPC are mutually exclusive. Use one or the other, not both. ## Tune backpressure The preceding tabs start the Consensus Layer with backpressure enabled at the default threshold (`16`). Backpressure throttles execution to match the speed of disk writes, bounding Execution Layer memory growth during startup or extended sync when the node is far behind. If you observe sustained memory pressure on a host that meets the [node requirements](/arc/references/node-requirements), restart the Consensus Layer with a lower threshold: ```shell theme={null} --execution-persistence-backpressure \ --execution-persistence-backpressure-threshold=10 ``` ## Migrate to V2 storage v0.8.0 upgrades Reth to v2.2.0, which introduces a V2 on-disk storage layout for the Execution Layer. **Migration is not required for v0.8.0 but will be required for v0.9.0.** Migrating now is recommended so the next upgrade does not require it. **Fresh installs** bootstrapped from the Circle-provided snapshots are already in V2 format. No migration is needed. **Upgrading from v0.7.x** requires either restoring from a V2 snapshot (the simplest path) or migrating the existing `datadir` in place: * Restoring from a V2 snapshot: stop the node, clear `$ARC_EXECUTION`, and re-run the snapshot download steps in this guide. * Migrating in place: use `arc-node-execution db migrate-v2` as described in the following section. ### Migrate in place 1. Stop the CL first, then stop the EL. Note the current head block number. 2. Run the migration. `--datadir` must precede `migrate-v2`: ```shell theme={null} arc-node-execution db \ --datadir "$ARC_EXECUTION" \ --chain arc-testnet \ migrate-v2 ``` Pass `--chain arc-mainnet` if your node is on mainnet. A successful run exits with `Migration complete. You should now restart the node and let it run the pipeline to rebuild the remaining data.` 3. Restart the EL alone (keep the CL stopped) with the pruning flag that matches your `datadir` (`--full`, `--minimal`, or omit for archive). Using the wrong flag causes a rebuild failure: ```shell theme={null} arc-node-execution node \ --chain arc-testnet \ --datadir "$ARC_EXECUTION" \ --full \ --disable-discovery \ --http \ --http.api eth,net,web3,txpool,trace,debug ``` The node rebuilds index tables cleared during migration. Wait until it returns to the head block number you noted in step 1. 4. Verify the rebuilt node is at tip (`eth_syncing` returns `false`) and that re-running `migrate-v2` is a no-op (`Storage is already v2, nothing to do`). 5. Start the CL and confirm live execution resumes. If the migration fails and you need to roll back, restore from a v0.7.x snapshot. **v0.7.3 is outdated.** Testnet operators must upgrade to v0.8.0 before timestamp `1788447600` (2026-09-03 15:00:00 UTC), and mainnet operators before timestamp `1789052400` (2026-09-10 15:00:00 UTC). Use the v0.8.0 view for current setup instructions and migration details. An Arc node syncs blocks from the network and serves a local JSON-RPC endpoint so you can independently verify every block and transaction. It runs two processes: the Execution Layer (EL), which executes transactions and maintains state, and the Consensus Layer (CL), which fetches finalized blocks from relay endpoints (trusted HTTP servers that distribute signed block data), verifies their signatures, and passes them to the EL. For a deeper explanation of how these components fit together, see [Running a node](/arc/concepts/running-a-node). Arc is currently in its testnet phase. During this period, the network may experience instability or unplanned downtime. Throughout this page, all references to Arc refer specifically to the Arc Testnet. Choose the install path that fits your environment. Install pre-built Arc binaries using the `arcup` script. This is the fastest path and requires no Rust toolchain or build dependencies. ## Prerequisites Before you begin, confirm that you have: * Reviewed the [node requirements](/arc/references/node-requirements) for hardware, software, and network endpoints * Prepared a Linux or macOS machine that meets the minimum system requirements * Installed [Foundry](https://book.getfoundry.sh/getting-started/installation) (optional, provides the `cast` command used to verify your node) ## Step 1: Install with `arcup` `arcup` installs the Arc binaries under `$ARC_HOME` (default `~/.arc`). Export `ARC_HOME` first if you want to install somewhere else; otherwise the default applies for both the installer and the environment file: ```shell theme={null} export ARC_HOME="${ARC_HOME:-$HOME/.arc}" curl -L https://raw.githubusercontent.com/circlefin/arc-node/main/arcup/install | bash ``` The installer places `arc-node-execution`, `arc-node-consensus`, `arc-snapshots`, and `arcup` itself into `$ARC_HOME/bin`. Load the produced environment file so the binaries are on your `PATH`: ```shell theme={null} source "$ARC_HOME/env" ``` Verify the installation: ```shell theme={null} arc-snapshots --version arc-node-execution --version arc-node-consensus --version ``` Each command prints a version string. To update the binaries later, run `arcup`. ## Step 2: Set environment variables The remaining steps use a small set of variables to keep paths consistent. Write them to a file and source it: ```shell theme={null} cat << "EOF" > ~/.arc_env ARC_HOME="${ARC_HOME:-$HOME/.arc}" # Linux runtime directory: ARC_RUN="/run/arc" # macOS runtime directory (uncomment if on macOS): #ARC_RUN="$ARC_HOME/run" ARC_EXECUTION=$ARC_HOME/execution ARC_CONSENSUS=$ARC_HOME/consensus EOF source ~/.arc_env ``` ## Step 3: Create data directories Create the data directories for the EL and CL, plus the runtime directory used for IPC sockets: ```shell theme={null} mkdir -p "$ARC_EXECUTION" "$ARC_CONSENSUS" sudo install -d -o $USER "$ARC_RUN" ``` On macOS, replace the second command with `mkdir -p "$ARC_RUN"` after uncommenting the macOS line in `~/.arc_env`. When running as a systemd service, the `RuntimeDirectory=arc` directive creates `/run/arc` automatically. You can skip the second command in that case. ## Step 4: Download blockchain snapshots Download snapshots so the node starts syncing from a recent block height rather than from genesis: ```shell theme={null} arc-snapshots download \ --chain=arc-testnet \ --execution-path "$ARC_EXECUTION" \ --consensus-path "$ARC_CONSENSUS" ``` This command fetches the latest snapshot URLs from `https://snapshots.arc.network`, downloads the snapshots, and extracts them into `$ARC_EXECUTION` and `$ARC_CONSENSUS`. Snapshots are large. Testnet snapshots are approximately **68 GB EL + 16 GB CL compressed** (around 103 GB and 36 GB extracted). The download takes 10–15 minutes on a stable 100 Mbps connection; slower or metered connections can take hours. Ensure you have at least 150 GB of free disk space, a stable network connection, and available CPU. The terminal stops producing output during extraction. This is expected. ## Step 5: Initialize the consensus layer Generate the CL private key file used for network identity. This is a one-time setup step: ```shell theme={null} arc-node-consensus init --home "$ARC_CONSENSUS" ``` ## Step 6: Start the execution layer Start the Execution Layer. This creates the IPC sockets that the Consensus Layer connects to. ```shell theme={null} arc-node-execution node \ --chain arc-testnet \ --datadir "$ARC_EXECUTION" \ --full \ --disable-discovery \ --ipcpath "$ARC_RUN/reth.ipc" \ --auth-ipc \ --auth-ipc.path "$ARC_RUN/auth.ipc" \ --http \ --http.addr 127.0.0.1 \ --http.port 8545 \ --http.api eth,net,web3,txpool,trace,debug \ --metrics 127.0.0.1:9001 \ --enable-arc-rpc \ --rpc.forwarder https://rpc.testnet.arc.io/ ``` The EL starts and waits for blocks. Log output shows that the IPC sockets exist at `$ARC_RUN/reth.ipc` and `$ARC_RUN/auth.ipc`. | Flag | Required | Description | | :--------------------------------------- | :---------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--chain arc-testnet` | Yes | Selects the Arc Testnet genesis configuration bundled in the binary | | `--datadir` | Yes | Path to the EL data directory | | `--full` | First start | Required on the first start from a pruned snapshot; optional after | | `--disable-discovery` | Yes | Disables P2P peer discovery (Arc uses relay endpoints instead) | | `--ipcpath` / `--auth-ipc` | Yes (IPC mode) | Paths for the IPC sockets the CL connects to | | `--http` / `--http.addr` | Yes | Enables the JSON-RPC endpoint on the specified address and port | | `--http.api` | Recommended | RPC namespaces to expose | | `--metrics` | Recommended | Enables Prometheus metrics on the specified address and port | | `--enable-arc-rpc` | Yes | Enables Arc-specific RPC methods | | `--rpc.forwarder` | Recommended | Routes submitted transactions to an RPC node for broadcast | | `--public-api` | Recommended for public-facing nodes | Hides pending-tx RPCs (a potential MEV vector) and warns if `--http.api` or `--ws.api` exposes namespaces beyond `eth`, `net`, `web3`, `rpc` | | `--rpc.max-connections` | Recommended for public-facing nodes | Maximum concurrent RPC connections. Default `250` (lowered from `500` in v0.7.1). Raise only if clients see `MaxConnections` errors | | `--rpc.max-subscriptions-per-connection` | Recommended for public-facing nodes | Maximum subscriptions per single WebSocket connection. Default `32` (lowered from `1024` in v0.7.1). Raise only if clients see `TooManySubscriptions` errors | On high-traffic public endpoints, raise `--rpc.max-connections` (default 250) and `--rpc.max-subscriptions-per-connection` (default 32) if clients see `MaxConnections` or `TooManySubscriptions` errors. The defaults bound WebSocket log-fanout memory growth and should only be raised, not lowered. The `--full` flag is required on the first start when bootstrapping from a pruned snapshot. It reconciles internal database tables that would otherwise fail a consistency check. After the initial startup completes, restart without `--full` if you prefer not to prune. `--chain arc-testnet` uses the bundled genesis configuration; replace with `--chain /path/to/genesis.json` if you have a custom genesis file. See [reth node](https://reth.rs/cli/reth/node/) for additional flags. ## Step 7: Start the consensus layer Open a separate terminal and start the Consensus Layer. The CL connects to the EL through the IPC sockets and begins fetching blocks from the network. ```shell theme={null} arc-node-consensus start \ --home "$ARC_CONSENSUS" \ --full \ --eth-socket "$ARC_RUN/reth.ipc" \ --execution-socket "$ARC_RUN/auth.ipc" \ --rpc.addr 127.0.0.1:31000 \ --follow \ --follow.endpoint https://rpc.testnet.arc.io,wss=rpc.testnet.arc.io \ --follow.endpoint https://rpc.drpc.testnet.arc.io,wss=rpc.drpc.testnet.arc.io \ --follow.endpoint https://rpc.blockdaemon.testnet.arc.io,wss=rpc.blockdaemon.testnet.arc.io/websocket \ --execution-persistence-backpressure \ --execution-persistence-backpressure-threshold=16 \ --metrics 127.0.0.1:29000 ``` | Flag | Required | Description | | :----------------------------------------------- | :------------- | :--------------------------------------------------------------------------------------------- | | `--home` | Yes | Path to the CL data directory (contains keys and state) | | `--full` | Recommended | Enables CL pruning to bound disk growth | | `--eth-socket` | Yes (IPC mode) | Path to the Execution Layer ETH RPC IPC socket | | `--execution-socket` | Yes (IPC mode) | Path to the Execution Layer Engine API IPC socket | | `--rpc.addr` | Recommended | CL RPC listen address and port | | `--follow` | Yes | Enables block-following mode through relay endpoints | | `--follow.endpoint` | Yes | Relay endpoint URLs. Specify multiple for redundancy. Format: `https://host,wss=host` | | `--execution-persistence-backpressure` | Recommended | Throttles execution to match disk-write speed, bounding EL memory growth | | `--execution-persistence-backpressure-threshold` | Recommended | Threshold for backpressure. Default `16` (must be `> 0`). Lower if you observe memory pressure | | `--metrics` | Recommended | Enables Prometheus metrics on the specified address and port | ## Step 8: Verify the node is syncing After both layers start, wait roughly 30 seconds, then query the local JSON-RPC endpoint for the latest block height: ```shell theme={null} curl -s -X POST http://localhost:8545 \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 1 }' ``` The `result` field is a hexadecimal block number that increases over time. If it stays at `0x0`, check the CL logs for errors. If you have [Foundry](https://book.getfoundry.sh/getting-started/installation) installed, you can run the same check with `cast`: ```shell theme={null} cast block-number --rpc-url http://localhost:8545 ``` Run it several times over a few seconds -- the block number increases as the node syncs. If the block number stays at `0x0` or the node does not start as expected, check these common causes: * **IPC socket files missing.** The EL writes sockets at `$ARC_RUN/reth.ipc` and `$ARC_RUN/auth.ipc` on startup. If they are missing after 30 seconds, the EL did not start -- review its terminal output for a panic or configuration error. * **CL cannot connect to the EL.** Start the EL first. If the CL was launched before the EL was ready, restart the CL. * **Snapshot extraction incomplete.** `arc-snapshots download` runs silently during extraction. If it was interrupted, `$ARC_EXECUTION` or `$ARC_CONSENSUS` may be partially populated. Re-run the download to overwrite the existing files. If you clear the directories first, note that `$ARC_CONSENSUS` also holds the CL private key written by `arc-node-consensus init`. Re-run that command after clearing, or your node loses its network identity. * **Path mismatch between EL and CL.** `$ARC_RUN` must resolve to the same directory in both shells. Re-source `~/.arc_env` in any terminal that lost its environment. Once your node is syncing steadily, you can [deploy it as a systemd service](/arc/tutorials/deploy-node-as-service) for production use, or [set up Prometheus and Grafana](/arc/tutorials/set-up-node-monitoring) to view the pre-built dashboards. Build Arc binaries from the [`arc-node`](https://github.com/circlefin/arc-node) repository. Use this path to audit the build, run on an unsupported platform, or pin to a specific commit. ## Prerequisites Before you begin, confirm that you have: * Reviewed the [node requirements](/arc/references/node-requirements) for hardware, software, and network endpoints * Prepared a Linux, macOS, or Windows machine that meets the minimum system requirements * Installed [Foundry](https://book.getfoundry.sh/getting-started/installation) (optional, provides the `cast` command used to verify your node) ## Step 1: Install build dependencies Install [Rust](https://rust-lang.org/tools/install/) if you don't have it: ```shell theme={null} curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source ~/.cargo/env ``` Install the platform packages required to build the EL database bindings: * **Ubuntu:** `sudo apt-get install libclang-dev pkg-config build-essential` * **macOS:** `brew install llvm pkg-config`, then `export LIBCLANG_PATH="$(brew --prefix llvm)/lib"` * **Windows:** `choco install llvm` or `winget install LLVM.LLVM` ## Step 2: Clone the repository Clone the [`arc-node`](https://github.com/circlefin/arc-node) repository and check out the version for your target network. See [node requirements](/arc/references/node-requirements#versions) for the current version: ```shell theme={null} git clone https://github.com/circlefin/arc-node.git cd arc-node git checkout v0.7.3 git submodule update --init --recursive ``` ## Step 3: Build and install The following commands build three Arc node binaries: `arc-node-execution`, `arc-node-consensus`, and `arc-snapshots`: ```shell theme={null} cargo install --path crates/node cargo install --path crates/malachite-app cargo install --path crates/snapshots ``` `cargo install` places compiled binaries into `~/.cargo/bin`, which is added to `PATH` by loading `~/.cargo/env`. To install elsewhere, add `--root $BASE_DIR` to each command (for instance, `--root /usr/local` places them under `/usr/local/bin`). Verify the installation: ```shell theme={null} arc-snapshots --version arc-node-execution --version arc-node-consensus --version ``` ## Step 4: Set environment variables The remaining steps use a small set of variables to keep paths consistent. Write them to a file and source it: ```shell theme={null} cat << "EOF" > ~/.arc_env ARC_HOME="${ARC_HOME:-$HOME/.arc}" # Linux runtime directory: ARC_RUN="/run/arc" # macOS runtime directory (uncomment if on macOS): #ARC_RUN="$ARC_HOME/run" ARC_EXECUTION=$ARC_HOME/execution ARC_CONSENSUS=$ARC_HOME/consensus EOF source ~/.arc_env ``` ## Step 5: Create data directories Create the data directories for the EL and CL, plus the runtime directory used for IPC sockets: ```shell theme={null} mkdir -p "$ARC_EXECUTION" "$ARC_CONSENSUS" sudo install -d -o $USER "$ARC_RUN" ``` On macOS, replace the second command with `mkdir -p "$ARC_RUN"` after uncommenting the macOS line in `~/.arc_env`. When running as a systemd service, the `RuntimeDirectory=arc` directive creates `/run/arc` automatically. You can skip the second command in that case. ## Step 6: Download blockchain snapshots Download snapshots so the node starts syncing from a recent block height rather than from genesis: ```shell theme={null} arc-snapshots download \ --chain=arc-testnet \ --execution-path "$ARC_EXECUTION" \ --consensus-path "$ARC_CONSENSUS" ``` This command fetches the latest snapshot URLs from `https://snapshots.arc.network`, downloads the snapshots, and extracts them into `$ARC_EXECUTION` and `$ARC_CONSENSUS`. Snapshots are large. Testnet snapshots are approximately **68 GB EL + 16 GB CL compressed** (around 103 GB and 36 GB extracted). The download takes 10–15 minutes on a stable 100 Mbps connection; slower or metered connections can take hours. Ensure you have at least 150 GB of free disk space, a stable network connection, and available CPU. The terminal stops producing output during extraction. This is expected. ## Step 7: Initialize the consensus layer Generate the CL private key file used for network identity. This is a one-time setup step: ```shell theme={null} arc-node-consensus init --home "$ARC_CONSENSUS" ``` ## Step 8: Start the execution layer Start the Execution Layer. This creates the IPC sockets that the Consensus Layer connects to. ```shell theme={null} arc-node-execution node \ --chain arc-testnet \ --datadir "$ARC_EXECUTION" \ --full \ --disable-discovery \ --ipcpath "$ARC_RUN/reth.ipc" \ --auth-ipc \ --auth-ipc.path "$ARC_RUN/auth.ipc" \ --http \ --http.addr 127.0.0.1 \ --http.port 8545 \ --http.api eth,net,web3,txpool,trace,debug \ --metrics 127.0.0.1:9001 \ --enable-arc-rpc \ --rpc.forwarder https://rpc.testnet.arc.io/ ``` The EL starts and waits for blocks. Log output shows that the IPC sockets exist at `$ARC_RUN/reth.ipc` and `$ARC_RUN/auth.ipc`. | Flag | Required | Description | | :--------------------------------------- | :---------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--chain arc-testnet` | Yes | Selects the Arc Testnet genesis configuration bundled in the binary | | `--datadir` | Yes | Path to the EL data directory | | `--full` | First start | Required on the first start from a pruned snapshot; optional after | | `--disable-discovery` | Yes | Disables P2P peer discovery (Arc uses relay endpoints instead) | | `--ipcpath` / `--auth-ipc` | Yes (IPC mode) | Paths for the IPC sockets the CL connects to | | `--http` / `--http.addr` | Yes | Enables the JSON-RPC endpoint on the specified address and port | | `--http.api` | Recommended | RPC namespaces to expose | | `--metrics` | Recommended | Enables Prometheus metrics on the specified address and port | | `--enable-arc-rpc` | Yes | Enables Arc-specific RPC methods | | `--rpc.forwarder` | Recommended | Routes submitted transactions to an RPC node for broadcast | | `--public-api` | Recommended for public-facing nodes | Hides pending-tx RPCs (a potential MEV vector) and warns if `--http.api` or `--ws.api` exposes namespaces beyond `eth`, `net`, `web3`, `rpc` | | `--rpc.max-connections` | Recommended for public-facing nodes | Maximum concurrent RPC connections. Default `250` (lowered from `500` in v0.7.1). Raise only if clients see `MaxConnections` errors | | `--rpc.max-subscriptions-per-connection` | Recommended for public-facing nodes | Maximum subscriptions per single WebSocket connection. Default `32` (lowered from `1024` in v0.7.1). Raise only if clients see `TooManySubscriptions` errors | On high-traffic public endpoints, raise `--rpc.max-connections` (default 250) and `--rpc.max-subscriptions-per-connection` (default 32) if clients see `MaxConnections` or `TooManySubscriptions` errors. The defaults bound WebSocket log-fanout memory growth and should only be raised, not lowered. The `--full` flag is required on the first start when bootstrapping from a pruned snapshot. It reconciles internal database tables that would otherwise fail a consistency check. After the initial startup completes, restart without `--full` if you prefer not to prune. `--chain arc-testnet` uses the bundled genesis configuration; replace with `--chain /path/to/genesis.json` if you have a custom genesis file. See [reth node](https://reth.rs/cli/reth/node/) for additional flags. ## Step 9: Start the consensus layer Open a separate terminal and start the Consensus Layer. The CL connects to the EL through the IPC sockets and begins fetching blocks from the network. ```shell theme={null} arc-node-consensus start \ --home "$ARC_CONSENSUS" \ --full \ --eth-socket "$ARC_RUN/reth.ipc" \ --execution-socket "$ARC_RUN/auth.ipc" \ --rpc.addr 127.0.0.1:31000 \ --follow \ --follow.endpoint https://rpc.testnet.arc.io,wss=rpc.testnet.arc.io \ --follow.endpoint https://rpc.drpc.testnet.arc.io,wss=rpc.drpc.testnet.arc.io \ --follow.endpoint https://rpc.blockdaemon.testnet.arc.io,wss=rpc.blockdaemon.testnet.arc.io/websocket \ --execution-persistence-backpressure \ --execution-persistence-backpressure-threshold=16 \ --metrics 127.0.0.1:29000 ``` | Flag | Required | Description | | :----------------------------------------------- | :------------- | :--------------------------------------------------------------------------------------------- | | `--home` | Yes | Path to the CL data directory (contains keys and state) | | `--full` | Recommended | Enables CL pruning to bound disk growth | | `--eth-socket` | Yes (IPC mode) | Path to the Execution Layer ETH RPC IPC socket | | `--execution-socket` | Yes (IPC mode) | Path to the Execution Layer Engine API IPC socket | | `--rpc.addr` | Recommended | CL RPC listen address and port | | `--follow` | Yes | Enables block-following mode through relay endpoints | | `--follow.endpoint` | Yes | Relay endpoint URLs. Specify multiple for redundancy. Format: `https://host,wss=host` | | `--execution-persistence-backpressure` | Recommended | Throttles execution to match disk-write speed, bounding EL memory growth | | `--execution-persistence-backpressure-threshold` | Recommended | Threshold for backpressure. Default `16` (must be `> 0`). Lower if you observe memory pressure | | `--metrics` | Recommended | Enables Prometheus metrics on the specified address and port | ## Step 10: Verify the node is syncing After both layers start, wait roughly 30 seconds, then query the local JSON-RPC endpoint for the latest block height: ```shell theme={null} curl -s -X POST http://localhost:8545 \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 1 }' ``` The `result` field is a hexadecimal block number that increases over time. If it stays at `0x0`, check the CL logs for errors. If you have [Foundry](https://book.getfoundry.sh/getting-started/installation) installed, you can run the same check with `cast`: ```shell theme={null} cast block-number --rpc-url http://localhost:8545 ``` Run it several times over a few seconds -- the block number increases as the node syncs. If the block number stays at `0x0` or the node does not start as expected, check these common causes: * **IPC socket files missing.** The EL writes sockets at `$ARC_RUN/reth.ipc` and `$ARC_RUN/auth.ipc` on startup. If they are missing after 30 seconds, the EL did not start -- review its terminal output for a panic or configuration error. * **CL cannot connect to the EL.** Start the EL first. If the CL was launched before the EL was ready, restart the CL. * **Snapshot extraction incomplete.** `arc-snapshots download` runs silently during extraction. If it was interrupted, `$ARC_EXECUTION` or `$ARC_CONSENSUS` may be partially populated. Re-run the download to overwrite the existing files. If you clear the directories first, note that `$ARC_CONSENSUS` also holds the CL private key written by `arc-node-consensus init`. Re-run that command after clearing, or your node loses its network identity. * **Path mismatch between EL and CL.** `$ARC_RUN` must resolve to the same directory in both shells. Re-source `~/.arc_env` in any terminal that lost its environment. Once your node is syncing steadily, you can [deploy it as a systemd service](/arc/tutorials/deploy-node-as-service) for production use, or [set up Prometheus and Grafana](/arc/tutorials/set-up-node-monitoring) to view the pre-built dashboards. Run the Arc node from pre-built Docker images using Docker Compose. Init containers handle snapshot download and CL initialization on the first run. ## Prerequisites Before you begin, confirm that you have: * Reviewed the [node requirements](/arc/references/node-requirements) for hardware, software, and network endpoints * Prepared a Linux machine that meets the minimum system requirements * Installed [Docker Engine 24+ with BuildKit](https://docs.docker.com/engine/install/) and [Docker Compose v2](https://docs.docker.com/compose/install/) ## Step 1: Set environment variables The Compose file reads images from environment variables. Set the version, data directory, and image references. See [node requirements](/arc/references/node-requirements#versions) for the current version: ```shell theme={null} export ARC_VERSION=0.7.3 export ARC_HOME=~/.arc export ARC_EXECUTION_IMAGE=docker.cloudsmith.io/circle/arc-network/arc-execution:$ARC_VERSION export ARC_CONSENSUS_IMAGE=docker.cloudsmith.io/circle/arc-network/arc-consensus:$ARC_VERSION ``` To build images locally instead of pulling from Cloudsmith, see [Installation: Docker](https://github.com/circlefin/arc-node/blob/main/docs/installation.md#docker) in the `arc-node` repository. ## Step 2: Prepare the data directory Create `$ARC_HOME` on the host before starting Docker Compose. If you skip this, Docker creates it as `root` and the snapshot init container fails with permission errors: ```shell theme={null} mkdir -p "${ARC_HOME:-$HOME/.arc}" ``` ## Step 3: Download the Compose file Fetch the `docker-compose.yml` for your release into a working directory: ```shell theme={null} curl -O https://raw.githubusercontent.com/circlefin/arc-node/v${ARC_VERSION}/deployments/docker-compose.yml ``` ## Step 4: Start the node From the directory containing `docker-compose.yml`: ```shell theme={null} docker compose up -d ``` On the first run, init containers automatically: 1. Download the latest testnet snapshots (approximately 84 GB compressed; see [snapshot download size](/arc/references/node-requirements#snapshot-download-size)) 2. Initialize the consensus layer private key 3. Prepare the shared IPC socket volume Subsequent runs detect that initialization is already complete and start immediately. The init container runs as `root` so it can set file ownership for the EL and CL services (UID 999). No manual `chown` is needed. ## Step 5: Verify the node is syncing On the first run, wait for the snapshot init container to finish: ```shell theme={null} docker compose logs -f arc-snapshots ``` Once the EL and CL containers are up, wait roughly 30 seconds, then query the latest block height: ```shell theme={null} curl -s -X POST http://localhost:8545 \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 1 }' ``` The `result` field is a hexadecimal block number that increases over time. If it stays at `0x0`, check the logs: ```shell theme={null} docker compose logs -f ``` The containers expose Prometheus metrics on the host at `localhost:9001/` (Execution Layer, root path) and `localhost:29000/metrics` (Consensus Layer). To scrape these endpoints with a local stack, see [Set up Prometheus and Grafana for an Arc node](/arc/tutorials/set-up-node-monitoring). ## Step 6: Stop and reset To stop the node while preserving data: ```shell theme={null} docker compose down ``` Node data persists in `~/.arc/` (or the path set by `$ARC_HOME`). To remove all data and start fresh: ```shell theme={null} docker compose down -v rm -rf ~/.arc ``` `docker compose down -v` removes the named sockets volume, and `rm -rf ~/.arc` permanently deletes the consensus layer private key -- your node's network identity. It cannot be recovered. ## Run on separate hosts The preceding tabs describe running the Execution Layer and Consensus Layer on the same host, communicating through IPC sockets. To run them on separate hosts, swap IPC for authenticated RPC. This section applies to the binary install paths (`arcup` and source build); the Docker path is single-host by default. ### Generate a JWT secret The EL and CL authenticate to each other with a shared JWT secret. Generate it once, then securely copy it to both hosts: ```shell theme={null} openssl rand -hex 32 | tr -d "\n" > "$ARC_HOME/jwtsecret" chmod 600 "$ARC_HOME/jwtsecret" ``` ### Execution layer flags for RPC mode Remove the IPC flags (`--ipcpath`, `--auth-ipc`, `--auth-ipc.path`) from the EL command and add: ```shell theme={null} --authrpc.addr 0.0.0.0 \ --authrpc.port 8551 \ --authrpc.jwtsecret "$ARC_HOME/jwtsecret" ``` `--authrpc.addr 0.0.0.0` exposes port 8551 on every network interface. Restrict access with a firewall rule or private network -- the Engine API controls block production and must not be reachable from the public internet. ### Consensus layer flags for RPC mode Remove `--eth-socket` and `--execution-socket` from the CL command, and add: ```shell theme={null} --eth-rpc-endpoint http://:8545 \ --execution-endpoint http://:8551 \ --execution-jwt "$ARC_HOME/jwtsecret" ``` Replace `` with the IP address or hostname of the EL host. IPC and RPC are mutually exclusive. Use one or the other, not both. ## Enable backpressure under memory pressure The preceding tabs start the Consensus Layer with backpressure enabled at the default threshold (`16`). Backpressure throttles execution to match the speed of disk writes, bounding Execution Layer memory growth during startup or extended sync when the node is far behind. If you observe sustained memory pressure on a host that meets the [node requirements](/arc/references/node-requirements), lower the threshold to `10` for more aggressive throttling. First, add the `reth` namespace to the EL `--http.api` flag so the CL can read backpressure metrics: ```shell theme={null} --http.api eth,net,web3,txpool,trace,debug,reth ``` Then restart the CL with the lower threshold: ```shell theme={null} --execution-persistence-backpressure \ --execution-persistence-backpressure-threshold=10 ``` **v0.7.2: Arc Testnet hardfork activates 2026-06-18 14:00:00 UTC.** The Zero7 hardfork activates at Unix timestamp `1781791200`. Testnet nodes must be on v0.7.2 by that timestamp; earlier versions stop syncing after activation. For the full upgrade notes, see [CHANGELOG.md](https://github.com/circlefin/arc-node/blob/main/CHANGELOG.md) and [BREAKING\_CHANGES.md](https://github.com/circlefin/arc-node/blob/main/BREAKING_CHANGES.md#v072). An Arc node syncs blocks from the network and serves a local JSON-RPC endpoint so you can independently verify every block and transaction. It runs two processes: the Execution Layer (EL), which executes transactions and maintains state, and the Consensus Layer (CL), which fetches finalized blocks from relay endpoints (trusted HTTP servers that distribute signed block data), verifies their signatures, and passes them to the EL. For a deeper explanation of how these components fit together, see [Running a node](/arc/concepts/running-a-node). Arc is currently in its testnet phase. During this period, the network may experience instability or unplanned downtime. Throughout this page, all references to Arc refer specifically to the Arc Testnet. Choose the install path that fits your environment. Install pre-built Arc binaries using the `arcup` script. This is the fastest path and requires no Rust toolchain or build dependencies. ## Prerequisites Before you begin, confirm that you have: * Reviewed the [node requirements](/arc/references/node-requirements) for hardware, software, and network endpoints * Prepared a Linux or macOS machine that meets the minimum system requirements * Installed [Foundry](https://book.getfoundry.sh/getting-started/installation) (optional, provides the `cast` command used to verify your node) ## Step 1: Install with `arcup` `arcup` installs the Arc binaries under `$ARC_HOME` (default `~/.arc`). Export `ARC_HOME` first if you want to install somewhere else; otherwise the default applies for both the installer and the environment file: ```shell theme={null} export ARC_HOME="${ARC_HOME:-$HOME/.arc}" curl -L https://raw.githubusercontent.com/circlefin/arc-node/main/arcup/install | bash ``` The installer places `arc-node-execution`, `arc-node-consensus`, `arc-snapshots`, and `arcup` itself into `$ARC_HOME/bin`. Load the produced environment file so the binaries are on your `PATH`: ```shell theme={null} source "$ARC_HOME/env" ``` Verify the installation: ```shell theme={null} arc-snapshots --version arc-node-execution --version arc-node-consensus --version ``` Each command prints a version string. To update the binaries later, run `arcup`. ## Step 2: Set environment variables The remaining steps use a small set of variables to keep paths consistent. Write them to a file and source it: ```shell theme={null} cat << "EOF" > ~/.arc_env ARC_HOME="${ARC_HOME:-$HOME/.arc}" # Linux runtime directory: ARC_RUN="/run/arc" # macOS runtime directory (uncomment if on macOS): #ARC_RUN="$ARC_HOME/run" ARC_EXECUTION=$ARC_HOME/execution ARC_CONSENSUS=$ARC_HOME/consensus EOF source ~/.arc_env ``` ## Step 3: Create data directories Create the data directories for the EL and CL, plus the runtime directory used for IPC sockets: ```shell theme={null} mkdir -p "$ARC_EXECUTION" "$ARC_CONSENSUS" sudo install -d -o $USER "$ARC_RUN" ``` On macOS, replace the second command with `mkdir -p "$ARC_RUN"` after uncommenting the macOS line in `~/.arc_env`. When running as a systemd service, the `RuntimeDirectory=arc` directive creates `/run/arc` automatically. You can skip the second command in that case. ## Step 4: Download blockchain snapshots Download snapshots so the node starts syncing from a recent block height rather than from genesis: ```shell theme={null} arc-snapshots download \ --chain=arc-testnet \ --execution-path "$ARC_EXECUTION" \ --consensus-path "$ARC_CONSENSUS" ``` This command fetches the latest snapshot URLs from `https://snapshots.arc.network`, downloads the snapshots, and extracts them into `$ARC_EXECUTION` and `$ARC_CONSENSUS`. Snapshots are large. Testnet snapshots are approximately **68 GB EL + 16 GB CL compressed** (around 103 GB and 36 GB extracted). The download takes 10–15 minutes on a stable 100 Mbps connection; slower or metered connections can take hours. Ensure you have at least 150 GB of free disk space, a stable network connection, and available CPU. The terminal stops producing output during extraction. This is expected. ## Step 5: Initialize the consensus layer Generate the CL private key file used for network identity. This is a one-time setup step: ```shell theme={null} arc-node-consensus init --home "$ARC_CONSENSUS" ``` ## Step 6: Start the execution layer Start the Execution Layer. This creates the IPC sockets that the Consensus Layer connects to. ```shell theme={null} arc-node-execution node \ --chain arc-testnet \ --datadir "$ARC_EXECUTION" \ --full \ --disable-discovery \ --ipcpath "$ARC_RUN/reth.ipc" \ --auth-ipc \ --auth-ipc.path "$ARC_RUN/auth.ipc" \ --http \ --http.addr 127.0.0.1 \ --http.port 8545 \ --http.api eth,net,web3,txpool,trace,debug \ --metrics 127.0.0.1:9001 \ --enable-arc-rpc \ --rpc.forwarder https://rpc.testnet.arc.io/ ``` The EL starts and waits for blocks. Log output shows that the IPC sockets exist at `$ARC_RUN/reth.ipc` and `$ARC_RUN/auth.ipc`. | Flag | Required | Description | | :--------------------------------------- | :---------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--chain arc-testnet` | Yes | Selects the Arc Testnet genesis configuration bundled in the binary | | `--datadir` | Yes | Path to the EL data directory | | `--full` | First start | Required on the first start from a pruned snapshot; optional after | | `--disable-discovery` | Yes | Disables P2P peer discovery (Arc uses relay endpoints instead) | | `--ipcpath` / `--auth-ipc` | Yes (IPC mode) | Paths for the IPC sockets the CL connects to | | `--http` / `--http.addr` | Yes | Enables the JSON-RPC endpoint on the specified address and port | | `--http.api` | Recommended | RPC namespaces to expose | | `--metrics` | Recommended | Enables Prometheus metrics on the specified address and port | | `--enable-arc-rpc` | Yes | Enables Arc-specific RPC methods | | `--rpc.forwarder` | Recommended | Routes submitted transactions to an RPC node for broadcast | | `--public-api` | Recommended for public-facing nodes | Hides pending-tx RPCs (a potential MEV vector) and warns if `--http.api` or `--ws.api` exposes namespaces beyond `eth`, `net`, `web3`, `rpc` | | `--rpc.max-connections` | Recommended for public-facing nodes | Maximum concurrent RPC connections. Default `250` (lowered from `500` in v0.7.1). Raise only if clients see `MaxConnections` errors | | `--rpc.max-subscriptions-per-connection` | Recommended for public-facing nodes | Maximum subscriptions per single WebSocket connection. Default `32` (lowered from `1024` in v0.7.1). Raise only if clients see `TooManySubscriptions` errors | On high-traffic public endpoints, raise `--rpc.max-connections` (default 250) and `--rpc.max-subscriptions-per-connection` (default 32) if clients see `MaxConnections` or `TooManySubscriptions` errors. The defaults bound WebSocket log-fanout memory growth and should only be raised, not lowered. The `--full` flag is required on the first start when bootstrapping from a pruned snapshot. It reconciles internal database tables that would otherwise fail a consistency check. After the initial startup completes, restart without `--full` if you prefer not to prune. `--chain arc-testnet` uses the bundled genesis configuration; replace with `--chain /path/to/genesis.json` if you have a custom genesis file. See [reth node](https://reth.rs/cli/reth/node/) for additional flags. ## Step 7: Start the consensus layer Open a separate terminal and start the Consensus Layer. The CL connects to the EL through the IPC sockets and begins fetching blocks from the network. ```shell theme={null} arc-node-consensus start \ --home "$ARC_CONSENSUS" \ --full \ --eth-socket "$ARC_RUN/reth.ipc" \ --execution-socket "$ARC_RUN/auth.ipc" \ --rpc.addr 127.0.0.1:31000 \ --follow \ --follow.endpoint https://rpc.testnet.arc.io,wss=rpc.testnet.arc.io \ --follow.endpoint https://rpc.drpc.testnet.arc.io,wss=rpc.drpc.testnet.arc.io \ --follow.endpoint https://rpc.blockdaemon.testnet.arc.io,wss=rpc.blockdaemon.testnet.arc.io/websocket \ --execution-persistence-backpressure \ --execution-persistence-backpressure-threshold=16 \ --metrics 127.0.0.1:29000 ``` | Flag | Required | Description | | :----------------------------------------------- | :------------- | :--------------------------------------------------------------------------------------------- | | `--home` | Yes | Path to the CL data directory (contains keys and state) | | `--full` | Recommended | Enables CL pruning to bound disk growth | | `--eth-socket` | Yes (IPC mode) | Path to the Execution Layer ETH RPC IPC socket | | `--execution-socket` | Yes (IPC mode) | Path to the Execution Layer Engine API IPC socket | | `--rpc.addr` | Recommended | CL RPC listen address and port | | `--follow` | Yes | Enables block-following mode through relay endpoints | | `--follow.endpoint` | Yes | Relay endpoint URLs. Specify multiple for redundancy. Format: `https://host,wss=host` | | `--execution-persistence-backpressure` | Recommended | Throttles execution to match disk-write speed, bounding EL memory growth | | `--execution-persistence-backpressure-threshold` | Recommended | Threshold for backpressure. Default `16` (must be `> 0`). Lower if you observe memory pressure | | `--metrics` | Recommended | Enables Prometheus metrics on the specified address and port | ## Step 8: Verify the node is syncing After both layers start, wait roughly 30 seconds, then query the local JSON-RPC endpoint for the latest block height: ```shell theme={null} curl -s -X POST http://localhost:8545 \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 1 }' ``` The `result` field is a hexadecimal block number that increases over time. If it stays at `0x0`, check the CL logs for errors. If you have [Foundry](https://book.getfoundry.sh/getting-started/installation) installed, you can run the same check with `cast`: ```shell theme={null} cast block-number --rpc-url http://localhost:8545 ``` Run it several times over a few seconds -- the block number increases as the node syncs. If the block number stays at `0x0` or the node does not start as expected, check these common causes: * **IPC socket files missing.** The EL writes sockets at `$ARC_RUN/reth.ipc` and `$ARC_RUN/auth.ipc` on startup. If they are not present after 30 seconds, the EL did not start -- review its terminal output for a panic or configuration error. * **CL cannot connect to the EL.** Start the EL first. If the CL was launched before the EL was ready, restart the CL. * **Snapshot extraction incomplete.** `arc-snapshots download` runs silently during extraction. If it was interrupted, `$ARC_EXECUTION` or `$ARC_CONSENSUS` may be partially populated. Re-run the download to overwrite the existing files. If you clear the directories first, note that `$ARC_CONSENSUS` also holds the CL private key written by `arc-node-consensus init`. Re-run that command after clearing, or your node loses its network identity. * **Path mismatch between EL and CL.** `$ARC_RUN` must resolve to the same directory in both shells. Re-source `~/.arc_env` in any terminal that lost its environment. Once your node is syncing steadily, you can [deploy it as a systemd service](/arc/tutorials/deploy-node-as-service) for production use, or [set up Prometheus and Grafana](/arc/tutorials/set-up-node-monitoring) to view the pre-built dashboards. Build Arc binaries from the [`arc-node`](https://github.com/circlefin/arc-node) repository. Use this path to audit the build, run on an unsupported platform, or pin to a specific commit. ## Prerequisites Before you begin, confirm that you have: * Reviewed the [node requirements](/arc/references/node-requirements) for hardware, software, and network endpoints * Prepared a Linux, macOS, or Windows machine that meets the minimum system requirements * Installed [Foundry](https://book.getfoundry.sh/getting-started/installation) (optional, provides the `cast` command used to verify your node) ## Step 1: Install build dependencies Install [Rust](https://rust-lang.org/tools/install/) if you don't have it: ```shell theme={null} curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source ~/.cargo/env ``` Install the platform packages required to build the EL database bindings: * **Ubuntu:** `sudo apt-get install libclang-dev pkg-config build-essential` * **macOS:** `brew install llvm pkg-config`, then `export LIBCLANG_PATH="$(brew --prefix llvm)/lib"` * **Windows:** `choco install llvm` or `winget install LLVM.LLVM` ## Step 2: Clone the repository Clone the [`arc-node`](https://github.com/circlefin/arc-node) repository and check out the version for your target network. See [node requirements](/arc/references/node-requirements#versions) for the current version: ```shell theme={null} git clone https://github.com/circlefin/arc-node.git cd arc-node git checkout v0.7.2 git submodule update --init --recursive ``` ## Step 3: Build and install The following commands build three Arc node binaries: `arc-node-execution`, `arc-node-consensus`, and `arc-snapshots`: ```shell theme={null} cargo install --path crates/node cargo install --path crates/malachite-app cargo install --path crates/snapshots ``` `cargo install` places compiled binaries into `~/.cargo/bin`, which is added to `PATH` by loading `~/.cargo/env`. To install elsewhere, add `--root $BASE_DIR` to each command (for instance, `--root /usr/local` places them under `/usr/local/bin`). Verify the installation: ```shell theme={null} arc-snapshots --version arc-node-execution --version arc-node-consensus --version ``` ## Step 4: Set environment variables The remaining steps use a small set of variables to keep paths consistent. Write them to a file and source it: ```shell theme={null} cat << "EOF" > ~/.arc_env ARC_HOME="${ARC_HOME:-$HOME/.arc}" # Linux runtime directory: ARC_RUN="/run/arc" # macOS runtime directory (uncomment if on macOS): #ARC_RUN="$ARC_HOME/run" ARC_EXECUTION=$ARC_HOME/execution ARC_CONSENSUS=$ARC_HOME/consensus EOF source ~/.arc_env ``` ## Step 5: Create data directories Create the data directories for the EL and CL, plus the runtime directory used for IPC sockets: ```shell theme={null} mkdir -p "$ARC_EXECUTION" "$ARC_CONSENSUS" sudo install -d -o $USER "$ARC_RUN" ``` On macOS, replace the second command with `mkdir -p "$ARC_RUN"` after uncommenting the macOS line in `~/.arc_env`. When running as a systemd service, the `RuntimeDirectory=arc` directive creates `/run/arc` automatically. You can skip the second command in that case. ## Step 6: Download blockchain snapshots Download snapshots so the node starts syncing from a recent block height rather than from genesis: ```shell theme={null} arc-snapshots download \ --chain=arc-testnet \ --execution-path "$ARC_EXECUTION" \ --consensus-path "$ARC_CONSENSUS" ``` This command fetches the latest snapshot URLs from `https://snapshots.arc.network`, downloads the snapshots, and extracts them into `$ARC_EXECUTION` and `$ARC_CONSENSUS`. Snapshots are large. Testnet snapshots are approximately **68 GB EL + 16 GB CL compressed** (around 103 GB and 36 GB extracted). The download takes 10–15 minutes on a stable 100 Mbps connection; slower or metered connections can take hours. Ensure you have at least 150 GB of free disk space, a stable network connection, and available CPU. The terminal stops producing output during extraction. This is expected. ## Step 7: Initialize the consensus layer Generate the CL private key file used for network identity. This is a one-time setup step: ```shell theme={null} arc-node-consensus init --home "$ARC_CONSENSUS" ``` ## Step 8: Start the execution layer Start the Execution Layer. This creates the IPC sockets that the Consensus Layer connects to. ```shell theme={null} arc-node-execution node \ --chain arc-testnet \ --datadir "$ARC_EXECUTION" \ --full \ --disable-discovery \ --ipcpath "$ARC_RUN/reth.ipc" \ --auth-ipc \ --auth-ipc.path "$ARC_RUN/auth.ipc" \ --http \ --http.addr 127.0.0.1 \ --http.port 8545 \ --http.api eth,net,web3,txpool,trace,debug \ --metrics 127.0.0.1:9001 \ --enable-arc-rpc \ --rpc.forwarder https://rpc.testnet.arc.io/ ``` The EL starts and waits for blocks. Log output shows that the IPC sockets exist at `$ARC_RUN/reth.ipc` and `$ARC_RUN/auth.ipc`. | Flag | Required | Description | | :--------------------------------------- | :---------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--chain arc-testnet` | Yes | Selects the Arc Testnet genesis configuration bundled in the binary | | `--datadir` | Yes | Path to the EL data directory | | `--full` | First start | Required on the first start from a pruned snapshot; optional after | | `--disable-discovery` | Yes | Disables P2P peer discovery (Arc uses relay endpoints instead) | | `--ipcpath` / `--auth-ipc` | Yes (IPC mode) | Paths for the IPC sockets the CL connects to | | `--http` / `--http.addr` | Yes | Enables the JSON-RPC endpoint on the specified address and port | | `--http.api` | Recommended | RPC namespaces to expose | | `--metrics` | Recommended | Enables Prometheus metrics on the specified address and port | | `--enable-arc-rpc` | Yes | Enables Arc-specific RPC methods | | `--rpc.forwarder` | Recommended | Routes submitted transactions to an RPC node for broadcast | | `--public-api` | Recommended for public-facing nodes | Hides pending-tx RPCs (a potential MEV vector) and warns if `--http.api` or `--ws.api` exposes namespaces beyond `eth`, `net`, `web3`, `rpc` | | `--rpc.max-connections` | Recommended for public-facing nodes | Maximum concurrent RPC connections. Default `250` (lowered from `500` in v0.7.1). Raise only if clients see `MaxConnections` errors | | `--rpc.max-subscriptions-per-connection` | Recommended for public-facing nodes | Maximum subscriptions per single WebSocket connection. Default `32` (lowered from `1024` in v0.7.1). Raise only if clients see `TooManySubscriptions` errors | On high-traffic public endpoints, raise `--rpc.max-connections` (default 250) and `--rpc.max-subscriptions-per-connection` (default 32) if clients see `MaxConnections` or `TooManySubscriptions` errors. The defaults bound WebSocket log-fanout memory growth and should only be raised, not lowered. The `--full` flag is required on the first start when bootstrapping from a pruned snapshot. It reconciles internal database tables that would otherwise fail a consistency check. After the initial startup completes, restart without `--full` if you prefer not to prune. `--chain arc-testnet` uses the bundled genesis configuration; replace with `--chain /path/to/genesis.json` if you have a custom genesis file. See [reth node](https://reth.rs/cli/reth/node/) for additional flags. ## Step 9: Start the consensus layer Open a separate terminal and start the Consensus Layer. The CL connects to the EL through the IPC sockets and begins fetching blocks from the network. ```shell theme={null} arc-node-consensus start \ --home "$ARC_CONSENSUS" \ --full \ --eth-socket "$ARC_RUN/reth.ipc" \ --execution-socket "$ARC_RUN/auth.ipc" \ --rpc.addr 127.0.0.1:31000 \ --follow \ --follow.endpoint https://rpc.testnet.arc.io,wss=rpc.testnet.arc.io \ --follow.endpoint https://rpc.drpc.testnet.arc.io,wss=rpc.drpc.testnet.arc.io \ --follow.endpoint https://rpc.blockdaemon.testnet.arc.io,wss=rpc.blockdaemon.testnet.arc.io/websocket \ --execution-persistence-backpressure \ --execution-persistence-backpressure-threshold=16 \ --metrics 127.0.0.1:29000 ``` | Flag | Required | Description | | :----------------------------------------------- | :------------- | :--------------------------------------------------------------------------------------------- | | `--home` | Yes | Path to the CL data directory (contains keys and state) | | `--full` | Recommended | Enables CL pruning to bound disk growth | | `--eth-socket` | Yes (IPC mode) | Path to the Execution Layer ETH RPC IPC socket | | `--execution-socket` | Yes (IPC mode) | Path to the Execution Layer Engine API IPC socket | | `--rpc.addr` | Recommended | CL RPC listen address and port | | `--follow` | Yes | Enables block-following mode through relay endpoints | | `--follow.endpoint` | Yes | Relay endpoint URLs. Specify multiple for redundancy. Format: `https://host,wss=host` | | `--execution-persistence-backpressure` | Recommended | Throttles execution to match disk-write speed, bounding EL memory growth | | `--execution-persistence-backpressure-threshold` | Recommended | Threshold for backpressure. Default `16` (must be `> 0`). Lower if you observe memory pressure | | `--metrics` | Recommended | Enables Prometheus metrics on the specified address and port | ## Step 10: Verify the node is syncing After both layers start, wait roughly 30 seconds, then query the local JSON-RPC endpoint for the latest block height: ```shell theme={null} curl -s -X POST http://localhost:8545 \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 1 }' ``` The `result` field is a hexadecimal block number that increases over time. If it stays at `0x0`, check the CL logs for errors. If you have [Foundry](https://book.getfoundry.sh/getting-started/installation) installed, you can run the same check with `cast`: ```shell theme={null} cast block-number --rpc-url http://localhost:8545 ``` Run it several times over a few seconds -- the block number increases as the node syncs. If the block number stays at `0x0` or the node does not start as expected, check these common causes: * **IPC socket files missing.** The EL writes sockets at `$ARC_RUN/reth.ipc` and `$ARC_RUN/auth.ipc` on startup. If they are not present after 30 seconds, the EL did not start -- review its terminal output for a panic or configuration error. * **CL cannot connect to the EL.** Start the EL first. If the CL was launched before the EL was ready, restart the CL. * **Snapshot extraction incomplete.** `arc-snapshots download` runs silently during extraction. If it was interrupted, `$ARC_EXECUTION` or `$ARC_CONSENSUS` may be partially populated. Re-run the download to overwrite the existing files. If you clear the directories first, note that `$ARC_CONSENSUS` also holds the CL private key written by `arc-node-consensus init`. Re-run that command after clearing, or your node loses its network identity. * **Path mismatch between EL and CL.** `$ARC_RUN` must resolve to the same directory in both shells. Re-source `~/.arc_env` in any terminal that lost its environment. Once your node is syncing steadily, you can [deploy it as a systemd service](/arc/tutorials/deploy-node-as-service) for production use, or [set up Prometheus and Grafana](/arc/tutorials/set-up-node-monitoring) to view the pre-built dashboards. Run the Arc node from pre-built Docker images using Docker Compose. Init containers handle snapshot download and CL initialization on the first run. ## Prerequisites Before you begin, confirm that you have: * Reviewed the [node requirements](/arc/references/node-requirements) for hardware, software, and network endpoints * Prepared a Linux machine that meets the minimum system requirements * Installed [Docker Engine 24+ with BuildKit](https://docs.docker.com/engine/install/) and [Docker Compose v2](https://docs.docker.com/compose/install/) ## Step 1: Set environment variables The Compose file reads images from environment variables. Set the version, data directory, and image references. See [node requirements](/arc/references/node-requirements#versions) for the current version: ```shell theme={null} export ARC_VERSION=0.7.2 export ARC_HOME=~/.arc export ARC_EXECUTION_IMAGE=docker.cloudsmith.io/circle/arc-network/arc-execution:$ARC_VERSION export ARC_CONSENSUS_IMAGE=docker.cloudsmith.io/circle/arc-network/arc-consensus:$ARC_VERSION ``` To build images locally instead of pulling from Cloudsmith, see [Installation: Docker](https://github.com/circlefin/arc-node/blob/main/docs/installation.md#docker) in the `arc-node` repository. ## Step 2: Prepare the data directory Create `$ARC_HOME` on the host before starting Docker Compose. If you skip this, Docker creates it as `root` and the snapshot init container fails with permission errors: ```shell theme={null} mkdir -p "${ARC_HOME:-$HOME/.arc}" ``` ## Step 3: Download the Compose file Fetch the `docker-compose.yml` for your release into a working directory: ```shell theme={null} curl -O https://raw.githubusercontent.com/circlefin/arc-node/v${ARC_VERSION}/deployments/docker-compose.yml ``` ## Step 4: Start the node From the directory containing `docker-compose.yml`: ```shell theme={null} docker compose up -d ``` On the first run, init containers automatically: 1. Download the latest testnet snapshots (approximately 84 GB compressed; see [snapshot download size](/arc/references/node-requirements#snapshot-download-size)) 2. Initialize the consensus layer private key 3. Prepare the shared IPC socket volume Subsequent runs detect that initialization is already complete and start immediately. The init container runs as `root` so it can set file ownership for the EL and CL services (UID 999). No manual `chown` is needed. ## Step 5: Verify the node is syncing On the first run, wait for the snapshot init container to finish: ```shell theme={null} docker compose logs -f arc-snapshots ``` Once the EL and CL containers are up, wait roughly 30 seconds, then query the latest block height: ```shell theme={null} curl -s -X POST http://localhost:8545 \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 1 }' ``` The `result` field is a hexadecimal block number that increases over time. If it stays at `0x0`, check the logs: ```shell theme={null} docker compose logs -f ``` The containers expose Prometheus metrics on the host at `localhost:9001/` (Execution Layer, root path) and `localhost:29000/metrics` (Consensus Layer). To scrape these endpoints with a local stack, see [Set up Prometheus and Grafana for an Arc node](/arc/tutorials/set-up-node-monitoring). ## Step 6: Stop and reset To stop the node while preserving data: ```shell theme={null} docker compose down ``` Node data persists in `~/.arc/` (or the path set by `$ARC_HOME`). To remove all data and start fresh: ```shell theme={null} docker compose down -v rm -rf ~/.arc ``` `docker compose down -v` removes the named sockets volume, and `rm -rf ~/.arc` permanently deletes the consensus layer private key -- your node's network identity. It cannot be recovered. ## Run on separate hosts The tabs above describe running the Execution Layer and Consensus Layer on the same host, communicating through IPC sockets. To run them on separate hosts, swap IPC for authenticated RPC. This section applies to the binary install paths (`arcup` and source build); the Docker path is single-host by default. ### Generate a JWT secret The EL and CL authenticate to each other with a shared JWT secret. Generate it once, then securely copy it to both hosts: ```shell theme={null} openssl rand -hex 32 | tr -d "\n" > "$ARC_HOME/jwtsecret" chmod 600 "$ARC_HOME/jwtsecret" ``` ### Execution layer flags for RPC mode Remove the IPC flags (`--ipcpath`, `--auth-ipc`, `--auth-ipc.path`) from the EL command and add: ```shell theme={null} --authrpc.addr 0.0.0.0 \ --authrpc.port 8551 \ --authrpc.jwtsecret "$ARC_HOME/jwtsecret" ``` `--authrpc.addr 0.0.0.0` exposes port 8551 on every network interface. Restrict access with a firewall rule or private network -- the Engine API controls block production and must not be reachable from the public internet. ### Consensus layer flags for RPC mode Remove `--eth-socket` and `--execution-socket` from the CL command, and add: ```shell theme={null} --eth-rpc-endpoint http://:8545 \ --execution-endpoint http://:8551 \ --execution-jwt "$ARC_HOME/jwtsecret" ``` Replace `` with the IP address or hostname of the EL host. IPC and RPC are mutually exclusive. Use one or the other, not both. ## Enable backpressure under memory pressure The tabs above start the Consensus Layer with backpressure enabled at the default threshold (`16`). Backpressure throttles execution to match the speed of disk writes, bounding Execution Layer memory growth during startup or extended sync when the node is far behind. If you observe sustained memory pressure on a host that meets the [node requirements](/arc/references/node-requirements), lower the threshold to `10` for more aggressive throttling. First, add the `reth` namespace to the EL `--http.api` flag so the CL can read backpressure metrics: ```shell theme={null} --http.api eth,net,web3,txpool,trace,debug,reth ``` Then restart the CL with the lower threshold: ```shell theme={null} --execution-persistence-backpressure \ --execution-persistence-backpressure-threshold=10 ``` **v0.7.1: Arc Testnet hardfork activates 2026-05-27 15:08:37 UTC.** The Zero5 and Zero6 hardforks activate at Unix timestamp `1779894517`. Testnet nodes must be on v0.7.1 by that timestamp; earlier versions stop syncing after activation. This release also tightens two Execution Layer RPC defaults (`--rpc.max-connections` and `--rpc.max-subscriptions-per-connection`). See [Step 6](#step-6-start-the-execution-layer) for guidance on raising them. For the full upgrade notes, see [CHANGELOG.md](https://github.com/circlefin/arc-node/blob/main/CHANGELOG.md) and [BREAKING\_CHANGES.md](https://github.com/circlefin/arc-node/blob/main/BREAKING_CHANGES.md#v071). An Arc node syncs blocks from the network and serves a local JSON-RPC endpoint so you can independently verify every block and transaction. It runs two processes: the Execution Layer (EL), which executes transactions and maintains state, and the Consensus Layer (CL), which fetches finalized blocks from relay endpoints (trusted HTTP servers that distribute signed block data), verifies their signatures, and passes them to the EL. For a deeper explanation of how these components fit together, see [Running a node](/arc/concepts/running-a-node). Arc is currently in its testnet phase. During this period, the network may experience instability or unplanned downtime. Throughout this page, all references to Arc refer specifically to the Arc Testnet. Choose the install path that fits your environment. Install pre-built Arc binaries using the `arcup` script. This is the fastest path and requires no Rust toolchain or build dependencies. ## Prerequisites Before you begin, confirm that you have: * Reviewed the [node requirements](/arc/references/node-requirements) for hardware, software, and network endpoints * Prepared a Linux or macOS machine that meets the minimum system requirements * Installed [Foundry](https://book.getfoundry.sh/getting-started/installation) (optional, provides the `cast` command used to verify your node) ## Step 1: Install with `arcup` `arcup` installs the Arc binaries under `$ARC_HOME` (default `~/.arc`). Export `ARC_HOME` first if you want to install somewhere else; otherwise the default applies for both the installer and the environment file: ```shell theme={null} export ARC_HOME="${ARC_HOME:-$HOME/.arc}" curl -L https://raw.githubusercontent.com/circlefin/arc-node/main/arcup/install | bash ``` The installer places `arc-node-execution`, `arc-node-consensus`, `arc-snapshots`, and `arcup` itself into `$ARC_HOME/bin`. Load the produced environment file so the binaries are on your `PATH`: ```shell theme={null} source "$ARC_HOME/env" ``` Verify the installation: ```shell theme={null} arc-snapshots --version arc-node-execution --version arc-node-consensus --version ``` Each command prints a version string. To update the binaries later, run `arcup`. ## Step 2: Set environment variables The remaining steps use a small set of variables to keep paths consistent. Write them to a file and source it: ```shell theme={null} cat << "EOF" > ~/.arc_env ARC_HOME="${ARC_HOME:-$HOME/.arc}" # Linux runtime directory: ARC_RUN="/run/arc" # macOS runtime directory (uncomment if on macOS): #ARC_RUN="$ARC_HOME/run" ARC_EXECUTION=$ARC_HOME/execution ARC_CONSENSUS=$ARC_HOME/consensus EOF source ~/.arc_env ``` ## Step 3: Create data directories Create the data directories for the EL and CL, plus the runtime directory used for IPC sockets: ```shell theme={null} mkdir -p "$ARC_EXECUTION" "$ARC_CONSENSUS" sudo install -d -o $USER "$ARC_RUN" ``` On macOS, replace the second command with `mkdir -p "$ARC_RUN"` after uncommenting the macOS line in `~/.arc_env`. When running as a systemd service, the `RuntimeDirectory=arc` directive creates `/run/arc` automatically. You can skip the second command in that case. ## Step 4: Download blockchain snapshots Download snapshots so the node starts syncing from a recent block height rather than from genesis: ```shell theme={null} arc-snapshots download \ --chain=arc-testnet \ --execution-path "$ARC_EXECUTION" \ --consensus-path "$ARC_CONSENSUS" ``` This command fetches the latest snapshot URLs from `https://snapshots.arc.network`, downloads the snapshots, and extracts them into `$ARC_EXECUTION` and `$ARC_CONSENSUS`. Snapshots are large. Testnet snapshots are approximately **68 GB EL + 16 GB CL compressed** (around 103 GB and 36 GB extracted). The download takes 10–15 minutes on a stable 100 Mbps connection; slower or metered connections can take hours. Ensure you have at least 150 GB of free disk space, a stable network connection, and available CPU. The terminal stops producing output during extraction. This is expected. ## Step 5: Initialize the consensus layer Generate the CL private key file used for network identity. This is a one-time setup step: ```shell theme={null} arc-node-consensus init --home "$ARC_CONSENSUS" ``` ## Step 6: Start the execution layer Start the Execution Layer. This creates the IPC sockets that the Consensus Layer connects to. ```shell theme={null} arc-node-execution node \ --chain arc-testnet \ --datadir "$ARC_EXECUTION" \ --full \ --disable-discovery \ --ipcpath "$ARC_RUN/reth.ipc" \ --auth-ipc \ --auth-ipc.path "$ARC_RUN/auth.ipc" \ --http \ --http.addr 127.0.0.1 \ --http.port 8545 \ --http.api eth,net,web3,txpool,trace,debug \ --metrics 127.0.0.1:9001 \ --enable-arc-rpc \ --rpc.forwarder https://rpc.testnet.arc.io/ ``` The EL starts and waits for blocks. Log output shows that the IPC sockets exist at `$ARC_RUN/reth.ipc` and `$ARC_RUN/auth.ipc`. | Flag | Required | Description | | :--------------------------------------- | :---------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--chain arc-testnet` | Yes | Selects the Arc Testnet genesis configuration bundled in the binary | | `--datadir` | Yes | Path to the EL data directory | | `--full` | First start | Required on the first start from a pruned snapshot; optional after | | `--disable-discovery` | Yes | Disables P2P peer discovery (Arc uses relay endpoints instead) | | `--ipcpath` / `--auth-ipc` | Yes (IPC mode) | Paths for the IPC sockets the CL connects to | | `--http` / `--http.addr` | Yes | Enables the JSON-RPC endpoint on the specified address and port | | `--http.api` | Recommended | RPC namespaces to expose | | `--metrics` | Recommended | Enables Prometheus metrics on the specified address and port | | `--enable-arc-rpc` | Yes | Enables Arc-specific RPC methods | | `--rpc.forwarder` | Recommended | Routes submitted transactions to an RPC node for broadcast | | `--public-api` | Recommended for public-facing nodes | Hides pending-tx RPCs (a potential MEV vector) and warns if `--http.api` or `--ws.api` exposes namespaces beyond `eth`, `net`, `web3`, `rpc` | | `--rpc.max-connections` | Recommended for public-facing nodes | Maximum concurrent RPC connections. Default `250` (lowered from `500` in v0.7.1). Raise only if clients see `MaxConnections` errors | | `--rpc.max-subscriptions-per-connection` | Recommended for public-facing nodes | Maximum subscriptions per single WebSocket connection. Default `32` (lowered from `1024` in v0.7.1). Raise only if clients see `TooManySubscriptions` errors | On high-traffic public endpoints, raise `--rpc.max-connections` (default 250) and `--rpc.max-subscriptions-per-connection` (default 32) if clients see `MaxConnections` or `TooManySubscriptions` errors. The defaults bound WebSocket log-fanout memory growth and should only be raised, not lowered. The `--full` flag is required on the first start when bootstrapping from a pruned snapshot. It reconciles internal database tables that would otherwise fail a consistency check. After the initial startup completes, restart without `--full` if you prefer not to prune. `--chain arc-testnet` uses the bundled genesis configuration; replace with `--chain /path/to/genesis.json` if you have a custom genesis file. See [reth node](https://reth.rs/cli/reth/node/) for additional flags. ## Step 7: Start the consensus layer Open a separate terminal and start the Consensus Layer. The CL connects to the EL through the IPC sockets and begins fetching blocks from the network. ```shell theme={null} arc-node-consensus start \ --home "$ARC_CONSENSUS" \ --full \ --eth-socket "$ARC_RUN/reth.ipc" \ --execution-socket "$ARC_RUN/auth.ipc" \ --rpc.addr 127.0.0.1:31000 \ --follow \ --follow.endpoint https://rpc.testnet.arc.io,wss=rpc.testnet.arc.io \ --follow.endpoint https://rpc.drpc.testnet.arc.io,wss=rpc.drpc.testnet.arc.io \ --follow.endpoint https://rpc.blockdaemon.testnet.arc.io,wss=rpc.blockdaemon.testnet.arc.io/websocket \ --execution-persistence-backpressure \ --execution-persistence-backpressure-threshold=16 \ --metrics 127.0.0.1:29000 ``` | Flag | Required | Description | | :----------------------------------------------- | :------------- | :--------------------------------------------------------------------------------------------- | | `--home` | Yes | Path to the CL data directory (contains keys and state) | | `--full` | Recommended | Enables CL pruning to bound disk growth | | `--eth-socket` | Yes (IPC mode) | Path to the Execution Layer ETH RPC IPC socket | | `--execution-socket` | Yes (IPC mode) | Path to the Execution Layer Engine API IPC socket | | `--rpc.addr` | Recommended | CL RPC listen address and port | | `--follow` | Yes | Enables block-following mode through relay endpoints | | `--follow.endpoint` | Yes | Relay endpoint URLs. Specify multiple for redundancy. Format: `https://host,wss=host` | | `--execution-persistence-backpressure` | Recommended | Throttles execution to match disk-write speed, bounding EL memory growth | | `--execution-persistence-backpressure-threshold` | Recommended | Threshold for backpressure. Default `16` (must be `> 0`). Lower if you observe memory pressure | | `--metrics` | Recommended | Enables Prometheus metrics on the specified address and port | ## Step 8: Verify the node is syncing After both layers start, wait roughly 30 seconds, then query the local JSON-RPC endpoint for the latest block height: ```shell theme={null} curl -s -X POST http://localhost:8545 \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 1 }' ``` The `result` field is a hexadecimal block number that increases over time. If it stays at `0x0`, check the CL logs for errors. If you have [Foundry](https://book.getfoundry.sh/getting-started/installation) installed, you can run the same check with `cast`: ```shell theme={null} cast block-number --rpc-url http://localhost:8545 ``` Run it several times over a few seconds -- the block number increases as the node syncs. If the block number stays at `0x0` or the node does not start as expected, check these common causes: * **IPC socket files missing.** The EL writes sockets at `$ARC_RUN/reth.ipc` and `$ARC_RUN/auth.ipc` on startup. If they are not present after 30 seconds, the EL did not start -- review its terminal output for a panic or configuration error. * **CL cannot connect to the EL.** Start the EL first. If the CL was launched before the EL was ready, restart the CL. * **Snapshot extraction incomplete.** `arc-snapshots download` runs silently during extraction. If it was interrupted, `$ARC_EXECUTION` or `$ARC_CONSENSUS` may be partially populated. Re-run the download to overwrite the existing files. If you clear the directories first, note that `$ARC_CONSENSUS` also holds the CL private key written by `arc-node-consensus init`. Re-run that command after clearing, or your node loses its network identity. * **Path mismatch between EL and CL.** `$ARC_RUN` must resolve to the same directory in both shells. Re-source `~/.arc_env` in any terminal that lost its environment. Once your node is syncing steadily, you can [deploy it as a systemd service](/arc/tutorials/deploy-node-as-service) for production use, or [set up Prometheus and Grafana](/arc/tutorials/set-up-node-monitoring) to view the pre-built dashboards. Build Arc binaries from the [`arc-node`](https://github.com/circlefin/arc-node) repository. Use this path to audit the build, run on an unsupported platform, or pin to a specific commit. ## Prerequisites Before you begin, confirm that you have: * Reviewed the [node requirements](/arc/references/node-requirements) for hardware, software, and network endpoints * Prepared a Linux, macOS, or Windows machine that meets the minimum system requirements * Installed [Foundry](https://book.getfoundry.sh/getting-started/installation) (optional, provides the `cast` command used to verify your node) ## Step 1: Install build dependencies Install [Rust](https://rust-lang.org/tools/install/) if you don't have it: ```shell theme={null} curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source ~/.cargo/env ``` Install the platform packages required to build the EL database bindings: * **Ubuntu:** `sudo apt-get install libclang-dev pkg-config build-essential` * **macOS:** `brew install llvm pkg-config`, then `export LIBCLANG_PATH="$(brew --prefix llvm)/lib"` * **Windows:** `choco install llvm` or `winget install LLVM.LLVM` ## Step 2: Clone the repository Clone the [`arc-node`](https://github.com/circlefin/arc-node) repository and check out the version for your target network. See [node requirements](/arc/references/node-requirements#versions) for the current version: ```shell theme={null} git clone https://github.com/circlefin/arc-node.git cd arc-node git checkout v0.7.1 git submodule update --init --recursive ``` ## Step 3: Build and install The following commands build three Arc node binaries: `arc-node-execution`, `arc-node-consensus`, and `arc-snapshots`: ```shell theme={null} cargo install --path crates/node cargo install --path crates/malachite-app cargo install --path crates/snapshots ``` `cargo install` places compiled binaries into `~/.cargo/bin`, which is added to `PATH` by loading `~/.cargo/env`. To install elsewhere, add `--root $BASE_DIR` to each command (for instance, `--root /usr/local` places them under `/usr/local/bin`). Verify the installation: ```shell theme={null} arc-snapshots --version arc-node-execution --version arc-node-consensus --version ``` ## Step 4: Set environment variables The remaining steps use a small set of variables to keep paths consistent. Write them to a file and source it: ```shell theme={null} cat << "EOF" > ~/.arc_env ARC_HOME="${ARC_HOME:-$HOME/.arc}" # Linux runtime directory: ARC_RUN="/run/arc" # macOS runtime directory (uncomment if on macOS): #ARC_RUN="$ARC_HOME/run" ARC_EXECUTION=$ARC_HOME/execution ARC_CONSENSUS=$ARC_HOME/consensus EOF source ~/.arc_env ``` ## Step 5: Create data directories Create the data directories for the EL and CL, plus the runtime directory used for IPC sockets: ```shell theme={null} mkdir -p "$ARC_EXECUTION" "$ARC_CONSENSUS" sudo install -d -o $USER "$ARC_RUN" ``` On macOS, replace the second command with `mkdir -p "$ARC_RUN"` after uncommenting the macOS line in `~/.arc_env`. When running as a systemd service, the `RuntimeDirectory=arc` directive creates `/run/arc` automatically. You can skip the second command in that case. ## Step 6: Download blockchain snapshots Download snapshots so the node starts syncing from a recent block height rather than from genesis: ```shell theme={null} arc-snapshots download \ --chain=arc-testnet \ --execution-path "$ARC_EXECUTION" \ --consensus-path "$ARC_CONSENSUS" ``` This command fetches the latest snapshot URLs from `https://snapshots.arc.network`, downloads the snapshots, and extracts them into `$ARC_EXECUTION` and `$ARC_CONSENSUS`. Snapshots are large. Testnet snapshots are approximately **68 GB EL + 16 GB CL compressed** (around 103 GB and 36 GB extracted). The download takes 10–15 minutes on a stable 100 Mbps connection; slower or metered connections can take hours. Ensure you have at least 150 GB of free disk space, a stable network connection, and available CPU. The terminal stops producing output during extraction. This is expected. ## Step 7: Initialize the consensus layer Generate the CL private key file used for network identity. This is a one-time setup step: ```shell theme={null} arc-node-consensus init --home "$ARC_CONSENSUS" ``` ## Step 8: Start the execution layer Start the Execution Layer. This creates the IPC sockets that the Consensus Layer connects to. ```shell theme={null} arc-node-execution node \ --chain arc-testnet \ --datadir "$ARC_EXECUTION" \ --full \ --disable-discovery \ --ipcpath "$ARC_RUN/reth.ipc" \ --auth-ipc \ --auth-ipc.path "$ARC_RUN/auth.ipc" \ --http \ --http.addr 127.0.0.1 \ --http.port 8545 \ --http.api eth,net,web3,txpool,trace,debug \ --metrics 127.0.0.1:9001 \ --enable-arc-rpc \ --rpc.forwarder https://rpc.testnet.arc.io/ ``` The EL starts and waits for blocks. Log output shows that the IPC sockets exist at `$ARC_RUN/reth.ipc` and `$ARC_RUN/auth.ipc`. | Flag | Required | Description | | :--------------------------------------- | :---------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--chain arc-testnet` | Yes | Selects the Arc Testnet genesis configuration bundled in the binary | | `--datadir` | Yes | Path to the EL data directory | | `--full` | First start | Required on the first start from a pruned snapshot; optional after | | `--disable-discovery` | Yes | Disables P2P peer discovery (Arc uses relay endpoints instead) | | `--ipcpath` / `--auth-ipc` | Yes (IPC mode) | Paths for the IPC sockets the CL connects to | | `--http` / `--http.addr` | Yes | Enables the JSON-RPC endpoint on the specified address and port | | `--http.api` | Recommended | RPC namespaces to expose | | `--metrics` | Recommended | Enables Prometheus metrics on the specified address and port | | `--enable-arc-rpc` | Yes | Enables Arc-specific RPC methods | | `--rpc.forwarder` | Recommended | Routes submitted transactions to an RPC node for broadcast | | `--public-api` | Recommended for public-facing nodes | Hides pending-tx RPCs (a potential MEV vector) and warns if `--http.api` or `--ws.api` exposes namespaces beyond `eth`, `net`, `web3`, `rpc` | | `--rpc.max-connections` | Recommended for public-facing nodes | Maximum concurrent RPC connections. Default `250` (lowered from `500` in v0.7.1). Raise only if clients see `MaxConnections` errors | | `--rpc.max-subscriptions-per-connection` | Recommended for public-facing nodes | Maximum subscriptions per single WebSocket connection. Default `32` (lowered from `1024` in v0.7.1). Raise only if clients see `TooManySubscriptions` errors | On high-traffic public endpoints, raise `--rpc.max-connections` (default 250) and `--rpc.max-subscriptions-per-connection` (default 32) if clients see `MaxConnections` or `TooManySubscriptions` errors. The defaults bound WebSocket log-fanout memory growth and should only be raised, not lowered. The `--full` flag is required on the first start when bootstrapping from a pruned snapshot. It reconciles internal database tables that would otherwise fail a consistency check. After the initial startup completes, restart without `--full` if you prefer not to prune. `--chain arc-testnet` uses the bundled genesis configuration; replace with `--chain /path/to/genesis.json` if you have a custom genesis file. See [reth node](https://reth.rs/cli/reth/node/) for additional flags. ## Step 9: Start the consensus layer Open a separate terminal and start the Consensus Layer. The CL connects to the EL through the IPC sockets and begins fetching blocks from the network. ```shell theme={null} arc-node-consensus start \ --home "$ARC_CONSENSUS" \ --full \ --eth-socket "$ARC_RUN/reth.ipc" \ --execution-socket "$ARC_RUN/auth.ipc" \ --rpc.addr 127.0.0.1:31000 \ --follow \ --follow.endpoint https://rpc.testnet.arc.io,wss=rpc.testnet.arc.io \ --follow.endpoint https://rpc.drpc.testnet.arc.io,wss=rpc.drpc.testnet.arc.io \ --follow.endpoint https://rpc.blockdaemon.testnet.arc.io,wss=rpc.blockdaemon.testnet.arc.io/websocket \ --execution-persistence-backpressure \ --execution-persistence-backpressure-threshold=16 \ --metrics 127.0.0.1:29000 ``` | Flag | Required | Description | | :----------------------------------------------- | :------------- | :--------------------------------------------------------------------------------------------- | | `--home` | Yes | Path to the CL data directory (contains keys and state) | | `--full` | Recommended | Enables CL pruning to bound disk growth | | `--eth-socket` | Yes (IPC mode) | Path to the Execution Layer ETH RPC IPC socket | | `--execution-socket` | Yes (IPC mode) | Path to the Execution Layer Engine API IPC socket | | `--rpc.addr` | Recommended | CL RPC listen address and port | | `--follow` | Yes | Enables block-following mode through relay endpoints | | `--follow.endpoint` | Yes | Relay endpoint URLs. Specify multiple for redundancy. Format: `https://host,wss=host` | | `--execution-persistence-backpressure` | Recommended | Throttles execution to match disk-write speed, bounding EL memory growth | | `--execution-persistence-backpressure-threshold` | Recommended | Threshold for backpressure. Default `16` (must be `> 0`). Lower if you observe memory pressure | | `--metrics` | Recommended | Enables Prometheus metrics on the specified address and port | ## Step 10: Verify the node is syncing After both layers start, wait roughly 30 seconds, then query the local JSON-RPC endpoint for the latest block height: ```shell theme={null} curl -s -X POST http://localhost:8545 \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 1 }' ``` The `result` field is a hexadecimal block number that increases over time. If it stays at `0x0`, check the CL logs for errors. If you have [Foundry](https://book.getfoundry.sh/getting-started/installation) installed, you can run the same check with `cast`: ```shell theme={null} cast block-number --rpc-url http://localhost:8545 ``` Run it several times over a few seconds -- the block number increases as the node syncs. If the block number stays at `0x0` or the node does not start as expected, check these common causes: * **IPC socket files missing.** The EL writes sockets at `$ARC_RUN/reth.ipc` and `$ARC_RUN/auth.ipc` on startup. If they are not present after 30 seconds, the EL did not start -- review its terminal output for a panic or configuration error. * **CL cannot connect to the EL.** Start the EL first. If the CL was launched before the EL was ready, restart the CL. * **Snapshot extraction incomplete.** `arc-snapshots download` runs silently during extraction. If it was interrupted, `$ARC_EXECUTION` or `$ARC_CONSENSUS` may be partially populated. Re-run the download to overwrite the existing files. If you clear the directories first, note that `$ARC_CONSENSUS` also holds the CL private key written by `arc-node-consensus init`. Re-run that command after clearing, or your node loses its network identity. * **Path mismatch between EL and CL.** `$ARC_RUN` must resolve to the same directory in both shells. Re-source `~/.arc_env` in any terminal that lost its environment. Once your node is syncing steadily, you can [deploy it as a systemd service](/arc/tutorials/deploy-node-as-service) for production use, or [set up Prometheus and Grafana](/arc/tutorials/set-up-node-monitoring) to view the pre-built dashboards. Run the Arc node from pre-built Docker images using Docker Compose. Init containers handle snapshot download and CL initialization on the first run. ## Prerequisites Before you begin, confirm that you have: * Reviewed the [node requirements](/arc/references/node-requirements) for hardware, software, and network endpoints * Prepared a Linux machine that meets the minimum system requirements * Installed [Docker Engine 24+ with BuildKit](https://docs.docker.com/engine/install/) and [Docker Compose v2](https://docs.docker.com/compose/install/) ## Step 1: Set environment variables The Compose file reads images from environment variables. Set the version, data directory, and image references. See [node requirements](/arc/references/node-requirements#versions) for the current version: ```shell theme={null} export ARC_VERSION=0.7.1 export ARC_HOME=~/.arc export ARC_EXECUTION_IMAGE=docker.cloudsmith.io/circle/arc-network/arc-execution:$ARC_VERSION export ARC_CONSENSUS_IMAGE=docker.cloudsmith.io/circle/arc-network/arc-consensus:$ARC_VERSION ``` To build images locally instead of pulling from Cloudsmith, see [Installation: Docker](https://github.com/circlefin/arc-node/blob/main/docs/installation.md#docker) in the `arc-node` repository. ## Step 2: Prepare the data directory Create `$ARC_HOME` on the host before starting Docker Compose. If you skip this, Docker creates it as `root` and the snapshot init container fails with permission errors: ```shell theme={null} mkdir -p "${ARC_HOME:-$HOME/.arc}" ``` ## Step 3: Download the Compose file Fetch the `docker-compose.yml` for your release into a working directory: ```shell theme={null} curl -O https://raw.githubusercontent.com/circlefin/arc-node/v${ARC_VERSION}/deployments/docker-compose.yml ``` ## Step 4: Start the node From the directory containing `docker-compose.yml`: ```shell theme={null} docker compose up -d ``` On the first run, init containers automatically: 1. Download the latest testnet snapshots (approximately 84 GB compressed; see [snapshot download size](/arc/references/node-requirements#snapshot-download-size)) 2. Initialize the consensus layer private key 3. Prepare the shared IPC socket volume Subsequent runs detect that initialization is already complete and start immediately. The init container runs as `root` so it can set file ownership for the EL and CL services (UID 999). No manual `chown` is needed. ## Step 5: Verify the node is syncing On the first run, wait for the snapshot init container to finish: ```shell theme={null} docker compose logs -f arc-snapshots ``` Once the EL and CL containers are up, wait roughly 30 seconds, then query the latest block height: ```shell theme={null} curl -s -X POST http://localhost:8545 \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 1 }' ``` The `result` field is a hexadecimal block number that increases over time. If it stays at `0x0`, check the logs: ```shell theme={null} docker compose logs -f ``` The containers expose Prometheus metrics on the host at `localhost:9001/` (Execution Layer, root path) and `localhost:29000/metrics` (Consensus Layer). To scrape these endpoints with a local stack, see [Set up Prometheus and Grafana for an Arc node](/arc/tutorials/set-up-node-monitoring). ## Step 6: Stop and reset To stop the node while preserving data: ```shell theme={null} docker compose down ``` Node data persists in `~/.arc/` (or the path set by `$ARC_HOME`). To remove all data and start fresh: ```shell theme={null} docker compose down -v rm -rf ~/.arc ``` `docker compose down -v` removes the named sockets volume, and `rm -rf ~/.arc` permanently deletes the consensus layer private key -- your node's network identity. It cannot be recovered. ## Run on separate hosts The tabs above describe running the Execution Layer and Consensus Layer on the same host, communicating through IPC sockets. To run them on separate hosts, swap IPC for authenticated RPC. This section applies to the binary install paths (`arcup` and source build); the Docker path is single-host by default. ### Generate a JWT secret The EL and CL authenticate to each other with a shared JWT secret. Generate it once, then securely copy it to both hosts: ```shell theme={null} openssl rand -hex 32 | tr -d "\n" > "$ARC_HOME/jwtsecret" chmod 600 "$ARC_HOME/jwtsecret" ``` ### Execution layer flags for RPC mode Remove the IPC flags (`--ipcpath`, `--auth-ipc`, `--auth-ipc.path`) from the EL command and add: ```shell theme={null} --authrpc.addr 0.0.0.0 \ --authrpc.port 8551 \ --authrpc.jwtsecret "$ARC_HOME/jwtsecret" ``` `--authrpc.addr 0.0.0.0` exposes port 8551 on every network interface. Restrict access with a firewall rule or private network -- the Engine API controls block production and must not be reachable from the public internet. ### Consensus layer flags for RPC mode Remove `--eth-socket` and `--execution-socket` from the CL command, and add: ```shell theme={null} --eth-rpc-endpoint http://:8545 \ --execution-endpoint http://:8551 \ --execution-jwt "$ARC_HOME/jwtsecret" ``` Replace `` with the IP address or hostname of the EL host. IPC and RPC are mutually exclusive. Use one or the other, not both. ## Enable backpressure under memory pressure The tabs above start the Consensus Layer with backpressure enabled at the default threshold (`16`). Backpressure throttles execution to match the speed of disk writes, bounding Execution Layer memory growth during startup or extended sync when the node is far behind. If you observe sustained memory pressure on a host that meets the [node requirements](/arc/references/node-requirements), lower the threshold to `10` for more aggressive throttling. First, add the `reth` namespace to the EL `--http.api` flag so the CL can read backpressure metrics: ```shell theme={null} --http.api eth,net,web3,txpool,trace,debug,reth ``` Then restart the CL with the lower threshold: ```shell theme={null} --execution-persistence-backpressure \ --execution-persistence-backpressure-threshold=10 ``` **Upgrade required by 2026-05-27 15:08:37 UTC.** The Zero5 and Zero6 hardforks activate at Unix timestamp `1779894517`. v0.7.0 stops syncing on Arc Testnet after activation. Switch the view at the top of this page to **v0.7.1 (latest)** and follow those instructions before the deadline. See [BREAKING\_CHANGES.md](https://github.com/circlefin/arc-node/blob/main/BREAKING_CHANGES.md#v071) for migration details. An Arc node syncs blocks from the network and serves a local JSON-RPC endpoint so you can independently verify every block and transaction. It runs two processes: the Execution Layer (EL), which executes transactions and maintains state, and the Consensus Layer (CL), which fetches finalized blocks from relay endpoints (trusted HTTP servers that distribute signed block data), verifies their signatures, and passes them to the EL. For a deeper explanation of how these components fit together, see [Running a node](/arc/concepts/running-a-node). Arc is currently in its testnet phase. During this period, the network may experience instability or unplanned downtime. Throughout this page, all references to Arc refer specifically to the Arc Testnet. Choose the install path that fits your environment. Install pre-built Arc binaries using the `arcup` script. This is the fastest path and requires no Rust toolchain or build dependencies. ## Prerequisites Before you begin, confirm that you have: * Reviewed the [node requirements](/arc/references/node-requirements) for hardware, software, and network endpoints * Prepared a Linux or macOS machine that meets the minimum system requirements * Installed [Foundry](https://book.getfoundry.sh/getting-started/installation) (optional, provides the `cast` command used to verify your node) ## Step 1: Install with `arcup` `arcup` installs the Arc binaries under `$ARC_HOME` (default `~/.arc`). Export `ARC_HOME` first if you want to install somewhere else; otherwise the default applies for both the installer and the environment file: ```shell theme={null} export ARC_HOME="${ARC_HOME:-$HOME/.arc}" curl -L https://raw.githubusercontent.com/circlefin/arc-node/main/arcup/install | bash ``` The installer places `arc-node-execution`, `arc-node-consensus`, `arc-snapshots`, and `arcup` itself into `$ARC_HOME/bin`. Load the produced environment file so the binaries are on your `PATH`: ```shell theme={null} source "$ARC_HOME/env" ``` Verify the installation: ```shell theme={null} arc-snapshots --version arc-node-execution --version arc-node-consensus --version ``` Each command prints a version string. To update the binaries later, run `arcup`. ## Step 2: Set environment variables The remaining steps use a small set of variables to keep paths consistent. Write them to a file and source it: ```shell theme={null} cat << "EOF" > ~/.arc_env ARC_HOME="${ARC_HOME:-$HOME/.arc}" # Linux runtime directory: ARC_RUN="/run/arc" # macOS runtime directory (uncomment if on macOS): #ARC_RUN="$ARC_HOME/run" ARC_EXECUTION=$ARC_HOME/execution ARC_CONSENSUS=$ARC_HOME/consensus EOF source ~/.arc_env ``` ## Step 3: Create data directories Create the data directories for the EL and CL, plus the runtime directory used for IPC sockets: ```shell theme={null} mkdir -p "$ARC_EXECUTION" "$ARC_CONSENSUS" sudo install -d -o $USER "$ARC_RUN" ``` On macOS, replace the second command with `mkdir -p "$ARC_RUN"` after uncommenting the macOS line in `~/.arc_env`. When running as a systemd service, the `RuntimeDirectory=arc` directive creates `/run/arc` automatically. You can skip the second command in that case. ## Step 4: Download blockchain snapshots Download snapshots so the node starts syncing from a recent block height rather than from genesis: ```shell theme={null} arc-snapshots download \ --chain=arc-testnet \ --execution-path "$ARC_EXECUTION" \ --consensus-path "$ARC_CONSENSUS" ``` This command fetches the latest snapshot URLs from `https://snapshots.arc.network`, downloads the snapshots, and extracts them into `$ARC_EXECUTION` and `$ARC_CONSENSUS`. Snapshots are large. Testnet snapshots are approximately **68 GB EL + 16 GB CL compressed** (around 103 GB and 36 GB extracted). The download takes 10–15 minutes on a stable 100 Mbps connection; slower or metered connections can take hours. Ensure you have at least 150 GB of free disk space, a stable network connection, and available CPU. The terminal stops producing output during extraction. This is expected. ## Step 5: Initialize the consensus layer Generate the CL private key file used for network identity. This is a one-time setup step: ```shell theme={null} arc-node-consensus init --home "$ARC_CONSENSUS" ``` ## Step 6: Start the execution layer Start the Execution Layer. This creates the IPC sockets that the Consensus Layer connects to. ```shell theme={null} arc-node-execution node \ --chain arc-testnet \ --datadir "$ARC_EXECUTION" \ --full \ --disable-discovery \ --ipcpath "$ARC_RUN/reth.ipc" \ --auth-ipc \ --auth-ipc.path "$ARC_RUN/auth.ipc" \ --http \ --http.addr 127.0.0.1 \ --http.port 8545 \ --http.api eth,net,web3,txpool,trace,debug \ --metrics 127.0.0.1:9001 \ --enable-arc-rpc \ --rpc.forwarder https://rpc.testnet.arc.io/ ``` The EL starts and waits for blocks. Log output shows that the IPC sockets exist at `$ARC_RUN/reth.ipc` and `$ARC_RUN/auth.ipc`. | Flag | Required | Description | | :------------------------- | :---------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------- | | `--chain arc-testnet` | Yes | Selects the Arc Testnet genesis configuration bundled in the binary | | `--datadir` | Yes | Path to the EL data directory | | `--full` | First start | Required on the first start from a pruned snapshot; optional after | | `--disable-discovery` | Yes | Disables P2P peer discovery (Arc uses relay endpoints instead) | | `--ipcpath` / `--auth-ipc` | Yes (IPC mode) | Paths for the IPC sockets the CL connects to | | `--http` / `--http.addr` | Yes | Enables the JSON-RPC endpoint on the specified address and port | | `--http.api` | Recommended | RPC namespaces to expose | | `--metrics` | Recommended | Enables Prometheus metrics on the specified address and port | | `--enable-arc-rpc` | Yes | Enables Arc-specific RPC methods | | `--rpc.forwarder` | Recommended | Routes submitted transactions to an RPC node for broadcast | | `--public-api` | Recommended for public-facing nodes | Hides pending-tx RPCs (a potential MEV vector) and warns if `--http.api` or `--ws.api` exposes namespaces beyond `eth`, `net`, `web3`, `rpc` | The `--full` flag is required on the first start when bootstrapping from a pruned snapshot. It reconciles internal database tables that would otherwise fail a consistency check. After the initial startup completes, restart without `--full` if you prefer not to prune. `--chain arc-testnet` uses the bundled genesis configuration; replace with `--chain /path/to/genesis.json` if you have a custom genesis file. See [reth node](https://reth.rs/cli/reth/node/) for additional flags. ## Step 7: Start the consensus layer Open a separate terminal and start the Consensus Layer. The CL connects to the EL through the IPC sockets and begins fetching blocks from the network. ```shell theme={null} arc-node-consensus start \ --home "$ARC_CONSENSUS" \ --full \ --eth-socket "$ARC_RUN/reth.ipc" \ --execution-socket "$ARC_RUN/auth.ipc" \ --rpc.addr 127.0.0.1:31000 \ --follow \ --follow.endpoint https://rpc.testnet.arc.io,wss=rpc.testnet.arc.io \ --follow.endpoint https://rpc.drpc.testnet.arc.io,wss=rpc.drpc.testnet.arc.io \ --follow.endpoint https://rpc.blockdaemon.testnet.arc.io,wss=rpc.blockdaemon.testnet.arc.io/websocket \ --execution-persistence-backpressure \ --execution-persistence-backpressure-threshold=16 \ --metrics 127.0.0.1:29000 ``` | Flag | Required | Description | | :----------------------------------------------- | :------------- | :--------------------------------------------------------------------------------------------- | | `--home` | Yes | Path to the CL data directory (contains keys and state) | | `--full` | Recommended | Enables CL pruning to bound disk growth | | `--eth-socket` | Yes (IPC mode) | Path to the Execution Layer ETH RPC IPC socket | | `--execution-socket` | Yes (IPC mode) | Path to the Execution Layer Engine API IPC socket | | `--rpc.addr` | Recommended | CL RPC listen address and port | | `--follow` | Yes | Enables block-following mode through relay endpoints | | `--follow.endpoint` | Yes | Relay endpoint URLs. Specify multiple for redundancy. Format: `https://host,wss=host` | | `--execution-persistence-backpressure` | Recommended | Throttles execution to match disk-write speed, bounding EL memory growth | | `--execution-persistence-backpressure-threshold` | Recommended | Threshold for backpressure. Default `16` (must be `> 0`). Lower if you observe memory pressure | | `--metrics` | Recommended | Enables Prometheus metrics on the specified address and port | ## Step 8: Verify the node is syncing After both layers start, wait roughly 30 seconds, then query the local JSON-RPC endpoint for the latest block height: ```shell theme={null} curl -s -X POST http://localhost:8545 \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 1 }' ``` The `result` field is a hexadecimal block number that increases over time. If it stays at `0x0`, check the CL logs for errors. If you have [Foundry](https://book.getfoundry.sh/getting-started/installation) installed, you can run the same check with `cast`: ```shell theme={null} cast block-number --rpc-url http://localhost:8545 ``` Run it several times over a few seconds -- the block number increases as the node syncs. If the block number stays at `0x0` or the node does not start as expected, check these common causes: * **IPC socket files missing.** The EL writes sockets at `$ARC_RUN/reth.ipc` and `$ARC_RUN/auth.ipc` on startup. If they are not present after 30 seconds, the EL did not start -- review its terminal output for a panic or configuration error. * **CL cannot connect to the EL.** Start the EL first. If the CL was launched before the EL was ready, restart the CL. * **Snapshot extraction incomplete.** `arc-snapshots download` runs silently during extraction. If it was interrupted, `$ARC_EXECUTION` or `$ARC_CONSENSUS` may be partially populated. Re-run the download to overwrite the existing files. If you clear the directories first, note that `$ARC_CONSENSUS` also holds the CL private key written by `arc-node-consensus init`. Re-run that command after clearing, or your node loses its network identity. * **Path mismatch between EL and CL.** `$ARC_RUN` must resolve to the same directory in both shells. Re-source `~/.arc_env` in any terminal that lost its environment. Once your node is syncing steadily, you can [deploy it as a systemd service](/arc/tutorials/deploy-node-as-service) for production use, or [set up Prometheus and Grafana](/arc/tutorials/set-up-node-monitoring) to view the pre-built dashboards. Build Arc binaries from the [`arc-node`](https://github.com/circlefin/arc-node) repository. Use this path to audit the build, run on an unsupported platform, or pin to a specific commit. ## Prerequisites Before you begin, confirm that you have: * Reviewed the [node requirements](/arc/references/node-requirements) for hardware, software, and network endpoints * Prepared a Linux, macOS, or Windows machine that meets the minimum system requirements * Installed [Foundry](https://book.getfoundry.sh/getting-started/installation) (optional, provides the `cast` command used to verify your node) ## Step 1: Install build dependencies Install [Rust](https://rust-lang.org/tools/install/) if you don't have it: ```shell theme={null} curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source ~/.cargo/env ``` Install the platform packages required to build the EL database bindings: * **Ubuntu:** `sudo apt-get install libclang-dev pkg-config build-essential` * **macOS:** `brew install llvm pkg-config`, then `export LIBCLANG_PATH="$(brew --prefix llvm)/lib"` * **Windows:** `choco install llvm` or `winget install LLVM.LLVM` ## Step 2: Clone the repository Clone the [`arc-node`](https://github.com/circlefin/arc-node) repository and check out the version for your target network. See [node requirements](/arc/references/node-requirements#versions) for the current version: ```shell theme={null} git clone https://github.com/circlefin/arc-node.git cd arc-node git checkout v0.7.0 git submodule update --init --recursive ``` ## Step 3: Build and install The following commands build three Arc node binaries: `arc-node-execution`, `arc-node-consensus`, and `arc-snapshots`: ```shell theme={null} cargo install --path crates/node cargo install --path crates/malachite-app cargo install --path crates/snapshots ``` `cargo install` places compiled binaries into `~/.cargo/bin`, which is added to `PATH` by loading `~/.cargo/env`. To install elsewhere, add `--root $BASE_DIR` to each command (for instance, `--root /usr/local` places them under `/usr/local/bin`). Verify the installation: ```shell theme={null} arc-snapshots --version arc-node-execution --version arc-node-consensus --version ``` ## Step 4: Set environment variables The remaining steps use a small set of variables to keep paths consistent. Write them to a file and source it: ```shell theme={null} cat << "EOF" > ~/.arc_env ARC_HOME="${ARC_HOME:-$HOME/.arc}" # Linux runtime directory: ARC_RUN="/run/arc" # macOS runtime directory (uncomment if on macOS): #ARC_RUN="$ARC_HOME/run" ARC_EXECUTION=$ARC_HOME/execution ARC_CONSENSUS=$ARC_HOME/consensus EOF source ~/.arc_env ``` ## Step 5: Create data directories Create the data directories for the EL and CL, plus the runtime directory used for IPC sockets: ```shell theme={null} mkdir -p "$ARC_EXECUTION" "$ARC_CONSENSUS" sudo install -d -o $USER "$ARC_RUN" ``` On macOS, replace the second command with `mkdir -p "$ARC_RUN"` after uncommenting the macOS line in `~/.arc_env`. When running as a systemd service, the `RuntimeDirectory=arc` directive creates `/run/arc` automatically. You can skip the second command in that case. ## Step 6: Download blockchain snapshots Download snapshots so the node starts syncing from a recent block height rather than from genesis: ```shell theme={null} arc-snapshots download \ --chain=arc-testnet \ --execution-path "$ARC_EXECUTION" \ --consensus-path "$ARC_CONSENSUS" ``` This command fetches the latest snapshot URLs from `https://snapshots.arc.network`, downloads the snapshots, and extracts them into `$ARC_EXECUTION` and `$ARC_CONSENSUS`. Snapshots are large. Testnet snapshots are approximately **68 GB EL + 16 GB CL compressed** (around 103 GB and 36 GB extracted). The download takes 10–15 minutes on a stable 100 Mbps connection; slower or metered connections can take hours. Ensure you have at least 150 GB of free disk space, a stable network connection, and available CPU. The terminal stops producing output during extraction. This is expected. ## Step 7: Initialize the consensus layer Generate the CL private key file used for network identity. This is a one-time setup step: ```shell theme={null} arc-node-consensus init --home "$ARC_CONSENSUS" ``` ## Step 8: Start the execution layer Start the Execution Layer. This creates the IPC sockets that the Consensus Layer connects to. ```shell theme={null} arc-node-execution node \ --chain arc-testnet \ --datadir "$ARC_EXECUTION" \ --full \ --disable-discovery \ --ipcpath "$ARC_RUN/reth.ipc" \ --auth-ipc \ --auth-ipc.path "$ARC_RUN/auth.ipc" \ --http \ --http.addr 127.0.0.1 \ --http.port 8545 \ --http.api eth,net,web3,txpool,trace,debug \ --metrics 127.0.0.1:9001 \ --enable-arc-rpc \ --rpc.forwarder https://rpc.testnet.arc.io/ ``` The EL starts and waits for blocks. Log output shows that the IPC sockets exist at `$ARC_RUN/reth.ipc` and `$ARC_RUN/auth.ipc`. | Flag | Required | Description | | :------------------------- | :---------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------- | | `--chain arc-testnet` | Yes | Selects the Arc Testnet genesis configuration bundled in the binary | | `--datadir` | Yes | Path to the EL data directory | | `--full` | First start | Required on the first start from a pruned snapshot; optional after | | `--disable-discovery` | Yes | Disables P2P peer discovery (Arc uses relay endpoints instead) | | `--ipcpath` / `--auth-ipc` | Yes (IPC mode) | Paths for the IPC sockets the CL connects to | | `--http` / `--http.addr` | Yes | Enables the JSON-RPC endpoint on the specified address and port | | `--http.api` | Recommended | RPC namespaces to expose | | `--metrics` | Recommended | Enables Prometheus metrics on the specified address and port | | `--enable-arc-rpc` | Yes | Enables Arc-specific RPC methods | | `--rpc.forwarder` | Recommended | Routes submitted transactions to an RPC node for broadcast | | `--public-api` | Recommended for public-facing nodes | Hides pending-tx RPCs (a potential MEV vector) and warns if `--http.api` or `--ws.api` exposes namespaces beyond `eth`, `net`, `web3`, `rpc` | The `--full` flag is required on the first start when bootstrapping from a pruned snapshot. It reconciles internal database tables that would otherwise fail a consistency check. After the initial startup completes, restart without `--full` if you prefer not to prune. `--chain arc-testnet` uses the bundled genesis configuration; replace with `--chain /path/to/genesis.json` if you have a custom genesis file. See [reth node](https://reth.rs/cli/reth/node/) for additional flags. ## Step 9: Start the consensus layer Open a separate terminal and start the Consensus Layer. The CL connects to the EL through the IPC sockets and begins fetching blocks from the network. ```shell theme={null} arc-node-consensus start \ --home "$ARC_CONSENSUS" \ --full \ --eth-socket "$ARC_RUN/reth.ipc" \ --execution-socket "$ARC_RUN/auth.ipc" \ --rpc.addr 127.0.0.1:31000 \ --follow \ --follow.endpoint https://rpc.testnet.arc.io,wss=rpc.testnet.arc.io \ --follow.endpoint https://rpc.drpc.testnet.arc.io,wss=rpc.drpc.testnet.arc.io \ --follow.endpoint https://rpc.blockdaemon.testnet.arc.io,wss=rpc.blockdaemon.testnet.arc.io/websocket \ --execution-persistence-backpressure \ --execution-persistence-backpressure-threshold=16 \ --metrics 127.0.0.1:29000 ``` | Flag | Required | Description | | :----------------------------------------------- | :------------- | :--------------------------------------------------------------------------------------------- | | `--home` | Yes | Path to the CL data directory (contains keys and state) | | `--full` | Recommended | Enables CL pruning to bound disk growth | | `--eth-socket` | Yes (IPC mode) | Path to the Execution Layer ETH RPC IPC socket | | `--execution-socket` | Yes (IPC mode) | Path to the Execution Layer Engine API IPC socket | | `--rpc.addr` | Recommended | CL RPC listen address and port | | `--follow` | Yes | Enables block-following mode through relay endpoints | | `--follow.endpoint` | Yes | Relay endpoint URLs. Specify multiple for redundancy. Format: `https://host,wss=host` | | `--execution-persistence-backpressure` | Recommended | Throttles execution to match disk-write speed, bounding EL memory growth | | `--execution-persistence-backpressure-threshold` | Recommended | Threshold for backpressure. Default `16` (must be `> 0`). Lower if you observe memory pressure | | `--metrics` | Recommended | Enables Prometheus metrics on the specified address and port | ## Step 10: Verify the node is syncing After both layers start, wait roughly 30 seconds, then query the local JSON-RPC endpoint for the latest block height: ```shell theme={null} curl -s -X POST http://localhost:8545 \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 1 }' ``` The `result` field is a hexadecimal block number that increases over time. If it stays at `0x0`, check the CL logs for errors. If you have [Foundry](https://book.getfoundry.sh/getting-started/installation) installed, you can run the same check with `cast`: ```shell theme={null} cast block-number --rpc-url http://localhost:8545 ``` Run it several times over a few seconds -- the block number increases as the node syncs. If the block number stays at `0x0` or the node does not start as expected, check these common causes: * **IPC socket files missing.** The EL writes sockets at `$ARC_RUN/reth.ipc` and `$ARC_RUN/auth.ipc` on startup. If they are not present after 30 seconds, the EL did not start -- review its terminal output for a panic or configuration error. * **CL cannot connect to the EL.** Start the EL first. If the CL was launched before the EL was ready, restart the CL. * **Snapshot extraction incomplete.** `arc-snapshots download` runs silently during extraction. If it was interrupted, `$ARC_EXECUTION` or `$ARC_CONSENSUS` may be partially populated. Re-run the download to overwrite the existing files. If you clear the directories first, note that `$ARC_CONSENSUS` also holds the CL private key written by `arc-node-consensus init`. Re-run that command after clearing, or your node loses its network identity. * **Path mismatch between EL and CL.** `$ARC_RUN` must resolve to the same directory in both shells. Re-source `~/.arc_env` in any terminal that lost its environment. Once your node is syncing steadily, you can [deploy it as a systemd service](/arc/tutorials/deploy-node-as-service) for production use, or [set up Prometheus and Grafana](/arc/tutorials/set-up-node-monitoring) to view the pre-built dashboards. Run the Arc node from pre-built Docker images using Docker Compose. Init containers handle snapshot download and CL initialization on the first run. ## Prerequisites Before you begin, confirm that you have: * Reviewed the [node requirements](/arc/references/node-requirements) for hardware, software, and network endpoints * Prepared a Linux machine that meets the minimum system requirements * Installed [Docker Engine 24+ with BuildKit](https://docs.docker.com/engine/install/) and [Docker Compose v2](https://docs.docker.com/compose/install/) ## Step 1: Set environment variables The Compose file reads images from environment variables. Set the version, data directory, and image references. See [node requirements](/arc/references/node-requirements#versions) for the current version: ```shell theme={null} export ARC_VERSION=0.7.0 export ARC_HOME=~/.arc export ARC_EXECUTION_IMAGE=docker.cloudsmith.io/circle/arc-network/arc-execution:$ARC_VERSION export ARC_CONSENSUS_IMAGE=docker.cloudsmith.io/circle/arc-network/arc-consensus:$ARC_VERSION ``` To build images locally instead of pulling from Cloudsmith, see [Installation: Docker](https://github.com/circlefin/arc-node/blob/main/docs/installation.md#docker) in the `arc-node` repository. ## Step 2: Prepare the data directory Create `$ARC_HOME` on the host before starting Docker Compose. If you skip this, Docker creates it as `root` and the snapshot init container fails with permission errors: ```shell theme={null} mkdir -p "${ARC_HOME:-$HOME/.arc}" ``` ## Step 3: Download the Compose file Fetch the `docker-compose.yml` for your release into a working directory: ```shell theme={null} curl -O https://raw.githubusercontent.com/circlefin/arc-node/v${ARC_VERSION}/deployments/docker-compose.yml ``` ## Step 4: Start the node From the directory containing `docker-compose.yml`: ```shell theme={null} docker compose up -d ``` On the first run, init containers automatically: 1. Download the latest testnet snapshots (approximately 84 GB compressed; see [snapshot download size](/arc/references/node-requirements#snapshot-download-size)) 2. Initialize the consensus layer private key 3. Prepare the shared IPC socket volume Subsequent runs detect that initialization is already complete and start immediately. The init container runs as `root` so it can set file ownership for the EL and CL services (UID 999). No manual `chown` is needed. ## Step 5: Verify the node is syncing On the first run, wait for the snapshot init container to finish: ```shell theme={null} docker compose logs -f arc-snapshots ``` Once the EL and CL containers are up, wait roughly 30 seconds, then query the latest block height: ```shell theme={null} curl -s -X POST http://localhost:8545 \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 1 }' ``` The `result` field is a hexadecimal block number that increases over time. If it stays at `0x0`, check the logs: ```shell theme={null} docker compose logs -f ``` The containers expose Prometheus metrics on the host at `localhost:9001/` (Execution Layer, root path) and `localhost:29000/metrics` (Consensus Layer). To scrape these endpoints with a local stack, see [Set up Prometheus and Grafana for an Arc node](/arc/tutorials/set-up-node-monitoring). ## Step 6: Stop and reset To stop the node while preserving data: ```shell theme={null} docker compose down ``` Node data persists in `~/.arc/` (or the path set by `$ARC_HOME`). To remove all data and start fresh: ```shell theme={null} docker compose down -v rm -rf ~/.arc ``` `docker compose down -v` removes the named sockets volume, and `rm -rf ~/.arc` permanently deletes the consensus layer private key -- your node's network identity. It cannot be recovered. ## Run on separate hosts The tabs above describe running the Execution Layer and Consensus Layer on the same host, communicating through IPC sockets. To run them on separate hosts, swap IPC for authenticated RPC. This section applies to the binary install paths (`arcup` and source build); the Docker path is single-host by default. ### Generate a JWT secret The EL and CL authenticate to each other with a shared JWT secret. Generate it once, then securely copy it to both hosts: ```shell theme={null} openssl rand -hex 32 | tr -d "\n" > "$ARC_HOME/jwtsecret" chmod 600 "$ARC_HOME/jwtsecret" ``` ### Execution layer flags for RPC mode Remove the IPC flags (`--ipcpath`, `--auth-ipc`, `--auth-ipc.path`) from the EL command and add: ```shell theme={null} --authrpc.addr 0.0.0.0 \ --authrpc.port 8551 \ --authrpc.jwtsecret "$ARC_HOME/jwtsecret" ``` `--authrpc.addr 0.0.0.0` exposes port 8551 on every network interface. Restrict access with a firewall rule or private network -- the Engine API controls block production and must not be reachable from the public internet. ### Consensus layer flags for RPC mode Remove `--eth-socket` and `--execution-socket` from the CL command, and add: ```shell theme={null} --eth-rpc-endpoint http://:8545 \ --execution-endpoint http://:8551 \ --execution-jwt "$ARC_HOME/jwtsecret" ``` Replace `` with the IP address or hostname of the EL host. IPC and RPC are mutually exclusive. Use one or the other, not both. ## Enable backpressure under memory pressure The tabs above start the Consensus Layer with backpressure enabled at the default threshold (`16`). Backpressure throttles execution to match the speed of disk writes, bounding Execution Layer memory growth during startup or extended sync when the node is far behind. If you observe sustained memory pressure on a host that meets the [node requirements](/arc/references/node-requirements), lower the threshold to `10` for more aggressive throttling. First, add the `reth` namespace to the EL `--http.api` flag so the CL can read backpressure metrics: ```shell theme={null} --http.api eth,net,web3,txpool,trace,debug,reth ``` Then restart the CL with the lower threshold: ```shell theme={null} --execution-persistence-backpressure \ --execution-persistence-backpressure-threshold=10 ``` **Upgrade required by 2026-05-27 15:08:37 UTC.** The Zero5 and Zero6 hardforks activate at Unix timestamp `1779894517`. v0.6.0 is two releases behind and stops syncing on Arc Testnet after activation. Switch the view at the top of this page to **v0.7.1 (latest)** and follow those instructions before the deadline. See [BREAKING\_CHANGES.md](https://github.com/circlefin/arc-node/blob/main/BREAKING_CHANGES.md#v071) for migration details. An Arc node syncs blocks from the network and serves a local JSON-RPC endpoint so you can independently verify every block and transaction. It runs two processes: the Execution Layer (EL), which executes transactions and maintains state, and the Consensus Layer (CL), which fetches finalized blocks from relay endpoints (trusted HTTP servers that distribute signed block data), verifies their signatures, and passes them to the EL. For a deeper explanation of how these components fit together, see [Running a node](/arc/concepts/running-a-node). Arc is currently in its testnet phase. During this period, the network may experience instability or unplanned downtime. Throughout this page, all references to Arc refer specifically to the Arc Testnet. Choose the install path that fits your environment. Docker support was added in v0.7.0. Use **v0.7.1 (latest)** for a Docker-based install. Install pre-built Arc binaries using the `arcup` script. This is the fastest path and requires no Rust toolchain or build dependencies. ## Prerequisites Before you begin, confirm that you have: * Reviewed the [node requirements](/arc/references/node-requirements) for hardware, software, and network endpoints * Prepared a Linux or macOS machine that meets the minimum system requirements * Installed [Foundry](https://book.getfoundry.sh/getting-started/installation) (optional, provides the `cast` command used to verify your node) ## Step 1: Install with `arcup` `arcup` installs the Arc binaries under `$ARC_HOME` (default `~/.arc`). Export `ARC_HOME` first if you want to install somewhere else; otherwise the default applies for both the installer and the environment file: ```shell theme={null} export ARC_HOME="${ARC_HOME:-$HOME/.arc}" curl -L https://raw.githubusercontent.com/circlefin/arc-node/main/arcup/install | bash ``` The installer places `arc-node-execution`, `arc-node-consensus`, `arc-snapshots`, and `arcup` itself into `$ARC_HOME/bin`. Load the produced environment file so the binaries are on your `PATH`: ```shell theme={null} source "$ARC_HOME/env" ``` Verify the installation: ```shell theme={null} arc-snapshots --version arc-node-execution --version arc-node-consensus --version ``` Each command prints a version string. To update the binaries later, run `arcup`. ## Step 2: Set environment variables The remaining steps use a small set of variables to keep paths consistent. Write them to a file and source it: ```shell theme={null} cat << "EOF" > ~/.arc_env ARC_HOME="${ARC_HOME:-$HOME/.arc}" # Linux runtime directory: ARC_RUN="/run/arc" # macOS runtime directory (uncomment if on macOS): #ARC_RUN="$ARC_HOME/run" ARC_EXECUTION=$ARC_HOME/execution ARC_CONSENSUS=$ARC_HOME/consensus EOF source ~/.arc_env ``` ## Step 3: Create data directories Create the data directories for the EL and CL, plus the runtime directory used for IPC sockets: ```shell theme={null} mkdir -p "$ARC_EXECUTION" "$ARC_CONSENSUS" sudo install -d -o $USER "$ARC_RUN" ``` On macOS, replace the second command with `mkdir -p "$ARC_RUN"` after uncommenting the macOS line in `~/.arc_env`. When running as a systemd service, the `RuntimeDirectory=arc` directive creates `/run/arc` automatically. You can skip the second command in that case. ## Step 4: Download blockchain snapshots Download snapshots so the node starts syncing from a recent block height rather than from genesis: ```shell theme={null} arc-snapshots download \ --chain=arc-testnet \ --execution-path "$ARC_EXECUTION" \ --consensus-path "$ARC_CONSENSUS" ``` This command fetches the latest snapshot URLs from `https://snapshots.arc.network`, downloads the snapshots, and extracts them into `$ARC_EXECUTION` and `$ARC_CONSENSUS`. Snapshots are large. Testnet snapshots are approximately **68 GB EL + 16 GB CL compressed** (around 103 GB and 36 GB extracted). The download takes 10–15 minutes on a stable 100 Mbps connection; slower or metered connections can take hours. Ensure you have at least 150 GB of free disk space, a stable network connection, and available CPU. The terminal stops producing output during extraction. This is expected. ## Step 5: Initialize the consensus layer Generate the CL private key file used for network identity. This is a one-time setup step: ```shell theme={null} arc-node-consensus init --home "$ARC_CONSENSUS" ``` ## Step 6: Start the execution layer Start the Execution Layer. This creates the IPC sockets that the Consensus Layer connects to. ```shell theme={null} arc-node-execution node \ --chain arc-testnet \ --datadir "$ARC_EXECUTION" \ --disable-discovery \ --ipcpath "$ARC_RUN/reth.ipc" \ --auth-ipc \ --auth-ipc.path "$ARC_RUN/auth.ipc" \ --http \ --http.addr 127.0.0.1 \ --http.port 8545 \ --http.api eth,net,web3,txpool,trace,debug \ --metrics 127.0.0.1:9001 \ --enable-arc-rpc \ --rpc.forwarder https://rpc.testnet.arc.io/ ``` The EL starts and waits for blocks. Log output shows that the IPC sockets exist at `$ARC_RUN/reth.ipc` and `$ARC_RUN/auth.ipc`. | Flag | Required | Description | | :------------------------- | :------------- | :------------------------------------------------------------------ | | `--chain arc-testnet` | Yes | Selects the Arc Testnet genesis configuration bundled in the binary | | `--datadir` | Yes | Path to the EL data directory | | `--disable-discovery` | Yes | Disables P2P peer discovery (Arc uses relay endpoints instead) | | `--ipcpath` / `--auth-ipc` | Yes (IPC mode) | Paths for the IPC sockets the CL connects to | | `--http` / `--http.addr` | Yes | Enables the JSON-RPC endpoint on the specified address and port | | `--http.api` | Recommended | RPC namespaces to expose | | `--metrics` | Recommended | Enables Prometheus metrics on the specified address and port | | `--enable-arc-rpc` | Yes | Enables Arc-specific RPC methods | | `--rpc.forwarder` | Recommended | Routes submitted transactions to an RPC node for broadcast | `--chain arc-testnet` uses the bundled genesis configuration; replace with `--chain /path/to/genesis.json` if you have a custom genesis file. See [reth node](https://reth.rs/cli/reth/node/) for additional flags. ## Step 7: Start the consensus layer Open a separate terminal and start the Consensus Layer. The CL connects to the EL through the IPC sockets and begins fetching blocks from the network. ```shell theme={null} arc-node-consensus start \ --home "$ARC_CONSENSUS" \ --eth-socket "$ARC_RUN/reth.ipc" \ --execution-socket "$ARC_RUN/auth.ipc" \ --rpc.addr 127.0.0.1:31000 \ --follow \ --follow.endpoint https://rpc.testnet.arc.io,wss=rpc.testnet.arc.io \ --follow.endpoint https://rpc.drpc.testnet.arc.io,wss=rpc.drpc.testnet.arc.io \ --follow.endpoint https://rpc.blockdaemon.testnet.arc.io,wss=rpc.blockdaemon.testnet.arc.io \ --metrics 127.0.0.1:29000 ``` | Flag | Required | Description | | :------------------- | :------------- | :------------------------------------------------------------------------------------ | | `--home` | Yes | Path to the CL data directory (contains keys and state) | | `--eth-socket` | Yes (IPC mode) | Path to the Execution Layer ETH RPC IPC socket | | `--execution-socket` | Yes (IPC mode) | Path to the Execution Layer Engine API IPC socket | | `--rpc.addr` | Recommended | CL RPC listen address and port | | `--follow` | Yes | Enables block-following mode through relay endpoints | | `--follow.endpoint` | Yes | Relay endpoint URLs. Specify multiple for redundancy. Format: `https://host,wss=host` | | `--metrics` | Recommended | Enables Prometheus metrics on the specified address and port | ## Step 8: Verify the node is syncing After both layers start, wait roughly 30 seconds, then query the local JSON-RPC endpoint for the latest block height: ```shell theme={null} curl -s -X POST http://localhost:8545 \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 1 }' ``` The `result` field is a hexadecimal block number that increases over time. If it stays at `0x0`, check the CL logs for errors. If you have [Foundry](https://book.getfoundry.sh/getting-started/installation) installed, you can run the same check with `cast`: ```shell theme={null} cast block-number --rpc-url http://localhost:8545 ``` Run it several times over a few seconds -- the block number increases as the node syncs. If the block number stays at `0x0` or the node does not start as expected, check these common causes: * **IPC socket files missing.** The EL writes sockets at `$ARC_RUN/reth.ipc` and `$ARC_RUN/auth.ipc` on startup. If they are not present after 30 seconds, the EL did not start -- review its terminal output for a panic or configuration error. * **CL cannot connect to the EL.** Start the EL first. If the CL was launched before the EL was ready, restart the CL. * **Snapshot extraction incomplete.** `arc-snapshots download` runs silently during extraction. If it was interrupted, `$ARC_EXECUTION` or `$ARC_CONSENSUS` may be partially populated. Re-run the download to overwrite the existing files. If you clear the directories first, note that `$ARC_CONSENSUS` also holds the CL private key written by `arc-node-consensus init`. Re-run that command after clearing, or your node loses its network identity. * **Path mismatch between EL and CL.** `$ARC_RUN` must resolve to the same directory in both shells. Re-source `~/.arc_env` in any terminal that lost its environment. Once your node is syncing steadily, you can [deploy it as a systemd service](/arc/tutorials/deploy-node-as-service) for production use, or [set up Prometheus and Grafana](/arc/tutorials/set-up-node-monitoring) to view the pre-built dashboards. Build Arc binaries from the [`arc-node`](https://github.com/circlefin/arc-node) repository. Use this path to audit the build, run on an unsupported platform, or pin to a specific commit. ## Prerequisites Before you begin, confirm that you have: * Reviewed the [node requirements](/arc/references/node-requirements) for hardware, software, and network endpoints * Prepared a Linux, macOS, or Windows machine that meets the minimum system requirements * Installed [Foundry](https://book.getfoundry.sh/getting-started/installation) (optional, provides the `cast` command used to verify your node) ## Step 1: Install build dependencies Install [Rust](https://rust-lang.org/tools/install/) if you don't have it: ```shell theme={null} curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source ~/.cargo/env ``` Install the platform packages required to build the EL database bindings: * **Ubuntu:** `sudo apt-get install libclang-dev pkg-config build-essential` * **macOS:** `brew install llvm pkg-config`, then `export LIBCLANG_PATH="$(brew --prefix llvm)/lib"` * **Windows:** `choco install llvm` or `winget install LLVM.LLVM` ## Step 2: Clone the repository Clone the [`arc-node`](https://github.com/circlefin/arc-node) repository and check out the version for your target network. See [node requirements](/arc/references/node-requirements#versions) for the current version: ```shell theme={null} git clone https://github.com/circlefin/arc-node.git cd arc-node git checkout v0.6.0 git submodule update --init --recursive ``` ## Step 3: Build and install The following commands build three Arc node binaries: `arc-node-execution`, `arc-node-consensus`, and `arc-snapshots`: ```shell theme={null} cargo install --path crates/node cargo install --path crates/malachite-app cargo install --path crates/snapshots ``` `cargo install` places compiled binaries into `~/.cargo/bin`, which is added to `PATH` by loading `~/.cargo/env`. To install elsewhere, add `--root $BASE_DIR` to each command (for instance, `--root /usr/local` places them under `/usr/local/bin`). Verify the installation: ```shell theme={null} arc-snapshots --version arc-node-execution --version arc-node-consensus --version ``` ## Step 4: Set environment variables The remaining steps use a small set of variables to keep paths consistent. Write them to a file and source it: ```shell theme={null} cat << "EOF" > ~/.arc_env ARC_HOME="${ARC_HOME:-$HOME/.arc}" # Linux runtime directory: ARC_RUN="/run/arc" # macOS runtime directory (uncomment if on macOS): #ARC_RUN="$ARC_HOME/run" ARC_EXECUTION=$ARC_HOME/execution ARC_CONSENSUS=$ARC_HOME/consensus EOF source ~/.arc_env ``` ## Step 5: Create data directories Create the data directories for the EL and CL, plus the runtime directory used for IPC sockets: ```shell theme={null} mkdir -p "$ARC_EXECUTION" "$ARC_CONSENSUS" sudo install -d -o $USER "$ARC_RUN" ``` On macOS, replace the second command with `mkdir -p "$ARC_RUN"` after uncommenting the macOS line in `~/.arc_env`. When running as a systemd service, the `RuntimeDirectory=arc` directive creates `/run/arc` automatically. You can skip the second command in that case. ## Step 6: Download blockchain snapshots Download snapshots so the node starts syncing from a recent block height rather than from genesis: ```shell theme={null} arc-snapshots download \ --chain=arc-testnet \ --execution-path "$ARC_EXECUTION" \ --consensus-path "$ARC_CONSENSUS" ``` This command fetches the latest snapshot URLs from `https://snapshots.arc.network`, downloads the snapshots, and extracts them into `$ARC_EXECUTION` and `$ARC_CONSENSUS`. Snapshots are large. Testnet snapshots are approximately **68 GB EL + 16 GB CL compressed** (around 103 GB and 36 GB extracted). The download takes 10–15 minutes on a stable 100 Mbps connection; slower or metered connections can take hours. Ensure you have at least 150 GB of free disk space, a stable network connection, and available CPU. The terminal stops producing output during extraction. This is expected. ## Step 7: Initialize the consensus layer Generate the CL private key file used for network identity. This is a one-time setup step: ```shell theme={null} arc-node-consensus init --home "$ARC_CONSENSUS" ``` ## Step 8: Start the execution layer Start the Execution Layer. This creates the IPC sockets that the Consensus Layer connects to. ```shell theme={null} arc-node-execution node \ --chain arc-testnet \ --datadir "$ARC_EXECUTION" \ --disable-discovery \ --ipcpath "$ARC_RUN/reth.ipc" \ --auth-ipc \ --auth-ipc.path "$ARC_RUN/auth.ipc" \ --http \ --http.addr 127.0.0.1 \ --http.port 8545 \ --http.api eth,net,web3,txpool,trace,debug \ --metrics 127.0.0.1:9001 \ --enable-arc-rpc \ --rpc.forwarder https://rpc.testnet.arc.io/ ``` The EL starts and waits for blocks. Log output shows that the IPC sockets exist at `$ARC_RUN/reth.ipc` and `$ARC_RUN/auth.ipc`. | Flag | Required | Description | | :------------------------- | :------------- | :------------------------------------------------------------------ | | `--chain arc-testnet` | Yes | Selects the Arc Testnet genesis configuration bundled in the binary | | `--datadir` | Yes | Path to the EL data directory | | `--disable-discovery` | Yes | Disables P2P peer discovery (Arc uses relay endpoints instead) | | `--ipcpath` / `--auth-ipc` | Yes (IPC mode) | Paths for the IPC sockets the CL connects to | | `--http` / `--http.addr` | Yes | Enables the JSON-RPC endpoint on the specified address and port | | `--http.api` | Recommended | RPC namespaces to expose | | `--metrics` | Recommended | Enables Prometheus metrics on the specified address and port | | `--enable-arc-rpc` | Yes | Enables Arc-specific RPC methods | | `--rpc.forwarder` | Recommended | Routes submitted transactions to an RPC node for broadcast | `--chain arc-testnet` uses the bundled genesis configuration; replace with `--chain /path/to/genesis.json` if you have a custom genesis file. See [reth node](https://reth.rs/cli/reth/node/) for additional flags. ## Step 9: Start the consensus layer Open a separate terminal and start the Consensus Layer. The CL connects to the EL through the IPC sockets and begins fetching blocks from the network. ```shell theme={null} arc-node-consensus start \ --home "$ARC_CONSENSUS" \ --eth-socket "$ARC_RUN/reth.ipc" \ --execution-socket "$ARC_RUN/auth.ipc" \ --rpc.addr 127.0.0.1:31000 \ --follow \ --follow.endpoint https://rpc.testnet.arc.io,wss=rpc.testnet.arc.io \ --follow.endpoint https://rpc.drpc.testnet.arc.io,wss=rpc.drpc.testnet.arc.io \ --follow.endpoint https://rpc.blockdaemon.testnet.arc.io,wss=rpc.blockdaemon.testnet.arc.io \ --metrics 127.0.0.1:29000 ``` | Flag | Required | Description | | :------------------- | :------------- | :------------------------------------------------------------------------------------ | | `--home` | Yes | Path to the CL data directory (contains keys and state) | | `--eth-socket` | Yes (IPC mode) | Path to the Execution Layer ETH RPC IPC socket | | `--execution-socket` | Yes (IPC mode) | Path to the Execution Layer Engine API IPC socket | | `--rpc.addr` | Recommended | CL RPC listen address and port | | `--follow` | Yes | Enables block-following mode through relay endpoints | | `--follow.endpoint` | Yes | Relay endpoint URLs. Specify multiple for redundancy. Format: `https://host,wss=host` | | `--metrics` | Recommended | Enables Prometheus metrics on the specified address and port | ## Step 10: Verify the node is syncing After both layers start, wait roughly 30 seconds, then query the local JSON-RPC endpoint for the latest block height: ```shell theme={null} curl -s -X POST http://localhost:8545 \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 1 }' ``` The `result` field is a hexadecimal block number that increases over time. If it stays at `0x0`, check the CL logs for errors. If you have [Foundry](https://book.getfoundry.sh/getting-started/installation) installed, you can run the same check with `cast`: ```shell theme={null} cast block-number --rpc-url http://localhost:8545 ``` Run it several times over a few seconds -- the block number increases as the node syncs. If the block number stays at `0x0` or the node does not start as expected, check these common causes: * **IPC socket files missing.** The EL writes sockets at `$ARC_RUN/reth.ipc` and `$ARC_RUN/auth.ipc` on startup. If they are not present after 30 seconds, the EL did not start -- review its terminal output for a panic or configuration error. * **CL cannot connect to the EL.** Start the EL first. If the CL was launched before the EL was ready, restart the CL. * **Snapshot extraction incomplete.** `arc-snapshots download` runs silently during extraction. If it was interrupted, `$ARC_EXECUTION` or `$ARC_CONSENSUS` may be partially populated. Re-run the download to overwrite the existing files. If you clear the directories first, note that `$ARC_CONSENSUS` also holds the CL private key written by `arc-node-consensus init`. Re-run that command after clearing, or your node loses its network identity. * **Path mismatch between EL and CL.** `$ARC_RUN` must resolve to the same directory in both shells. Re-source `~/.arc_env` in any terminal that lost its environment. Once your node is syncing steadily, you can [deploy it as a systemd service](/arc/tutorials/deploy-node-as-service) for production use, or [set up Prometheus and Grafana](/arc/tutorials/set-up-node-monitoring) to view the pre-built dashboards. ## Run on separate hosts The tabs above describe running the Execution Layer and Consensus Layer on the same host, communicating through IPC sockets. To run them on separate hosts, swap IPC for authenticated RPC. This section applies to the binary install paths (`arcup` and source build). ### Generate a JWT secret The EL and CL authenticate to each other with a shared JWT secret. Generate it once, then securely copy it to both hosts: ```shell theme={null} openssl rand -hex 32 | tr -d "\n" > "$ARC_HOME/jwtsecret" chmod 600 "$ARC_HOME/jwtsecret" ``` ### Execution layer flags for RPC mode Remove the IPC flags (`--ipcpath`, `--auth-ipc`, `--auth-ipc.path`) from the EL command and add: ```shell theme={null} --authrpc.addr 0.0.0.0 \ --authrpc.port 8551 \ --authrpc.jwtsecret "$ARC_HOME/jwtsecret" ``` `--authrpc.addr 0.0.0.0` exposes port 8551 on every network interface. Restrict access with a firewall rule or private network -- the Engine API controls block production and must not be reachable from the public internet. ### Consensus layer flags for RPC mode Remove `--eth-socket` and `--execution-socket` from the CL command, and add: ```shell theme={null} --eth-rpc-endpoint http://:8545 \ --execution-endpoint http://:8551 \ --execution-jwt "$ARC_HOME/jwtsecret" ``` Replace `` with the IP address or hostname of the EL host. IPC and RPC are mutually exclusive. Use one or the other, not both. ## Enable backpressure under memory pressure (alpha) Backpressure is an alpha feature in v0.6.0 that throttles execution to match the speed of disk writes, bounding Execution Layer memory growth. Enable it only if you observe sustained memory pressure on a host that meets the [node requirements](/arc/references/node-requirements). First, add the `reth` namespace to the EL `--http.api` flag so the CL can read backpressure metrics: ```shell theme={null} --http.api eth,net,web3,txpool,trace,debug,reth ``` Then start the CL with backpressure enabled at threshold `10`: ```shell theme={null} --execution-persistence-backpressure \ --execution-persistence-backpressure-threshold=10 ``` # Send USDC with a transaction memo Source: https://docs.arc.io/arc/tutorials/send-usdc-with-transaction-memo Attach memo metadata to a USDC transfer on Arc Testnet and query the emitted Memo events. Use the `Memo` contract to add a memo to a USDC transfer on Arc Testnet. This tutorial shows the full flow with viem, ethers, Python, and curl. You will encode the inner USDC transfer, submit it with `Memo.memo(...)`, decode the receipt, and query past memo events by `memoId`. To learn how the `Memo` contract preserves your wallet as the sender and orders its events, see [Transaction memos](/arc/concepts/transaction-memos). ## Prerequisites Before you begin, ensure that you've: * Installed [Node.js v22+](https://nodejs.org/) for the TypeScript examples, or Python 3.10+ for the Python example. * Created an [Arc Testnet wallet](/arc/references/connect-to-arc) controlled by an externally owned account (EOA). Smart contract wallets aren't supported. See [Wallet types](/arc/concepts/transaction-memos#wallet-types) for details. * Funded the wallet with testnet USDC from the [Circle Faucet](https://faucet.circle.com/). * Chosen a recipient address on Arc Testnet. ## Step 1. Set up the project Create a new project and install the dependencies for the client library you want to use: ```bash Node.js theme={null} mkdir arc-transaction-memo cd arc-transaction-memo npm init -y npm install dotenv ethers tsx typescript viem ``` ```bash Python theme={null} mkdir arc-transaction-memo cd arc-transaction-memo python3 -m venv .venv source .venv/bin/activate pip install web3 python-dotenv ``` Create an `.env` file: ```bash Shell theme={null} touch .env ``` Add your configuration: ```text .env theme={null} PRIVATE_KEY=YOUR_PRIVATE_KEY RECIPIENT_ADDRESS=RECIPIENT_ADDRESS RPC_URL=https://rpc.testnet.arc.io ``` Replace `YOUR_PRIVATE_KEY` with the `0x`-prefixed private key for the funded wallet. Replace `RECIPIENT_ADDRESS` with the wallet that should receive USDC. ## Step 2. Review the contract address and ABI Arc Testnet uses the following predeployed transaction memo contracts: | Contract | Address | | :------- | :----------------------------------------------------------------------------------------------------------------------------- | | `Memo` | [`0x5294E9927c3306DcBaDb03fe70b92e01cCede505`](https://testnet.arcscan.app/address/0x5294E9927c3306DcBaDb03fe70b92e01cCede505) | | `USDC` | [`0x3600000000000000000000000000000000000000`](https://testnet.arcscan.app/address/0x3600000000000000000000000000000000000000) | For the full address list, see [contract addresses](/arc/references/contract-addresses#transaction-extensions). Create `memo-abi.json` in your project: ```json memo-abi.json theme={null} [ { "type": "function", "name": "memo", "stateMutability": "nonpayable", "inputs": [ { "name": "target", "type": "address" }, { "name": "data", "type": "bytes" }, { "name": "memoId", "type": "bytes32" }, { "name": "memoData", "type": "bytes" } ], "outputs": [] }, { "type": "event", "name": "BeforeMemo", "anonymous": false, "inputs": [{ "name": "memoIndex", "type": "uint256", "indexed": true }] }, { "type": "event", "name": "Memo", "anonymous": false, "inputs": [ { "name": "sender", "type": "address", "indexed": true }, { "name": "target", "type": "address", "indexed": true }, { "name": "callDataHash", "type": "bytes32", "indexed": false }, { "name": "memoId", "type": "bytes32", "indexed": true }, { "name": "memo", "type": "bytes", "indexed": false }, { "name": "memoIndex", "type": "uint256", "indexed": false } ] } ] ``` The script you build in the next steps loads this ABI file from the project root. ## Step 3. Configure the client connection Create the script file for your client library. The first chunk reads the wallet and recipient configuration from `.env`, sets the contract addresses, loads the `Memo` ABI, and creates the clients that sign and send requests. The example imports the `arcTestnet` chain definition from `viem/chains`, which requires viem v2.38 or later. Create `viem-memo.ts`: ```typescript TypeScript theme={null} import "dotenv/config"; import { readFileSync } from "node:fs"; import { type Abi, type Address, createPublicClient, createWalletClient, encodeFunctionData, erc20Abi, getAddress, http, keccak256, parseAbiItem, parseEventLogs, parseUnits, stringToHex, } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { arcTestnet } from "viem/chains"; const rpcUrl = process.env.RPC_URL ?? "https://rpc.testnet.arc.io"; const privateKey = process.env.PRIVATE_KEY as `0x${string}`; const recipient = getAddress(process.env.RECIPIENT_ADDRESS as Address); const memoAddress = "0x5294E9927c3306DcBaDb03fe70b92e01cCede505"; const usdcAddress = "0x3600000000000000000000000000000000000000"; const memoAbi = JSON.parse(readFileSync("memo-abi.json", "utf8")) as Abi; const account = privateKeyToAccount(privateKey); const publicClient = createPublicClient({ chain: arcTestnet, transport: http(rpcUrl), }); const walletClient = createWalletClient({ account, chain: arcTestnet, transport: http(rpcUrl), }); ``` The public client reads chain state, and the wallet client signs and submits transactions from your funded account. Create `ethers-memo.ts`: ```typescript TypeScript theme={null} import "dotenv/config"; import { readFileSync } from "node:fs"; import { ethers } from "ethers"; const rpcUrl = process.env.RPC_URL ?? "https://rpc.testnet.arc.io"; const privateKey = process.env.PRIVATE_KEY as string; const recipient = ethers.getAddress(process.env.RECIPIENT_ADDRESS as string); const memoAddress = "0x5294E9927c3306DcBaDb03fe70b92e01cCede505"; const usdcAddress = "0x3600000000000000000000000000000000000000"; const explorerUrl = "https://testnet.arcscan.app"; const memoAbi = JSON.parse(readFileSync("memo-abi.json", "utf8")); const erc20Abi = [ "function transfer(address to, uint256 amount) returns (bool)", ]; const provider = new ethers.JsonRpcProvider(rpcUrl, { chainId: 5042002, name: "arc-testnet", }); const wallet = new ethers.Wallet(privateKey, provider); const memoInterface = new ethers.Interface(memoAbi); const erc20Interface = new ethers.Interface(erc20Abi); ``` The provider reads chain state, the wallet signs transactions, and the two interfaces encode and decode `Memo` and ERC-20 calldata. Create `python-memo.py`: ```python Python theme={null} import json import os from dotenv import load_dotenv from web3 import Web3 from web3.logs import DISCARD load_dotenv() rpc_url = os.getenv("RPC_URL", "https://rpc.testnet.arc.io") private_key = os.environ["PRIVATE_KEY"] recipient = Web3.to_checksum_address(os.environ["RECIPIENT_ADDRESS"]) memo_address = Web3.to_checksum_address("0x5294E9927c3306DcBaDb03fe70b92e01cCede505") usdc_address = Web3.to_checksum_address("0x3600000000000000000000000000000000000000") explorer_url = "https://testnet.arcscan.app" with open("memo-abi.json", encoding="utf-8") as abi_file: memo_abi = json.load(abi_file) erc20_abi = [ { "type": "function", "name": "transfer", "stateMutability": "nonpayable", "inputs": [ {"name": "to", "type": "address"}, {"name": "amount", "type": "uint256"}, ], "outputs": [{"name": "", "type": "bool"}], }, ] w3 = Web3(Web3.HTTPProvider(rpc_url)) account = w3.eth.account.from_key(private_key) memo = w3.eth.contract(address=memo_address, abi=memo_abi) usdc = w3.eth.contract(address=usdc_address, abi=erc20_abi) ``` The `Web3` instance connects to the RPC endpoint, and the two contract objects encode and decode `Memo` and ERC-20 calldata. ## Step 4. Encode the transfer and memo values Add the values that define the memo transfer. The encoded ERC-20 `transfer` call becomes the inner call that `Memo.memo(...)` forwards to USDC. The `memoId` is a 32-byte identifier your application uses to look the memo up later, and the memo bytes carry the metadata itself. Append to `viem-memo.ts`: ```typescript TypeScript theme={null} const transferData = encodeFunctionData({ abi: erc20Abi, functionName: "transfer", args: [recipient, parseUnits("1", 6)], }); const callDataHash = keccak256(transferData); const memoId = keccak256(stringToHex("invoice-2026-0001")); const memoBytes = stringToHex("order=2026-0001"); ``` Append to `ethers-memo.ts`: ```typescript TypeScript theme={null} const transferData = erc20Interface.encodeFunctionData("transfer", [ recipient, ethers.parseUnits("1", 6), ]); const callDataHash = ethers.keccak256(transferData); const memoId = ethers.id("invoice-2026-0001"); const memoBytes = ethers.toUtf8Bytes("order=2026-0001"); ``` Append to `python-memo.py`: ```python Python theme={null} transfer_data = usdc.functions.transfer(recipient, 1_000_000)._encode_transaction_data() transfer_bytes = bytes.fromhex(transfer_data[2:]) call_data_hash = Web3.keccak(hexstr=transfer_data) memo_id = Web3.keccak(text="invoice-2026-0001") memo_bytes = b"order=2026-0001" ``` The transfer amount is `1` USDC, expressed with six decimals. The script also hashes the transfer calldata so a later step can match it against the `callDataHash` field of the emitted `Memo` event. ## Step 5. Confirm the `Memo` contract is deployed Before you send the transaction, check that the `Memo` address has deployed bytecode. If `eth_getCode` returns `0x`, the contract is not available on the RPC endpoint you are using, and the script stops with an error. Append to `viem-memo.ts`: ```typescript TypeScript theme={null} const memoCode = await publicClient.getCode({ address: memoAddress }); if (!memoCode || memoCode === "0x") { throw new Error(`Memo contract is not deployed at ${memoAddress}`); } ``` Append to `ethers-memo.ts`: ```typescript TypeScript theme={null} const memoCode = await provider.getCode(memoAddress); if (memoCode === "0x") { throw new Error(`Memo contract is not deployed at ${memoAddress}`); } ``` Append to `python-memo.py`: ```python Python theme={null} memo_code = w3.eth.get_code(memo_address) if memo_code == b"": raise RuntimeError(f"Memo contract is not deployed at {memo_address}") ``` ## Step 6. Send the memo transaction Call `Memo.memo(...)` with the four arguments from Step 4: the USDC contract as the target, the encoded transfer calldata, the `memoId`, and the memo bytes. Then wait for the receipt and confirm the transaction succeeded. Append to `viem-memo.ts`: ```typescript TypeScript theme={null} const hash = await walletClient.writeContract({ address: memoAddress, abi: memoAbi, functionName: "memo", args: [usdcAddress, transferData, memoId, memoBytes], }); const receipt = await publicClient.waitForTransactionReceipt({ hash }); if (receipt.status !== "success") { throw new Error(`Memo transaction reverted: ${hash}`); } ``` Append to `ethers-memo.ts`: ```typescript TypeScript theme={null} const tx = await wallet.sendTransaction({ to: memoAddress, data: memoInterface.encodeFunctionData("memo", [ usdcAddress, transferData, memoId, memoBytes, ]), }); const receipt = await tx.wait(); if (!receipt) { throw new Error("Transaction was not mined"); } if (receipt.status !== 1) { throw new Error(`Memo transaction reverted: ${tx.hash}`); } console.log("Transaction:", `${explorerUrl}/tx/${tx.hash}`); console.log("Block:", receipt.blockNumber); ``` web3.py builds the transaction explicitly, so this chunk also sets the nonce and the EIP-1559 fee fields before signing and sending: ```python Python theme={null} latest_block = w3.eth.get_block("latest") priority_fee = w3.to_wei(1, "gwei") base_fee = latest_block.get("baseFeePerGas", w3.eth.gas_price) transaction = memo.functions.memo( usdc_address, transfer_bytes, memo_id, memo_bytes, ).build_transaction( { "from": account.address, "nonce": w3.eth.get_transaction_count(account.address), "chainId": w3.eth.chain_id, "maxPriorityFeePerGas": priority_fee, "maxFeePerGas": (base_fee * 2) + priority_fee, } ) signed = account.sign_transaction(transaction) raw_transaction = getattr(signed, "raw_transaction", None) or signed.rawTransaction tx_hash = w3.eth.send_raw_transaction(raw_transaction) receipt = w3.eth.wait_for_transaction_receipt(tx_hash) if receipt["status"] != 1: raise RuntimeError(f"Memo transaction reverted: {Web3.to_hex(tx_hash)}") print("Transaction:", f"{explorer_url}/tx/{Web3.to_hex(tx_hash)}") print("Block:", receipt["blockNumber"]) ``` ## Step 7. Decode and verify the memo events A successful memo transfer emits one `BeforeMemo` event and one `Memo` event alongside the USDC `Transfer`. Decode the receipt logs and verify that the `Memo` event carries the sender, target, calldata hash, `memoId`, and memo bytes the script sent. Append to `viem-memo.ts`: ```typescript TypeScript theme={null} const events = parseEventLogs({ abi: memoAbi, logs: receipt.logs, }); const beforeMemoEvents = events.filter( (event) => event.eventName === "BeforeMemo", ); const memoEvents = events.filter((event) => event.eventName === "Memo"); if (beforeMemoEvents.length !== 1 || memoEvents.length !== 1) { throw new Error("Expected exactly one BeforeMemo event and one Memo event"); } const memoArgs = memoEvents[0].args as { sender: Address; target: Address; callDataHash: `0x${string}`; memoId: `0x${string}`; memo: `0x${string}`; memoIndex: bigint; }; if (getAddress(memoArgs.sender) !== account.address) { throw new Error(`Unexpected memo sender: ${memoArgs.sender}`); } if (getAddress(memoArgs.target) !== getAddress(usdcAddress)) { throw new Error(`Unexpected memo target: ${memoArgs.target}`); } if (memoArgs.callDataHash !== callDataHash) { throw new Error(`Unexpected callDataHash: ${memoArgs.callDataHash}`); } if (memoArgs.memoId !== memoId || memoArgs.memo !== memoBytes) { throw new Error("Memo event did not include the expected memoId and memo"); } console.log( "Transaction:", `${arcTestnet.blockExplorers.default.url}/tx/${hash}`, ); console.log("Block:", receipt.blockNumber.toString()); console.log("Decoded memo events:", events); ``` Append to `ethers-memo.ts`: ```typescript TypeScript theme={null} const beforeMemoEvents: ethers.LogDescription[] = []; const memoEvents: ethers.LogDescription[] = []; for (const log of receipt.logs) { if (log.address.toLowerCase() !== memoAddress.toLowerCase()) continue; const parsed = memoInterface.parseLog(log); if (!parsed) continue; if (parsed.name === "BeforeMemo") beforeMemoEvents.push(parsed); if (parsed.name === "Memo") memoEvents.push(parsed); console.log(parsed.name, parsed.args); } if (beforeMemoEvents.length !== 1 || memoEvents.length !== 1) { throw new Error("Expected exactly one BeforeMemo event and one Memo event"); } const memoArgs = memoEvents[0].args; if (memoArgs.sender.toLowerCase() !== wallet.address.toLowerCase()) { throw new Error(`Unexpected memo sender: ${memoArgs.sender}`); } if (memoArgs.target.toLowerCase() !== usdcAddress.toLowerCase()) { throw new Error(`Unexpected memo target: ${memoArgs.target}`); } if (memoArgs.callDataHash !== callDataHash) { throw new Error(`Unexpected callDataHash: ${memoArgs.callDataHash}`); } if ( memoArgs.memoId !== memoId || ethers.hexlify(memoArgs.memo) !== ethers.hexlify(memoBytes) ) { throw new Error("Memo event did not include the expected memoId and memo"); } ``` Append to `python-memo.py`: ```python Python theme={null} before_memo_events = memo.events.BeforeMemo().process_receipt(receipt, errors=DISCARD) memo_events = memo.events.Memo().process_receipt(receipt, errors=DISCARD) if len(before_memo_events) != 1 or len(memo_events) != 1: raise RuntimeError("Expected exactly one BeforeMemo event and one Memo event") memo_args = memo_events[0]["args"] if Web3.to_checksum_address(memo_args["sender"]) != account.address: raise RuntimeError(f"Unexpected memo sender: {memo_args['sender']}") if Web3.to_checksum_address(memo_args["target"]) != usdc_address: raise RuntimeError(f"Unexpected memo target: {memo_args['target']}") if memo_args["callDataHash"] != call_data_hash: raise RuntimeError(f"Unexpected callDataHash: {memo_args['callDataHash'].hex()}") if memo_args["memoId"] != memo_id or memo_args["memo"] != memo_bytes: raise RuntimeError("Memo event did not include the expected memoId and memo") print("BeforeMemo:", dict(before_memo_events[0]["args"])) print("Memo:", dict(memo_args)) ``` ## Step 8. Query memo events by `memoId` `memoId` is an indexed event field, so you can find the memo again later without the transaction hash. Query the `Memo` logs for the `memoId` the script sent and confirm exactly one match. Append to `viem-memo.ts`: ```typescript TypeScript theme={null} const memoEvent = parseAbiItem( "event Memo(address indexed sender,address indexed target,bytes32 callDataHash,bytes32 indexed memoId,bytes memo,uint256 memoIndex)", ); const matchingLogs = await publicClient.getLogs({ address: memoAddress, event: memoEvent, args: { memoId }, fromBlock: receipt.blockNumber, toBlock: receipt.blockNumber, }); if (matchingLogs.length !== 1) { throw new Error( `Expected one Memo log for memoId, found ${matchingLogs.length}`, ); } console.log("Memo events matching memoId:", matchingLogs); ``` Append to `ethers-memo.ts`: ```typescript TypeScript theme={null} const memoTopic = memoInterface.getEvent("Memo")?.topicHash; if (!memoTopic) { throw new Error("Memo event topic not found"); } const matchingLogs = await provider.getLogs({ address: memoAddress, topics: [memoTopic, null, null, memoId], fromBlock: receipt.blockNumber, toBlock: receipt.blockNumber, }); if (matchingLogs.length !== 1) { throw new Error( `Expected one Memo log for memoId, found ${matchingLogs.length}`, ); } console.log( "Memo events matching memoId:", matchingLogs.map((log) => memoInterface.parseLog(log)), ); ``` Append to `python-memo.py`: ```python Python theme={null} matching_logs = memo.events.Memo().get_logs( argument_filters={"memoId": memo_id}, from_block=receipt["blockNumber"], to_block=receipt["blockNumber"], ) if len(matching_logs) != 1: raise RuntimeError(f"Expected one Memo log for memoId, found {len(matching_logs)}") print("Memo events matching memoId:", matching_logs) ``` ## Step 9. Run the script Run the completed script for your client library: ```bash Viem theme={null} npx tsx --env-file=.env viem-memo.ts ``` ```bash Ethers theme={null} npx tsx --env-file=.env ethers-memo.ts ``` ```bash Python theme={null} python python-memo.py ``` The exact formatting differs by client library, but successful output includes the transaction URL, block number, decoded `BeforeMemo` and `Memo` event data, and one historical log match for the same `memoId`: ```text theme={null} Transaction: https://testnet.arcscan.app/tx/0x... Block: 123456 BeforeMemo: { memoIndex: 42 } Memo: { sender: "0xYourWallet...", target: "0x3600000000000000000000000000000000000000", callDataHash: "0x...", memoId: "0x...", memo: "0x...", memoIndex: 42 } Memo events matching memoId: [ ... ] ``` ## Step 10. Check JSON-RPC directly with curl Use curl for read-only JSON-RPC checks, such as verifying deployed bytecode, reading a transaction receipt, or querying `Memo` logs. Use a client library such as viem, ethers, or web3.py to sign and submit the transaction itself. ```bash eth_getCode theme={null} MEMO_ADDRESS=0x5294E9927c3306DcBaDb03fe70b92e01cCede505 RPC_URL=https://rpc.testnet.arc.io curl --request POST "$RPC_URL" \ --header "content-type: application/json" \ --data '{ "jsonrpc": "2.0", "method": "eth_getCode", "params": ["'"$MEMO_ADDRESS"'", "latest"], "id": 1 }' ``` ```bash eth_getTransactionReceipt theme={null} TRANSACTION_HASH=0xYOUR_TRANSACTION_HASH RPC_URL=https://rpc.testnet.arc.io curl --request POST "$RPC_URL" \ --header "content-type: application/json" \ --data '{ "jsonrpc": "2.0", "method": "eth_getTransactionReceipt", "params": ["'"$TRANSACTION_HASH"'"], "id": 1 }' ``` ```bash eth_getLogs theme={null} MEMO_ADDRESS=0x5294E9927c3306DcBaDb03fe70b92e01cCede505 MEMO_EVENT_TOPIC=0xeb15ee720798341c37739df41be53acfbbf70ae6802dade35457beec6e47a5e4 MEMO_ID=0xYOUR_32_BYTE_MEMO_ID FROM_BLOCK=0xYOUR_RECEIPT_BLOCK_NUMBER TO_BLOCK=0xYOUR_RECEIPT_BLOCK_NUMBER RPC_URL=https://rpc.testnet.arc.io curl --request POST "$RPC_URL" \ --header "content-type: application/json" \ --data '{ "jsonrpc": "2.0", "method": "eth_getLogs", "params": [{ "address": "'"$MEMO_ADDRESS"'", "fromBlock": "'"$FROM_BLOCK"'", "toBlock": "'"$TO_BLOCK"'", "topics": ["'"$MEMO_EVENT_TOPIC"'", null, null, "'"$MEMO_ID"'"] }], "id": 1 }' ``` You can now attach memos to USDC transfers and reconcile them from the emitted events. For the event schema, nested memo behavior, and the guardrails that constrain memo calls, see [Transaction memos](/arc/concepts/transaction-memos). # Set up Prometheus and Grafana for an Arc node Source: https://docs.arc.io/arc/tutorials/set-up-node-monitoring Deploy a local Prometheus and Grafana stack on the same host as your Arc node to scrape metrics and view the pre-built dashboards. Arc is currently in its testnet phase. During this period, the network may experience instability or unplanned downtime. Throughout this page, all references to Arc refer specifically to the Arc Testnet. Deploy Prometheus and Grafana in Docker on the same host as your Arc node, scrape metrics from the Execution Layer (EL) and Consensus Layer (CL), and load the pre-built Arc dashboards. The stack binds to `127.0.0.1` only and is reached over SSH port forwarding from a remote operator workstation. ## Prerequisites * You have a running Arc node that meets the [node requirements](/arc/references/node-requirements), set up either from the [Run an Arc node](/arc/tutorials/run-an-arc-node) tutorial or [deployed as a systemd service](/arc/tutorials/deploy-node-as-service) * You have installed [Docker Engine 24+ with BuildKit](https://docs.docker.com/engine/install/) and [Docker Compose v2](https://docs.docker.com/compose/install/) on the node host * Your node is started with `--metrics 127.0.0.1:9001` on the EL and `--metrics 127.0.0.1:29000` on the CL (the default in [Run an Arc node](/arc/tutorials/run-an-arc-node)) * You have cloned the [`arc-node`](https://github.com/circlefin/arc-node) repository on the host, to reuse the pre-built Grafana dashboards ## Step 1: Verify the metrics endpoints Confirm both metrics endpoints are reachable on the host before deploying the stack. The Execution Layer serves metrics at the root path, not at `/metrics`: ```shell theme={null} curl -s http://127.0.0.1:9001 | head curl -s http://127.0.0.1:29000/metrics | head ``` Both commands return Prometheus-formatted text. If either fails, fix the node startup or `--metrics` flag before continuing. See [Monitor a node](/arc/tutorials/monitor-a-node) for troubleshooting. ## Step 2: Create the monitoring directory This guide stores all monitoring files under `$ARC_HOME/monitoring`. Create the directory layout: ```shell theme={null} ARC_HOME="${ARC_HOME:-$HOME/.arc}" ARC_MONITORING="${ARC_MONITORING:-$ARC_HOME/monitoring}" mkdir -p "$ARC_MONITORING"/grafana-provisioning/datasources mkdir -p "$ARC_MONITORING"/grafana-provisioning/dashboards mkdir -p "$ARC_MONITORING"/prometheus-data mkdir -p "$ARC_MONITORING"/dashboards ``` ## Step 3: Copy the pre-built Arc dashboards The `arc-node` repository ships ready-to-use Grafana dashboards. From the `arc-node` repository root, copy them into the monitoring directory: ```shell theme={null} cp -r deployments/monitoring/config-grafana/provisioning/dashboards-data/* \ "$ARC_MONITORING"/dashboards/ ``` ## Step 4: Configure Prometheus Write the following file to `$ARC_MONITORING/prometheus.yml`. The EL job scrapes the root path; the CL job scrapes `/metrics`: ```yaml theme={null} global: scrape_interval: 1s scrape_configs: - job_name: "arc_execution" metrics_path: "/" scrape_interval: 1s static_configs: - targets: ["127.0.0.1:9001"] labels: client_name: "reth" client_type: "execution" - job_name: "arc_consensus" metrics_path: "/metrics" scrape_interval: 1s static_configs: - targets: ["127.0.0.1:29000"] labels: client_name: "malachite" client_type: "consensus" ``` ## Step 5: Configure the Grafana datasource Write the following file to `$ARC_MONITORING/grafana-provisioning/datasources/prometheus.yml`: ```yaml theme={null} apiVersion: 1 datasources: - name: prometheus uid: prometheus type: prometheus url: http://127.0.0.1:9090 isDefault: true editable: true ``` ## Step 6: Configure dashboard provisioning Write the following file to `$ARC_MONITORING/grafana-provisioning/dashboards/default.yml`: ```yaml theme={null} apiVersion: 1 providers: - name: "arc" orgId: 1 folder: "" type: file disableDeletion: false editable: true updateIntervalSeconds: 10 options: path: /var/lib/grafana/dashboards ``` ## Step 7: Create the Docker Compose file Write the following file to `$ARC_MONITORING/compose.yaml`: ```yaml theme={null} services: prometheus: image: prom/prometheus user: "0" network_mode: host command: - --config.file=/etc/prometheus/prometheus.yml - --storage.tsdb.path=/prometheus - --web.listen-address=127.0.0.1:9090 volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro - ./prometheus-data:/prometheus restart: unless-stopped grafana: image: grafana/grafana-oss network_mode: host environment: GF_SERVER_HTTP_ADDR: 127.0.0.1 GF_SERVER_HTTP_PORT: 3000 GF_SECURITY_ADMIN_USER: admin GF_SECURITY_ADMIN_PASSWORD: admin volumes: - ./grafana-provisioning:/etc/grafana/provisioning:ro - ./dashboards:/var/lib/grafana/dashboards:ro restart: unless-stopped ``` The Arc metrics endpoints in this guide bind to `127.0.0.1` only. `network_mode: host` is the simplest way for Prometheus to scrape them without changing the Arc node configuration or exposing metrics publicly. ## Step 8: Start the stack ```shell theme={null} cd "$ARC_MONITORING" docker compose up -d ``` Confirm both containers are running: ```shell theme={null} docker compose ps ``` Check that Grafana and Prometheus are healthy: ```shell theme={null} curl -s http://127.0.0.1:3000/api/health curl -s http://127.0.0.1:9090/-/ready ``` ## Step 9: Access the dashboards The stack binds Grafana and Prometheus to `127.0.0.1` only. They are not exposed to the public internet. If you are operating on the same machine, open: * `http://127.0.0.1:3000` for Grafana * `http://127.0.0.1:9090` for Prometheus If you are connecting from a remote workstation, use SSH port forwarding: ```shell theme={null} ssh -N \ -L 3000:127.0.0.1:3000 \ -L 9090:127.0.0.1:9090 \ user@YOUR_SERVER_IP ``` Then open on your local machine: * `http://localhost:3000` * `http://localhost:9090` Grafana default credentials from the Compose file above are username `admin` and password `admin`. Change them before exposing Grafana through any reverse proxy or shared access setup. After Prometheus scrapes both endpoints and Grafana loads the provisioned dashboards, you see metrics for execution layer activity, consensus layer activity, block height, validator telemetry, and connected peers. Newly started Prometheus instances need a short time to collect enough samples; if some panels are empty immediately after startup, wait a minute and refresh. ## Troubleshooting ### Browser shows `ERR_CONNECTION_REFUSED` If you are connecting to a remote server, confirm you are opening `http://localhost:3000` and `http://localhost:9090` on your local machine, not `http://YOUR_SERVER_IP:3000`. Also confirm the SSH tunnel is still running. ### Prometheus restarts with a permissions error If Prometheus logs include an error about `queries.active` or write permission under `/prometheus`, fix ownership of the local data directory: ```shell theme={null} sudo chown -R 65534:65534 "$ARC_MONITORING"/prometheus-data docker compose restart prometheus ``` ### Grafana is healthy but dashboards show no data Confirm Prometheus targets are up: ```shell theme={null} curl -s http://127.0.0.1:9090/api/v1/targets ``` Re-check the Arc metrics endpoints: ```shell theme={null} curl -s http://127.0.0.1:9001 | head curl -s http://127.0.0.1:29000/metrics | head ``` If Prometheus reaches the targets but some panels remain empty, wait for more samples to accumulate and refresh. ### Check Arc node health directly You can verify the Arc node independently of Grafana: ```shell theme={null} cast block-number --rpc-url http://127.0.0.1:8545 sudo journalctl -u arc-execution -f sudo journalctl -u arc-consensus -f ``` # Build on Arc Source: https://docs.arc.io/build Quickstarts, tutorials, and SDKs for building onchain finance applications on Arc. Arc gives you a complete developer platform for building financial applications with stablecoins. Use [App Kits](/app-kit) to add bridging, swapping, and crosschain token flows to any app, deploy smart contracts on a purpose-built L1 network, or integrate third-party developer tools for RPC access, data indexing, and compliance. ## Get started RPC endpoints, chain ID, and network configuration for Arc testnet. Deploy, test, and interact with a Solidity smart contract on Arc. [App Kits](/app-kit) are a suite of SDKs for adding bridging, swapping, token transfers, and unified crosschain balances to any app—across EVM chains, Solana, and Circle Wallets, not just Arc. ## Network quickstarts Step-by-step guides for working directly with the Arc network. Most tutorials require an [Arc testnet RPC connection](/arc/references/connect-to-arc) and a funded wallet. | Quickstart | What you'll build | | ----------------------------------------------------------------- | --------------------------------------------------------------------------------- | | [Deploy contracts](/arc/tutorials/deploy-contracts) | Deploy pre-audited ERC-20, ERC-721, and ERC-1155 templates with Circle Contracts. | | [Interact with contracts](/arc/tutorials/interact-with-contracts) | Mint, transfer, and airdrop tokens using deployed contracts. | | [Monitor contract events](/arc/tutorials/monitor-contract-events) | Set up webhooks and event monitors for onchain activity. | ## Developer tools Smart wallets, paymasters, and session keys from ecosystem providers. Managed RPC access from Alchemy, QuickNode, Blockdaemon, and dRPC. Query onchain data with Envio, Goldsky, The Graph, and Thirdweb. Transaction monitoring and wallet screening from Elliptic and TRM Labs. ## Sample applications Browse working examples and reference implementations in the [sample apps](/arc/references/sample-applications) gallery. # Agentic Economy Source: https://docs.arc.io/build/agentic-economy Enable autonomous AI agents to coordinate, contract, and settle value in real time on Arc. Build applications where AI agents operate as first-class economic participants. Arc provides onchain identity ([ERC-8004](https://eips.ethereum.org/EIPS/eip-8004)), reputation, and job settlement standards ([ERC-8183](https://eips.ethereum.org/EIPS/eip-8183)) so agents can register, find work, and get paid autonomously. For the payment primitives that agents settle with, see [P2P Payments](/build/payments). For agent-initiated checkout flows, see [eCommerce Checkout](/build/ecommerce). ## Sample apps Production-ready examples on GitHub you can fork and customize. AI-powered work validation and USDC settlement to automate escrow flows using Circle Wallets, Refund Protocol, and Contract Platform. Autonomous AI agent pays for premium API endpoints in USDC fractions using Circle Nanopayments and the x402 protocol. ## Quickstarts Get up and running with agentic workflows in minutes. **Beginner**. Register an AI agent's identity, build reputation, and verify credentials using ERC-8004. **Intermediate**. Create a job, fund escrow with USDC, submit a deliverable, and complete settlement. ## Why Arc for the agentic economy Arc is purpose-built for stablecoin finance. These capabilities directly support agentic economy applications. The [ERC-8004](https://eips.ethereum.org/EIPS/eip-8004) standard provides a native registry for agent identity, reputation events, and credential verification. [Register your first AI agent](/arc/tutorials/register-your-first-ai-agent) to see it in action. The [ERC-8183](https://eips.ethereum.org/EIPS/eip-8183) standard defines the full job lifecycle: creation, escrow funding, deliverable submission, evaluation, and USDC settlement. [Create your first ERC-8183 job](/arc/tutorials/create-your-first-erc-8183-job) to try the workflow. Agents need fast, deterministic confirmation to close jobs and release funds. [Deterministic finality](/arc/concepts/deterministic-finality)—the guarantee that a confirmed transaction cannot be reversed—confirms transactions in under a second. Agents transact in a stable unit of account without managing volatile gas tokens. See [gas and fees](/arc/references/gas-and-fees) for Arc's stable fee model. Built-in integration points for transaction monitoring and wallet screening from [Elliptic and TRM Labs](/arc/tools/compliance-vendors), essential for agent-to-agent value transfer at scale. Agents can use any EVM-compatible SDK (ethers.js, viem, web3.py) to interact with Arc contracts—see [EVM compatibility](/arc/references/evm-differences) for details. [Deploy on Arc](/arc/tutorials/deploy-on-arc) to get started, or use the [Arc MCP Server](/ai/mcp) for AI-assisted development. # Borrow and lend Source: https://docs.arc.io/build/borrow-and-lend Build collateralized lending protocols on Arc with instant settlement, native USDC borrowing, and programmable liquidation. Onchain lending on Arc uses a collateral-backed borrowing model: users lock crypto assets in a smart contract and borrow USDC against them. Arc's [deterministic finality](/arc/concepts/deterministic-finality) settles collateral deposits and repayments in under a second, and its USDC-denominated gas gives liquidators and borrowers predictable, stable transaction costs—removing volatile fee overhead from interest rate and liquidation threshold calculations. For decentralized trading and speculative position markets, see [Prediction markets](/build/prediction-markets). For stablecoin exchange infrastructure, see [Stablecoin FX](/build/stablecoin-fx). ## Sample apps Open-source examples on GitHub you can fork and customize. Lending against collateral on Arc: deposit cirBTC and borrow USDC against it using a custom lending smart contract, Circle Modular Wallets, and Supabase. ## Quickstarts Get up and running with lending contracts in minutes. **Beginner**. Deploy, test, and interact with a Solidity smart contract on Arc using Foundry. **Intermediate**. Execute multiple USDC transfers in a single transaction—useful for pool distributions and bulk repayments. ## Why build lending on Arc Arc is purpose-built for stablecoin finance. These capabilities directly support lending and borrowing applications. [Deterministic finality](/arc/concepts/deterministic-finality) confirms collateral deposits and repayments in under a second, reducing the window during which positions can become eligible for liquidation. USDC is Arc's native settlement currency—no wrapping, synthetic pools, or bridging overhead. See [contract addresses](/arc/references/contract-addresses) for USDC token details. USDC-denominated gas removes fee volatility from liquidation calculations. Liquidators can model profitability precisely. See [gas and fees](/arc/references/gas-and-fees) for current rates. [Batched transactions](/arc/concepts/batched-transactions) let liquidators bundle collateral seizure and repayment into a single atomic transaction, reducing gas overhead and liquidation complexity. Integrate Chronicle, Chainlink, or other providers from the [oracles](/arc/tools/oracles) page to price collateral accurately for loan-to-value enforcement. Solidity lending contracts deploy unchanged on Arc. See [EVM compatibility](/arc/references/evm-differences) for differences to check before deploying. # eCommerce Checkout Source: https://docs.arc.io/build/ecommerce Accept stablecoin payments in your online store with fast settlement, low fees, and built-in compliance on Arc. Add stablecoin checkout to your eCommerce application. Arc gives you [instant settlement](/arc/concepts/deterministic-finality), programmable escrow, and native [compliance hooks](/arc/tools/compliance-vendors) so you can accept USDC and EURC payments with confidence. For peer-to-peer transfers, see [P2P Payments](/build/payments). For multi-currency conversion at checkout, see [Stablecoin FX](/build/stablecoin-fx). For agent-initiated purchases, see [Agentic Economy](/build/agentic-economy). ## Sample apps Production-ready examples on GitHub you can fork and customize. Accept USDC payments for in-app purchases using Circle Developer Controlled Wallets, Next.js, and Supabase. Conditional USDC settlement and refund logic for escrow-based checkout flows using Circle Wallets, Refund Protocol, and Contract Platform. ## Quickstarts Get up and running with checkout flows in minutes. **Beginner**. Transfer stablecoins between wallets using [App Kit](/app-kit/send). **Beginner**. Deploy payment-related smart contracts (ERC-20 tokens, NFT receipts) on Arc testnet. **Beginner**. Accept payments from other chains by [bridging USDC](/app-kit/bridge) using App Kits. **Intermediate**. Set up webhooks and event monitors for onchain activity such as payment confirmations. ## Why Arc for eCommerce Arc is purpose-built for stablecoin finance, which simplifies building eCommerce applications. [Sub-second deterministic finality](/arc/concepts/deterministic-finality)—the guarantee that a confirmed transaction cannot be reversed—means no chargebacks and immediate payment confirmation for both merchants and customers. Customers and merchants transact in USDC, not volatile gas tokens. Any transaction-submitting component (relayer, checkout backend, or escrow contract) must hold USDC for gas. See [gas and fees](/arc/references/gas-and-fees) for details on Arc's stable fee model. Native hooks for KYC/AML screening from [Elliptic and TRM Labs](/arc/tools/compliance-vendors) let you meet regulatory requirements without building custom infrastructure. Accept payments from any chain [supported by App Kit](/app-kit/references/supported-blockchains) via [bridging](/app-kit/bridge), so customers can pay from the chain they already use. Smart contracts enable milestone-based payment release, refund protocols, and conditional settlement logic for marketplace and SaaS checkout flows. See the [Arc escrow](https://github.com/circlefin/arc-escrow) sample app for a working example. Build with Solidity, ethers.js, or viem. Your existing web3 development workflows work unchanged on Arc—see [EVM compatibility](/arc/references/evm-differences) for details. [Deploy on Arc](/arc/tutorials/deploy-on-arc) to get started. # EVM differences Source: https://docs.arc.io/build/evm-differences The complete reference for Arc's protocol-level differences from Ethereum: USDC as native gas, value transfer rules, SELFDESTRUCT semantics, EIP-7708 Transfer events, the fee market, and opcode behavior. Arc is an EVM-compatible Layer-1 blockchain. Solidity, Foundry, Hardhat, Viem, ethers.js, and standard Ethereum wallets work without modification, and you can deploy existing contracts unchanged in most cases. Arc targets the **Osaka** hard fork as its baseline, including features such as EIP-7702 (set-code transactions). Arc also ships select features from Ethereum's upcoming **Amsterdam** hard fork ahead of upstream, notably [EIP-7708](https://eips.ethereum.org/EIPS/eip-7708) (standard `Transfer` logs for native value movements). This page is the complete reference for where Arc's protocol-level behavior diverges from Ethereum. All comparisons are against Ethereum at the **Osaka** hard fork, Arc's baseline. Most differences are transparent to application code, but a few change execution semantics in ways that matter when you port a contract. If you are porting an existing contract, start with the [Porting contracts to Arc checklist](/arc/tutorials/porting-contracts-to-arc). Tools that locally simulate the EVM (such as Foundry's `anvil`) run a standard EVM, not Arc's, so they cannot reproduce Arc-specific behavior. Features that depend on it (the native-coin precompiles, EIP-7708 `Transfer` events, and USDC blocklist enforcement) only surface when you test against an Arc RPC endpoint. ## Integration guidance This page is a complete inventory of Arc's protocol-level divergences from Ethereum. The guides linked here translate those differences into practical guidance for specific integration types: Balance display, transaction history, and USDC fee handling. Deposit detection, withdrawal processing, and CCTP-based liquidity management. Single-asset USDC configuration, deposit detection, and instant settlement for fiat ramp providers. USDC pool implementation, decimal handling in pool math, and contract porting. Indexing EIP-7708 Transfer events and avoiding double-counting from the dual emitter. USDC-denominated gas infrastructure and decimal precision in fee calculation. ## USDC as the native gas token Arc uses USDC as its native token. The single most important thing to understand is that **native USDC and the ERC-20 USDC interface are the same asset**, not two separate tokens that happen to share a name. | Interface | Decimals | Used for | | :--------- | :------- | :----------------------------------------------------- | | **Native** | 18 | Gas accounting, native sends, and `msg.value` | | **ERC-20** | 6 | application-level transfers, approvals, and allowances | The ERC-20 interface lives at the [USDC contract address](/arc/references/contract-addresses#usdc) and provides familiar functions such as `transferFrom`, `approve`, and allowance management. A native send and an ERC-20 `transfer` both move the same underlying balance. On other EVM blockchains, the native token has no ERC-20 interface, so protocols deploy a WETH-style wrapper contract to expose `transfer` and `approve`. On Arc, the built-in ERC-20 interface at `0x3600000000000000000000000000000000000000` already covers those use cases, so no wrapper contract is needed. Because both interfaces operate on one balance, `USDC.balanceOf(addr)` and `addr.balance` are two views of the same value. They use different decimals (6 vs 18), so never compare or mix their raw values without converting first. `eth_getBalance` returns 18-decimal native precision. Using `formatUnits(balance, 6)` instead of `formatUnits(balance, 18)` overstates every displayed balance by a factor of 10¹². This single-asset model has consequences that do not exist on other EVM chains: * **The ERC-20 view truncates, so it is not exact.** The 6-decimal `balanceOf` drops anything less than 1×10⁻⁶ USDC, so a native balance of `0.0000001` USDC reads as `0` and `100.0000001` USDC reads as `100`. A `balanceOf` of `0` does not imply a native balance of `0`. * **A native transfer can revert even with a sufficient balance.** On Ethereum, a transfer with sufficient funds always succeeds. On Arc, transfers to the zero address or to or from a blocklisted address fail regardless of balance. * **Don't treat an ERC-20 allowance as a complete spending control on a contract.** `USDC.approve` bounds `transferFrom` calls only. If the contract you're building also exposes functions that send native USDC, those paths move USDC regardless of any allowance. Account for all transfer paths in your access controls, not just the ERC-20 interface. * **Guard rescue functions against sweeping USDC.** A `recoverToken`-style function that sweeps stranded ERC-20 tokens also drains the contract's native USDC balance, because native USDC and ERC-20 USDC are the same balance. Add an explicit guard to prevent USDC from being swept unintentionally. * **Native USDC and ERC-20 USDC share one underlying balance.** Don't display them as separate rows; always show a single USDC balance. Arc uses USDC for gas. Relayers, paymasters, and any wallet that submits transactions on Arc must hold USDC. For the conceptual model, including EURC and USYC support, see [Stablecoin native model](/arc/concepts/stablecoin-native-model). For how balance changes surface as events, see [USDC system events](/arc/references/usdc-system-events). ## Execution and opcode differences | Behavior | Ethereum (Osaka) | Arc | Developer impact | | :------------------------------------------------- | :--------------------------------------------------- | :--------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------- | | `PREVRANDAO` | Beacon chain RANDAO mix | Always returns `0` | No onchain randomness. Use an oracle or verifiable random function (VRF). | | `SELFDESTRUCT` | EIP-6780 semantics | EIP-6780 plus native value rules; emits a `Transfer` log on success (see [SELFDESTRUCT](#selfdestruct)) | Several patterns that succeed on mainnet revert on Arc. | | Non-zero-value `CALL` to a self-destructed account | Succeeds; value credited | **Reverts** (a transfer to a destructed account is a forbidden burn) | The largest semantic departure. See [SELFDESTRUCT](#selfdestruct). | | `parentBeaconBlockRoot` / EIP-4788 | Beacon-roots contract returns the parent beacon root | Set to the parent execution block hash; the beacon-roots contract is omitted, so reads return empty (`0x`) | Do not treat the beacon-roots oracle as functional or as a randomness source. | | Blob transactions (EIP-4844, type-3) | Supported | Not supported; the mempool rejects type-3 transactions | Do not submit blob transactions. `BLOBHASH` returns `0` and `BLOBBASEFEE` returns `1`. | | Withdrawals (EIP-4895) | May be present | Always empty | `block.withdrawals` is always empty. | [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702) set-code transactions, `CREATE2` (including EIP-7610 residual-storage behavior), and EIP-2935 historical block hashes all behave as on Ethereum. In particular, the EIP-2935 block-hash-history contract is deployed and functional, unlike the EIP-4788 beacon-roots contract noted in the table. ## Value transfer rules On Arc, there are two ways a transfer can fail: sending to the zero address (token burning is not allowed) or sending to or from a blocklisted address. In both cases the transaction is included in a block and gas is consumed. * **Transfers to the zero address are forbidden.** A value-bearing transfer to `0x0` reverts with `"Zero address not allowed"`; a zero-value transfer to `0x0` succeeds. (Mint and burn, the only operations that involve `0x0`, go through the native-coin precompile.) * **Burning is forbidden.** Self-destructing to yourself with a balance, or transferring value to an account that has already self-destructed, reverts. * **Transfers to or from a blocklisted address revert.** Check `receipt.status === 0` after any USDC transfer to detect a blocklist revert and handle it explicitly. * **Sending native value to a contract is not guaranteed to succeed.** A call to a contract that forwards native value can revert for any of the reasons listed here, which breaks a common DeFi assumption. * **Sending to an address with no code (`EXTCODESIZE == 0`) succeeds** and emits a `Transfer` log. Sending value to a precompile address reverts. A liquidity pool that pairs native USDC against the ERC-20 USDC interface (as if they were two assets) is meaningless on Arc, because they are one asset. Don't mix `msg.value` with `USDC.balanceOf()` in pool or LTV math: they use different decimals (18 vs 6) and raw values are off by 10¹². ## `SELFDESTRUCT` `SELFDESTRUCT` is allowed on Arc, including during contract deployment, and follows [EIP-6780](https://eips.ethereum.org/EIPS/eip-6780) (the account is fully deleted only if it was created in the same transaction). A self-destruct **reverts** when its value transfer would violate a native value rule: | Condition | Result | | :------------------------------------------------- | :------- | | Beneficiary is the contract itself, with a balance | Reverts | | Beneficiary is the zero address, with a balance | Reverts | | Source or beneficiary is blocklisted | Reverts | | Beneficiary has already self-destructed | Reverts | | Balance is zero (any beneficiary) | Succeeds | Three behaviors differ from every other EVM chain and deserve attention: **1. Self-destructing a contract that holds USDC moves that USDC out.** On Arc a contract's USDC is its native balance, held in the account, so `SELFDESTRUCT` transfers it to the beneficiary. On other chains the contract's ERC-20 USDC balance lives in the token contract and is unaffected by self-destruct. **2. A non-zero-value call to a self-destructed account reverts.** On Ethereum, sending value to an address that self-destructed earlier in the same transaction succeeds. On Arc it is treated as a transfer to a destructed account, a forbidden burn, and reverts. ```solidity theme={null} // Within one transaction: contractA.selfDestruct(payable(b)); // succeeds; emits Transfer(A, B) // Later in the same transaction, any non-zero-value send to A: (bool ok, ) = address(contractA).call{value: 1}(""); // reverts on Arc ``` **3. A successful self-destruct that moves a balance emits a `Transfer` log.** Unlike Ethereum, the moved native value is recorded as an [EIP-7708](https://eips.ethereum.org/EIPS/eip-7708) `Transfer` log from the system emitter (18 decimals). Index it like any other native movement; see [USDC system events](/arc/references/usdc-system-events). ## Native USDC Transfer events (EIP-7708) On a standard EVM chain, a plain native send emits no log. Arc's [EIP-7708](https://eips.ethereum.org/EIPS/eip-7708) implementation emits a standard ERC-20 `Transfer` log from a system address for **every** native USDC movement: native sends, contract endowments, self-destruct transfers, and the precompile-backed mint, burn, and transfer operations. This gives indexers one universal record of balance changes. The system emitter log uses 18 decimals and is distinct from the ERC-20 USDC contract's own 6-decimal `Transfer`. Two indexing mistakes to avoid: * **Filter on the system emitter, not the ERC-20 contract address.** Plain native USDC sends emit no log at `0x3600…0000`. Filtering the ERC-20 address alone silently misses them. * **Don't count logs from both addresses.** An ERC-20 `transfer()` emits a log from both the system emitter and the ERC-20 contract. Counting both inflates balances. Use the system emitter only. For emitter addresses, the exact log format, and indexing guidance, see [USDC system events](/arc/references/usdc-system-events). ## Fee market and block behavior * **The base fee is paid to the block beneficiary, not burned.** Arc has no EIP-1559 burn. Both the base fee and the priority fee are credited to the block's beneficiary. * **The next block's base fee is published in the parent header's `extra_data`** (an 8-byte big-endian value). Read it there rather than re-deriving it. The fee is computed from an exponentially smoothed view of gas usage and is clamped to bounded minimum and maximum values, so it moves predictably. See [Stable fee design](/arc/concepts/stable-fee-design) and [Gas and fees](/arc/references/gas-and-fees). * **The minimum base fee is 20 Gwei.** Transactions with `maxFeePerGas` lower than 20 Gwei are silently dropped by the mempool. They produce no error receipt and never appear in a block. Set `maxFeePerGas` to at least 20 Gwei before submitting any transaction to Arc. * **Block timestamps are non-decreasing, not strictly increasing.** Timestamps come from the proposer's wall clock at one-second granularity, so sub-second blocks may share a timestamp. Use the block number for ordering, and do not assume `block.timestamp` strictly increases between blocks. * **Finality is deterministic and instant.** Transactions finalize on inclusion; offchain systems can act after a single confirmation. See [Deterministic finality](/arc/concepts/deterministic-finality). # P2P Payments Source: https://docs.arc.io/build/payments Build instant, low-cost peer-to-peer payment flows with stablecoin-native transfers and deterministic finality on Arc. Build direct, peer-to-peer payment flows using stablecoins on Arc. P2P payments let users send value directly to each other without an intermediary, settled onchain in under a second. From [simple transfers](/app-kit/send) to full checkout experiences with [crosschain bridging](/app-kit/bridge) using App Kits, Arc provides the infrastructure for fast, low-cost, [compliant](/arc/tools/compliance-vendors) payments. For merchant-facing checkout flows, see [eCommerce Checkout](/build/ecommerce). For cross-currency swaps between stablecoins, see [Stablecoin FX](/build/stablecoin-fx). ## Sample apps Production-ready examples on GitHub you can fork and customize. Accept USDC payments for in-app purchases using Circle Developer Controlled Wallets, Next.js, and Supabase. Unified USDC balance and crosschain transfers using Circle Gateway, Next.js, and Supabase. Multichain treasury system with crosschain capital movement using Circle Developer-Controlled Wallets, Gateway, and Bridge Kit. Autonomous AI agent pays for premium API endpoints in USDC fractions using Circle Nanopayments and the x402 protocol. ## Quickstarts Get up and running with payment flows in minutes. **Beginner**. Transfer stablecoins between wallets using [App Kit](/app-kit/send). **Beginner**. Move USDC across blockchains using [App Kits](/app-kit/bridge). **Intermediate**. Aggregate USDC across blockchains into a single spendable balance with [Unified Balance](/app-kit/unified-balance), which consolidates multichain USDC into one virtual balance. ## Why Arc for payments Arc is purpose-built for stablecoin finance. These capabilities directly support payment applications. [Deterministic finality](/arc/concepts/deterministic-finality)—the guarantee that a confirmed transaction cannot be reversed or reorganized—means payments confirm in under a second, giving senders and recipients immediate certainty. USDC-denominated gas at stable, predictable prices keeps transaction costs minimal. See [gas and fees](/arc/references/gas-and-fees) for current rates. Built-in integration points for transaction monitoring and wallet screening from providers like [Elliptic and TRM Labs](/arc/tools/compliance-vendors). Native support for USDC and EURC. See [contract addresses](/arc/references/contract-addresses) for token details, and use the [Swap](/app-kit/swap) capability to exchange between currencies. Transaction ordering guarantees prevent front-running and ensure payments settle in the order they are submitted. Learn more about Arc's [consensus layer](/arc/concepts/consensus-layer). Full [EVM compatibility](/arc/references/evm-differences) means your existing Solidity, Hardhat, and Foundry workflows work unchanged. [Deploy on Arc](/arc/tutorials/deploy-on-arc) to get started. # Prediction markets Source: https://docs.arc.io/build/prediction-markets Prediction markets on Arc use trustless oracle resolution, position token trading, and USDC settlement to let participants stake on future outcomes. A prediction market lets participants stake collateral on the outcome of a future event. Each market issues position tokens—long (YES) and short (NO)—that represent a claim on the outcome. Participants trade these tokens through an automated market maker (AMM), and winning positions are redeemed for collateral after an oracle resolves the event. On Arc, prediction markets benefit from [sub-second finality](/arc/concepts/deterministic-finality), stable USDC-denominated gas fees, and standard EVM tooling, making it practical to run active markets with frequent trades at low cost. For stablecoin exchange and swap infrastructure, see [Stablecoin FX](/build/stablecoin-fx). For agent-initiated market participation, see [Agentic Economy](/build/agentic-economy). ## Sample apps Open-source examples on GitHub you can fork and customize. Decentralized prediction markets with UMA Optimistic Oracle V2 resolution, constant-product AMM trading, and collateral-backed position tokens on Arc Testnet. ## Quickstarts Get up and running with prediction market contracts in minutes. **Beginner**. Deploy, test, and interact with a Solidity smart contract on Arc using Foundry. **Intermediate**. Set up webhooks and event monitors to track onchain activity from your deployed contracts. ## Why Arc for prediction markets Arc is purpose-built for stablecoin finance. These capabilities directly support prediction market applications. [Deterministic finality](/arc/concepts/deterministic-finality) confirms position token trades and collateral deposits in under a second, removing settlement uncertainty from active markets. USDC-denominated gas at stable, predictable prices makes frequent AMM trades economical. See [gas and fees](/arc/references/gas-and-fees) for current rates. Integrate UMA Optimistic Oracle, Chainlink, or other providers from the [oracles ecosystem](/arc/tools/oracles) to resolve markets without a centralized arbitrator. Position tokens are standard ERC-20 tokens, tradable on any AMM and transferable between wallets using standard EVM tooling. See [contract addresses](/arc/references/contract-addresses) for available tokens. Built-in integration points for wallet screening from [Elliptic and TRM Labs](/arc/tools/compliance-vendors), essential for regulated prediction market products. Deploy and iterate on Solidity market contracts using Hardhat, Foundry, or any EVM-compatible tools. See [EVM compatibility](/arc/references/evm-differences) for differences to check before deploying. # Stablecoin FX Source: https://docs.arc.io/build/stablecoin-fx Build real-time, onchain stablecoin FX products with transparent pricing, instant settlement, and predictable fees on Arc. Build onchain stablecoin FX and swap products on Arc. With native multi-currency support, [sub-second settlement](/arc/concepts/deterministic-finality), and composable crosschain liquidity with [App Kits](/app-kit), Arc gives you the infrastructure for transparent, low-cost currency exchange. For direct wallet-to-wallet transfers, see [P2P Payments](/build/payments). For merchant checkout flows, see [eCommerce Checkout](/build/ecommerce). ## Sample apps Production-ready examples on GitHub you can fork and customize. Real-time USDC and EURC swaps with configurable platform fee collection using App Kit, developer-controlled wallets, and Supabase. Multichain treasury system with crosschain capital movement using Circle Developer-Controlled Wallets, Gateway, and Bridge Kit. Unified USDC balance and crosschain transfers using Circle Gateway, Next.js, and Supabase. ## Quickstarts Get up and running with FX and swap flows in minutes. **Beginner**. Exchange one token for another on the same blockchain using the [Swap](/app-kit/swap) capability. **Intermediate**. Combine [bridging](/app-kit/bridge) and [swapping](/app-kit/swap) to exchange tokens across blockchains. **Beginner**. Transfer USDC between blockchains using [App Kit](/app-kit/bridge). **Intermediate**. Combine USDC from multiple blockchains into a single, instantly spendable balance. ## Why Arc for stablecoin FX Arc is purpose-built for stablecoin finance. These capabilities directly support FX and swap applications. [Deterministic finality](/arc/concepts/deterministic-finality)—the guarantee that a confirmed transaction cannot be reversed or reorganized—settles FX trades in under a second, giving counterparties immediate certainty. Onchain swap rates with configurable slippage tolerance give users full visibility into execution price. See [set slippage tolerance](/app-kit/tutorials/swap/set-slippage-tolerance-or-stop-limit). Swap between USDC, EURC, and other supported stablecoins natively on Arc. See [contract addresses](/arc/references/contract-addresses) for available tokens and [swap fees](/app-kit/concepts/swap-fees) for pricing details. Stable USDC-denominated [gas fees](/arc/references/gas-and-fees) remove fee volatility from FX calculations, so you can quote accurate rates. App Kits [bridges](/app-kit/bridge) and [swaps](/app-kit/swap) compose across chains for multi-leg FX routing without custom infrastructure. Collect a custom spread fee on every swap without writing new smart contracts. See [collect swap fee](/app-kit/tutorials/swap/collect-swap-fee). # Welcome to Arc docs Source: https://docs.arc.io/index Everything you need to build onchain finance with stablecoins: start fast, scale reliably.
# Arc developer documentation

Everything you need to build onchain finance with stablecoins: start fast, scale reliably.

## Use cases Launch instant, low-cost, peer-to-peer payments with stablecoin-native transfers and deterministic settlement. Accept stablecoin payments in your online store with fast settlement and built-in compliance. Build real-time, onchain stablecoin FX products with transparent pricing, instant settlement, and predictable fees. Enable autonomous AI agents to coordinate, contract, and settle value in real time. Build decentralized prediction markets with trustless oracle resolution and position token trading. Build collateralized lending protocols with instant settlement and native USDC borrowing.
## What's new
Mar 10 Release App Kits is now available
# Integrate with Arc Source: https://docs.arc.io/integrate Connect wallets, exchanges, and infrastructure to the Arc network. Arc is [EVM-compatible](/arc/references/evm-differences)—it supports the same bytecode, RPC methods, and tooling as Ethereum. Three key differences drive most integration work: * **USDC as gas**: fees are paid in USDC. Any relayer, paymaster, or funded wallet account must hold USDC. This affects gas estimation, fee display, and balance APIs. * **Deterministic finality**—transactions finalize in under one second with no risk of reorganization. A single confirmation is sufficient. * **Dual USDC interface**—native balance uses 18 decimals while the ERC-20 interface uses 6 decimals. Both share the same underlying balance. See [EVM compatibility](/arc/references/evm-differences) for a full list of differences from Ethereum. ## Exchanges Integrate Arc deposits, withdrawals, and USDC liquidity management into your exchange. Overview of deposit, withdrawal, and bridging workflows for exchanges. Subscribe to blocks and detect incoming USDC transfers with one-confirmation finality. Build, sign, and broadcast withdrawal transactions with USDC gas. Move USDC liquidity to and from Arc using Cross-Chain Transfer Protocol. Configure Fireblocks, BitGo, SAFE, and other custody platforms for Arc. ## On/off-ramps Add Arc as a supported network in your fiat-to-crypto ramp service. Chain registration, deposit detection, withdrawal processing, and UI display for ramp providers. ## Wallets Add Arc as a supported network in your wallet application. Overview of Arc wallet integration for embedded wallets, smart wallets, and browser wallet extensions. Chain configuration, balance display, transaction history, and fee handling for wallet providers. Arc's two-state transaction model: pending or final, with no intermediate confirmation states. Fetch, estimate, and display transaction fees in USDC. ## Infrastructure Add Arc support to node services, data indexers, oracle networks, block explorers, and compliance tools. Key differences from standard EVM chains and chain metadata for infrastructure providers. Unified transfer events, no-reorg indexing, and block streaming guidance. Finality configuration, CCTP routing, and relay infrastructure for bridge protocols. Blocklist enforcement, Memo contract monitoring, and compliance tool integrations. ## Essentials RPC endpoints, chain ID, WebSocket URLs, and explorer links. Deploy, test, and interact with a Solidity smart contract on Arc. Full EVM differences table, dual USDC interface, and integration pitfalls. Operate your own Arc node for independent verification or direct RPC access. # Connect to Arc Source: https://docs.arc.io/integrate/connect-to-arc Set up your wallet and configure your development environment for Arc Testnet. Connect a wallet to Arc Testnet using one-click setup or manual configuration. ## Wallet setup Use the button below to add Arc Testnet to your wallet automatically. ### Manual setup Arc uses USDC as the native gas token (18 decimals). If your wallet supports **custom gas tokens**, ensure display/decimals are set correctly. Wallets that don't support custom gas tokens still work for signing and sending transactions—balances may display as "ETH" but the underlying token is USDC. See [Gas and fees](/arc/references/gas-and-fees) for details. Open MetaMask → **Settings** → **Networks** → **Add network** → **Add a network manually**. | Field | Value | | :------------------ | :---------------------------- | | **Network name** | Arc Testnet | | **New RPC URL** | `https://rpc.testnet.arc.io` | | **Chain ID** | 5042002 | | **Currency symbol** | USDC | | **Explorer URL** | `https://testnet.arcscan.app` | Click **Save**, then switch to Arc Testnet. Open Rabby → click the **network selector** (top-left) → **Add Custom Network**. | Field | Value | | :----------------- | :---------------------------- | | **Chain Name** | Arc Testnet | | **Chain ID** | 5042002 | | **RPC URL** | `https://rpc.testnet.arc.io` | | **Currency** | USDC | | **Block Explorer** | `https://testnet.arcscan.app` | Click **Confirm**, then select Arc Testnet from the network list. Open Coinbase Wallet → **Settings** → **Networks** → **Add custom network**. | Field | Value | | :------------------ | :---------------------------- | | **Network name** | Arc Testnet | | **RPC URL** | `https://rpc.testnet.arc.io` | | **Chain ID** | 5042002 | | **Currency symbol** | USDC | | **Block explorer** | `https://testnet.arcscan.app` | Click **Save**, then switch to Arc Testnet. Open Rainbow → **Settings** (gear icon) → **Networks** → **Custom Network**. | Field | Value | | :----------------- | :---------------------------- | | **Network name** | Arc Testnet | | **RPC URL** | `https://rpc.testnet.arc.io` | | **Chain ID** | 5042002 | | **Symbol** | USDC | | **Block explorer** | `https://testnet.arcscan.app` | Click **Save**, then switch to Arc Testnet. ## Network details | Parameter | Value | | :-------- | :------------------------------------------------- | | Network | Arc Testnet | | Chain ID | `5042002` | | Currency | USDC | | Explorer | [testnet.arcscan.app](https://testnet.arcscan.app) | | Faucet | [faucet.circle.com](https://faucet.circle.com) | ### RPC endpoints ```text Primary theme={null} https://rpc.testnet.arc.io ``` ```text Alchemy theme={null} https://arc-testnet.g.alchemy.com/v2/YOUR_API_KEY ``` ```text Blockdaemon theme={null} https://rpc.blockdaemon.testnet.arc.io ``` ```text dRPC theme={null} https://rpc.drpc.testnet.arc.io ``` ```text QuickNode theme={null} https://rpc.quicknode.testnet.arc.io ``` ### WebSocket endpoints ```text Primary theme={null} wss://rpc.testnet.arc.io ``` ```text Alchemy theme={null} wss://arc-testnet.g.alchemy.com/v2/YOUR_API_KEY ``` ```text Blockdaemon theme={null} wss://rpc.blockdaemon.testnet.arc.io:443/websocket ``` ```text dRPC theme={null} wss://rpc.drpc.testnet.arc.io ``` ```text QuickNode theme={null} wss://rpc.quicknode.testnet.arc.io ``` ## Frontend wallet libraries Use `wagmi` and `viem` to integrate Arc Testnet into [ConnectKit](https://family.co/docs/connectkit), [Reown AppKit](https://docs.reown.com/appkit/overview), or a bare [WalletConnect](https://docs.walletconnect.com) connector. Don't display native USDC and ERC-20 USDC as separate balance rows. They're the same balance; always show a single USDC balance. ### Arc Testnet chain definition `viem` ships Arc Testnet as a built-in chain. No manual definition is needed. ```typescript theme={null} import { arcTestnet } from "viem/chains"; ``` ### Configure wallet connection Pick the tab that matches your setup. ```bash theme={null} npm install connectkit wagmi viem ``` ```typescript theme={null} import { arcTestnet, mainnet } from "viem/chains"; import { createConfig, http } from "wagmi"; import { getDefaultConfig } from "connectkit"; const config = createConfig( getDefaultConfig({ chains: [arcTestnet, mainnet], transports: { [arcTestnet.id]: http("https://rpc.testnet.arc.io"), [mainnet.id]: http("https://cloudflare-eth.com"), }, walletConnectProjectId: process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID, appName: "Your App Name", }), ); ``` When `mainnet` is omitted from `chains`, ConnectKit falls back to its own `eth.merkle.io` endpoint for ENS name lookups, which blocks cross-origin browser requests. Include `mainnet` with a CORS-safe transport (such as the Cloudflare endpoint shown earlier) to prevent this. ```bash theme={null} npm install @reown/appkit @reown/appkit-adapter-wagmi wagmi viem @tanstack/react-query ``` Import `defineChain` from `@reown/appkit/networks`, not from `viem`. AppKit requires two additional fields (`caipNetworkId` and `chainNamespace`) that `viem`'s version omits; using the wrong import causes a runtime error. ```typescript theme={null} import { defineChain } from "@reown/appkit/networks"; import { WagmiAdapter } from "@reown/appkit-adapter-wagmi"; import { createAppKit } from "@reown/appkit/react"; const projectId = process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID; const arcTestnet = defineChain({ id: 5042002, caipNetworkId: "eip155:5042002", chainNamespace: "eip155", name: "Arc Testnet", nativeCurrency: { decimals: 18, name: "USDC", symbol: "USDC" }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.io"], webSocket: ["wss://rpc.testnet.arc.io"], }, }, blockExplorers: { default: { name: "ArcScan", url: "https://testnet.arcscan.app" }, }, testnet: true, }); const wagmiAdapter = new WagmiAdapter({ networks: [arcTestnet], projectId, }); createAppKit({ adapters: [wagmiAdapter], networks: [arcTestnet], projectId, metadata: { name: "Your App Name", description: "Your App Description", url: "https://yourdomain.com", icons: ["https://yourdomain.com/icon.png"], }, }); ``` Get a project ID at [dashboard.reown.com](https://dashboard.reown.com). The same project ID works for the bare WalletConnect connector. For `wagmi` apps that need WalletConnect sessions without Reown AppKit's full modal UI. ```typescript theme={null} import { arcTestnet } from "viem/chains"; import { createConfig, http } from "wagmi"; import { walletConnect } from "wagmi/connectors"; const config = createConfig({ chains: [arcTestnet], connectors: [ walletConnect({ projectId: process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID, metadata: { name: "Your App", description: "Your App Description", url: "https://yourdomain.com", icons: ["https://yourdomain.com/icon.png"], }, }), ], transports: { [arcTestnet.id]: http("https://rpc.testnet.arc.io"), }, }); ``` Arc Testnet (chain ID 5042002) is not registered in the WalletConnect chain registry. On first connection, WalletConnect issues a `wallet_addEthereumChain` call per user. # DeFi integration Source: https://docs.arc.io/integrate/defi Port AMMs, liquidity pools, and DeFi protocols to Arc using USDC as the canonical pool token and native gas asset. For DeFi protocols on Arc, use the ERC-20 USDC interface at `0x3600000000000000000000000000000000000000` for all pool accounting, token addresses, and reserve math. Arc's native USDC has a [built-in ERC-20 interface](/arc/concepts/stablecoin-native-model) at that address, so no WETH-style adapter contract is needed. ## Key differences for DeFi protocols Three Arc design decisions affect every DeFi porting decision: | Concern | Arc behavior | Impact on DeFi protocols | | :------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **One balance, two decimal views** | Native USDC uses 18 decimals (`msg.value`, `address.balance`); the ERC-20 interface uses 6 decimals (`balanceOf`, pool math). Both views draw from the same underlying balance. | Use 6-decimal ERC-20 values exclusively in pool invariant math. Mixing 18-decimal native values with 6-decimal ERC-20 values breaks constant-product calculations by a factor of 1012. Do not pair the native interface against the ERC-20 interface as two separate pool tokens; they are the same asset. | | **No protocol adapter needed** | Native USDC has a built-in ERC-20 interface at `0x3600000000000000000000000000000000000000`. No WETH-style adapter is required. See [EVM differences](/arc/references/evm-differences) for the full list of protocol divergences. | Use the ERC-20 address directly as the pool pair token. | | **Allowances do not gate native transfers** | ERC-20 `approve` and `allowance` control only the `transferFrom` path. Native transfers (`msg.value`, direct sends) bypass the allowance check entirely. | Pool sweep logic that relies on allowance limits does not protect the native balance. | ## What you need Each integration guide in this section requires the following: | Requirement | Details | | :----------------------------- | :----------------------------------------------------------------------------------------------------------------------------- | | RPC access | `https://rpc.testnet.arc.io` | | Chain ID | `5042002` (testnet) | | USDC ERC-20 contract | [`0x3600000000000000000000000000000000000000`](https://testnet.arcscan.app/address/0x3600000000000000000000000000000000000000) | | Existing EVM contract codebase | A Solidity codebase to port to Arc | ## Integration guides Port liquidity pools and AMM contracts to Arc using the ERC-20 USDC address as the canonical pool token. # How-to: Port an AMM or liquidity pool to Arc Source: https://docs.arc.io/integrate/defi/amm-and-pools Migrate an AMM, liquidity pool, or DEX router to Arc by applying the correct USDC token address, decimal conversion factor, and Arc-specific EVM behaviors. Use Arc's ERC-20 USDC interface exclusively for all pool accounting, token addresses, and reserve math. The native interface (`address.balance`, `msg.value`) uses 18 decimals and is for gas operations only; mixing it with the 6-decimal ERC-20 interface silently breaks constant-product calculations by a factor of 1012. The following steps apply this principle and address five other changes required before deploying to Arc. ## Prerequisites Before you begin, ensure that you've: * Read the [stablecoin-native model](/arc/concepts/stablecoin-native-model) concept page to understand Arc's dual USDC interface * Reviewed the [EVM differences reference](/arc/references/evm-differences) for a complete list of protocol divergences * Obtained Arc testnet RPC access at `https://rpc.testnet.arc.io` (chain ID `5042002`) ## Steps ### Step 1. Set the pool token to the ERC-20 USDC address Use `0x3600000000000000000000000000000000000000` as the USDC pair address in all pool and router configuration. On other EVM blockchains, protocols use a WETH-style adapter contract to give the native asset an ERC-20 interface. Arc's native USDC already has a built-in ERC-20 interface, so no adapter is needed. The address `0x3600000000000000000000000000000000000000` is that interface. It already implements `transfer`, `approve`, and `transferFrom` over the native balance. Do not pair native USDC against the ERC-20 USDC interface as two separate pool tokens. Both interfaces draw from the same underlying balance, so pairing them is equivalent to pairing an asset with itself. A pool configured this way is immediately insolvent. Two additional mistakes to avoid: * The zero address (`0x0000000000000000000000000000000000000000`) is not USDC. Do not use it as a token address. Value-bearing transfers to it revert on Arc; see [Value transfer rules](/arc/references/evm-differences#value-transfer-rules). * Any address other than `0x3600000000000000000000000000000000000000` used to represent USDC creates a separate, unrelated token that does not share the native balance. Because Arc's native USDC already exposes a full ERC-20 interface, there is no WETH-equivalent address to configure. Update your pool factory, router configuration, and any initialization scripts to reference `0x3600000000000000000000000000000000000000` directly. ### Step 2. Account for the 10¹² decimal offset in offchain tooling Arc's native USDC has 18 decimals (`address.balance`, `eth_getBalance`, `msg.value`) and the ERC-20 interface has 6 decimals (`balanceOf`, `transfer`). The conversion factor between them is 1012. Pool contracts that follow the ERC-20 prescription do not encounter this offset: deposits go through `transferFrom`, reserves are read from `balanceOf`, and `nonpayable` functions block native USDC from entering pool logic. The offset matters in offchain code: monitoring scripts, indexers, and SDK integrations that read `eth_getBalance` or `address.balance` must convert to 6-decimal units before comparing or displaying values. Use these constants in your offchain scripts and SDK integrations: ```typescript theme={null} const OFFSET: bigint = 12n; // native USDC decimals (18) minus ERC-20 USDC decimals (6) // Normalize a value from eth_getBalance or address.balance to 6-decimal ERC-20 units // before displaying or comparing with ERC-20 amounts function toERC20Units(nativeAmount: bigint): bigint { return nativeAmount / 10n ** OFFSET; } ``` ### Step 3. Remove WETH-style wrap/unwrap code paths On most EVM blockchains, the native asset has no ERC-20 interface, so pools use a wrapper contract (WETH, WMATIC, and so on) with `deposit()` and `withdraw()` functions. On Arc, native USDC already has a built-in ERC-20 interface. Any WETH-style wrap/unwrap code paths are unnecessary and create double-accounting bugs by crediting balances twice. Replace the wrapped native address constant and remove all `deposit()` and `withdraw()` call sites: ```solidity theme={null} // Wrapped native token address on the source chain // [!code --] address constant WRAPPED_NATIVE = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; // [!code --] // ERC-20 USDC address on Arc // [!code ++] address constant USDC = 0x3600000000000000000000000000000000000000; // [!code ++] // Wrap before interacting with the pool // [!code --] IWETH(WRAPPED_NATIVE).deposit{value: msg.value}(); // [!code --] IERC20(WRAPPED_NATIVE).approve(address(pool), msg.value); // [!code --] pool.swap(WRAPPED_NATIVE, tokenOut, msg.value, minAmountOut); // [!code --] // Use ERC-20 USDC directly; no wrapping needed // [!code ++] IERC20(USDC).approve(address(pool), amount); // [!code ++] pool.swap(USDC, tokenOut, amount, minAmountOut); // [!code ++] // Unwrap after receiving wrapped native from the pool // [!code --] pool.removeLiquidity(WRAPPED_NATIVE, lpAmount, minAmountOut); // [!code --] IWETH(WRAPPED_NATIVE).withdraw(wethReceived); // [!code --] // Remove liquidity directly; no unwrapping needed // [!code ++] pool.removeLiquidity(USDC, lpAmount, minAmountOut); // [!code ++] ``` Internal-only wrapping that is not exposed to users (for example, an internal accounting abstraction similar to what Chainlink uses) is acceptable. Arc's native USDC already exposes a full ERC-20 interface at `0x3600000000000000000000000000000000000000`. Any user-facing wrapped USDC contract is unnecessary and fragments liquidity. ### Step 4. Audit allowance-dependent sweep and security logic ERC-20 allowances control transfers initiated through `transferFrom`. They do not gate direct native transfers. A contract holding an `approve` from a user can still have its native USDC balance moved by a direct native send (`call{value: ...}`) without any allowance check. Any sweep mechanism or authorization boundary that relies on ERC-20 allowance limits to protect the contract's native USDC balance is ineffective. Audit your pool's security logic for this pattern and apply one of the following approaches: * Mark the contract `nonpayable` on functions that should not receive native USDC, preventing accidental native deposits. * Add an explicit `receive()` function that either reverts or records the incoming balance, so the contract behaves predictably on both the native and ERC-20 paths. ### Step 5. Replace onchain randomness and audit emergency-shutdown patterns Two EVM opcodes behave differently on Arc and require review before deployment. **PREVRANDAO always returns `0` on Arc.** Any protocol that uses `block.difficulty` or `prevrandao` for randomness receives a constant value, making lottery mechanics, shuffle algorithms, and similar constructs predictable and exploitable. Replace these with a verifiable random function (VRF) or oracle-based randomness. See [Oracles](/arc/tools/oracles) for available providers on Arc. Emergency-shutdown and escape-hatch patterns that use `SELFDESTRUCT` also require review. On Arc, a contract's USDC balance is its native balance, so `SELFDESTRUCT` transfers that USDC to the beneficiary address. On Ethereum, the ERC-20 USDC balance lives in the token contract and is unaffected by `SELFDESTRUCT`. **`SELFDESTRUCT` moves native USDC on Arc.** Protocols with emergency-shutdown patterns that call `SELFDESTRUCT` will transfer the contract's entire USDC balance to the beneficiary. Audit every code path that calls `SELFDESTRUCT` and verify that the USDC transfer is intentional. ### Step 6. Test against an Arc RPC endpoint, not a local Anvil fork Local EVM simulators run a standard EVM and cannot reproduce the following Arc-specific behaviors: * Native-coin precompile behavior * EIP-7708 Transfer events on native USDC movements * Blocklist enforcement on value transfers * Arc's native USDC value transfer rules Run all pool invariant math and value transfer tests against `https://rpc.testnet.arc.io` (chain ID `5042002`). For a complete porting walkthrough covering contract deployment and verification, see [Porting contracts to Arc](/arc/tutorials/porting-contracts-to-arc). # Deploy on Arc Source: https://docs.arc.io/integrate/deploy-on-arc Learn how to deploy, test, and interact with a Solidity smart contract on the Arc Testnet. Arc is currently in its testnet phase. During this period, the network may experience instability or unplanned downtime. **Note:** Throughout this page, all references to Arc refer specifically to the Arc Testnet. In this tutorial, you'll use [Foundry](https://getfoundry.sh/) to deploy and interact with the default `Counter` contract on the Arc Testnet. `forge init` scaffolds a working Solidity project—contract, tests, and deployment script—so you can ship to Arc without writing any new contract code. By the end, you'll have configured Foundry for Arc, deployed `Counter`, and called it from the command line with `cast`. ## Prerequisites Before you begin, ensure that you've: * Confirmed access to a Unix-like shell (macOS, Linux, or Windows with [WSL](https://learn.microsoft.com/en-us/windows/wsl/install)) * Installed [`curl`](https://curl.se/) (used by the Foundry installer) * Installed a code editor such as [VS Code](https://code.visualstudio.com/) ## Step 1. Set up Foundry Install Foundry's command-line tools (`forge`, `cast`, `anvil`, `chisel`): ```shell theme={null} curl -L https://foundry.paradigm.xyz | bash ``` After the installer finishes, run the `source` command shown in the installer output, or open a new terminal session. Then install Foundry's tools: ```shell theme={null} foundryup ``` Navigate to the directory where you want to create your project, then initialize a new Solidity project: ```shell theme={null} forge init hello-arc && cd hello-arc ``` This generates `src/Counter.sol`, a matching test file at `test/Counter.t.sol`, and a deployment script at `script/Counter.s.sol`. ## Step 2. Configure Foundry for Arc Create a `.env` file in the project root with the Arc Testnet RPC URL: ```ini theme={null} ARC_TESTNET_RPC_URL="https://rpc.testnet.arc.io" ``` Load the variables into your shell: ```shell theme={null} source .env ``` Never commit your `.env` file to version control. Store private keys and sensitive variables securely. ## Step 3. Test the contract Run the included tests to compile the contract and verify it works locally: ```shell theme={null} forge test ``` The `Counter` tests pass, confirming compilation and local correctness. ## Step 4. Deploy to Arc Testnet ### 4.1. Create and fund a wallet Generate a new keypair with `cast`: ```shell theme={null} cast wallet new ``` The command returns an address and private key: ```text theme={null} Successfully created new keypair. Address: 0xB815A0c4bC23930119324d4359dB65e27A846A2d Private key: 0xcc1b30a6af68ea9a9917f1dd••••••••••••••••••••••••••••••••••••••97c5 ``` Keep your private key secure. Never share it or commit it to source control. Use environment variables or a secrets manager for any non-test deployment. Add the private key to your `.env` file and reload: ```ini theme={null} PRIVATE_KEY="0x..." ``` ```shell theme={null} source .env ``` Arc uses USDC for gas. Request testnet USDC from the [Circle Faucet](https://faucet.circle.com) to fund your deployment wallet. Visit the [Circle Faucet](https://faucet.circle.com), select **Arc Testnet**, paste your wallet address, and request testnet USDC. Arc uses USDC as its native gas token—transaction fees are paid in USDC instead of a separate cryptocurrency—so this funds the wallet for deployment. Testnet USDC is for testing purposes only. It has no real-world value and must not be used in production. ### 4.2. Deploy the contract Deploy `Counter` to Arc Testnet: ```shell theme={null} forge create src/Counter.sol:Counter \ --rpc-url $ARC_TESTNET_RPC_URL \ --private-key $PRIVATE_KEY \ --broadcast ``` After deployment completes, you'll see output similar to: ```text theme={null} Deployer: 0xB815A0c4bC23930119324d4359dB65e27A846A2d Deployed to: 0x32368037b14819C9e5Dbe96b3d67C59b8c65c4BF Transaction hash: 0xeba0fcb5e528d586db0aeb2465a8fad0299330a9773ca62818a1827560a67346 ``` Save the `Deployed to` address from the output to your `.env` file and reload: ```ini theme={null} COUNTER_ADDRESS="0x..." ``` ```shell theme={null} source .env ``` ### 4.3. Verify the contract on Arc Testnet Explorer Arc Testnet Explorer runs Blockscout, so you can publish your contract's source code with `forge verify-contract` and Foundry's Blockscout verifier. Verified contracts show a **Contract** tab on the explorer with source code, ABI, and a read/write UI. Run the verification command from your Foundry project root, using the same compiler settings you used to deploy: ```shell theme={null} forge verify-contract $COUNTER_ADDRESS src/Counter.sol:Counter \ --chain-id 5042002 \ --verifier blockscout \ --verifier-url https://testnet.arcscan.app/api/ ``` If your contract's constructor takes arguments, ABI-encode them with `cast abi-encode` and pass the result with `--constructor-args`. For example: ```shell theme={null} forge verify-contract $CONTRACT_ADDRESS src/MyToken.sol:MyToken \ --chain-id 5042002 \ --verifier blockscout \ --verifier-url https://testnet.arcscan.app/api/ \ --constructor-args $(cast abi-encode "constructor(string,string)" "MyToken" "MTK") ``` You can also submit source code manually from the [contract verification page](https://testnet.arcscan.app/contract-verification) on the explorer if you didn't deploy with Foundry. After verification succeeds, open the deployed address on [testnet.arcscan.app](https://testnet.arcscan.app) to confirm the **Contract** tab now shows the verified source and lets you call functions directly from the UI. ## Step 5. Interact with your contract Confirm the deployment on the [Arc Testnet Explorer](https://testnet.arcscan.app) by pasting the transaction hash from the previous step. Read the current counter value with `cast call`: ```shell theme={null} cast call $COUNTER_ADDRESS "number()(uint256)" \ --rpc-url $ARC_TESTNET_RPC_URL ``` A freshly deployed `Counter` returns `0`. Increment it onchain with `cast send`: ```shell theme={null} cast send $COUNTER_ADDRESS "increment()" \ --rpc-url $ARC_TESTNET_RPC_URL \ --private-key $PRIVATE_KEY ``` Re-run the `cast call` command. The returned value is now `1`. You now have a working deployment pipeline on Arc Testnet. To deploy production-ready tokens or NFTs without writing Solidity, see [Deploy contracts](/arc/tutorials/deploy-contracts). # EVM differences Source: https://docs.arc.io/integrate/evm-differences The complete reference for Arc's protocol-level differences from Ethereum: USDC as native gas, value transfer rules, SELFDESTRUCT semantics, EIP-7708 Transfer events, the fee market, and opcode behavior. Arc is an EVM-compatible Layer-1 blockchain. Solidity, Foundry, Hardhat, Viem, ethers.js, and standard Ethereum wallets work without modification, and you can deploy existing contracts unchanged in most cases. Arc targets the **Osaka** hard fork as its baseline, including features such as EIP-7702 (set-code transactions). Arc also ships select features from Ethereum's upcoming **Amsterdam** hard fork ahead of upstream, notably [EIP-7708](https://eips.ethereum.org/EIPS/eip-7708) (standard `Transfer` logs for native value movements). This page is the complete reference for where Arc's protocol-level behavior diverges from Ethereum. All comparisons are against Ethereum at the **Osaka** hard fork, Arc's baseline. Most differences are transparent to application code, but a few change execution semantics in ways that matter when you port a contract. If you are porting an existing contract, start with the [Porting contracts to Arc checklist](/arc/tutorials/porting-contracts-to-arc). Tools that locally simulate the EVM (such as Foundry's `anvil`) run a standard EVM, not Arc's, so they cannot reproduce Arc-specific behavior. Features that depend on it (the native-coin precompiles, EIP-7708 `Transfer` events, and USDC blocklist enforcement) only surface when you test against an Arc RPC endpoint. ## Integration guidance This page is a complete inventory of Arc's protocol-level divergences from Ethereum. The guides linked here translate those differences into practical guidance for specific integration types: Balance display, transaction history, and USDC fee handling. Deposit detection, withdrawal processing, and CCTP-based liquidity management. Single-asset USDC configuration, deposit detection, and instant settlement for fiat ramp providers. USDC pool implementation, decimal handling in pool math, and contract porting. Indexing EIP-7708 Transfer events and avoiding double-counting from the dual emitter. USDC-denominated gas infrastructure and decimal precision in fee calculation. ## USDC as the native gas token Arc uses USDC as its native token. The single most important thing to understand is that **native USDC and the ERC-20 USDC interface are the same asset**, not two separate tokens that happen to share a name. | Interface | Decimals | Used for | | :--------- | :------- | :----------------------------------------------------- | | **Native** | 18 | Gas accounting, native sends, and `msg.value` | | **ERC-20** | 6 | application-level transfers, approvals, and allowances | The ERC-20 interface lives at the [USDC contract address](/arc/references/contract-addresses#usdc) and provides familiar functions such as `transferFrom`, `approve`, and allowance management. A native send and an ERC-20 `transfer` both move the same underlying balance. On other EVM blockchains, the native token has no ERC-20 interface, so protocols deploy a WETH-style wrapper contract to expose `transfer` and `approve`. On Arc, the built-in ERC-20 interface at `0x3600000000000000000000000000000000000000` already covers those use cases, so no wrapper contract is needed. Because both interfaces operate on one balance, `USDC.balanceOf(addr)` and `addr.balance` are two views of the same value. They use different decimals (6 vs 18), so never compare or mix their raw values without converting first. `eth_getBalance` returns 18-decimal native precision. Using `formatUnits(balance, 6)` instead of `formatUnits(balance, 18)` overstates every displayed balance by a factor of 10¹². This single-asset model has consequences that do not exist on other EVM chains: * **The ERC-20 view truncates, so it is not exact.** The 6-decimal `balanceOf` drops anything less than 1×10⁻⁶ USDC, so a native balance of `0.0000001` USDC reads as `0` and `100.0000001` USDC reads as `100`. A `balanceOf` of `0` does not imply a native balance of `0`. * **A native transfer can revert even with a sufficient balance.** On Ethereum, a transfer with sufficient funds always succeeds. On Arc, transfers to the zero address or to or from a blocklisted address fail regardless of balance. * **Don't treat an ERC-20 allowance as a complete spending control on a contract.** `USDC.approve` bounds `transferFrom` calls only. If the contract you're building also exposes functions that send native USDC, those paths move USDC regardless of any allowance. Account for all transfer paths in your access controls, not just the ERC-20 interface. * **Guard rescue functions against sweeping USDC.** A `recoverToken`-style function that sweeps stranded ERC-20 tokens also drains the contract's native USDC balance, because native USDC and ERC-20 USDC are the same balance. Add an explicit guard to prevent USDC from being swept unintentionally. * **Native USDC and ERC-20 USDC share one underlying balance.** Don't display them as separate rows; always show a single USDC balance. Arc uses USDC for gas. Relayers, paymasters, and any wallet that submits transactions on Arc must hold USDC. For the conceptual model, including EURC and USYC support, see [Stablecoin native model](/arc/concepts/stablecoin-native-model). For how balance changes surface as events, see [USDC system events](/arc/references/usdc-system-events). ## Execution and opcode differences | Behavior | Ethereum (Osaka) | Arc | Developer impact | | :------------------------------------------------- | :--------------------------------------------------- | :--------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------- | | `PREVRANDAO` | Beacon chain RANDAO mix | Always returns `0` | No onchain randomness. Use an oracle or verifiable random function (VRF). | | `SELFDESTRUCT` | EIP-6780 semantics | EIP-6780 plus native value rules; emits a `Transfer` log on success (see [SELFDESTRUCT](#selfdestruct)) | Several patterns that succeed on mainnet revert on Arc. | | Non-zero-value `CALL` to a self-destructed account | Succeeds; value credited | **Reverts** (a transfer to a destructed account is a forbidden burn) | The largest semantic departure. See [SELFDESTRUCT](#selfdestruct). | | `parentBeaconBlockRoot` / EIP-4788 | Beacon-roots contract returns the parent beacon root | Set to the parent execution block hash; the beacon-roots contract is omitted, so reads return empty (`0x`) | Do not treat the beacon-roots oracle as functional or as a randomness source. | | Blob transactions (EIP-4844, type-3) | Supported | Not supported; the mempool rejects type-3 transactions | Do not submit blob transactions. `BLOBHASH` returns `0` and `BLOBBASEFEE` returns `1`. | | Withdrawals (EIP-4895) | May be present | Always empty | `block.withdrawals` is always empty. | [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702) set-code transactions, `CREATE2` (including EIP-7610 residual-storage behavior), and EIP-2935 historical block hashes all behave as on Ethereum. In particular, the EIP-2935 block-hash-history contract is deployed and functional, unlike the EIP-4788 beacon-roots contract noted in the table. ## Value transfer rules On Arc, there are two ways a transfer can fail: sending to the zero address (token burning is not allowed) or sending to or from a blocklisted address. In both cases the transaction is included in a block and gas is consumed. * **Transfers to the zero address are forbidden.** A value-bearing transfer to `0x0` reverts with `"Zero address not allowed"`; a zero-value transfer to `0x0` succeeds. (Mint and burn, the only operations that involve `0x0`, go through the native-coin precompile.) * **Burning is forbidden.** Self-destructing to yourself with a balance, or transferring value to an account that has already self-destructed, reverts. * **Transfers to or from a blocklisted address revert.** Check `receipt.status === 0` after any USDC transfer to detect a blocklist revert and handle it explicitly. * **Sending native value to a contract is not guaranteed to succeed.** A call to a contract that forwards native value can revert for any of the reasons listed here, which breaks a common DeFi assumption. * **Sending to an address with no code (`EXTCODESIZE == 0`) succeeds** and emits a `Transfer` log. Sending value to a precompile address reverts. A liquidity pool that pairs native USDC against the ERC-20 USDC interface (as if they were two assets) is meaningless on Arc, because they are one asset. Don't mix `msg.value` with `USDC.balanceOf()` in pool or LTV math: they use different decimals (18 vs 6) and raw values are off by 10¹². ## `SELFDESTRUCT` `SELFDESTRUCT` is allowed on Arc, including during contract deployment, and follows [EIP-6780](https://eips.ethereum.org/EIPS/eip-6780) (the account is fully deleted only if it was created in the same transaction). A self-destruct **reverts** when its value transfer would violate a native value rule: | Condition | Result | | :------------------------------------------------- | :------- | | Beneficiary is the contract itself, with a balance | Reverts | | Beneficiary is the zero address, with a balance | Reverts | | Source or beneficiary is blocklisted | Reverts | | Beneficiary has already self-destructed | Reverts | | Balance is zero (any beneficiary) | Succeeds | Three behaviors differ from every other EVM chain and deserve attention: **1. Self-destructing a contract that holds USDC moves that USDC out.** On Arc a contract's USDC is its native balance, held in the account, so `SELFDESTRUCT` transfers it to the beneficiary. On other chains the contract's ERC-20 USDC balance lives in the token contract and is unaffected by self-destruct. **2. A non-zero-value call to a self-destructed account reverts.** On Ethereum, sending value to an address that self-destructed earlier in the same transaction succeeds. On Arc it is treated as a transfer to a destructed account, a forbidden burn, and reverts. ```solidity theme={null} // Within one transaction: contractA.selfDestruct(payable(b)); // succeeds; emits Transfer(A, B) // Later in the same transaction, any non-zero-value send to A: (bool ok, ) = address(contractA).call{value: 1}(""); // reverts on Arc ``` **3. A successful self-destruct that moves a balance emits a `Transfer` log.** Unlike Ethereum, the moved native value is recorded as an [EIP-7708](https://eips.ethereum.org/EIPS/eip-7708) `Transfer` log from the system emitter (18 decimals). Index it like any other native movement; see [USDC system events](/arc/references/usdc-system-events). ## Native USDC Transfer events (EIP-7708) On a standard EVM chain, a plain native send emits no log. Arc's [EIP-7708](https://eips.ethereum.org/EIPS/eip-7708) implementation emits a standard ERC-20 `Transfer` log from a system address for **every** native USDC movement: native sends, contract endowments, self-destruct transfers, and the precompile-backed mint, burn, and transfer operations. This gives indexers one universal record of balance changes. The system emitter log uses 18 decimals and is distinct from the ERC-20 USDC contract's own 6-decimal `Transfer`. Two indexing mistakes to avoid: * **Filter on the system emitter, not the ERC-20 contract address.** Plain native USDC sends emit no log at `0x3600…0000`. Filtering the ERC-20 address alone silently misses them. * **Don't count logs from both addresses.** An ERC-20 `transfer()` emits a log from both the system emitter and the ERC-20 contract. Counting both inflates balances. Use the system emitter only. For emitter addresses, the exact log format, and indexing guidance, see [USDC system events](/arc/references/usdc-system-events). ## Fee market and block behavior * **The base fee is paid to the block beneficiary, not burned.** Arc has no EIP-1559 burn. Both the base fee and the priority fee are credited to the block's beneficiary. * **The next block's base fee is published in the parent header's `extra_data`** (an 8-byte big-endian value). Read it there rather than re-deriving it. The fee is computed from an exponentially smoothed view of gas usage and is clamped to bounded minimum and maximum values, so it moves predictably. See [Stable fee design](/arc/concepts/stable-fee-design) and [Gas and fees](/arc/references/gas-and-fees). * **The minimum base fee is 20 Gwei.** Transactions with `maxFeePerGas` lower than 20 Gwei are silently dropped by the mempool. They produce no error receipt and never appear in a block. Set `maxFeePerGas` to at least 20 Gwei before submitting any transaction to Arc. * **Block timestamps are non-decreasing, not strictly increasing.** Timestamps come from the proposer's wall clock at one-second granularity, so sub-second blocks may share a timestamp. Use the block number for ordering, and do not assume `block.timestamp` strictly increases between blocks. * **Finality is deterministic and instant.** Transactions finalize on inclusion; offchain systems can act after a single confirmation. See [Deterministic finality](/arc/concepts/deterministic-finality). # Exchange Integration Source: https://docs.arc.io/integrate/exchanges Integrate Arc deposits, withdrawals, and USDC liquidity management into your exchange. Arc is an EVM-compatible Layer 1 blockchain where USDC is the native gas token. For exchanges, this means simpler hot wallet operations, instant settlement finality, and a single asset to manage for both trading and transaction fees. ## Key differences for exchanges Three Arc design decisions affect every exchange integration: | Concern | Arc behavior | Impact on exchanges | | :------------------------- | :----------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------- | | **Deterministic finality** | Transactions finalize in under 1 second with no reorgs | Credit deposits after a single confirmation. No multi-block wait required. | | **USDC as gas** | Transaction fees are paid in USDC | No separate gas token funding for hot wallets. One asset covers fees and transfers. | | **Dual USDC interface** | Native balance (18 decimals) and ERC-20 interface (6 decimals) share the same underlying balance | One USDC balance underlies both interfaces. `eth_getBalance` and system emitter events cover all deposit activity. | ## Operational areas Exchange integration covers three workflows: * **Deposits**—Subscribe to new blocks and detect incoming USDC transfers. Deterministic finality means a single confirmation is final. * **Withdrawals**—Build, sign, and broadcast USDC transfers from your hot wallet. Gas is paid in USDC from the same balance. * **Liquidity (CCTP bridging)**—Move USDC between Arc and other blockchains using Cross-Chain Transfer Protocol to manage treasury and liquidity pools. ## Arc behaviors relevant to this integration The following Arc behaviors can affect exchange operations. Each is covered in [EVM differences](/arc/references/evm-differences): * **Filter deposit events on the system emitter, not the ERC-20 contract address:** Every native USDC movement emits a `Transfer` log from the system emitter (`0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE`). Plain native sends emit no log at the ERC-20 contract address (`0x3600000000000000000000000000000000000000`); filtering that address alone silently misses them. An ERC-20 `transfer()` emits logs at both addresses; count only the system emitter to avoid double-crediting. System emitter log values are in 18-decimal precision. See [Detect deposits](/integrate/exchanges/deposits). * **`balanceOf` zero does not mean no USDC:** `balanceOf()` returns 0 for amounts under 1×10⁻⁶ USDC (6-decimal minimum). Accounts hold sub-minimum dust spendable as gas. See [USDC as the native gas token](/arc/references/evm-differences#usdc-as-the-native-gas-token). * **Blocklist enforcement:** Transfers to or from a blocklisted address revert at runtime. The transaction is included in the block and consumes gas, but state changes roll back. Check the destination address against `isBlacklisted()` on the USDC contract before submitting withdrawals. See [Process withdrawals](/integrate/exchanges/withdrawals#step-2-check-the-blocklist). * **Deposit attribution in EIP-3009 relayer flows:** For deposits submitted by an EIP-3009 (`transferWithAuthorization`) relayer, attributing from `tx.from` credits the relayer's address, not the depositor's. Use the `from` field in the system emitter's `Transfer` event for accurate attribution. See [Detect deposits](/integrate/exchanges/deposits). * **Block timestamps are non-decreasing, not strictly increasing:** Sub-second blocks can share the same timestamp. Use block number, not `block.timestamp`, for deposit sequencing and event ordering. See [Fee market and block behavior](/arc/references/evm-differences#fee-market-and-block-behavior). * **Minimum base fee is 20 Gwei:** Transactions with `maxFeePerGas` under 20 Gwei are rejected by the mempool. This applies to all hot wallet operations, including withdrawals and deposit sweeps. See [Process withdrawals](/integrate/exchanges/withdrawals#step-5-build-the-eip-1559-transaction). * **Transaction attribution with Memo contracts:** For transfers routed through the Memo contract (`0x5294E9927c3306DcBaDb03fe70b92e01cCede505`) or Multicall3From (`0x522fAf9A91c41c443c66765030741e4AaCe147D0`), the CallFrom precompile preserves the original sender, so `tx.from` and the `Transfer` event's `from` are still your EOA wallet. For compliance attribution, read from the `Transfer` logs and the `Memo` event's indexed `sender`. See [Process withdrawals](/integrate/exchanges/withdrawals#attach-memos-for-compliance). ## What you need Before you start, confirm you have the following: | Requirement | Details | | :------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------- | | RPC access | `https://rpc.testnet.arc.io` (HTTPS) or `wss://rpc.testnet.arc.io` (WebSocket) | | Chain ID | `5042002` (testnet) | | USDC ERC-20 contract | [`0x3600000000000000000000000000000000000000`](https://testnet.arcscan.app/address/0x3600000000000000000000000000000000000000) | | CCTP contracts (for bridging) | TokenMessengerV2: [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://testnet.arcscan.app/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA), Domain: `26` | | Memo contract (compliance metadata) | [`0x5294E9927c3306DcBaDb03fe70b92e01cCede505`](https://testnet.arcscan.app/address/0x5294E9927c3306DcBaDb03fe70b92e01cCede505) | | Custody provider supporting custom EVM | See [Add custody platform support](/integrate/exchanges/custody) for configured providers and setup steps | ## Integration guides Subscribe to blocks and detect incoming USDC transfers with single-confirmation finality. Build, sign, and broadcast withdrawal transactions using USDC for both value and gas. Move USDC liquidity to and from Arc using Cross-Chain Transfer Protocol. Register Arc in supported custody platforms. # How to: Bridge USDC with CCTP Source: https://docs.arc.io/integrate/exchanges/cctp-bridging Move USDC liquidity to and from Arc using Cross-Chain Transfer Protocol for exchange hot wallet funding and liquidity rebalancing. [Circle's Cross-Chain Transfer Protocol (CCTP)](https://developers.circle.com/cctp) uses a burn-and-mint model to transfer native USDC between blockchains without wrapped or bridged token variants. Arc's CCTP domain is `26`. Transfers into Arc mint USDC directly to your recipient address; transfers out burn USDC and mint on the destination blockchain. Because Arc has instant finality, outbound transfers reach attestation faster than transfers from blockchains that require multiple confirmations. ## Prerequisites Before you begin, ensure you have: * An RPC endpoint for Arc Testnet (`https://rpc.testnet.arc.io`) * An RPC endpoint for the source or destination blockchain (for example, Ethereum Sepolia) * USDC balance on the sending blockchain * Familiarity with the [CCTP developer docs](https://developers.circle.com/stablecoins/cctp-getting-started) * A TypeScript environment with `viem` installed Test against Arc Testnet only. Local EVM simulators run a standard EVM. They don't replicate EIP-7708 Transfer events, the USDC precompile, or blocklist enforcement. ## Contract addresses | Contract | Address | Domain | | :----------------------------- | :----------------------------------------------------------------------------------------------------------------------------- | :----- | | **TokenMessengerV2** (Arc) | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://testnet.arcscan.app/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) | 26 | | **MessageTransmitterV2** (Arc) | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://testnet.arcscan.app/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) | 26 | | **USDC ERC-20** (Arc) | [`0x3600000000000000000000000000000000000000`](https://testnet.arcscan.app/address/0x3600000000000000000000000000000000000000) | — | For CCTP contract addresses on other blockchains, see the [CCTP contract addresses](https://developers.circle.com/stablecoins/evm-smart-contracts) reference. ## Bridge USDC between Arc and other blockchains Use the inbound workflow to fund your exchange hot wallet on Arc from another blockchain, and the outbound workflow to rebalance liquidity from Arc to another blockchain. ### Step 1. Format the recipient address CCTP requires the recipient as a `bytes32` value. Left-pad the 20-byte Ethereum address with zeros: ```typescript theme={null} import { pad, type Address } from "viem"; const recipientAddress: Address = "0xYourArcHotWalletAddress"; const mintRecipient = pad(recipientAddress, { size: 32 }); ``` ### Step 2. Approve USDC on the source blockchain Approve the source blockchain's TokenMessengerV2 contract to spend your USDC: ```typescript theme={null} import { parseUnits } from "viem"; const amount = parseUnits("10000", 6); // 10,000 USDC const approvalTx = await sourceWalletClient.writeContract({ address: SOURCE_USDC_ADDRESS, abi: [ { name: "approve", type: "function", inputs: [ { name: "spender", type: "address" }, { name: "amount", type: "uint256" }, ], outputs: [{ type: "bool" }], stateMutability: "nonpayable", }, ], functionName: "approve", args: [SOURCE_TOKEN_MESSENGER_ADDRESS, amount], }); ``` ### Step 3. Call `depositForBurn` on the source blockchain Burn USDC on the source blockchain, targeting Arc (domain `26`): ```typescript theme={null} const ARC_DOMAIN = 26; const burnTx = await sourceWalletClient.writeContract({ address: SOURCE_TOKEN_MESSENGER_ADDRESS, abi: [ { name: "depositForBurn", type: "function", inputs: [ { name: "amount", type: "uint256" }, { name: "destinationDomain", type: "uint32" }, { name: "mintRecipient", type: "bytes32" }, { name: "burnToken", type: "address" }, ], outputs: [{ type: "uint64" }], stateMutability: "nonpayable", }, ], functionName: "depositForBurn", args: [amount, ARC_DOMAIN, mintRecipient, SOURCE_USDC_ADDRESS], }); ``` ### Step 4. Retrieve the message hash After the burn transaction confirms, extract the `MessageSent` event to get the message hash: ```typescript theme={null} const burnReceipt = await sourcePublicClient.waitForTransactionReceipt({ hash: burnTx, }); const messageSentEvent = burnReceipt.logs.find( (log) => log.topics[0] === "0x2fa9ca894982930190727e75500a97d8dc500233a5065e0f3126c48fbe0343c0", ); const messageBytes = messageSentEvent?.data; const messageHash = keccak256(messageBytes!); ``` ### Step 5. Wait for attestation Poll Circle's attestation service until the attestation is available: ```typescript theme={null} async function getAttestation(messageHash: string): Promise { const url = `https://iris-api.circle.com/v2/attestations/${messageHash}`; while (true) { const response = await fetch(url); const data = await response.json(); if (data.status === "complete") { return data.attestation; } // Poll every 10 seconds await new Promise((resolve) => setTimeout(resolve, 10_000)); } } const attestation = await getAttestation(messageHash); ``` Attestation typically takes approximately 60 seconds but varies based on the source blockchain's finality time. Transfers from blockchains with longer finality (such as Ethereum) take longer than those from blockchains with fast finality. ### Step 6. Call `receiveMessage` on Arc Submit the message and attestation to Arc's MessageTransmitterV2 to mint USDC: ```typescript theme={null} const ARC_MESSAGE_TRANSMITTER = "0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275"; const receiveTx = await arcWalletClient.writeContract({ address: ARC_MESSAGE_TRANSMITTER, abi: [ { name: "receiveMessage", type: "function", inputs: [ { name: "message", type: "bytes" }, { name: "attestation", type: "bytes" }, ], outputs: [{ type: "bool" }], stateMutability: "nonpayable", }, ], functionName: "receiveMessage", args: [messageBytes, attestation], }); ``` Once the transaction confirms, USDC is minted to the recipient address on Arc. ### Step 1. Format the recipient address Pad the destination address to `bytes32`: ```typescript theme={null} import { pad, type Address } from "viem"; const destinationAddress: Address = "0xYourDestinationAddress"; const mintRecipient = pad(destinationAddress, { size: 32 }); ``` ### Step 2. Approve USDC on Arc Approve Arc's TokenMessengerV2 to spend your USDC: ```typescript theme={null} import { parseUnits } from "viem"; const ARC_USDC = "0x3600000000000000000000000000000000000000"; const ARC_TOKEN_MESSENGER = "0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA"; const amount = parseUnits("10000", 6); // 10,000 USDC const approvalTx = await arcWalletClient.writeContract({ address: ARC_USDC, abi: [ { name: "approve", type: "function", inputs: [ { name: "spender", type: "address" }, { name: "amount", type: "uint256" }, ], outputs: [{ type: "bool" }], stateMutability: "nonpayable", }, ], functionName: "approve", args: [ARC_TOKEN_MESSENGER, amount], }); ``` ### Step 3. Call `depositForBurn` on Arc Burn USDC on Arc, targeting the destination domain: ```typescript theme={null} // Example: Ethereum = 0, Avalanche = 1, Arbitrum = 3 const DESTINATION_DOMAIN = 0; // Replace with your target domain const burnTx = await arcWalletClient.writeContract({ address: ARC_TOKEN_MESSENGER, abi: [ { name: "depositForBurn", type: "function", inputs: [ { name: "amount", type: "uint256" }, { name: "destinationDomain", type: "uint32" }, { name: "mintRecipient", type: "bytes32" }, { name: "burnToken", type: "address" }, ], outputs: [{ type: "uint64" }], stateMutability: "nonpayable", }, ], functionName: "depositForBurn", args: [amount, DESTINATION_DOMAIN, mintRecipient, ARC_USDC], }); ``` ### Step 4. Retrieve the message hash Extract the message from the burn transaction receipt: ```typescript theme={null} const burnReceipt = await arcPublicClient.waitForTransactionReceipt({ hash: burnTx, }); const messageSentEvent = burnReceipt.logs.find( (log) => log.topics[0] === "0x2fa9ca894982930190727e75500a97d8dc500233a5065e0f3126c48fbe0343c0", ); const messageBytes = messageSentEvent?.data; const messageHash = keccak256(messageBytes!); ``` ### Step 5. Wait for attestation Poll for the attestation. Because Arc has instant finality, attestation for outbound transfers is typically faster: ```typescript theme={null} const attestation = await getAttestation(messageHash); ``` Outbound transfers from Arc benefit from instant finality. The attestation service can process the message once the block is produced, without waiting for additional confirmations. ### Step 6. Call `receiveMessage` on the destination blockchain Submit the message and attestation to the destination blockchain's MessageTransmitterV2: ```typescript theme={null} const receiveTx = await destinationWalletClient.writeContract({ address: DESTINATION_MESSAGE_TRANSMITTER, abi: [ { name: "receiveMessage", type: "function", inputs: [ { name: "message", type: "bytes" }, { name: "attestation", type: "bytes" }, ], outputs: [{ type: "bool" }], stateMutability: "nonpayable", }, ], functionName: "receiveMessage", args: [messageBytes, attestation], }); ``` Once confirmed, USDC is minted to the recipient on the destination blockchain. ## Monitor transfer status Track a CCTP transfer by polling the attestation API: ```typescript theme={null} import { keccak256 } from "viem"; async function checkTransferStatus(messageHash: string) { const response = await fetch( `https://iris-api.circle.com/v2/attestations/${messageHash}`, ); const data = await response.json(); // Possible statuses: "pending", "complete" return data.status; } ``` Each CCTP message can only be received once. If `receiveMessage` reverts, verify that the message has not already been processed by checking the `usedNonces` mapping on the destination MessageTransmitterV2 contract. ## CCTP V2 `depositForBurnWithHook` CCTP V2 introduces `depositForBurnWithHook`, which allows you to specify a destination caller and attach a hook for post-mint actions. This is useful for triggering automated workflows (such as depositing into a vault) immediately after USDC is minted: ```typescript theme={null} const burnWithHookTx = await arcWalletClient.writeContract({ address: ARC_TOKEN_MESSENGER, abi: [ { name: "depositForBurnWithHook", type: "function", inputs: [ { name: "amount", type: "uint256" }, { name: "destinationDomain", type: "uint32" }, { name: "mintRecipient", type: "bytes32" }, { name: "burnToken", type: "address" }, { name: "destinationCaller", type: "bytes32" }, { name: "hookData", type: "bytes" }, ], outputs: [{ type: "uint64" }], stateMutability: "nonpayable", }, ], functionName: "depositForBurnWithHook", args: [ amount, DESTINATION_DOMAIN, mintRecipient, ARC_USDC, destinationCaller, // bytes32-padded address authorized to call receiveMessage hookData, // Encoded calldata for post-mint execution ], }); ``` When `destinationCaller` is set to a non-zero value, only that address can call `receiveMessage` for this transfer on the destination blockchain. Set it to `0x000000` to allow any address to complete the transfer. ## See also * [Contract addresses](/arc/references/contract-addresses): full list of CCTP and other deployed contracts on Arc * [CCTP developer docs](https://developers.circle.com/stablecoins/cctp-getting-started): attestation API reference and supported domains * [Exchange integration overview](/integrate/exchanges): deposits, withdrawals, and liquidity management * [Deterministic finality](/arc/concepts/deterministic-finality): how Arc's instant finality affects crosschain transfers # Custody Platform Integration Source: https://docs.arc.io/integrate/exchanges/custody Reference for adding Arc to a custody platform: register Arc as a custom EVM chain, configure signing policies, and handle the differences that matter—USDC as native gas, deterministic finality, and unified balance management. Arc is fully EVM-compatible—same key derivation, same signing curve (secp256k1), same transaction formats. If your custody platform supports custom EVM chains, you can add Arc without code changes. This guide covers what's different and what to configure. Arc uses USDC as its native gas token. A single USDC balance covers both transfer value and transaction fees. ## What custody engineers need to know Arc behaves like any EVM chain with three key differences: | Property | Ethereum | Arc | | ----------- | ----------------------------------- | -------------------------------------------------- | | Gas token | ETH (18 decimals) | USDC (6 display decimals, 18 internal decimals) | | Finality | Probabilistic (\~12 min for safety) | Deterministic, sub-second. 1 confirmation = final. | | Gas funding | Requires separate ETH balance | No separate funding—USDC covers everything | Everything else is standard: * Address derivation: BIP-44 path `m/44'/60'/0'/0/x` * Signing algorithm: secp256k1 (identical to Ethereum) * Transaction types: Legacy (type 0) and EIP-1559 (type 2) both supported * Smart contracts: Solidity, same EVM opcodes * Multi-sig: Standard Ethereum multi-sig contracts (including SAFE) work without modification ## Network configuration Use these parameters when registering Arc as a custom EVM chain: | Parameter | Testnet value | | ------------------------ | ------------------------------ | | Chain ID | `5042002` | | RPC (HTTPS) | `https://rpc.testnet.arc.io` | | RPC (WebSocket) | `wss://rpc.testnet.arc.io` | | Block explorer | `https://testnet.arcscan.app` | | Native currency symbol | `USDC` | | Native currency decimals | `18` (internal representation) | | Display decimals | `6` | | EIP-1559 support | Yes (recommended) | | Minimum base fee | 20 Gwei | The native currency uses 18 decimals internally (like ETH uses Wei) but represents USDC which has 6 display decimals. Configure your balance display to show 6 decimal places to users while using 18 decimals for transaction construction. ### USDC ERC-20 contract For ERC-20 interactions (approvals, `transferFrom`, allowance checks), the USDC contract is deployed at a precompile address: ```text theme={null} 0x3600000000000000000000000000000000000000 ``` Native USDC transfers (simple sends) and ERC-20 `transfer()` calls both move the same underlying balance. There is no wrapped/unwrapped distinction. ## Register Arc in your custody platform Most custody platforms provide a "custom EVM chain" or "custom network" configuration flow. The following sections provide platform-specific guidance and a generic template. ### Provider configuration reference | Provider | Integration method | Notes | | ---------- | ------------------------------------- | ---------------------------------------------------------------- | | Fireblocks | Workspace Settings → Add EVM Network | Use "EVM-based chain" template. Set native asset to USDC. | | BitGo | Admin → Coin Management → Custom EVM | Register as custom ERC-20 chain. Configure 1-block finality. | | Cactus | Network Management → Add Custom Chain | Standard EVM chain wizard. Set gas token symbol to USDC. | | Zodia | Network Configuration → Custom EVM | Use provided chain ID and RPC. No special signing config needed. | | SAFE | Add custom network in web interface | Works natively—same contract addresses and factory. | | Taurus | TaurusProtect → Network Settings | Register RPC endpoint. Configure display decimals separately. | | Copper | ClearLoop → Custom Networks | Standard EVM registration. Set confirmation threshold to 1. | | CEFFU | Asset Management → Custom Chain | Follow EVM chain onboarding flow. Specify USDC as fee token. | ### Generic custom EVM chain template Use this configuration when your platform has a general-purpose "add custom EVM chain" form: ```typescript theme={null} const arcNetworkConfig = { chainId: 5042002, name: "Arc Testnet", rpcUrl: "https://rpc.testnet.arc.io", wsUrl: "wss://rpc.testnet.arc.io", blockExplorer: "https://testnet.arcscan.app", nativeCurrency: { name: "USDC", symbol: "USDC", decimals: 18, // Internal representation (like Wei for ETH) }, // Display configuration displayDecimals: 6, // Show balances with 6 decimal places // Finality configuration confirmationsRequired: 1, // Deterministic finality—1 block is final // Transaction type preference eip1559: true, // No separate gas token funding address needed }; ``` ## Transaction signing Arc uses standard Ethereum transaction signing. No custom signing schemes or transaction types are required. ### Recommended: EIP-1559 (type 2) ```typescript theme={null} import { createWalletClient, http, parseUnits } from "viem"; const arcTestnet = { id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USDC", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.io"] }, }, }; const client = createWalletClient({ chain: arcTestnet, transport: http(), }); // Standard EIP-1559 transaction—identical to Ethereum const txHash = await client.sendTransaction({ account, // Your custody-managed account to: "0x742d35CC6634c0532925a3B844bc9e7595F2Bd28", value: parseUnits("100", 18), // 100 USDC (18 decimals internally) maxFeePerGas: parseUnits("30", 9), // 30 Gwei maxPriorityFeePerGas: parseUnits("1", 9), // 1 Gwei tip }); // With deterministic finality, 1 confirmation means the transaction is final ``` ### Key signing details * **Curve:** secp256k1 (same as Ethereum) * **Address derivation:** Keccak-256 hash of public key, last 20 bytes * **HD path:** `m/44'/60'/0'/0/x` (coin type 60, same as Ethereum) * **Transaction serialization:** RLP encoding, identical to Ethereum * **Chain ID in signature:** Required (EIP-155). Use `5042002` for testnet. ## MPC and multi-sig considerations ### MPC wallets MPC (multi-party computation) signing works identically to Ethereum: * Key shares are generated for the secp256k1 curve * Threshold signing produces a standard ECDSA signature * The resulting address is a standard Ethereum address * No Arc-specific MPC protocol modifications are needed ### Multi-sig wallets (Safe) SAFE multi-sig contracts deploy and operate on Arc without modification: * Same factory contract addresses * Same proxy pattern * Same signature verification logic * Transaction confirmation follows the same flow—but only 1 onchain confirmation is needed due to deterministic finality ## Operational differences ### Balance management Because USDC is both the transfer currency and the gas token, custody operations are simpler: * **No Gas Station network:** You don't need a separate process to fund addresses with gas tokens. * **Unified balance:** A single `eth_getBalance` call returns the USDC balance available for both transfers and fees. * **Threshold alerts:** Set a single low-balance threshold instead of monitoring separate gas and transfer balances. ```typescript theme={null} import { createPublicClient, http, formatUnits } from "viem"; const client = createPublicClient({ transport: http("https://rpc.testnet.arc.io"), }); const balance = await client.getBalance({ address: "0xYourCustodyAddress", }); // Display with 6 decimals (USDC precision) const displayBalance = formatUnits(balance, 18); // For user-facing display, round to 6 decimal places const usdcDisplay = parseFloat(displayBalance).toFixed(6); ``` ### Finality and confirmation tracking Arc provides deterministic finality. Once a transaction is included in a block, it is final—no reorgs, no uncle blocks, no chain reorganizations. This means: * Set confirmation requirements to **1** in your custody platform * Remove pending-state tracking logic (no need to wait for additional confirmations) * Transaction receipts are immediately authoritative * No need to handle "dropped and replaced" scenarios If your platform requires a minimum confirmation count greater than 1, setting it to 1 is still safe on Arc. There is no security benefit to waiting for additional blocks. ### Blocklist enforcement USDC transfers to or from a blocklisted address revert at runtime. The transaction is included in a block and gas is consumed. After sending, check `receipt.status` to detect a revert: ```typescript theme={null} const receipt = await client.waitForTransactionReceipt({ hash: txHash }); if (receipt.status === "reverted") { // Transaction reverted (e.g. blocklist): onchain, gas was consumed } ``` ### Fee estimation Gas estimation works identically to Ethereum: ```typescript theme={null} // Use standard eth_estimateGas and eth_gasPrice / eth_maxPriorityFeePerGas const gasEstimate = await client.estimateGas({ account: "0xYourCustodyAddress", to: "0xRecipientAddress", value: parseUnits("1000", 18), // 1000 USDC }); const feeData = await client.estimateFeesPerGas(); // Total fee in USDC (18 decimals internal) const maxFee = gasEstimate * feeData.maxFeePerGas; ``` The minimum base fee is 20 Gwei. Set your gas price floor accordingly to avoid underpriced transaction rejections. ## Verification and testing After configuring Arc in your custody platform: 1. **Generate a test address** using your standard HD derivation path and verify it matches what you'd get on Ethereum for the same seed. 2. **Fund the address** with testnet USDC through the Arc faucet. 3. **Send a transaction** and confirm it appears on the [block explorer](https://testnet.arcscan.app) in seconds. 4. **Verify the receipt** shows `status: 1` (success) with a single block confirmation. 5. **Test balance display** to ensure your platform shows the correct USDC amount (6 decimal places). The same address and private key work on both Arc and Ethereum. If your platform already derives Ethereum addresses for a given seed, those same addresses are valid on Arc. # How-to: Detect and process deposits Source: https://docs.arc.io/integrate/exchanges/deposits Implement deposit detection on Arc by monitoring native USDC Transfer events from the system emitter, confirming with deterministic finality, and sweeping funds. Detect USDC deposits on Arc by generating addresses, monitoring the native USDC `Transfer` event from the system emitter `0xffff…fffe` (Arc's [EIP-7708](https://eips.ethereum.org/EIPS/eip-7708) implementation), crediting after a single block confirmation (deterministic finality guarantees no reorgs), and sweeping funds into a hot wallet. ## Prerequisites Before you begin, ensure that you've: * Obtained access to an Arc RPC endpoint (`https://rpc.testnet.arc.io`) or WebSocket (`wss://rpc.testnet.arc.io`) * Installed an HD wallet library for generating deposit addresses (for example, `ethers` or `viem`) * Familiarized yourself with Ethereum JSON-RPC methods and event log filtering * Set up a database to track processed deposits and prevent double-crediting ## Steps ### Step 1. Generate deposit addresses Arc uses standard Ethereum addresses (0x-prefixed, 20 bytes, EIP-55 checksum). Derive deposit addresses using the same HD wallet approach as Ethereum—one unique address per user. ```typescript theme={null} import { HDNodeWallet, Mnemonic } from "ethers"; // Derive a deposit address for a given user index function getDepositAddress(mnemonic: string, userIndex: number): string { const hdNode = HDNodeWallet.fromMnemonic( Mnemonic.fromPhrase(mnemonic), `m/44'/60'/0'/0/${userIndex}`, ); return hdNode.address; } ``` Store the mapping between user IDs and their derived address index. Never expose the mnemonic or private keys in client-side code. ### Step 2. Subscribe to new blocks Use `eth_subscribe("newHeads")` over WebSocket for real-time block notifications, or poll `eth_blockNumber` over HTTP as a fallback. ```typescript theme={null} import { WebSocketProvider, JsonRpcProvider } from "ethers"; // Option A: WebSocket subscription (recommended) const wsProvider = new WebSocketProvider("wss://rpc.testnet.arc.io"); wsProvider.on("block", async (blockNumber: number) => { console.log(`New block: ${blockNumber}`); await processBlock(blockNumber); }); // Option B: HTTP polling fallback const httpProvider = new JsonRpcProvider("https://rpc.testnet.arc.io"); let lastProcessedBlock = await httpProvider.getBlockNumber(); setInterval(async () => { const currentBlock = await httpProvider.getBlockNumber(); for (let block = lastProcessedBlock + 1; block <= currentBlock; block++) { await processBlock(block); } lastProcessedBlock = currentBlock; }, 2000); ``` ### Step 3. Detect incoming transfers with the native USDC Transfer event Every native USDC movement emits a standard ERC-20 `Transfer` log from the system address `0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE` (Arc's [EIP-7708](https://eips.ethereum.org/EIPS/eip-7708) implementation). This single stream covers plain native sends and the native leg of ERC-20 transfers, with values in **18 decimals**. Filter it to catch every deposit, including native sends that emit no event on the ERC-20 contract. Do not filter the ERC-20 USDC contract (`0x3600…0000`) for deposit detection. Its `Transfer` events cover only ERC-20-interface activity, so a plain native send produces no log there and the deposit is missed. Filter the system emitter (`0xffff…fffe`) instead. See [USDC system events](/arc/references/usdc-system-events) for the full event matrix. **Event signature:** ```solidity theme={null} Transfer(address indexed from, address indexed to, uint256 value) ``` **Topic0:** `0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef` **Native USDC system emitter:** `0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE` Use `eth_getLogs` to filter for transfers to your deposit addresses: ```typescript theme={null} import { Interface, Log, JsonRpcProvider } from "ethers"; // Native USDC system emitter (EIP-7708 Transfer logs, 18 decimals) const NATIVE_USDC_EMITTER = "0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE"; const TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"; const provider = new JsonRpcProvider("https://rpc.testnet.arc.io"); const erc20Interface = new Interface([ "event Transfer(address indexed from, address indexed to, uint256 value)", ]); async function processBlock(blockNumber: number): Promise { const logs = await provider.getLogs({ address: NATIVE_USDC_EMITTER, topics: [ TRANSFER_TOPIC, null, // any sender null, // any recipient—filter client-side for your addresses ], fromBlock: blockNumber, toBlock: blockNumber, }); for (const log of logs) { const parsed = erc20Interface.parseLog({ topics: log.topics as string[], data: log.data, }); if (!parsed) continue; const to = parsed.args.to as string; const value = parsed.args.value as bigint; // 18 decimals (native) // Check if the recipient is one of your deposit addresses if (isDepositAddress(to)) { await creditDeposit({ txHash: log.transactionHash, logIndex: log.index, to, amount: value, // 18-decimal native USDC blockNumber, }); } } } ``` Do not credit the same deposit twice. An ERC-20 `transfer()` emits a log from both the system emitter (`0xffff…fffe`, 18 decimals) and the ERC-20 contract (`0x3600…0000`, 6 decimals); filter only the system emitter. Likewise, do not reconcile `eth_getBalance` against Transfer events—they represent the same balance. ### Step 4. Confirm the deposit Arc provides deterministic finality—once a transaction is included in a block, it is final with no possibility of reorg. You can safely credit deposits after 1 confirmation. ```typescript theme={null} import { JsonRpcProvider } from "ethers"; const provider = new JsonRpcProvider("https://rpc.testnet.arc.io"); async function isConfirmed(txHash: string): Promise { const receipt = await provider.getTransactionReceipt(txHash); if (!receipt || receipt.status === 0) { return false; // Transaction failed or not yet mined } // On Arc, 1 confirmation = final. No reorgs possible. const currentBlock = await provider.getBlockNumber(); return currentBlock >= receipt.blockNumber; } ``` Since Arc has deterministic finality, you do not need to wait for multiple confirmations. Credit the user once the block containing their transaction is produced. ### Step 5. Credit deposits at full precision The native system `Transfer` event emits values in 18-decimal native USDC. Store and credit the raw 18-decimal value. Don't truncate to 6-decimal ERC-20 units: amounts smaller than 1×10⁻⁶ USDC are valid and spendable as gas. Truncating them to 6-decimal ERC-20 units under-credits the user's balance. ```typescript theme={null} // Credit the raw 18-decimal value — no conversion needed async function creditDeposit(deposit: { txHash: string; logIndex: number; to: string; amount: bigint; // 18-decimal native USDC blockNumber: bigint; }): Promise { // Store amount as 18-decimal native USDC await db.insert({ ...deposit }); } ``` The native system `Transfer` event and `eth_getBalance` use 18 decimals. The ERC-20 contract's `Transfer` and `balanceOf` use 6. Don't convert 18-decimal deposit amounts to 6-decimal before crediting. Amounts smaller than 1×10⁻⁶ USDC are under-credited if truncated. ### Step 6. Sweep deposits to a hot wallet Consolidate deposited funds from individual user addresses into your hot wallet. Use EIP-1559 transactions with Arc's minimum base fee of 20 Gwei. ```typescript theme={null} import { Wallet, JsonRpcProvider, parseUnits } from "ethers"; const provider = new JsonRpcProvider("https://rpc.testnet.arc.io"); async function sweepDeposit( depositPrivateKey: string, hotWalletAddress: string, amount: bigint, ): Promise { const wallet = new Wallet(depositPrivateKey, provider); const feeData = await provider.getFeeData(); const maxFeePerGas = feeData.maxFeePerGas ?? parseUnits("30", "gwei"); const maxPriorityFeePerGas = feeData.maxPriorityFeePerGas ?? parseUnits("1", "gwei"); // Approximately 21,000 gas units for a native USDC send const gasLimit = 65_000n; const tx = await wallet.sendTransaction({ to: hotWalletAddress, value: amount, // 18-decimal native USDC type: 2, // EIP-1559 maxFeePerGas, maxPriorityFeePerGas, gasLimit, }); const receipt = await tx.wait(); return receipt!.hash; } ``` ## Common mistakes * **Filtering the wrong emitter:** Plain native USDC sends emit no `Transfer` on the ERC-20 contract (`0x3600…0000`). Filter the system emitter (`0xffff…fffe`) so you do not miss native deposits. * **Double-counting:** An ERC-20 `transfer()` logs from both emitters. Filter only the system emitter, and do not reconcile `eth_getBalance` against Transfer events for the same address. * **Decimal mismatch:** The system emitter's values use 18 decimals. Store and credit them as 18-decimal native USDC. Converting to 6-decimal before crediting truncates sub-6-decimal amounts, resulting in under-crediting. * **Waiting for multiple confirmations:** Arc has deterministic finality. Waiting for 6+ confirmations adds unnecessary latency with no security benefit. * **Hardcoded keys:** Never embed private keys in source code. Use environment variables or a secrets manager for sweep wallet credentials. * **Using `tx.from` for attribution:** For EIP-3009 relayer-submitted deposits, `tx.from` is the relayer's address. Crediting from it assigns the deposit to the relayer, not the depositor. Read the `from` field from the system emitter's `Transfer` event instead. Check `receipt.status === 0` in any send or sweep flow. A transfer to or from a blocklisted address reverts at runtime: the transaction is included in the block and gas is consumed, but state changes roll back. * **Fee floor:** Transactions with `maxFeePerGas` lower than 20 Gwei are rejected with no error receipt and never appear in a block. Set `maxFeePerGas` to at least 20 Gwei when building sweep transactions. # How to: Process Withdrawals Source: https://docs.arc.io/integrate/exchanges/withdrawals Build, sign, and broadcast USDC withdrawal transactions from your exchange hot wallet on Arc. Send USDC withdrawals from an exchange hot wallet on Arc by validating addresses, estimating gas, building EIP-1559 transactions, and confirming with deterministic finality. Send withdrawals as **native USDC** transfers—the recommended path for exchanges: cheaper gas (\~21,000 vs \~65,000 units) and receivable by any address. The Memo contract lets you attach compliance metadata to the same transaction. ## Prerequisites Before you begin, ensure you have: * An Arc Testnet RPC endpoint (`https://rpc.testnet.arc.io`) * A funded hot wallet with USDC for both transfer amounts and gas fees * The USDC ERC-20 contract address: `0x3600000000000000000000000000000000000000` * [viem](https://viem.sh) installed (`npm install viem`) ## Steps ### Step 1. Validate the destination address Verify that the withdrawal address is a valid EIP-55 checksum-validated Ethereum address before building the transaction. This prevents sending funds to malformed addresses. ```typescript theme={null} import { getAddress, isAddress, zeroAddress } from "viem"; function validateDestination(address: string): string { if (!isAddress(address)) { throw new Error(`Invalid address: ${address}`); } if (address.toLowerCase() === zeroAddress) { throw new Error( 'Transfer to address(0) reverts: "Zero address not allowed"', ); } // Return EIP-55 checksum-validated address return getAddress(address); } const destination = validateDestination( "0x742d35CC6634c0532925a3B844bc9e7595F2Bd28", ); ``` The address must be: * 20 bytes (40 hex characters) with a `0x` prefix * Valid per EIP-55 checksum rules * Not `address(0)` — a native USDC send to `address(0)` is mined but reverts with `"Zero address not allowed"`, consuming gas without transferring funds ### Step 2. Estimate gas units Call `eth_estimateGas` to determine the gas units required for the native USDC send. The `value` field is denominated in 18-decimal native wei. ```typescript theme={null} import { createPublicClient, http } from "viem"; const client = createPublicClient({ transport: http("https://rpc.testnet.arc.io"), }); // 1 USDC in 18-decimal native wei const amount = 1_000_000_000_000_000_000n; const gasEstimate = await client.estimateGas({ account: hotWalletAddress, to: destination as `0x${string}`, value: amount, }); console.log(`Estimated gas units: ${gasEstimate}`); // Typical native USDC send: ~21,000 gas units ``` `eth_estimateGas` returns gas units, not a cost in USDC. To calculate the cost, multiply by the effective gas price. A native USDC send uses approximately 21,000 gas units, while an ERC-20 `transfer()` call uses approximately 65,000. ### Step 3. Calculate gas cost Use `eth_gasPrice` to get the current suggested gas price, then compute the total fee. ```typescript theme={null} const gasPrice = await client.getGasPrice(); // Gas cost formula: gas_units * gas_price = cost in USDC wei (18 decimals) const estimatedCost = gasEstimate * gasPrice; // Convert to human-readable USDC (18 decimals for native gas accounting) const costInUsdc = Number(estimatedCost) / 1e18; console.log(`Estimated fee: ${costInUsdc} USDC`); ``` **Gas cost formula:** ```text theme={null} cost_usdc_wei = gas_used * effective_gas_price cost_usdc = cost_usdc_wei / 10^18 ``` For example, a native USDC send using 21,000 gas at 20 Gwei: ```text theme={null} 21,000 * 20,000,000,000 = 420,000,000,000,000 wei = 0.00042 USDC ``` ### Step 4. Build the EIP-1559 transaction Construct a type-2 (EIP-1559) transaction with `maxFeePerGas` set to at least 20 Gwei. The fee fields and the `value` field are all denominated in USDC wei (18 decimals). ```typescript theme={null} import { createWalletClient, http, parseGwei } from "viem"; const walletClient = createWalletClient({ account: hotWalletAccount, // Your signing account transport: http("https://rpc.testnet.arc.io"), }); const txHash = await walletClient.sendTransaction({ to: destination as `0x${string}`, value: amount, // 18-decimal native USDC gas: gasEstimate, maxFeePerGas: parseGwei("25"), // Must be >= 20 Gwei maxPriorityFeePerGas: parseGwei("1"), chain: { id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USDC", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.io"] } }, }, }); console.log(`Transaction hash: ${txHash}`); ``` Transactions with `maxFeePerGas` under 20 Gwei may remain pending or fail to execute. Send withdrawals as native USDC (a plain value transfer). It is cheaper than an ERC-20 `transfer()` and any address can receive it. Native sends still emit a `Transfer` log from the system emitter (`0xffff…fffe`), so indexers and block explorers still capture them. Reach for the ERC-20 `transfer()` only when you need 6-decimal exactness or ERC-20 call semantics. If you use ERC-20 `transfer()`, count only the system emitter log in your reconciliation. The ERC-20 `transfer()` emits from both the system emitter and the ERC-20 contract, and counting both records the withdrawal twice. ### Step 5. Confirm inclusion Arc provides deterministic finality. Once a transaction is included in a block, it is final—no reorgs, no need to wait for additional confirmations. ```typescript theme={null} const receipt = await client.waitForTransactionReceipt({ hash: txHash }); if (receipt.status === "success") { console.log(`Withdrawal confirmed in block ${receipt.blockNumber}`); // Credit the withdrawal as complete—no further checks needed } else { console.error("Transaction reverted"); // Handle failure (see Step 6) } ``` Unlike other blockchains, you do not need to wait for multiple block confirmations. A single block inclusion is final on Arc. ### Step 6. Handle failures Common failure scenarios and how to address them: | Failure | Cause | Resolution | | :------------------- | :--------------------------------------------------------- | :---------------------------------------------------------- | | Transaction reverts | Destination is blocklisted | Verify the destination address before retrying | | Transaction reverts | Destination is `address(0)` — gas consumed, funds not sent | Add a zero-address check in Step 1 | | Transaction pending | `maxFeePerGas` too low | Resubmit with `maxFeePerGas >= 20 Gwei` | | Out of gas | Gas estimate too low | Add a buffer (for example, multiply estimate by 1.2) | | Insufficient balance | Hot wallet underfunded | Top up the hot wallet—USDC covers both the transfer and gas | ```typescript theme={null} async function processWithdrawal(to: string, amount: bigint): Promise { const validAddress = validateDestination(to); // amount is 18-decimal native USDC wei const gas = await client.estimateGas({ account: hotWalletAddress, to: validAddress as `0x${string}`, value: amount, }); const txHash = await walletClient.sendTransaction({ to: validAddress as `0x${string}`, value: amount, gas: (gas * 120n) / 100n, // 20% buffer maxFeePerGas: parseGwei("25"), maxPriorityFeePerGas: parseGwei("1"), }); const receipt = await client.waitForTransactionReceipt({ hash: txHash }); if (receipt.status !== "success") { throw new Error(`Withdrawal failed: tx ${txHash} reverted`); } return txHash; } ``` ## Attach memos for compliance Use the Memo contract to attach metadata (such as internal withdrawal IDs or compliance references) to transfers. The Memo contract wraps the encoded USDC transfer call, routes it through CallFrom so the USDC transfer still sees your hot wallet as `msg.sender`, and emits a `Memo` event for reconciliation. Compliance tools that read call traces may record the Memo contract as the sender. Use Transfer events for accurate attribution. **Memo contract address:** `0x5294E9927c3306DcBaDb03fe70b92e01cCede505` For the full viem, ethers, Python, and cURL flow, see [Send USDC with a transaction memo](/arc/tutorials/send-usdc-with-transaction-memo). Store a deterministic `memoId` or encoded memo value that links the onchain transfer to your internal withdrawal record. ## See also * [Gas and fees](/arc/references/gas-and-fees)—Fee model details and base fee mechanics * [Deterministic finality](/arc/concepts/deterministic-finality)—Why single-block confirmation is safe * [Detect deposits](/integrate/exchanges/deposits)—The corresponding deposit detection guide * [Contract addresses](/arc/references/contract-addresses)—All system contract addresses # Infrastructure Integration Source: https://docs.arc.io/integrate/infrastructure Add Arc support to node services, data indexers, oracle networks, block explorers, and compliance tools. Arc is an EVM-compatible blockchain, so standard Ethereum tooling works out of the box. However, several architectural differences affect how infrastructure providers index data, stream blocks, and expose balance APIs. ## Key differences from Ethereum | Area | Ethereum behavior | Arc behavior | | ----------------- | -------------------------------------------------------------------- | --------------------------------------------------------------- | | Native token | `eth_getBalance` returns ETH (18 decimals) | `eth_getBalance` returns USDC (18 decimals) | | Finality | Probabilistic—requires 12+ minutes and multiple confirmations | Deterministic—once a block is committed, it is permanent | | Reorgs | Possible—indexers must handle chain reorganizations and uncle blocks | Never—no reorganizations occur | | Block time | \~12 seconds | Sub-second—multiple blocks may share the same `block.timestamp` | | `PREVRANDAO` | Randomness beacon value | Always returns `0` | | Blob transactions | Supported (EIP-4844) | Not supported | | Consensus | Proof-of-stake (Casper) | Malachite BFT with permissioned PoA validators | ## Chain metadata | Property | Value | | ------------------- | -------------------------------------------------- | | Chain ID | `5042002` | | RPC (HTTPS) | `https://rpc.testnet.arc.io` | | WebSocket | `wss://rpc.testnet.arc.io` | | Block explorer | [testnet.arcscan.app](https://testnet.arcscan.app) | | CCTP domain | `26` | | EVM target | Osaka hard fork | | USDC ERC-20 address | `0x3600000000000000000000000000000000000000` | Additional RPC endpoints are available through [Blockdaemon, dRPC, and QuickNode](/arc/tools/node-providers). ## Integration considerations ### Balance APIs `eth_getBalance` returns the account's native balance in USDC at 18-decimal precision. Use `formatUnits(balance, 18)`, not `formatUnits(balance, 6)`. If your platform displays balances, label the value as USDC rather than ETH. The same underlying balance is also accessible through the ERC-20 interface at 6-decimal precision. Transfer log precision depends on the emitting interface: system emitter events use 18-decimal precision, ERC-20 events use 6-decimal. ### No-reorg indexing Arc's [deterministic finality](/arc/concepts/deterministic-finality) means you never need to handle chain reorganizations or uncle blocks. Every block your indexer receives is permanent. You can treat a single block confirmation as final and skip reorg-recovery logic entirely. ### Sub-second block streaming Blocks arrive faster than once per second. Your ingestion pipeline must handle high-throughput streaming without assuming a minimum interval between blocks. Multiple consecutive blocks may share the same `block.timestamp` because sub-second blocks can fall in the same wall-clock second. ### Randomness Don't treat `PREVRANDAO` as a source of randomness on Arc. It always returns `0`, not a random beacon value. Any contract using it for lottery selection, shuffle logic, or relay logic will always receive `0`. ### USDC transfer events Build transfer history from the system emitter (`0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE`), not the ERC-20 contract (`0x3600000000000000000000000000000000000000`). Native USDC transfers emit no log at the ERC-20 address, so filtering on the ERC-20 address alone misses them. An ERC-20 `transfer()` emits from both addresses, so filtering on both double-counts every ERC-20 transfer. For emitter addresses, the exact log format, and indexing guidance, see [Index events](/integrate/infrastructure/indexing-events). ### Sender attribution For EIP-3009 relayer transactions, using `tx.from` attributes transfers to the relayer EOA, not the token sender. Use the `from` field in the EIP-7708 Transfer event from the system emitter for accurate sender attribution. ### Blocklist enforcement A transfer to or from a blocklisted address reverts at runtime. The transaction is included in the block and gas is consumed, but state changes roll back. Any indexer, relayer, or settlement flow that submits USDC transfers must check `receipt.status === 0` to detect a blocklist revert. ## Self-hosted access For independent verification or direct RPC access without third-party providers, you can run your own Arc node. The execution client (`arc-node-execution`) is Reth-based, and the consensus client (`arc-node-consensus`) is Malachite-based. Architecture overview and requirements for operating an Arc node. Step-by-step guide to install, configure, and start both clients. ## Sub-pages Unified transfer events, no-reorg indexing, and block streaming guidance for data indexers. Blocklist enforcement, Memo contract monitoring, and compliance tool integrations. # How to: Add Arc to Your Bridge Protocol Source: https://docs.arc.io/integrate/infrastructure/bridges Register Arc as a supported chain in your bridge or interoperability protocol, configure confirmation requirements, and deploy relay contracts. Register Arc as a supported chain in your bridge or interoperability protocol. Arc's deterministic finality, USDC-native gas model, and CCTP integration require specific configuration choices that differ from probabilistic-finality chains. ## Prerequisites Before you begin: * Familiarity with deploying contracts on EVM chains using Foundry or Hardhat * Access to an Arc RPC endpoint (`https://rpc.testnet.arc.io`) * A funded deployer wallet with USDC on Arc (USDC is the gas token) * Understanding of your protocol's chain registration and relay architecture ## Chain metadata Register Arc with the following parameters: | Property | Value | | :------------------------ | :-------------------------------- | | **Chain ID** | `5042002` (testnet) | | **RPC (HTTPS)** | `https://rpc.testnet.arc.io` | | **RPC (WebSocket)** | `wss://rpc.testnet.arc.io` | | **Block explorer** | `https://testnet.arcscan.app` | | **Native gas token** | USDC | | **Native token decimals** | 18 (native), 6 (ERC-20 interface) | | **EVM target** | Osaka hard fork | | **Block time** | Sub-second | | **Finality** | Deterministic (BFT consensus) | | **CCTP domain** | `26` | ## Steps ### Step 1. Configure finality and confirmation requirements Arc uses deterministic BFT finality. Once a block is committed, it is irreversible. There are no reorgs. Set your required confirmations to **1**. A single confirmation on Arc provides the same settlement guarantee as 64+ confirmations on Ethereum or 20+ on other L2s. | Chain type | Typical confirmations | Arc confirmations | | :----------------- | :------------------------ | :---------------- | | Ethereum (PoS) | 64 blocks (\~13 min) | 1 block (\<1 s) | | Optimistic rollups | 7 days (challenge period) | 1 block (\<1 s) | | Other L2s | 10–20 blocks | 1 block (\<1 s) | If your protocol uses `safe` or `finalized` block tags in RPC calls, both resolve to the latest block on Arc. You do not need separate handling for pending vs. finalized states. For your bridge configuration: ```typescript theme={null} import { defineChain } from "viem"; export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USDC", symbol: "USDC", decimals: 18, }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.io"], webSocket: ["wss://rpc.testnet.arc.io"], }, }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app", }, }, }); // Bridge confirmation config const arcBridgeConfig = { chainId: 5042002, requiredConfirmations: 1, // Deterministic finality—1 is sufficient finalityType: "deterministic" as const, avgBlockTimeMs: 500, }; ``` ### Step 2. Route USDC through CCTP Arc uses Circle's Cross-Chain Transfer Protocol (CCTP) as the canonical USDC bridge. CCTP uses a burn-and-mint model, meaning USDC on Arc is always native—never wrapped or locked. | Contract | Address | Notes | | :----------------------- | :----------------------------------------------------------------------------------------------------------------------------- | :----------------------------- | | **TokenMessengerV2** | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://testnet.arcscan.app/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) | Initiates crosschain burns | | **MessageTransmitterV2** | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://testnet.arcscan.app/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) | Receives and attests messages | | **USDC** | [`0x3600000000000000000000000000000000000000`](https://testnet.arcscan.app/address/0x3600000000000000000000000000000000000000) | Native USDC (ERC-20 interface) | | **CCTP domain** | `26` | Use in `depositForBurn` calls | If your bridge aggregates routes, prefer the CCTP path for USDC transfers to and from Arc over any lock-and-mint or liquidity-pool approach. ### Step 3. Deploy relay and adapter contracts Arc is EVM-compatible (Osaka hard fork target). Standard deployment tooling works without modification: ```typescript theme={null} import { createWalletClient, http } from "viem"; import { privateKeyToAccount } from "viem/accounts"; // Deploy using standard viem workflow const account = privateKeyToAccount( process.env.DEPLOYER_PRIVATE_KEY as `0x${string}`, ); const walletClient = createWalletClient({ account, chain: arcTestnet, transport: http("https://rpc.testnet.arc.io"), }); // CREATE2 deterministic deployment works as expected // Permit2 is available at the canonical address const PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3"; const MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11"; ``` **Key deployment considerations:** * CREATE2 factory works at the standard address for deterministic deploys * Permit2 is deployed at `0x000000000022D473030F116dDEE9F6B43aC78BA3` * Multicall3 is available at the standard address * No EIP-4844 blob transactions—use `type: 2` (EIP-1559) transactions * `PREVRANDAO` always returns `0`—do not use it for randomness in relay selection If your contracts reference `block.prevrandao` for relay shuffling or random selection, replace it with an external oracle or deterministic round-robin approach on Arc. ### Step 4. Fund relayers with USDC for gas Arc uses USDC as its gas token. Your relay executors and watchers need USDC balances to submit transactions. `eth_getBalance` on Arc returns native USDC at 18-decimal precision. Use `formatUnits(balance, 18)`, not `formatUnits(balance, 6)`. Using 6 decimals overstates every balance by a factor of 10¹². ```typescript theme={null} import { parseUnits, formatUnits } from "viem"; // Check relayer gas balance (USDC with 18 decimals at native level) const balance = await publicClient.getBalance({ address: relayerAddress, }); console.log(`Relayer balance: ${formatUnits(balance, 18)} USDC`); // Fund relayer via ERC-20 transfer (6 decimals) const USDC_ADDRESS = "0x3600000000000000000000000000000000000000"; const fundTx = await walletClient.writeContract({ address: USDC_ADDRESS, abi: [ { name: "transfer", type: "function", inputs: [ { name: "to", type: "address" }, { name: "amount", type: "uint256" }, ], outputs: [{ type: "bool" }], stateMutability: "nonpayable", }, ], functionName: "transfer", args: [relayerAddress, parseUnits("1000", 6)], // 1,000 USDC }); ``` Fund relayers with USDC. It's the only asset needed to pay for gas on Arc. **Gas cost estimation:** Arc's fee model uses a smoothed moving average inspired by EIP-1559. Gas prices are stable and predictable. A typical relay transaction costs well under \$0.01 in USDC gas fees. ### Step 5. Connect your relay infrastructure For high-throughput relay operations, connect to Arc using WebSocket for real-time block and event streaming: ```typescript theme={null} import { createPublicClient, webSocket, http } from "viem"; // WebSocket for real-time event monitoring (relay watchers) const wsClient = createPublicClient({ chain: arcTestnet, transport: webSocket("wss://rpc.testnet.arc.io"), }); // HTTP for transaction submission (relay executors) const httpClient = createPublicClient({ chain: arcTestnet, transport: http("https://rpc.testnet.arc.io"), }); // Watch for bridge events with immediate finality const unwatch = wsClient.watchContractEvent({ address: YOUR_BRIDGE_CONTRACT, abi: bridgeAbi, eventName: "MessageSent", onLogs: (logs) => { // Each log is final on receipt—no need to wait for confirmations for (const log of logs) { processRelayMessage(log); } }, }); ``` Build transfer history from the system emitter (`0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE`), not the ERC-20 contract (`0x3600000000000000000000000000000000000000`). Native USDC transfers emit no log at the ERC-20 address, so filtering on the ERC-20 address alone misses them. An ERC-20 `transfer()` emits from both addresses, so filtering on both double-counts every ERC-20 transfer. **Node provider options** for relay infrastructure: | Provider | Notes | | :---------- | :--------------------------------------- | | Alchemy | Managed RPC, WebSocket support | | Blockdaemon | Enterprise-grade node infrastructure | | dRPC | Decentralized RPC network | | QuickNode | Managed endpoints with analytics | | Self-hosted | Run your own Arc node for lowest latency | For latency-sensitive relay operations, run a dedicated Arc node. Arc's node software is lightweight and designed for high-throughput block production. ### Step 6. Integrate price feeds (optional) If your bridge logic requires price oracles for fee estimation or value validation, the following oracle providers are available on Arc: | Provider | Use case | | :-------- | :------------------------ | | Chainlink | Price feeds, CCIP | | Pyth | High-frequency price data | | Redstone | Pull-based oracle model | | Stork | Low-latency price feeds | ## Integration checklist Use this checklist to verify your Arc integration is complete: * [ ] Chain ID `5042002` registered in your chain registry * [ ] Required confirmations set to `1` * [ ] USDC routed through CCTP (domain `26`)—no wrapped variants * [ ] Relay/adapter contracts deployed on Arc * [ ] Relayer wallets funded with USDC * [ ] WebSocket connection established for event monitoring * [ ] Gas estimation logic accounts for USDC denomination * [ ] No reliance on `PREVRANDAO` for randomness * [ ] No EIP-4844 blob transaction usage * [ ] Explorer links use `https://testnet.arcscan.app` ## Key differences from other EVM chains | Consideration | Typical EVM chain | Arc | | :---------------------- | :------------------------------- | :---------------------------- | | **Confirmation safety** | Wait 12–64 blocks | 1 block is final | | **Gas token** | ETH or chain-native token | USDC | | **Reorg handling** | Required | Not needed | | **USDC bridging** | Lock-and-mint or liquidity pools | CCTP burn-and-mint (native) | | **Block time** | 2–12 seconds | Sub-second | | **Fee volatility** | High (auction-based) | Low (smoothed moving average) | # How to: Monitor Blocklist Compliance Source: https://docs.arc.io/integrate/infrastructure/compliance Handle Arc's USDC blocklist enforcement in your compliance monitoring, including Memo and Multicall3From contract attribution. The USDC contract enforces blocklist restrictions at runtime. The Memo and Multicall3From contracts route transactions while preserving the original `msg.sender` through the CallFrom precompile; your monitoring must attribute these to the original sender. Subscribe to `Blocklisted` and `UnBlocklisted` events to maintain a local copy of the blocklist. ## Prerequisites Before you begin: * Access to an Arc RPC endpoint (`https://rpc.testnet.arc.io`) or WebSocket (`wss://rpc.testnet.arc.io`) * Familiarity with Ethereum event log filtering and transaction tracing * A local database or cache for storing blocklisted addresses * Understanding of your regulatory obligations (AML/CFT screening requirements) ## Contracts and addresses | Contract | Address | Purpose | | -------------- | -------------------------------------------- | ------------------------------------------------------ | | USDC | `0x3600000000000000000000000000000000000000` | Native stablecoin with built-in blocklist | | Memo | `0x5294E9927c3306DcBaDb03fe70b92e01cCede505` | Attaches metadata to transfers; preserves `msg.sender` | | Multicall3From | `0x522fAf9A91c41c443c66765030741e4AaCe147D0` | Batches multiple calls; preserves `msg.sender` | ## Steps ### Step 1. Understand blocklist enforcement Arc enforces the USDC blocklist across multiple stages of the transaction lifecycle: | Stage | When it applies | Behavior | | ---------------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | Pre-mempool | Transaction submitted to RPC node | If the sender is blocklisted, the RPC node rejects the transaction. No gas is consumed and no receipt is returned. | | Pre-execution | Transaction executes after entering mempool | If the sender became blocklisted between submission and execution, the transaction is rejected. No gas is consumed and no receipt is returned. | | Runtime transfer check | `transfer` or `transferFrom` is called | If either the `from` or `to` address is blocklisted, the call reverts. | ### Step 2. Monitor blocklist events Subscribe to the `Blocklisted` and `UnBlocklisted` events on the USDC contract to maintain a real-time view of restricted addresses. ```typescript theme={null} import { Contract, JsonRpcProvider } from "ethers"; const USDC_ADDRESS = "0x3600000000000000000000000000000000000000"; const provider = new JsonRpcProvider("https://rpc.testnet.arc.io"); const usdc = new Contract( USDC_ADDRESS, [ "event Blocklisted(address indexed account)", "event UnBlocklisted(address indexed account)", ], provider, ); // Subscribe to blocklist changes usdc.on("Blocklisted", (account: string) => { console.log(`Address blocklisted: ${account}`); addToLocalBlocklist(account); }); usdc.on("UnBlocklisted", (account: string) => { console.log(`Address unblocklisted: ${account}`); removeFromLocalBlocklist(account); }); ``` Build transfer history from the system emitter (`0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE`), not the ERC-20 contract (`0x3600000000000000000000000000000000000000`). Native USDC transfers emit no log at the ERC-20 address, so filtering on the ERC-20 address alone misses them. An ERC-20 `transfer()` emits from both addresses, so filtering on both double-counts every ERC-20 transfer. ### Step 3. Include Memo and Multicall3From in your monitoring scope The Memo and Multicall3From contracts use the CallFrom precompile to execute calls on behalf of the original sender. The blocklist is still enforced (the CallFrom precompile checks the original sender's blocklist status), but compliance monitors must attribute activity correctly. If you only monitor direct `from` addresses in transaction receipts, you will miss the true sender for transactions routed through Memo or Multicall3From. You must inspect calls to these contracts and attribute them to the original `msg.sender`. ```typescript theme={null} import { Interface, JsonRpcProvider, Log } from "ethers"; const MEMO_ADDRESS = "0x5294E9927c3306DcBaDb03fe70b92e01cCede505"; const MULTICALL3FROM_ADDRESS = "0x522fAf9A91c41c443c66765030741e4AaCe147D0"; const USDC_ADDRESS = "0x3600000000000000000000000000000000000000"; const SYSTEM_EMITTER = "0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE"; const TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"; const provider = new JsonRpcProvider("https://rpc.testnet.arc.io"); const erc20Interface = new Interface([ "event Transfer(address indexed from, address indexed to, uint256 value)", ]); async function checkTransactionCompliance(txHash: string): Promise { const tx = await provider.getTransaction(txHash); if (!tx) return; const receipt = await provider.getTransactionReceipt(txHash); if (!receipt) return; const originalSender = tx.from; // Flag if the transaction is routed through Memo or Multicall3From const isRoutedTransaction = tx.to?.toLowerCase() === MEMO_ADDRESS.toLowerCase() || tx.to?.toLowerCase() === MULTICALL3FROM_ADDRESS.toLowerCase(); if (isRoutedTransaction) { // Attribute all Transfer events in this transaction to the original sender const transfers = receipt.logs.filter( (log: Log) => log.address.toLowerCase() === SYSTEM_EMITTER.toLowerCase() && log.topics[0] === TRANSFER_TOPIC, ); for (const log of transfers) { const parsed = erc20Interface.parseLog({ topics: log.topics as string[], data: log.data, }); if (!parsed) continue; // Screen the original sender, not the contract address await screenAddress(originalSender, txHash); await screenAddress(parsed.args.to as string, txHash); } } } ``` ### Step 4. Build a transaction decision tree Use the following logic to determine whether a transaction involves a blocklisted address: | Check | Condition | Action | | ---------------------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------- | | 1. Direct sender | `tx.from` is in blocklist | No action needed: rejected at pre-mempool and pre-execution stages before mining. | | 2. Transfer recipient | `Transfer` event `to` is in blocklist | Flag: `transfer`/`transferFrom` will revert | | 3. Routed transfer recipient | `tx.to` is Memo or Multicall3From AND any Transfer `to` is in blocklist | Flag: runtime transfer check will revert | ```typescript theme={null} interface ComplianceResult { flagged: boolean; reason?: string; } async function evaluateTransaction(txHash: string): Promise { const tx = await provider.getTransaction(txHash); if (!tx) return { flagged: false }; // Check 1: Direct sender if (await isBlocklisted(tx.from)) { return { flagged: true, reason: "Sender is blocklisted" }; } const receipt = await provider.getTransactionReceipt(txHash); if (!receipt) return { flagged: false }; // Check 2-5: Inspect Transfer events for blocklisted recipients const transfers = receipt.logs.filter( (log: Log) => log.address.toLowerCase() === SYSTEM_EMITTER.toLowerCase() && log.topics[0] === TRANSFER_TOPIC, ); for (const log of transfers) { const parsed = erc20Interface.parseLog({ topics: log.topics as string[], data: log.data, }); if (!parsed) continue; const to = parsed.args.to as string; if (await isBlocklisted(to)) { return { flagged: true, reason: `Recipient ${to} is blocklisted` }; } } return { flagged: false }; } ``` ### Step 5. Integrate compliance vendor APIs Connect your monitoring pipeline to Elliptic or TRM Labs for automated risk scoring and sanctions screening. These vendors provide Arc-compatible APIs for real-time transaction analysis. ```typescript theme={null} // Example: screen an address against a compliance vendor API async function screenAddress( address: string, txHash: string, ): Promise { const response = await fetch( "https://api.your-compliance-vendor.com/screen", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.COMPLIANCE_API_KEY}`, }, body: JSON.stringify({ address, chain: "arc", transactionHash: txHash, }), }, ); const result = await response.json(); return result.risk_level === "high"; } ``` For vendor-specific integration details, see [Compliance vendors](/arc/tools/compliance-vendors). When your own system submits a USDC transfer, also check `receipt.status` after it confirms. A blocklist revert is onchain: the transaction is included and gas is consumed, but state rolls back. Monitoring `Blocklisted` events won't catch reverts that already occurred; `receipt.status === 0` is the authoritative signal. ## See also * [Compliance vendors](/arc/tools/compliance-vendors): Elliptic and TRM Labs integration details * [Infrastructure overview](/integrate/infrastructure): Arc architectural differences relevant to compliance monitoring # How to: Index Arc Events Source: https://docs.arc.io/integrate/infrastructure/indexing-events Configure your data indexer or block explorer to process Arc events, including unified USDC transfers, Memo events, blocklist changes, and CCTP messages. Monitor the native USDC `Transfer` event from the system address `0xffff…fffe` to capture every USDC transfer in one stream (Arc's EIP-7708 implementation). Gas deductions don't emit Transfer events. Index the ERC-20 USDC contract, Memo, blocklist, and CCTP events from their respective contracts for full transaction coverage. Skip reorg-handling logic entirely; Arc's deterministic finality means every block is permanent. Use block number (not timestamp) as your ordering key because sub-second blocks can share the same `block.timestamp`. ## Prerequisites Before you begin: * Access to an Arc RPC endpoint (`https://rpc.testnet.arc.io`) or WebSocket (`wss://rpc.testnet.arc.io`) * Familiarity with Ethereum JSON-RPC methods (`eth_getLogs`, `eth_subscribe`) * A database or indexing pipeline that supports high-throughput block ingestion * TypeScript environment with `ethers` or `viem` installed ## Steps ### Step 1. Connect to the block stream Use `eth_subscribe("newHeads")` over WebSocket for real-time block notifications. For historical back-fills, use `eth_getLogs` with `fromBlock`/`toBlock` ranges. ```typescript theme={null} import { WebSocketProvider, JsonRpcProvider } from "ethers"; // Real-time streaming const wsProvider = new WebSocketProvider("wss://rpc.testnet.arc.io"); wsProvider.on("block", async (blockNumber: number) => { await indexBlock(blockNumber); }); // Historical backfill const httpProvider = new JsonRpcProvider("https://rpc.testnet.arc.io"); async function backfill(startBlock: number, endBlock: number): Promise { const BATCH_SIZE = 1000; for (let from = startBlock; from <= endBlock; from += BATCH_SIZE) { const to = Math.min(from + BATCH_SIZE - 1, endBlock); const logs = await httpProvider.getLogs({ fromBlock: from, toBlock: to }); await processLogs(logs); } } ``` Arc produces sub-second blocks. Your ingestion pipeline must handle bursts of many blocks per second without falling behind. ### Step 2. Index native USDC Transfer events (EIP-7708) Every explicit USDC transfer emits a standard ERC-20 `Transfer` log from the system address `0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE` (Arc's [EIP-7708](https://eips.ethereum.org/EIPS/eip-7708) implementation). This stream covers native sends, the native leg of ERC-20 transfers, and mint and burn, with values in **18 decimals**. Gas deductions don't emit events. The `from` field in this log is the actual token sender. For EIP-3009 relayer transactions, `tx.from` is the relayer's address. Indexing it instead of the Transfer event's `from` records the wrong sender. The ERC-20 USDC contract at `0x3600…0000` also emits its own `Transfer` (6 decimals) for ERC-20-interface activity, so an ERC-20 `transfer()` produces a log from both emitters. Distinguish them by emitter address and never count the same movement twice. For the complete event matrix, the mint and burn mapping, and the historical pre-Zero5 events, see [USDC system events](/arc/references/usdc-system-events). | Property | Value | | -------- | -------------------------------------------------------------------- | | Emitter | `0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE` | | Event | `Transfer(address indexed from, address indexed to, uint256 value)` | | topic0 | `0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef` | | Decimals | 18 | ```typescript theme={null} import { Interface, JsonRpcProvider, Log } from "ethers"; // Native USDC system emitter (EIP-7708 Transfer logs, 18 decimals) const NATIVE_USDC_EMITTER = "0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE"; // ERC-20 USDC contract (emits its own 6-decimal Transfer and blocklist events) const USDC_ADDRESS = "0x3600000000000000000000000000000000000000"; const TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"; const erc20Interface = new Interface([ "event Transfer(address indexed from, address indexed to, uint256 value)", ]); const provider = new JsonRpcProvider("https://rpc.testnet.arc.io"); async function indexUsdcTransfers( fromBlock: number, toBlock: number, ): Promise { const logs = await provider.getLogs({ address: NATIVE_USDC_EMITTER, topics: [TRANSFER_TOPIC], fromBlock, toBlock, }); for (const log of logs) { const parsed = erc20Interface.parseLog({ topics: log.topics as string[], data: log.data, }); if (!parsed) continue; const from = parsed.args.from as string; const to = parsed.args.to as string; const value = parsed.args.value as bigint; // 18 decimals (native) await saveTransfer({ blockNumber: log.blockNumber, txHash: log.transactionHash, logIndex: log.index, from, to, value, }); } } ``` Filtering the native system emitter (`0xffff…fffe`) captures all explicit USDC transfers in one stream: native sends, the native leg of ERC-20 transfers, and mint and burn. Gas deductions don't emit events. If you also index the ERC-20 USDC contract (`0x3600…0000`) for its 6-decimal `Transfer` events, match on the emitter address so you do not count ERC-20 transfers twice. To backfill history across the Zero5 hard fork, read the historical `NativeCoin*` events from `0x1800…0000` for blocks before activation and `Transfer` from `0xffff…fffe` at and after it. See [USDC system events](/arc/references/usdc-system-events#historical-events-before-zero5) for signatures and the activation reference. ### Step 3. Index EURC and other ERC-20 token transfers EURC and other ERC-20 tokens emit standard `Transfer` events from their own contract addresses. Index these separately from USDC. | Token | Contract | Decimals | | ----- | -------------------------------------------- | -------- | | EURC | `0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a` | 6 | ```typescript theme={null} const EURC_ADDRESS = "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a"; async function indexEurcTransfers( fromBlock: number, toBlock: number, ): Promise { const logs = await provider.getLogs({ address: EURC_ADDRESS, topics: [TRANSFER_TOPIC], fromBlock, toBlock, }); for (const log of logs) { const parsed = erc20Interface.parseLog({ topics: log.topics as string[], data: log.data, }); if (!parsed) continue; await saveTransfer({ blockNumber: log.blockNumber, txHash: log.transactionHash, logIndex: log.index, from: parsed.args.from as string, to: parsed.args.to as string, value: parsed.args.value as bigint, }); } } ``` ### Step 4. Index Memo contract events The Memo contract lets an account attach an arbitrary memo to a forwarded call (for example, a USDC transfer) for correlation and reconciliation. It executes the call through the CallFrom precompile, preserving the original `msg.sender`, and emits a `Memo` event with the metadata. Index `Memo` events to store memo payloads alongside the calls they annotate. The Memo contract is available on Arc testnet as of June 18, 2026. | Property | Value | | -------- | ----------------------------------------------------------------------------------------------------------------------------------- | | Contract | `0x5294E9927c3306DcBaDb03fe70b92e01cCede505` | | Event | `Memo(address indexed sender, address indexed target, bytes32 callDataHash, bytes32 indexed memoId, bytes memo, uint256 memoIndex)` | ```typescript theme={null} const MEMO_CONTRACT = "0x5294E9927c3306DcBaDb03fe70b92e01cCede505"; const memoInterface = new Interface([ "event Memo(address indexed sender, address indexed target, bytes32 callDataHash, bytes32 indexed memoId, bytes memo, uint256 memoIndex)", ]); async function indexMemoEvents( fromBlock: number, toBlock: number, ): Promise { const logs = await provider.getLogs({ address: MEMO_CONTRACT, fromBlock, toBlock, }); for (const log of logs) { const parsed = memoInterface.parseLog({ topics: log.topics as string[], data: log.data, }); if (!parsed) continue; // skips the contract's BeforeMemo events const sender = parsed.args.sender as string; // original caller (msg.sender) const target = parsed.args.target as string; // contract the call was forwarded to const callDataHash = parsed.args.callDataHash as string; // hash of the forwarded calldata const memoId = parsed.args.memoId as string; // caller-supplied identifier (bytes32) const memoBytes = parsed.args.memo as string; // hex-encoded bytes const memoIndex = parsed.args.memoIndex as bigint; // sequential memo index // Decode the memo payload as UTF-8 text if applicable const memoText = Buffer.from(memoBytes.slice(2), "hex").toString("utf-8"); await saveMemo({ blockNumber: log.blockNumber, txHash: log.transactionHash, logIndex: log.index, sender, target, callDataHash, memoId, memo: memoText, memoIndex, }); } } ``` Correlate `Memo` events with the `Transfer` (or other target-contract) events they annotate by matching on `transactionHash`, or use the caller-supplied `memoId`. To match a specific target call, compare `callDataHash` with the hash of the calldata your application submitted. The memo provides context (such as an invoice ID or payment reference) for the forwarded call. ### Step 5. Index blocklist events The USDC contract emits `Blocklisted` and `UnBlocklisted` events when addresses are added to or removed from the blocklist. Track these to maintain an accurate set of restricted addresses. | Event | Signature | | ----------------- | ---------------------------------------- | | Address blocked | `Blocklisted(address indexed account)` | | Address unblocked | `UnBlocklisted(address indexed account)` | ```typescript theme={null} const blocklistInterface = new Interface([ "event Blocklisted(address indexed account)", "event UnBlocklisted(address indexed account)", ]); const BLOCKLISTED_TOPIC = blocklistInterface.getEvent("Blocklisted")!.topicHash; const UNBLOCKLISTED_TOPIC = blocklistInterface.getEvent("UnBlocklisted")!.topicHash; async function indexBlocklistEvents( fromBlock: number, toBlock: number, ): Promise { const logs = await provider.getLogs({ address: USDC_ADDRESS, topics: [[BLOCKLISTED_TOPIC, UNBLOCKLISTED_TOPIC]], fromBlock, toBlock, }); for (const log of logs) { const parsed = blocklistInterface.parseLog({ topics: log.topics as string[], data: log.data, }); if (!parsed) continue; const account = parsed.args.account as string; const isBlocked = parsed.name === "Blocklisted"; await updateBlocklist({ blockNumber: log.blockNumber, txHash: log.transactionHash, account, isBlocked, }); } } ``` ### Step 6. Index CCTP crosschain events The Cross-Chain Transfer Protocol (CCTP) uses two contracts on Arc: `TokenMessengerV2` for outbound burns and `MessageTransmitterV2` for inbound mints. | Direction | Contract | Address | Event | | --------------- | -------------------- | -------------------------------------------- | ----------------- | | Outbound (burn) | TokenMessengerV2 | `0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA` | `DepositForBurn` | | Inbound (mint) | MessageTransmitterV2 | `0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275` | `MessageReceived` | ```typescript theme={null} const TOKEN_MESSENGER = "0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA"; const MESSAGE_TRANSMITTER = "0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275"; const cctpInterface = new Interface([ "event DepositForBurn(uint64 indexed nonce, address indexed burnToken, uint256 amount, address indexed depositor, bytes32 mintRecipient, uint32 destinationDomain, bytes32 destinationTokenMessenger, bytes32 destinationCaller)", "event MessageReceived(address indexed caller, uint32 sourceDomain, uint64 indexed nonce, bytes32 sender, bytes messageBody)", ]); async function indexCctpEvents( fromBlock: number, toBlock: number, ): Promise { // Index outbound burns const burnLogs = await provider.getLogs({ address: TOKEN_MESSENGER, topics: [cctpInterface.getEvent("DepositForBurn")!.topicHash], fromBlock, toBlock, }); for (const log of burnLogs) { const parsed = cctpInterface.parseLog({ topics: log.topics as string[], data: log.data, }); if (!parsed) continue; await saveCctpBurn({ blockNumber: log.blockNumber, txHash: log.transactionHash, nonce: parsed.args.nonce, amount: parsed.args.amount, depositor: parsed.args.depositor, destinationDomain: parsed.args.destinationDomain, }); } // Index inbound mints const mintLogs = await provider.getLogs({ address: MESSAGE_TRANSMITTER, topics: [cctpInterface.getEvent("MessageReceived")!.topicHash], fromBlock, toBlock, }); for (const log of mintLogs) { const parsed = cctpInterface.parseLog({ topics: log.topics as string[], data: log.data, }); if (!parsed) continue; await saveCctpMint({ blockNumber: log.blockNumber, txHash: log.transactionHash, nonce: parsed.args.nonce, sourceDomain: parsed.args.sourceDomain, }); } } ``` ### Step 7. Use block number as your ordering key Multiple blocks can share the same `block.timestamp` because Arc produces sub-second blocks that fall in the same wall-clock second. Always use `blockNumber` (and `logIndex` in a block) as your canonical ordering key. ```typescript theme={null} interface IndexedEvent { blockNumber: number; // Primary ordering key logIndex: number; // Secondary ordering key in a block txHash: string; // ... event-specific fields } // Correct: order by block number function compareEvents(a: IndexedEvent, b: IndexedEvent): number { if (a.blockNumber !== b.blockNumber) { return a.blockNumber - b.blockNumber; } return a.logIndex - b.logIndex; } ``` Do not use `block.timestamp` for ordering. Two consecutive blocks (for example, block 100 and block 101) may both have `timestamp = 1700000000`. Sorting by timestamp produces ambiguous ordering. ### Step 8. Simplify your pipeline—no reorg handling required Arc provides [deterministic finality](/arc/concepts/deterministic-finality). Once a block appears, it is permanent. You can remove the following from your indexing pipeline: * Reorg detection and rollback logic * Confirmation-depth delays (no need to wait for N confirmations) * Uncle/ommer block handling * Chain reorganization event listeners ```typescript theme={null} // No need for confirmation buffers or reorg watchers. // Process each block exactly once as it arrives. async function indexBlock(blockNumber: number): Promise { // This block is final—it will never be reverted. await indexUsdcTransfers(blockNumber, blockNumber); await indexEurcTransfers(blockNumber, blockNumber); await indexMemoEvents(blockNumber, blockNumber); await indexBlocklistEvents(blockNumber, blockNumber); await indexCctpEvents(blockNumber, blockNumber); await markBlockProcessed(blockNumber); } ``` If your indexer restarts, resume from the last processed block number. You do not need to re-validate previously indexed blocks because they cannot be reverted. ## Event reference | Contract | Address | Events | | -------------------- | -------------------------------------------- | ------------------------------------------------------- | | Native USDC (system) | `0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE` | `Transfer` (18 decimals, EIP-7708) | | USDC (ERC-20) | `0x3600000000000000000000000000000000000000` | `Transfer` (6 decimals), `Blocklisted`, `UnBlocklisted` | | EURC | `0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a` | `Transfer` | | Memo | `0x5294E9927c3306DcBaDb03fe70b92e01cCede505` | `Memo` | | TokenMessengerV2 | `0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA` | `DepositForBurn` | | MessageTransmitterV2 | `0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275` | `MessageReceived` | ## See also * [USDC system events](/arc/references/usdc-system-events)—full event matrix, emitter addresses, decimals, and historical pre-Zero5 events * [Infrastructure overview](/integrate/infrastructure)—key differences from Ethereum and chain metadata * [Deterministic finality](/arc/concepts/deterministic-finality)—why reorgs never occur on Arc * [Detect and process deposits](/integrate/exchanges/deposits)—exchange-specific deposit workflow # On/off-ramp integration Source: https://docs.arc.io/integrate/on-off-ramps Add Arc as a supported network in your fiat-to-crypto and crypto-to-fiat ramp service. On/off-ramp platforms on Arc use USDC for both transfers and gas. No ETH float is required. Arc's sub-second finality removes the multi-confirmation wait that other EVM chains require. ## Key differences for on/off-ramp platforms Three Arc design decisions affect every ramp integration: | Concern | Arc behavior | Impact on ramp platforms | | :--------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **USDC as the native gas token** | Gas is paid in USDC. There is no separate ETH or gas token. | Use a single USDC asset entry per address. No ETH float for gas. No need to warn users to acquire a second token. | | **Deterministic finality** | Transactions finalize in under one second. There are no reorgs. | Credit buy orders and complete sell orders after 1 confirmation. Remove block-countdown UIs and multi-confirmation wait logic. | | **Two decimal views, one balance** | Native USDC uses 18 decimals (`eth_getBalance`, EIP-7708 events); the ERC-20 interface uses 6 decimals (`balanceOf`). Both draw from the same balance. | Filter Transfer events on the native system emitter (`0xffff…fffe`) and divide the 18-decimal `value` by 1012 for display. Use 6-decimal values for withdrawals. | ## What you need Before you start, confirm you have the following: | Requirement | Details | | :------------------- | :----------------------------------------------------------------------------------------------------------------------------- | | RPC access | `https://rpc.testnet.arc.io` | | Chain ID | `5042002` (testnet) | | USDC ERC-20 contract | [`0x3600000000000000000000000000000000000000`](https://testnet.arcscan.app/address/0x3600000000000000000000000000000000000000) | | CCTP domain | `26` (for crosschain USDC routing) | ## Arc behaviors relevant to this integration The following Arc behaviors can affect ramp operations. See [EVM differences](/arc/references/evm-differences) for details: * **Blocklist enforcement:** Transactions from or to blocklisted addresses are rejected before the mempool. Transfers involving a blocked address revert at runtime and consume gas. Screen destination addresses before broadcasting. * **EIP-7708 Transfer events:** Every native USDC movement emits an ERC-20 `Transfer` event from system emitter `0xffff…fffe`. Filtering only on the ERC-20 contract address misses native sends. ## Integration guides Chain configuration, deposit detection, withdrawal processing, UI display, and compliance screening for ramp providers. # How-to: Add Arc to your on/off-ramp platform Source: https://docs.arc.io/integrate/on-off-ramps/add-arc-to-your-platform Register Arc as a supported network in your fiat-to-crypto and crypto-to-fiat ramp service, including chain configuration, deposit detection, withdrawal processing, and UI display guidance. Add Arc as a supported blockchain for buy and sell orders in your on/off-ramp service. Arc shares its EVM foundations with networks you already support, but its native USDC model and deterministic finality simplify your integration and improve the end-user experience. ## Prerequisites Before you begin, ensure that you've: * Gained familiarity with EVM-based blockchain integrations. * Reviewed the [EVM differences](/integrate/evm-differences) page for Arc-specific protocol behavior. * Obtained access to Arc RPC endpoints for deposit monitoring and transaction broadcasting. * Set up infrastructure for deposit detection and withdrawal processing on EVM blockchains. * Installed the [`viem`](https://viem.sh/) TypeScript library (used in the address validation example). ## Steps ### Configure network parameters Register Arc with the following parameters in your platform's chain registry: | Parameter | Value | | :----------------------- | :------------------------------------------- | | Network name | `Arc` (testnet: `Arc Testnet`) | | Chain ID | `5042002` (testnet) | | RPC (HTTPS) | `https://rpc.testnet.arc.io` | | RPC (WebSocket) | `wss://rpc.testnet.arc.io` | | Block explorer | `https://testnet.arcscan.app` | | Native currency symbol | `USDC` | | Native currency decimals | `6` | | USDC ERC-20 contract | `0x3600000000000000000000000000000000000000` | | CCTP domain | `26` | | Confirmations required | `1` | Arc uses USDC as both the native gas token and the primary transfer asset. Configure your platform with a single asset entry for USDC on Arc. ### Detect deposits for buy orders When a user completes a fiat purchase and you need to detect the resulting onchain USDC deposit, use the same `Transfer` event monitoring pattern used by exchanges. Every native USDC movement emits a standard ERC-20 `Transfer(address,address,uint256)` event from the system emitter `0xffff…fffe`. Arc implements [EIP-7708](https://eips.ethereum.org/EIPS/eip-7708), an Ethereum Improvement Proposal that unifies native value movements and ERC-20 token transfers into a single standard `Transfer` event. This covers native sends and the native leg of ERC-20 transfers. Key points for ramp deposit detection: * Subscribe to blocks using WebSocket (`eth_subscribe("newHeads")`) or poll using HTTP. * Filter `Transfer` events on the native USDC system emitter (`0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE`). Do not filter on the ERC-20 contract (`0x3600…0000`), which misses plain native sends. * Credit immediately after 1 confirmation. Arc's deterministic finality guarantees no reorgs. * The `value` field uses 18 decimals (native). Divide by 10^12 for 6-decimal USDC. For complete implementation details including code samples, address generation, and sweep logic, see [Detect and process deposits](/integrate/exchanges/deposits). ### Process withdrawals for sell orders When a user sells crypto for fiat and you need to send USDC to a settlement address or back to the user, the withdrawal flow is identical to exchange withdrawals: 1. Validate the destination address (EIP-55 checksum). 2. Check the blocklist before sending. 3. Build and sign a native USDC transfer (the recommended path, cheaper in gas and universally receivable). 4. Broadcast and confirm (single confirmation = final). You pay gas fees in USDC from the same balance. For complete implementation details including transaction construction and gas estimation, see [Process withdrawals](/integrate/exchanges/withdrawals). ### Display Arc in your UI Arc's native USDC model affects how you present the network to end users: | UI element | Recommendation | | :---------------- | :------------------------------------------------------------------------------- | | Network name | Display as **Arc** (or **Arc Testnet** for test environments). | | Asset shown | USDC only. Do not display a separate gas token. | | Gas fee display | Show estimated fees in USDC (typically fractions of a cent). | | Confirmation time | Display "Instant" or "\< 1 second." No countdown or block wait indicator needed. | | Network icon | Use the Arc brand assets (contact the Arc team for logo files). | You can skip the "ensure you have ETH for gas" warning that other EVM blockchains require. Users receive USDC and can immediately transfer it without acquiring a second asset. ### Configure settlement timing Arc provides sub-second deterministic finality. Once a transaction is included in a block, it is final. There are no reorgs, no probabilistic confirmation windows, and no need to wait for additional blocks. For your ramp platform, this means: * **Buy orders**: Credit the user's crypto balance immediately upon 1 confirmation. * **Sell orders**: Mark the withdrawal as complete after the transaction receipt is returned. * **Settlement display**: Show instant confirmation to the user rather than a progress bar or block countdown. ### Validate addresses Arc uses standard Ethereum addresses: * 20 bytes, `0x`-prefixed (42 characters total). * EIP-55 mixed-case checksum encoding. * Compatible with existing Ethereum address validation logic in your platform. No additional address format validation is required beyond what you already implement for EVM blockchains. ```typescript theme={null} import { getAddress, isAddress } from "viem"; function validateArcAddress(address: string): string { if (!isAddress(address)) { throw new Error(`Invalid Arc address: ${address}`); } return getAddress(address); // Returns EIP-55 checksummed } ``` ### Screen for compliance Arc enforces a USDC blocklist at multiple levels: * **Pre-mempool**: Transactions from or to blocklisted addresses are rejected before entering the mempool. * **Runtime**: Transfers involving blocklisted addresses revert during execution. Your platform should check addresses against the onchain blocklist before initiating withdrawals. If a destination is blocklisted, the transaction reverts and gas is consumed without transferring funds. For implementation details on blocklist monitoring and event subscription, see [Monitor blocklist compliance](/integrate/infrastructure/compliance). Always verify destination addresses against the blocklist before broadcasting withdrawal transactions. Sending to a blocklisted address wastes gas fees and creates a failed transaction that you must handle in your reconciliation flow. ### Route USDC crosschain If your platform supports crosschain transfers (for example, a user buys USDC on Ethereum and wants delivery on Arc), use Circle's Cross-Chain Transfer Protocol (CCTP), a permissionless onchain utility that enables USDC to move natively between supported blockchains. Arc's CCTP domain is `26`. For bridging implementation details, see [Bridge USDC with CCTP](/integrate/exchanges/cctp-bridging). # Relayers and paymasters Source: https://docs.arc.io/integrate/relayers-and-paymasters Submit gasless transfers and sponsor UserOperations on Arc using USDC for gas. Arc uses USDC as its native gas token. For relayers and paymasters, this means your gas infrastructure runs entirely on USDC. Fund your signing account or paymaster contract with USDC and Arc handles the rest. ## Choose your path | Path | What it is | When to use it | | :--------------------- | :------------------------------------------------------------------------------------------------------------------------------------ | :--------------------------------------------------------------------- | | **EIP-3009 relayer** | An EOA that collects a user's `transferWithAuthorization` signature and submits the transaction, paying gas from its own USDC balance | Users hold USDC and want gasless transfers without account abstraction | | **ERC-4337 paymaster** | A contract that co-signs UserOperations and pays gas from its USDC balance, deposited into the EntryPoint | You need full account abstraction or are running a bundler service | ## What you need Before you start, confirm you have the following: | Requirement | Details | | :---------------------------------- | :----------------------------------------------------------------------------------------------------------------------------- | | RPC access | `https://rpc.testnet.arc.io` | | Chain ID | `5042002` (testnet) | | USDC ERC-20 contract | [`0x3600000000000000000000000000000000000000`](https://testnet.arcscan.app/address/0x3600000000000000000000000000000000000000) | | EntryPoint contract (ERC-4337 only) | EntryPoint v0.7 | ## Arc behaviors relevant to this integration The following Arc behaviors can affect relayer and paymaster operations. Each is covered in [EVM differences](/arc/references/evm-differences): * **`PREVRANDAO` always returns `0`**: If any contract in your stack uses `PREVRANDAO` for ordering or selection logic, it always reads `0` on Arc. Use an oracle or VRF instead. See [Execution and opcode differences](/arc/references/evm-differences#execution-and-opcode-differences). * **Blob transactions (EIP-4844 type-3) are rejected**: Arc does not support type-3 transactions. Do not relay or route them. See [Execution and opcode differences](/arc/references/evm-differences#execution-and-opcode-differences). * **The next block's base fee is published in `extra_data`**: If you build custom gas-estimation tooling, read the next base fee from the parent header's `extra_data` field (8-byte big-endian) rather than re-deriving it. See [Fee market and block behavior](/arc/references/evm-differences#fee-market-and-block-behavior). * **Relay fees use 18-decimal precision**: Native USDC (the gas token) has 18 decimals. Relay fees and paymaster charges are denominated in native USDC, not ERC-20 USDC (6 decimals). * **EIP-7702 set-code transactions work on Arc**: Arc's Osaka baseline includes [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702). EOAs can delegate execution to a contract by signing a type-4 authorization. Paymasters may receive UserOperations from EIP-7702-delegated accounts; the behavior is the same as for smart contract accounts. ## Integration guides Submit gasless USDC transfers using a user's `transferWithAuthorization` signature. Deploy and operate an ERC-4337 paymaster contract on Arc. # How-to: Deploy an ERC-4337 paymaster on Arc Source: https://docs.arc.io/integrate/relayers-and-paymasters/deploy-a-paymaster Deploy and fund an ERC-4337 paymaster on Arc. Arc's native gas token is USDC — fund your paymaster's EntryPoint deposit in native USDC (18-decimal units). Deploying an ERC-4337 paymaster on Arc follows the standard ERC-4337 flow with one key difference: Arc uses USDC as its native gas token. Your paymaster's EntryPoint balance is denominated in native USDC (18-decimal units), not the 6-decimal ERC-20 USDC units used in token transfers. A 10 USDC deposit requires `10 * 10^18`, not `10 * 10^6`. ## Prerequisites Before you begin, ensure that you've: * Familiarized yourself with ERC-4337 account abstraction and EntryPoint v0.7 * Configured Hardhat or Foundry for Arc testnet (chain ID `5042002`, RPC `https://rpc.testnet.arc.io`) * Configured Pimlico as your bundler for Arc testnet (chain ID `5042002`) * Prepared a paymaster contract based on the ERC-4337 reference implementation ## Steps ### Step 1. Fund the paymaster The paymaster's EntryPoint deposit is USDC. After deployment, deposit USDC into the EntryPoint by calling `depositTo`. Specify the amount in wei (18-decimal native USDC units). ```typescript theme={null} import { ethers } from "ethers"; // Verify the EntryPoint v0.7 address for Arc testnet before use const ENTRY_POINT_ADDRESS = "0x0000000071727De22E5E9d8BAf0edAc6f37da032"; const PAYMASTER_ADDRESS = ""; const ENTRY_POINT_ABI: string[] = [ "function depositTo(address account) external payable", "function balanceOf(address account) external view returns (uint256)", ]; const provider = new ethers.JsonRpcProvider("https://rpc.testnet.arc.io"); // Open .env in your editor and add your deployer private key: // DEPLOYER_PRIVATE_KEY=your-private-key // Keep this key secure. It controls the paymaster's funding account. const wallet = new ethers.Wallet(process.env.DEPLOYER_PRIVATE_KEY!, provider); const entryPoint = new ethers.Contract( ENTRY_POINT_ADDRESS, ENTRY_POINT_ABI, wallet, ); // Deposit 10 USDC: specify in wei (18-decimal native USDC units) const depositAmount: bigint = 10n * 10n ** 18n; const tx = await entryPoint.depositTo(PAYMASTER_ADDRESS, { value: depositAmount, }); await tx.wait(); // Check the current deposit balance // balanceOf returns 18-decimal native USDC units, not 6-decimal ERC-20 units const balance: bigint = await entryPoint.balanceOf(PAYMASTER_ADDRESS); const balanceUsdc: bigint = balance / 10n ** 18n; console.log("Paymaster deposit balance (native wei):", balance.toString()); console.log("Paymaster deposit balance (USDC):", balanceUsdc.toString()); ``` ### Step 2. Verify the deployment Confirm the following before sending live UserOperations: | Check | Command or call | | :----------------------------- | :------------------------------------------------------------------------------------ | | Contract has bytecode | `cast code
--rpc-url https://rpc.testnet.arc.io` returns non-empty bytecode | | EntryPoint deposit is funded | `entryPoint.balanceOf(paymasterAddress)` returns a non-zero balance | | Bundler accepts UserOperations | Submit a test UserOperation through Pimlico and confirm it processes | Arc's minimum base fee is 20 Gwei. If you operate your own bundler, ensure it sets `maxFeePerGas` to at least 20 Gwei when submitting bundle transactions. Pimlico configured for Arc handles this automatically. # How-to: Operate an EIP-3009 relayer on Arc Source: https://docs.arc.io/integrate/relayers-and-paymasters/eip-3009-relayer Submit gasless USDC transfers on Arc by collecting a user's transferWithAuthorization signature and broadcasting the transaction from a funded relayer EOA. An EIP-3009 (`transferWithAuthorization`) relayer is an Externally Owned Account (EOA) that collects a user's signed authorization and submits the USDC transfer to Arc. The relayer pays gas from its own USDC balance. Users never need a gas balance. ## Prerequisites Before you begin, ensure that you've: * Funded a relayer EOA with USDC on Arc testnet (USDC is the gas supply) * Obtained Arc testnet RPC access at `https://rpc.testnet.arc.io` (chain ID `5042002`) * Installed ethers.js v6 (`npm install ethers`) ## Steps ### Step 1. Collect the user's `transferWithAuthorization` signature The user signs an EIP-712 typed data message offchain. Your application builds the message and asks the user's wallet to sign it. The relayer submits the signed data; the user never sends a transaction. The USDC contract on Arc testnet uses the following EIP-712 domain: | Field | Value | | :------------------ | :------------------------------------------- | | `name` | `USDC` | | `version` | `2` | | `chainId` | `5042002` | | `verifyingContract` | `0x3600000000000000000000000000000000000000` | The `nonce` field is a random `bytes32` value generated per authorization, not the user's account nonce. It prevents replay attacks. ```typescript theme={null} import { ethers } from "ethers"; const userAddress: string = "0x..."; // the user's address (the authorization signer) const recipientAddress: string = "0x..."; const transferAmount: bigint = 10_000_000n; // 10 USDC in 6-decimal units const USDC_ADDRESS = "0x3600000000000000000000000000000000000000"; const CHAIN_ID = 5042002; const domain: ethers.TypedDataDomain = { name: "USDC", version: "2", chainId: CHAIN_ID, verifyingContract: USDC_ADDRESS, }; const types = { TransferWithAuthorization: [ { name: "from", type: "address" }, { name: "to", type: "address" }, { name: "value", type: "uint256" }, { name: "validAfter", type: "uint256" }, { name: "validBefore", type: "uint256" }, { name: "nonce", type: "bytes32" }, ], }; // Generate a random bytes32 nonce for replay protection const nonce: string = ethers.hexlify(ethers.randomBytes(32)); const validAfter: bigint = 0n; const validBefore: bigint = BigInt(Math.floor(Date.now() / 1000) + 3600); // 1 hour const message = { from: userAddress, to: recipientAddress, value: transferAmount, // amount in USDC token units (6 decimals) validAfter, validBefore, nonce, }; // userSigner is an ethers.js v6 Signer connected to the user's wallet const signature: string = await userSigner.signTypedData( domain, types, message, ); const sig: ethers.Signature = ethers.Signature.from(signature); // Pass sig.v, sig.r, and sig.s to the relayer along with the message fields ``` ### Step 2. Construct and submit the relay transaction The relayer calls `transferWithAuthorization` on the USDC contract with the signed parameters. The relayer EOA pays gas from its USDC balance. ```typescript theme={null} import { ethers } from "ethers"; const provider = new ethers.JsonRpcProvider("https://rpc.testnet.arc.io"); // Open .env in your editor and add your relayer private key: // RELAYER_PRIVATE_KEY=your-private-key // Keep this key secure. It controls the USDC gas supply for your relayer. const relayerWallet = new ethers.Wallet( process.env.RELAYER_PRIVATE_KEY!, provider, ); const USDC_ABI = [ "function transferWithAuthorization(" + "address from, address to, uint256 value, " + "uint256 validAfter, uint256 validBefore, " + "bytes32 nonce, uint8 v, bytes32 r, bytes32 s" + ") external", ]; const usdc = new ethers.Contract( "0x3600000000000000000000000000000000000000", USDC_ABI, relayerWallet, ); const tx = await usdc.transferWithAuthorization( message.from, message.to, message.value, message.validAfter, message.validBefore, message.nonce, sig.v, sig.r, sig.s, ); const receipt = await tx.wait(); ``` ### Step 3. Set gas parameters Set gas parameters on each relay transaction. The minimum `maxFeePerGas` on Arc is 20 Gwei. Transactions priced under this minimum may remain pending. Set `maxPriorityFeePerGas` to 0 Gwei, or 1 Gwei when load is high. A typical `transferWithAuthorization` uses about 65,000 gas units. Estimate gas and read the current fee data before broadcasting: ```typescript theme={null} const FLOOR_FEE: bigint = 20_000_000_000n; // 20 Gwei const feeData = await provider.getFeeData(); const maxFeePerGas: bigint = feeData.maxFeePerGas !== null && feeData.maxFeePerGas > FLOOR_FEE ? feeData.maxFeePerGas : FLOOR_FEE; const gasEstimate: bigint = await usdc.transferWithAuthorization.estimateGas( message.from, message.to, message.value, message.validAfter, message.validBefore, message.nonce, sig.v, sig.r, sig.s, ); const tx = await usdc.transferWithAuthorization( message.from, message.to, message.value, message.validAfter, message.validBefore, message.nonce, sig.v, sig.r, sig.s, { gasLimit: gasEstimate, maxFeePerGas, maxPriorityFeePerGas: 0n, // 0 Gwei; increase to 1 Gwei when load is high }, ); const receipt = await tx.wait(); ``` ### Step 4. Record the gas cost for billing To record how much USDC the relay cost for billing, convert the receipt values: ```typescript theme={null} const gasCostWei: bigint = receipt.gasUsed * receipt.effectiveGasPrice; const gasCostUsdc: bigint = gasCostWei / 10n ** 12n; // in ERC-20 USDC units (6 decimals) ``` `gasCostUsdc` is the gas fee in USDC's 6-decimal token units, suitable for logging or charging users for the relay service. ## Operational notes * Maintain a minimum USDC balance in your relayer EOA. Your USDC balance is your gas supply. There is no separate gas wallet to manage. * Monitor pending transactions and track nonces to handle resubmissions at higher gas prices. * The relayer EOA's USDC balance covers gas fees only. The transfer amount is debited from the authorized user's account (the `from` address in the signed message). Size your minimum balance to cover expected gas across your transaction volume. * Transactions involving a blocklisted `message.from` or `message.to` address revert at runtime, consuming the relayer's gas with no transfer. Check both addresses before submitting, or handle reverts in your monitoring layer. # Wallet integration Source: https://docs.arc.io/integrate/wallets Add Arc as a supported network in your wallet application. Wallets on Arc use standard EVM tools: `secp256k1` signing, EIP-1559 fees, and standard RPC methods. Arc's USDC-native design requires changes to balance display, fee labels, transaction history, and approval handling. ## Key differences for wallets Three Arc design decisions affect every wallet integration: | Concern | Arc behavior | Impact on wallets | | :--------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **One balance, two decimal views** | Native USDC uses 18 decimals (`eth_getBalance`, `address.balance`); the ERC-20 interface uses 6 decimals (`balanceOf`). Both views draw from the same underlying balance. | Scale `eth_getBalance` output using 18 decimals, not 6. Don't display native USDC and ERC-20 USDC as separate balance rows: they're the same balance. Show fees in USDC, never ETH or Gwei. | | **Deterministic finality** | Transactions finalize in under one second with no risk of reorganization. | One confirmation is final. Remove "X of N confirmations" progress bars and "Confirming" spinners. | | **Allowances do not gate native access** | ERC-20 `approve` and `allowance` control only the `transferFrom` path. All other transfer methods bypass allowance checks. | An ERC-20 allowance is not a cap on total USDC spending. For smart contract accounts, any module with execution rights can also transfer native USDC regardless of allowance state. | ## Arc behaviors relevant to this integration The following Arc behaviors can affect wallet operations. See the integration guides on this page and the [EVM differences](/arc/references/evm-differences) page for details: * **Blocklist enforcement:** The USDC blocklist is enforced at runtime. If either the sender or recipient of a `transfer` or `transferFrom` call is blocklisted, the call reverts. Check `receipt.status === 0` to detect reverts. For details, see [Transaction lifecycle](/integrate/wallets/transaction-lifecycle). * **Dust amounts:** `balanceOf()` returns 0 for amounts smaller than 1×10⁻⁶ USDC (the 6-decimal minimum). The native balance can hold amounts smaller than that threshold (dust). Dust is still spendable as gas. A `balanceOf()` of 0 does not mean the address has no USDC. * **Transaction history requires system emitter events:** Plain native USDC sends emit no log at the ERC-20 contract address (`0x3600…0000`). Filtering that address only captures ERC-20 transfers and misses native sends. Filter the system emitter (`0xffff…fffe`) instead. See [Add Arc to a wallet](/integrate/wallets/add-arc-to-a-wallet#step-3-index-transaction-history). * **Don't count `Transfer` logs from both the system emitter and the ERC-20 contract:** An ERC-20 `transfer()` emits from both. Counting both records the same transfer twice in your transaction history. Use the system emitter only. * **Fee floor:** Arc enforces a 20 Gwei minimum `maxFeePerGas`. Transactions under this floor are silently dropped by the mempool with no error receipt. See [Display transaction fees](/integrate/wallets/fee-display). ## What you need Before you start, confirm you have the following: | Requirement | Details | | :------------------- | :----------------------------------------------------------------------------------------------------------------------------- | | RPC access | `https://rpc.testnet.arc.io` | | Chain ID | `5042002` (testnet) | | USDC ERC-20 contract | [`0x3600000000000000000000000000000000000000`](https://testnet.arcscan.app/address/0x3600000000000000000000000000000000000000) | Test against Arc Testnet only. Local EVM simulators run a standard EVM. They don't replicate EIP-7708 Transfer events, the USDC precompile, or blocklist enforcement. ## Integration guides Chain configuration, balance display, transaction history, and fee handling for wallet providers. Arc uses two states: pending or final. There are no intermediate confirmation states. Fetch, estimate, and display transaction fees in USDC. # How-to: Add Arc to a wallet Source: https://docs.arc.io/integrate/wallets/add-arc-to-a-wallet Integrate Arc as a supported network in your wallet, including chain configuration, balance display, transaction history, and fee handling. Arc is an EVM-compatible blockchain that uses USDC as its native gas token. Signing uses `secp256k1`, identical to Ethereum; no new cryptographic code is required. USDC exists as a single balance with two interfaces (native and ERC-20), so display one unified balance row. Transactions finalize in under one second with no reorgs. ## Prerequisites Before you begin, ensure that you've: * Familiarized yourself with EIP-3085 (`wallet_addEthereumChain`) for adding custom networks * Obtained access to the Arc Testnet RPC endpoint * Understood ERC-20 event indexing ## Step 1. Configure the network Add Arc using the following [EIP-3085](https://eips.ethereum.org/EIPS/eip-3085) parameters. | Parameter | Value | | :------------------ | :----------------------------------------------- | | `chainId` | `0x4CEF52` (5042002) | | `chainName` | Arc Testnet | | `nativeCurrency` | `{ name: "USDC", symbol: "USDC", decimals: 18 }` | | `rpcUrls` | `["https://rpc.testnet.arc.io"]` | | `blockExplorerUrls` | `["https://testnet.arcscan.app"]` | Set `nativeCurrency.decimals` to `18` to match Arc's native precision. The `name` and `symbol` fields drive the "USDC" label in wallet UI. Display scaling to 6 decimals happens in your formatting layer (Step 2), not through this field. ```typescript theme={null} const arcTestnet = { chainId: "0x4CEF52", chainName: "Arc Testnet", nativeCurrency: { name: "USDC", symbol: "USDC", decimals: 18, }, rpcUrls: ["https://rpc.testnet.arc.io"], blockExplorerUrls: ["https://testnet.arcscan.app"], }; ``` ## Step 2. Display the balance Arc's native balance uses 18 decimals internally (like ETH on Ethereum), but represents USDC which has 6 display decimals. Convert accordingly. ### 2.1. Fetch the balance Call `eth_getBalance` to retrieve the user's USDC balance in 18-decimal wei: ```typescript theme={null} const balanceWei = await provider.getBalance(address); // 18-decimal BigInt ``` ### 2.2. Convert to display value Divide by 10^12 to convert from 18-decimal native wei to 6-decimal USDC: ```typescript theme={null} const DECIMALS_OFFSET = 12n; const displayAmount = balanceWei / 10n ** DECIMALS_OFFSET; // 6-decimal value const formatted = (Number(displayAmount) / 1e6).toFixed(6); // e.g. "1.500000" ``` ### 2.3. Show a single row USDC on Arc is a single asset with two interfaces (native and ERC-20). Both share the same underlying balance. Display one "USDC" row in the asset list, not separate "native" and "ERC-20" entries. ### 2.4. Handle token import If a user manually imports the linked USDC ERC-20 contract (`0x3600000000000000000000000000000000000000`), map it to the USDC asset they already hold. Do not create a second entry. Consider surfacing a message such as "This contract represents USDC on Arc (already in your wallet)" so users understand they have not added a new token. For the full list of deployed contracts, see [Contract addresses](/arc/references/contract-addresses). `balanceOf()` returns 0 for amounts smaller than 1×10⁻⁶ USDC (the 6-decimal minimum). The native balance can hold amounts smaller than that threshold (dust) that are still spendable as gas. A `balanceOf()` result of 0 does not prove the address holds no USDC. ## Step 3. Index transaction history Arc emits a standard ERC-20 `Transfer` log from the system address `0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE` for every explicit USDC transfer (Arc's [EIP-7708](https://eips.ethereum.org/EIPS/eip-7708) implementation). This stream covers native sends, ERC-20 transfers, mints, and burns. Gas deductions don't emit events. The native balance is the source of truth. ### 3.1. Identify the Transfer event ```solidity theme={null} event Transfer(address indexed from, address indexed to, uint256 value); ``` **Topic 0:** `0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef` **Emitter:** `0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE` (native USDC system emitter, 18 decimals) ### 3.2. Subscribe to transfers Use `eth_subscribe` or poll `eth_getLogs` filtered by the topic and the user's address. Scope the block range to avoid scanning from genesis; use the account creation block as `startBlock` or paginate in fixed-size chunks: ```typescript theme={null} const NATIVE_USDC_EMITTER = "0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE"; const TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"; // Filter for transfers involving the user (as sender or receiver) const paddedAddress = "0x" + address.slice(2).padStart(64, "0"); const logs = await provider.getLogs({ address: NATIVE_USDC_EMITTER, topics: [TRANSFER_TOPIC, [paddedAddress, null], [null, paddedAddress]], fromBlock: startBlock, // account creation block; paginate in chunks for large ranges toBlock: "latest", }); ``` ### 3.3. Parse the value The `value` field from the system emitter uses 18 decimals (the native balance). Convert to 6-decimal USDC for display: ```typescript theme={null} import { parseAbi, decodeEventLog } from "viem"; const abi = parseAbi([ "event Transfer(address indexed from, address indexed to, uint256 value)", ]); for (const log of logs) { const { args } = decodeEventLog({ abi, data: log.data, topics: log.topics }); const amount = Number(args.value) / 1e18; // 18-decimal native → human-readable USDC } ``` Don't filter the ERC-20 contract (`0x3600…0000`) for transaction history. Native USDC sends emit no log at that address. Filtering only the ERC-20 contract will miss them. Filter the system emitter (`0xffff…fffe`) instead. It covers all explicit USDC transfers in one stream: native sends, ERC-20 transfers, mints, and burns. Gas deductions don't emit events. See [USDC system events](/arc/references/usdc-system-events). ## Step 4. Handle fees Arc uses USDC as its native gas token with an EIP-1559 fee model and a smoothed base fee. For the full pricing model, see [Gas and fees](/arc/references/gas-and-fees). ### 4.1. Estimate gas cost The examples in this step use the ethers v6 `provider` API. ```typescript theme={null} const { maxFeePerGas } = await provider.getFeeData(); // Returns USDC wei (18 decimals) const gasLimit = await provider.estimateGas(tx); const feeWei = maxFeePerGas * gasLimit; // worst-case fee in 18-decimal USDC wei const feeUsdc = Number(feeWei) / 1e18; // Human-readable USDC ``` Set `maxFeePerGas` to at least 20 Gwei. Arc enforces a hard floor: transactions with `maxFeePerGas` lower than 20 Gwei are rejected with no error receipt. Ethereum and L2 defaults are often under this threshold, so clamp before submitting. ```typescript theme={null} const MIN_FEE = 20_000_000_000n; // 20 Gwei in USDC wei const { maxFeePerGas: fetched } = await provider.getFeeData(); const maxFeePerGas = fetched !== null && fetched > MIN_FEE ? fetched : MIN_FEE; ``` ### 4.2. Apply UI guidance | Element | Display | | :----------------- | :----------------------------------- | | Fee label | "Network fee" or "Gas fee" | | Fee denomination | USDC (for example, "0.000042 USDC") | | Currency symbol | Do not show "ETH" or "Gwei" to users | | Insufficient funds | "Insufficient USDC for gas" | If your wallet warns "insufficient ETH for gas," update that message to reference USDC instead. ## Step 5. Send transactions Transaction signing on Arc is identical to Ethereum. Use `secp256k1` ECDSA signatures with EIP-155 replay protection. Send USDC with a standard ERC-20 `transfer()`, the same flow as any ERC-20 token, with no choice of transfer method exposed to the user. ### 5.1. Handle precision and dust The ERC-20 interface uses 6 decimals, so an ERC-20 `transfer()` cannot move "dust" (amounts smaller than 1×10⁻⁶ USDC held in the 18-decimal native balance). Unlike other EVM chains where gas is a separate asset, gas on Arc also draws from the USDC balance. Reserve gas headroom when computing a max-send amount. The native balance can still hold dust, and dust can be spent as gas. ### 5.2. Handle onchain app interactions Treat USDC as a standard ERC-20 when users interact with onchain applications: approvals, `transferFrom`, swaps, and liquidity provision work without special handling. Because USDC is also the native asset, support contract calls that are `payable`: an app may require USDC sent as `msg.value` (a native value transfer) alongside calldata, rather than an ERC-20 `transfer`. Pass the value through as the contract expects; no special UX is required. ERC-20 `approve` and `allowance` govern only the `transferFrom` path. An ERC-20 allowance is not a cap on total USDC spending: the same balance can also leave as native value (`msg.value`). For smart contract accounts (embedded wallets, smart wallets, and session-key systems), do not rely on allowance state as a safety guarantee. Any module with execution rights can also transfer native USDC regardless of allowance state. Contract calls that trigger a transfer to or from a blocklisted address revert at runtime. The transaction is included in a block and gas is consumed, but state changes are rolled back. See [Transaction lifecycle](/integrate/wallets/transaction-lifecycle) for the full set of edge cases and [EVM compatibility](/arc/references/evm-differences) for other Arc-specific behaviors. ### 5.3. Handle transaction confirmations Arc provides [deterministic finality](/arc/concepts/deterministic-finality). A transaction is either pending (in the mempool) or final (included in a block). There are no intermediate confirmation states and no reorgs. | State | Meaning | | :---------- | :------------------------------------------- | | **Pending** | Transaction is in the mempool, not yet mined | | **Final** | Included in a block; irreversible | Once a transaction receipt is returned, you can immediately update the UI. No additional confirmations are needed. ### 5.4. Support account abstraction Arc supports ERC-4337 account abstraction for smart contract wallets. If your wallet supports AA flows (bundlers, paymasters, session keys), these work on Arc without modification. See [Account abstraction providers](/arc/tools/account-abstraction) for compatible infrastructure including Biconomy, Pimlico, ZeroDev, and Circle Wallets. ## Step 6. Verify your integration Use this checklist to confirm your wallet integration is complete: * [ ] Chain ID `5042002` added with correct RPC and explorer URLs * [ ] Native currency displays as "USDC" with 6 display decimals * [ ] `eth_getBalance` result converted from 18-decimal to 6-decimal for display * [ ] Single USDC balance row shown (no separate native/ERC-20 entries) * [ ] Imported USDC ERC-20 contract maps to the existing USDC asset (no duplicate entry) * [ ] No ETH references in UI labels, error messages, or fee displays * [ ] Transaction history uses the native `Transfer` event from the system emitter `0xffff...fffe` (18 decimals), not the ERC-20 contract * [ ] Gas fees displayed in USDC * [ ] Send amounts validated to 6 decimals; `payable` contract calls (USDC as `msg.value`) supported * [ ] One confirmation treated as final (no "confirming" spinner) * [ ] ERC-4337 AA flows work if your wallet supports smart accounts * [ ] Reverted transactions (`receipt.status === 0`) shown as "Failed" * [ ] Gas fee on a reverted transaction treated as consumed, not refunded * [ ] Retry after a revert uses a new nonce * [ ] `maxFeePerGas` set to at least 20 Gwei before submitting transactions # How to: Display Transaction Fees Source: https://docs.arc.io/integrate/wallets/fee-display Fetch, calculate, and display transaction fees as USD amounts in your wallet UI. Arc uses USDC as its native gas token, so fees are inherently denominated in USD. The `eth_gasPrice` and `eth_feeHistory` RPCs return values in USDC wei (18 decimals). The EWMA fee smoothing model keeps fees predictable—dramatic spikes are unlikely. Display fees as `$X.XX` (or `~$0.01`), never as "Gwei" or "ETH." ## Prerequisites Before you begin: * You have an RPC endpoint for Arc. * You are using an EIP-1559-compatible library (viem, ethers.js, or equivalent). * You understand that Arc's native currency is USDC. ## Steps ### Step 1. Fetch the current gas price Use `eth_gasPrice` for a single value or `eth_feeHistory` for historical base fees and priority fee percentiles. ```typescript theme={null} import { createPublicClient, http, formatUnits } from "viem"; import { arc } from "viem/chains"; const client = createPublicClient({ chain: arc, transport: http("https://rpc.arc.circle.com"), }); // Simple: current gas price in USDC wei const gasPrice = await client.getGasPrice(); console.log("Gas price (wei):", gasPrice); // Detailed: recent base fee history const feeHistory = await client.getFeeHistory({ blockCount: 4, rewardPercentiles: [25, 50, 75], }); console.log("Latest base fee (wei):", feeHistory.baseFeePerGas.at(-1)); ``` ### Step 2. Estimate gas for the transaction Use `eth_estimateGas` to determine how much gas your transaction requires. ```typescript theme={null} // Native USDC transfer: ~21,000 gas const simpleTransferGas = 21_000n; // ERC-20 token transfer or contract call: use estimateGas const contractGas = await client.estimateGas({ account: "0xYourAddress", to: "0xContractAddress", data: encodedCalldata, }); ``` Typical gas costs: | Transaction type | Approximate gas | | -------------------- | ---------------------- | | Native USDC send | 21,000 | | ERC-20 transfer | \~65,000 | | Contract interaction | Varies—always estimate | ### Step 3. Build EIP-1559 fee parameters Arc supports EIP-1559 transactions. Set `maxFeePerGas` and `maxPriorityFeePerGas`: ```typescript theme={null} const latestBlock = await client.getBlock(); const baseFee = latestBlock.baseFeePerGas!; // 2x base fee is generous—Arc's EWMA smoothing keeps fees stable const maxFeePerGas = baseFee * 2n; // Priority fee of 0 is acceptable on Arc (validators don't require tips) const maxPriorityFeePerGas = 0n; ``` Set `maxFeePerGas` to at least 20 Gwei. Arc enforces a hard floor: transactions with `maxFeePerGas` lower than 20 Gwei are rejected with no error receipt. The EWMA smoothing model keeps fees gradual and predictable. ### Step 4. Calculate the maximum fee in USD Multiply the gas limit by `maxFeePerGas`, then convert from 18-decimal USDC wei to a dollar amount. ```typescript theme={null} function calculateMaxFeeUsd(gasLimit: bigint, maxFeePerGas: bigint): string { const maxCostWei = gasLimit * maxFeePerGas; // USDC has 18 decimals as the native gas token on Arc // Since 1 USDC = $1, the numeric value IS the USD cost const usdCost = formatUnits(maxCostWei, 18); return usdCost; } // Example: simple transfer const maxFee = calculateMaxFeeUsd(21_000n, maxFeePerGas); console.log(`Max fee: $${Number(maxFee).toFixed(6)}`); ``` ### Step 5. Format fees for display Show fees as a dollar amount. Use `~` to indicate the value is an estimate. ```typescript theme={null} function formatFeeDisplay(gasLimit: bigint, maxFeePerGas: bigint): string { const maxCostWei = gasLimit * maxFeePerGas; const usdValue = Number(formatUnits(maxCostWei, 18)); if (usdValue < 0.01) { return "< $0.01"; } return `~$${usdValue.toFixed(2)}`; } ``` Standard libraries like ethers.js and viem label the native currency as "ETH" by default. You must override this in your UI. Displaying "0.00042 ETH" instead of "\~\$0.01" confuses users and misrepresents the cost. ## Worked examples ### Simple USDC transfer ```typescript theme={null} // Given: base fee = 20 Gwei (minimum), gas limit = 21,000 const baseFee = 20_000_000_000n; // 20 Gwei in wei const gasLimit = 21_000n; const maxFeePerGas = baseFee * 2n; // 40 Gwei const maxCostWei = gasLimit * maxFeePerGas; // 21,000 * 40,000,000,000 = 840,000,000,000,000 wei // = 0.00000084 USDC = $0.00000084 const display = formatFeeDisplay(gasLimit, maxFeePerGas); // Output: "< $0.01" ``` ### Contract interaction ```typescript theme={null} // Given: base fee = 500,000 Gwei (0.0005 USDC), gas limit = 65,000 const baseFee = 500_000_000_000_000n; // 500,000 Gwei in wei const gasLimit = 65_000n; const maxFeePerGas = baseFee * 2n; // 1,000,000 Gwei const maxCostWei = gasLimit * maxFeePerGas; // 65,000 * 1,000,000,000,000,000 = 65,000,000,000,000,000,000 wei // = 0.065 USDC = $0.065 const display = formatFeeDisplay(gasLimit, maxFeePerGas); // Output: "~$0.07" ``` Users are only charged `gasUsed * effectiveGasPrice`. The difference between `maxFeePerGas` and the actual effective gas price is refunded. It's safe to show the maximum estimate in your UI with language like "Max fee" or "Up to." ## See also * [Transaction lifecycle](/integrate/wallets/transaction-lifecycle) # Transaction Lifecycle Source: https://docs.arc.io/integrate/wallets/transaction-lifecycle Understand Arc's two-state transaction model, where transactions are either pending or final with no intermediate confirmation states. Arc uses a two-state transaction model. A transaction is either **pending** (in the mempool) or **final** (included in a committed block). There is no intermediate "confirming" state and no concept of accumulating confirmations. Once a block is committed by the Malachite consensus protocol, every transaction in that block is immediately and irreversibly final. This simplicity stems from Arc's BFT consensus. Validators pre-commit blocks with a two-thirds supermajority before production, which eliminates chain reorganizations, uncle blocks, and probabilistic finality entirely. ## Transaction states Arc transactions exist in exactly two states: | State | Location | Finality | Duration | | ------- | --------------- | ------------ | -------------------- | | Pending | Mempool | Not final | Sub-second (typical) | | Final | Committed block | Irreversible | Permanent | There is no "1 of 12 confirmations" counter. There is no safe-but-not-finalized window. A transaction transitions directly from pending to final. ## State diagram ```mermaid theme={null} stateDiagram-v2 [*] --> Pending: Submitted to mempool Pending --> Final: Included in committed block Pending --> Dropped: Nonce gap / underpriced [*] --> Rejected: Validation fails Final --> [*] Dropped --> [*] Rejected --> [*] ``` ## Comparison to Ethereum Ethereum uses probabilistic finality with multiple intermediate states. Arc eliminates all intermediate states through deterministic BFT consensus. | | Ethereum | Arc | | ------------------- | ------------------------------------- | ------------------------------ | | Pending state | In mempool, not yet included | In mempool, not yet included | | Confirming state | 1–64 slots (\~12 seconds to \~13 min) | None | | Finality | 64 slots (\~13 minutes) | Immediate upon block inclusion | | Reorganization risk | Possible before finalization | Impossible | | Block time | \~12 seconds | Sub-second | | Consensus | Gasper (LMD-GHOST + Casper FFG) | Malachite (Tendermint BFT) | ## Implications for wallet UX The two-state model simplifies wallet interfaces: * **No confirmation counter.** Remove any "X/N confirmations" progress bar or percentage indicator. * **No "confirming" spinner.** A transaction is either pending or done. * **Instant "Complete" status.** Show "Complete" or "Success" immediately when the transaction appears in a block. * **Simple state display.** Use two UI states: "Pending" while in the mempool, and "Complete" once included in a block. A wallet integration only needs to distinguish between a transaction hash that has no receipt (pending) and one that has a receipt with a block number (final). ## Edge cases ### Rejected transactions Some transactions are rejected immediately when submitted. The `eth_sendRawTransaction` call returns an error and the transaction never reaches the pending state. ### Runtime revert If a transaction violates the blocklist during execution, the transaction is included in a block but marked as failed. It consumes gas and appears onchain with a `status: 0` receipt. From a lifecycle perspective, it still reaches the final state—it is irreversibly included—but the state changes it attempted are reverted. To detect a runtime revert, check `receipt.status`: ```typescript theme={null} const receipt = await provider.getTransactionReceipt(txHash); if (receipt.status === 0) { // Transaction is final but failed: gas consumed, state rolled back } ``` Handle reverted transactions in your wallet UI: * **Show "Failed":** The transaction reached final state but didn't take effect. * **Treat consumed gas as non-refundable:** A blocklist revert charges only the gas used up to the point of the check. Remaining gas is refunded. * **Use a new nonce for any retry:** The reverted transaction's nonce is spent; a retry needs a new transaction. * **Show a plain error message:** "Transaction failed: the recipient or sender may be restricted" is more useful than a raw revert reason. ### High mempool load During periods of high network activity, transactions may remain in the pending state longer than usual. This does not affect finality guarantees. Once a transaction is included in a committed block, it is final regardless of how long it waited in the mempool. ### Dropped transactions Transactions can leave the mempool without reaching finality: * **Nonce gaps.** If a transaction has a nonce higher than expected, it waits for the gap to be filled. If the gap is never filled, the transaction remains pending indefinitely or is eventually evicted. * **Gas price lower than the minimum.** Transactions with `maxFeePerGas` lower than 20 Gwei are rejected with no error receipt and never appear in a block. * **Mempool eviction.** Under sustained high load, the lowest-priced transactions may be evicted to make room for higher-priced ones. Dropped transactions produce no onchain record. Wallets should implement timeout logic and allow users to retry or cancel (by submitting a replacement transaction with the same nonce).