# Agent Workflows import { Callout } from 'fumadocs-ui/components/callout'; import { Card, Cards } from 'fumadocs-ui/components/card'; import { Step, Steps } from 'fumadocs-ui/components/steps'; Walk through complete agent workflows — from the user's prompt to the final result. Each example shows every step the agent takes, the API calls it makes, and the data it works with. Example 1: Bridge USDC Across Chains [#example-1-bridge-usdc-across-chains] **User prompt:** ``` Bridge 100 USDC from Arbitrum to Base ``` Discover supported chains [#discover-supported-chains] The agent calls the chains endpoint to confirm Arbitrum and Base are supported and to get their chain IDs. ```bash GET https://app.across.to/api/swap/chains ``` The agent finds: * **Arbitrum** — chain ID `42161` * **Base** — chain ID `8453` Look up token addresses [#look-up-token-addresses] The agent fetches the token list for both chains to find the USDC contract addresses. ```bash GET https://app.across.to/api/swap/tokens?chainId=42161 GET https://app.across.to/api/swap/tokens?chainId=8453 ``` The agent finds: * USDC on Arbitrum: `0xaf88d065e77c8cC2239327C5EDb3A432268e5831` (6 decimals) * USDC on Base: `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` (6 decimals) Get a quote [#get-a-quote] The agent formats the amount (100 USDC = `100000000` with 6 decimals) and requests a quote. ```bash GET https://app.across.to/api/swap/approval ?originChainId=42161 &destinationChainId=8453 &inputToken=0xaf88d065e77c8cC2239327C5EDb3A432268e5831 &outputToken=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 &amount=100000000 &depositor=0xUserWallet &tradeType=minOutput ``` The response includes: * `expectedFillTime`: **2 seconds** * `simulationSuccess`: **true** * `approvalTxns`: 1 approval needed (first time only) * `swapTx`: the bridge transaction calldata Execute token approval [#execute-token-approval] The quote includes an approval transaction because this is the first time bridging USDC through this SpokePool. The agent sends it. ``` Transaction: Approve SpokePool to spend USDC Hash: 0xabc123... Status: Confirmed ``` On subsequent bridges, this step is skipped — the approval persists. Execute the bridge [#execute-the-bridge] The agent sends the main swap transaction using the calldata from the quote. ``` Transaction: Deposit 100 USDC into Arbitrum SpokePool Hash: 0xdef456... Status: Confirmed in block 284719304 ``` The agent reports back: > Bridged 100 USDC from Arbitrum to Base. Expected fill time: \~2 seconds. > Tx: [https://arbiscan.io/tx/0xdef456](https://arbiscan.io/tx/0xdef456)... Track the deposit [#track-the-deposit] The agent polls the status endpoint until the deposit is filled. ```bash GET https://app.across.to/api/deposit/status ?originChainId=42161 &depositTxHash=0xdef456... ``` After \~2 seconds: ```json { "status": "filled", "fillTxHash": "0x789abc...", "destinationChainId": 8453 } ``` The agent confirms: **Transfer complete.** 100 USDC arrived on Base. **Try it yourself.** Give your agent this prompt: ``` Bridge 100 USDC from Arbitrum to Base ``` With the [Across skill installed](/ai-agents), the agent handles the entire flow autonomously. *** Example 2: Bridge + DeFi Deposit [#example-2-bridge--defi-deposit] **User prompt:** ``` Bridge 100 USDC from Arbitrum to Ethereum, swap to ETH, and deposit into Aave ``` Discover chains and tokens [#discover-chains-and-tokens] The agent resolves chain IDs and token addresses, same as Example 1. * **Arbitrum** (42161) — USDC: `0xaf88d065e77c8cC2239327C5EDb3A432268e5831` * **Ethereum** (1) — WETH: `0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2` The agent identifies this as a cross-token bridge (USDC to WETH) with an embedded action. Build the embedded action [#build-the-embedded-action] The agent constructs an Aave deposit action targeting the WETH Gateway on Ethereum. ```json title="embedded action" { "target": "0x893411580e590D62dDBca8a703d61Cc4A8c7b2b9", "functionSignature": "function depositETH(address, address onBehalfOf, uint16 referralCode)", "args": [ { "value": "0x0000000000000000000000000000000000000000", "populateDynamically": false }, { "value": "0xUserWallet", "populateDynamically": false }, { "value": "0", "populateDynamically": false } ], "value": "0", "isNativeTransfer": false, "populateCallValueDynamically": true } ``` The key: `populateCallValueDynamically: true` tells the MulticallHandler to forward the entire ETH balance from the swap as `msg.value` to the Aave deposit. Get a quote with actions [#get-a-quote-with-actions] The agent uses `POST /swap/approval` with the actions array in the body and sets `recipient` to the MulticallHandler contract. ```bash POST https://app.across.to/api/swap/approval ?originChainId=42161 &destinationChainId=1 &inputToken=0xaf88d065e77c8cC2239327C5EDb3A432268e5831 &outputToken=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 &amount=100000000 &depositor=0xUserWallet &recipient=0x924a9f036260DdD5808007E1AA95f08eD08aA569 &tradeType=exactInput Body: { "actions": [] } ``` The response confirms `simulationSuccess: true` — the full flow (bridge + swap + Aave deposit) has been simulated successfully. Execute approvals and bridge [#execute-approvals-and-bridge] The agent sends any required approval transactions, then the main swap transaction. ``` Transaction: Deposit USDC into Arbitrum SpokePool (with embedded Aave action) Hash: 0xfed987... Status: Confirmed ``` What happens next (handled by the protocol): 1. A relayer fills the intent on Ethereum, delivering ETH to the MulticallHandler 2. The MulticallHandler executes the Aave `depositETH()` call 3. Aave mints aWETH tokens to the user's address Confirm the result [#confirm-the-result] The agent tracks the deposit and confirms the full flow completed. > Bridge + Aave deposit complete. 100 USDC on Arbitrum has been converted to an aWETH lending position on Ethereum. > Deposit tx: [https://arbiscan.io/tx/0xfed987](https://arbiscan.io/tx/0xfed987)... > Expected fill: \~2 seconds. **Try it yourself.** Give your agent this prompt: ``` Bridge 100 USDC from Arbitrum to Ethereum, swap to ETH, and deposit into Aave ``` This uses [embedded crosschain actions](/introduction/embedded-actions) to compose bridge + DeFi in a single user transaction. *** More Resources [#more-resources] Full developer guide for the 5-step bridge flow used in Example 1. Complete code walkthrough for the bridge + Aave pattern in Example 2. More prompts organized by task category. # AGENTS.md import { Callout } from 'fumadocs-ui/components/callout'; import { Step, Steps } from 'fumadocs-ui/components/steps'; What is AGENTS.md [#what-is-agentsmd] [AGENTS.md](https://agents.md/) is a convention for providing project context to AI coding agents. When an agent like Claude Code, Codex, or Cursor opens your repository, it reads the nearest `AGENTS.md` (or `CLAUDE.md`, `.cursorrules`, etc.) to understand the project's architecture, conventions, and integration patterns. Why You Need One [#why-you-need-one] Without an `AGENTS.md`, coding agents have to guess how your Across integration works — which API endpoints you use, how you format amounts, where your bridge logic lives, and what security constraints to follow. With one, your agent can: * Call the correct Swap API endpoints without hallucinating URLs * Use your integrator ID and configured chains * Follow your project's conventions for error handling and token addresses * Avoid common mistakes like hardcoding addresses or logging private keys Drop this file in your project root as `AGENTS.md`, `CLAUDE.md`, or `.cursorrules` — whichever your agent framework reads. The content is the same. The Template [#the-template] Copy this template into your repository and fill in the placeholders: ````markdown title="AGENTS.md" # Project Context [YOUR PROJECT NAME] — [brief description of what the project does]. Built with [framework/stack]. Uses Across Protocol for crosschain token transfers. # Across Integration - **Integrator ID**: `0xYOUR_INTEGRATOR_ID` (2-byte hex, register at https://app.across.to) - **API Base URL**: `https://app.across.to/api` (production) - **Testnet API**: `https://testnet.across.to/api` (Sepolia testnet) - **Supported Chains**: [list the chains your app supports, e.g., Ethereum, Arbitrum, Base, Optimism] - **Supported Tokens**: [list tokens, e.g., USDC, USDT, WETH, ETH] - **SDK**: `@across-protocol/app-sdk` (if using the SDK) # Build & Test ```bash [your install command, e.g., pnpm install] [your build command, e.g., pnpm build] [your test command, e.g., pnpm test] [your dev command, e.g., pnpm dev] ``` # Key Files - `[path/to/bridge-logic]` — Main bridge integration (Swap API calls) - `[path/to/config]` — Chain and token configuration - `[path/to/hooks-or-utils]` — Bridge hooks/utilities - `[path/to/types]` — Type definitions for API responses # API Patterns All crosschain transfers go through the Swap API: 1. **Discover chains**: `GET /swap/chains` — returns supported chains with IDs 2. **Look up tokens**: `GET /swap/tokens?chainId={id}` — returns tokens for a chain 3. **Get a quote**: `GET /swap/approval?originChainId=...&destinationChainId=...&inputToken=...&outputToken=...&amount=...&depositor=...&tradeType=minOutput` 4. **Execute**: Send `approvalTxns` first (if any), then `swapTx` 5. **Track**: `GET /deposit/status?originChainId=...&depositTxHash=...` For embedded actions (bridge + DeFi in one tx): - Use `POST /swap/approval` with `actions` array in the body - Set `recipient` to the MulticallHandler contract address # Conventions - NEVER hardcode token addresses — always fetch from `/swap/tokens` - NEVER hardcode chain IDs in logic — use `/swap/chains` as source of truth - Format amounts using the token's `decimals` field (USDC = 6, WETH = 18) - Use `tradeType: "minOutput"` for standard bridges (user receives at least the specified amount) - Use `tradeType: "exactInput"` for embedded actions (exact input, variable output) - Always include the integrator ID in production API calls # Security - NEVER log or expose private keys, seed phrases, or signing keys - NEVER commit `.env` files or credentials - Always check `simulationSuccess` on the quote before executing transactions - Validate `checks.allowance` and `checks.balance` before sending - Use the API-provided gas estimates — don't hardcode gas limits - For embedded actions, verify the `recipient` is the MulticallHandler, not the user address # Common Agent Tasks When asked to: - **"Bridge tokens"** — Use the 5-step Swap API flow: chains → tokens → quote → approve → execute - **"Get a quote"** — Call `/swap/approval` with the route parameters - **"Check bridge status"** — Call `/deposit/status` with the origin chain ID and deposit tx hash - **"Add a new chain"** — Check `/swap/chains` to verify support, then add chain config - **"Bridge and deposit into DeFi"** — Use embedded actions with POST `/swap/approval` ```` Customization Guide [#customization-guide] Fill in Project Context [#fill-in-project-context] Replace the placeholders in the first section with your project name, description, and tech stack. Be specific — agents use this to understand the overall architecture. Configure Across Integration [#configure-across-integration] Add your integrator ID (required for production), the chains your app supports, and which tokens you bridge. If you only bridge USDC on Arbitrum and Base, say so — the agent won't try to use unsupported routes. Map Your Key Files [#map-your-key-files] Point the agent to where your bridge logic lives. Include the config file where chains/tokens are defined, the main integration file that calls the API, and any shared types or utilities. Add Project-Specific Conventions [#add-project-specific-conventions] If your project has additional rules (e.g., "always use BigInt for amounts", "wrap API calls in a try/catch with our logger"), add them to the Conventions section. The agent will follow these patterns in generated code. Define Common Tasks [#define-common-tasks] Add task-specific instructions for prompts your team uses frequently. For example, if you often ask the agent to "add a new bridge route", describe the exact files to modify and the pattern to follow. # AI Agents import { Callout } from 'fumadocs-ui/components/callout'; import { Card, Cards } from 'fumadocs-ui/components/card'; Add Across to your agent with one command [#add-across-to-your-agent-with-one-command] ```bash title="terminal" npx skills add https://github.com/across-protocol/skills --yes ``` Compatible with **Claude Code**, **Codex**, **Cursor**, **Openclaw**, and more. Connect to the Across MCP Server [#connect-to-the-across-mcp-server] ```text title="MCP Server URL" https://mcp.across.to/mcp ``` Add this URL to any MCP-compatible client — **Claude Desktop**, **Claude Code**, **Cursor**, **VS Code Copilot**, **Windsurf**, **Codex**. *** What your agent gets [#what-your-agent-gets] Once installed, your AI agent can: * **Bridge tokens** across 24+ chains with natural language commands * **Get live quotes** — fees, estimated fill times, and optimal routes * **Execute crosschain swaps** — USDC, USDT, WETH, and more * **Track deposits** — check fill status and confirmations in real time * **Compose embedded actions** — bridge + swap + deposit into DeFi in one transaction Supported Agents [#supported-agents] | Agent Framework | Status | | --------------- | --------- | | Claude Code | Supported | | Codex | Supported | | Cursor | Supported | | Openclaw | Supported | Two ways to connect [#two-ways-to-connect] | Approach | Best for | Setup | | --------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | **Skills CLI** (`npx skills add`) | Claude Code, Codex, Cursor, Openclaw | One command — installs tool definitions directly into your agent config | | **[MCP Server](/ai-agents/mcp-server)** | Claude Desktop, Cursor, Windsurf, VS Code Copilot, Codex | Point your client at `https://mcp.across.to/mcp` — search docs, query chains, get live bridge fees | Use both together or pick whichever fits your workflow. Supported Chains [#supported-chains] Across currently supports **24+ mainnet chains** — including Ethereum, Arbitrum, Optimism, Base, Polygon, Unichain, and other major L2s. For the canonical list with contract addresses and explorer links, see the [Chains & Contracts](/chains-and-contracts) page. For the live machine-readable list, query the [`/swap/chains`](/api-reference/swap/chains/get) endpoint. How it works [#how-it-works] The skills package provides your agent with structured tool definitions that map directly to the Across Swap API. Your agent receives: 1. **Chain discovery** — query supported chains and tokens 2. **Quote generation** — get executable calldata for any crosschain transfer 3. **Transaction execution** — approval and bridge transaction helpers 4. **Status tracking** — monitor deposits until they're filled The agent handles the entire flow autonomously — from selecting the right route to executing the transaction. Example prompt [#example-prompt] Once the skill is installed, you can simply tell your agent: ``` Bridge 100 USDC from Arbitrum to Base ``` The agent will discover chains, look up token addresses, fetch a quote, handle approvals, and execute the bridge — all without any additional code. *** Explore [#explore] Search docs, query chains, and get live bridge fees from any MCP-compatible client. Hosted at mcp.across.to. Feed Across documentation to agents and RAG pipelines via llms.txt endpoints. Drop-in AGENTS.md for repos integrating Across — gives coding agents full context. Curated prompts for discovery, quoting, execution, tracking, and DeFi composition. End-to-end examples showing real agent sessions performing crosschain tasks. # Machine-Readable Docs import { Callout } from 'fumadocs-ui/components/callout'; import { Tabs, Tab } from 'fumadocs-ui/components/tabs'; Every page on this site is available in machine-readable markdown format. AI agents, RAG pipelines, and LLM tools can consume Across documentation without scraping HTML. Endpoints [#endpoints] | Endpoint | Content | Best For | | ------------------ | ---------------------------------------------- | ---------------------------------------------- | | `/llms.txt` | Page index with titles, URLs, and descriptions | Directory lookup — find the right page to read | | `/llms-full.txt` | All pages concatenated as markdown | RAG ingestion, large-context agents | | `/docs/.mdx` | Single page as raw markdown | Targeted reads — fetch exactly one page | When to Use Which [#when-to-use-which] * **Agent needs to find a page** — fetch `/llms.txt`, scan for the relevant title, then fetch that page's `.mdx` endpoint. * **Agent needs full context** — fetch `/llms-full.txt` and pass the entire corpus (or relevant chunks) into the context window. * **Agent already knows the page** — append `.mdx` to any docs URL to get raw markdown. For example, `/ai-agents/llms-txt.mdx` returns this page as markdown. The `/llms-full.txt` endpoint is large. If your agent has a limited context window, prefer `/llms.txt` to find the right page and then fetch it individually via `.mdx`. Fetching Examples [#fetching-examples] Page Index [#page-index] ```bash title="terminal" curl https://docs.across.to/llms.txt ``` ```ts title="agent.ts" const index = await fetch("https://docs.across.to/llms.txt").then(r => r.text()); console.log(index); ``` ```python title="agent.py" import requests index = requests.get("https://docs.across.to/llms.txt").text print(index) ``` Full Documentation Dump [#full-documentation-dump] ```bash title="terminal" curl https://docs.across.to/llms-full.txt ``` ```ts title="agent.ts" const full = await fetch("https://docs.across.to/llms-full.txt").then(r => r.text()); console.log(full); ``` ```python title="agent.py" import requests full = requests.get("https://docs.across.to/llms-full.txt").text print(full) ``` Single Page [#single-page] ```bash title="terminal" # Fetch the Swap API docs as markdown curl https://docs.across.to/introduction/swap-api.mdx ``` ```ts title="agent.ts" const page = await fetch("https://docs.across.to/introduction/swap-api.mdx") .then(r => r.text()); console.log(page); ``` ```python title="agent.py" import requests page = requests.get("https://docs.across.to/introduction/swap-api.mdx").text print(page) ``` Per-Page Actions [#per-page-actions] Every documentation page includes built-in buttons for AI consumption: * **Copy Markdown** — copies the page content as markdown to your clipboard * **Open in Claude / ChatGPT / Cursor** — opens the AI tool with the page URL pre-loaded as context These buttons appear at the top of each page and use the same `.mdx` endpoints described above. For RAG Pipelines [#for-rag-pipelines] The `/llms-full.txt` endpoint concatenates all pages with `# Title` headings. You can split on H1 headings to chunk by page: ```python title="chunker.py" import requests full_text = requests.get("https://docs.across.to/llms-full.txt").text # Split into per-page chunks chunks = [] current_chunk = "" for line in full_text.split("\n"): if line.startswith("# ") and current_chunk: chunks.append(current_chunk.strip()) current_chunk = line + "\n" else: current_chunk += line + "\n" if current_chunk.strip(): chunks.append(current_chunk.strip()) print(f"Split into {len(chunks)} page chunks") ``` Each chunk corresponds to one documentation page and can be embedded independently for retrieval. # MCP Server import { Callout } from 'fumadocs-ui/components/callout'; import { Tabs, Tab } from 'fumadocs-ui/components/tabs'; What is MCP? [#what-is-mcp] The [Model Context Protocol (MCP)](https://modelcontextprotocol.io) is an open standard that lets AI applications connect to external tools and data sources through a unified interface. The Across MCP server indexes Across documentation and exposes tools for searching docs, querying supported chains, fetching live bridge fees, and retrieving code examples — all from within your AI client. **Hosted instance available** — no local setup required. Point your client at `https://mcp.across.to/mcp` and you're ready to go. Connect to the Hosted Server [#connect-to-the-hosted-server] The fastest way to get started is to connect your AI client to the hosted MCP server. Edit your Claude Desktop config file: * **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` * **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` * **Linux**: `~/.config/Claude/claude_desktop_config.json` ```json title="claude_desktop_config.json" { "mcpServers": { "across-docs": { "url": "https://mcp.across.to/mcp" } } } ``` Restart Claude Desktop after saving. The Across tools will appear in the tool picker. ```bash title="terminal" claude mcp add --transport http across-docs https://mcp.across.to/mcp ``` Or add to your project's `.mcp.json`: ```json title=".mcp.json" { "mcpServers": { "across-docs": { "url": "https://mcp.across.to/mcp" } } } ``` Create or edit `.cursor/mcp.json` in your project (or `~/.cursor/mcp.json` for global): ```json title=".cursor/mcp.json" { "mcpServers": { "across-docs": { "url": "https://mcp.across.to/mcp" } } } ``` Add to `.vscode/settings.json`: ```json title="settings.json" { "mcp": { "servers": { "across-docs": { "url": "https://mcp.across.to/mcp" } } } } ``` Add to `~/.windsurf/mcp.json`: ```json title="mcp.json" { "mcpServers": { "across-docs": { "url": "https://mcp.across.to/mcp" } } } ``` Add to your MCP config: ```json title="mcp.json" { "mcpServers": { "across-docs": { "url": "https://mcp.across.to/mcp" } } } ``` Available Tools [#available-tools] Once connected, your AI agent has access to seven tools: | Tool | Description | | ---------------------- | ------------------------------------------------ | | `search_across_docs` | Full-text search across all Across documentation | | `get_page` | Retrieve a complete documentation page by path | | `get_api_reference` | Browse REST API endpoints and their parameters | | `get_supported_chains` | List mainnet and testnet chains with chain IDs | | `get_bridge_fees` | Query live bridge fees from the Across API | | `get_code_examples` | Get SDK and integration code samples | | `recrawl_docs` | Force a documentation refresh | Example Prompts [#example-prompts] Once configured, you can ask your AI agent things like: ``` Search the Across docs for how to do a crosschain swap ``` ``` What chains does Across Protocol support? ``` ``` What are the bridge fees to send USDC from Ethereum to Arbitrum? ``` ``` Show me code examples for using the Across SDK ``` ``` Show me the Across API endpoint for suggested fees ``` ``` How do I run an Across relayer? ``` Self-Hosting [#self-hosting] If you prefer to run the server locally instead of using the hosted instance: ```bash title="terminal" git clone https://github.com/across-protocol/mcp-server-across.git cd mcp-server-across npm install npm run build ``` Then point your client to the local build: ```json title="claude_desktop_config.json" { "mcpServers": { "across-docs": { "command": "node", "args": ["/absolute/path/to/mcp-server-across/dist/index.js"] } } } ``` ```bash title="terminal" claude mcp add across-docs node /absolute/path/to/mcp-server-across/dist/index.js ``` The server caches documentation locally at `~/.across-mcp/cache.json` and refreshes every 24 hours. Docker [#docker] ```bash title="terminal" docker build -t mcp-server-across . docker run -i mcp-server-across ``` With cache persistence: ```bash title="terminal" docker run -i -v across-mcp-cache:/root/.across-mcp mcp-server-across ``` Development [#development] ```bash title="terminal" npm run dev # watch mode npx @modelcontextprotocol/inspector node dist/index.js # test with MCP Inspector UI ``` Source Code [#source-code] The MCP server is open source: [github.com/across-protocol/mcp-server-across](https://github.com/across-protocol/mcp-server-across) # Prompt Library import { Callout } from 'fumadocs-ui/components/callout'; import { Tabs, Tab } from 'fumadocs-ui/components/tabs'; Prompts by Category [#prompts-by-category] Tested prompts organized by task category. Each prompt shows the natural-language instruction, the steps an agent should take, and the API endpoints involved. These prompts work best when the agent has the [Across skill installed](/ai-agents) or access to the [machine-readable documentation](/ai-agents/llms-txt). What chains does Across support? [#what-chains-does-across-support] **Prompt:** ``` What chains does Across support? List them with their chain IDs. ``` **Agent steps:** 1. Call `GET /swap/chains` 2. Format the response as a table of chain names and IDs **Endpoint:** `GET https://app.across.to/api/swap/chains` *** What tokens can I bridge from Arbitrum? [#what-tokens-can-i-bridge-from-arbitrum] **Prompt:** ``` What tokens can I bridge from Arbitrum using Across? ``` **Agent steps:** 1. Call `GET /swap/chains` to find Arbitrum's chain ID (42161) 2. Call `GET /swap/tokens?chainId=42161` 3. List all tokens with their symbols, addresses, and decimals **Endpoints:** `GET /swap/chains`, `GET /swap/tokens?chainId=42161` *** What are the limits for USDC? [#what-are-the-limits-for-usdc] **Prompt:** ``` What are the transfer limits for bridging USDC from Arbitrum to Base? ``` **Agent steps:** 1. Look up USDC addresses on both chains via `/swap/tokens` 2. Call `GET /swap/limits` with the token addresses and chain IDs 3. Report minimum and maximum transfer amounts **Endpoints:** `GET /swap/tokens`, `GET /swap/limits?originChainId=42161&destinationChainId=8453&inputToken=...&outputToken=...` Get a quote to bridge USDC [#get-a-quote-to-bridge-usdc] **Prompt:** ``` Get a quote to bridge 100 USDC from Arbitrum to Base. Show me the fees and estimated fill time. ``` **Agent steps:** 1. Look up chain IDs for Arbitrum (42161) and Base (8453) 2. Look up USDC addresses on both chains via `/swap/tokens` 3. Call `GET /swap/approval` with `amount=100000000` (100 USDC, 6 decimals) and `tradeType=minOutput` 4. Extract `fees`, `expectedFillTime`, and output amount from the response **Endpoint:** `GET /swap/approval?originChainId=42161&destinationChainId=8453&inputToken=...&outputToken=...&amount=100000000&depositor=...&tradeType=minOutput` *** Compare fees across routes [#compare-fees-across-routes] **Prompt:** ``` Compare the fees for bridging 1000 USDC from Ethereum to Arbitrum vs Ethereum to Base. ``` **Agent steps:** 1. Look up USDC addresses on Ethereum, Arbitrum, and Base 2. Call `/swap/approval` for Ethereum→Arbitrum with `amount=1000000000` 3. Call `/swap/approval` for Ethereum→Base with `amount=1000000000` 4. Compare `fees.totalFeeUsd` and `expectedFillTime` for both routes **Endpoint:** Two calls to `GET /swap/approval` with different `destinationChainId` values Bridge 100 USDC from Arbitrum to Base [#bridge-100-usdc-from-arbitrum-to-base] **Prompt:** ``` Bridge 100 USDC from Arbitrum to Base using my connected wallet. ``` **Agent steps:** 1. Call `GET /swap/chains` to confirm both chains are supported 2. Call `GET /swap/tokens?chainId=42161` and `GET /swap/tokens?chainId=8453` to find USDC addresses 3. Call `GET /swap/approval` with the route parameters and `depositor` set to the wallet address 4. Check `checks.balance` and `checks.allowance` on the response 5. Execute each transaction in `approvalTxns` (if any) 6. Execute `swapTx` — this is the bridge deposit **Endpoints:** `/swap/chains`, `/swap/tokens`, `/swap/approval` *** Handle approvals and execute [#handle-approvals-and-execute] **Prompt:** ``` I want to bridge 50 USDT from Ethereum to Optimism. Handle token approvals automatically. ``` **Agent steps:** 1. Discover chains and look up USDT addresses 2. Get a quote via `/swap/approval` 3. Check if `approvalTxns` is non-empty — if so, send each approval transaction and wait for confirmation 4. Send the `swapTx` transaction 5. Return the transaction hash and expected fill time **Endpoints:** `/swap/chains`, `/swap/tokens`, `/swap/approval` Check the status of a deposit [#check-the-status-of-a-deposit] **Prompt:** ``` Check the status of my bridge deposit with tx hash 0xabc123... on Arbitrum. ``` **Agent steps:** 1. Call `GET /deposit/status?originChainId=42161&depositTxHash=0xabc123...` 2. Report the status: `pending`, `filled`, or `expired` 3. If filled, show the fill transaction hash and destination chain **Endpoint:** `GET /deposit/status?originChainId=42161&depositTxHash=0xabc123...` *** Poll until filled [#poll-until-filled] **Prompt:** ``` Bridge 100 USDC from Arbitrum to Base and wait until the transfer is complete. ``` **Agent steps:** 1. Execute the full bridge flow (discover → quote → approve → execute) 2. After the deposit tx confirms, poll `GET /deposit/status` every 2 seconds 3. Continue polling until status is `filled` 4. Report the fill transaction hash and total time elapsed **Endpoints:** `/swap/chains`, `/swap/tokens`, `/swap/approval`, `/deposit/status` Bridge USDC and deposit into Aave [#bridge-usdc-and-deposit-into-aave] **Prompt:** ``` Bridge 100 USDC from Arbitrum to Ethereum, swap to ETH, and deposit into Aave's lending pool. ``` **Agent steps:** 1. Discover chains and look up USDC (Arbitrum) and WETH (Ethereum) addresses 2. Build an embedded action targeting Aave's WETH Gateway `depositETH()` function 3. Call `POST /swap/approval` with the `actions` array in the body and `recipient` set to the MulticallHandler contract 4. Execute approvals, then the swap transaction 5. The user receives an aWETH position on Ethereum **Endpoints:** `POST /swap/approval` with `actions` body Embedded actions require setting the `recipient` to the MulticallHandler contract address, not the user's wallet. The MulticallHandler receives the bridged tokens and executes the action on behalf of the user. *** Bridge ETH and add HubPool liquidity [#bridge-eth-and-add-hubpool-liquidity] **Prompt:** ``` Bridge ETH from Arbitrum to Ethereum and add it as liquidity to the Across HubPool. ``` **Agent steps:** 1. Look up the ETH/WETH addresses on both chains 2. Build an embedded action targeting the HubPool's `addLiquidity()` function 3. Call `POST /swap/approval` with the action and MulticallHandler as recipient 4. Execute the transaction — the user receives LP tokens on Ethereum **Endpoints:** `POST /swap/approval` with `actions` body # Chains & Contracts import { Callout } from 'fumadocs-ui/components/callout'; **Blast (Chain ID: 81457) — Deprecation Notice**
Across is deprecating support for the Blast chain (Chain ID: `81457`). Once it is disabled (targeting **20th July 2026**), Across routes to and from Blast will stop returning quotes, and the `/swap/chains` endpoint will stop listing Blast as a supported chain.
Deployed Contracts [#deployed-contracts] # Guides import { Card, Cards } from 'fumadocs-ui/components/card'; Browse Guides [#browse-guides] Understand the fundamentals — crosschain intents, intent architecture, the intent lifecycle, and Across V4's ZK settlement. Hands-on integration guides for building with Across. Step-by-step migration references for Solana, CCTP, non-EVM chains, BNB, and V2→V3 upgrades. # API Reference import { Callout } from 'fumadocs-ui/components/callout'; import { Tabs, Tab } from 'fumadocs-ui/components/tabs'; **Blast (Chain ID: 81457) — Deprecation Notice**
Across is deprecating support for the Blast chain (Chain ID: `81457`). Once it is disabled (targeting **20th July 2026**), Across routes to and from Blast will stop returning quotes, and the `/swap/chains` endpoint will stop listing Blast as a supported chain.
The Across API provides programmatic access to crosschain bridging and swap functionality. Use these endpoints to get quotes, check limits, query deposit status, and more. Server URLs [#server-urls] | Environment | Base URL | | ----------- | --------------------------- | | Production | `https://app.across.to/api` | Authentication [#authentication] All API requests require a Bearer token passed via the `Authorization` header, and an `integratorId` query parameter. Don't have an API key yet? [Get your API key and Integrator ID](/tools/integrator-id) in under a minute. Using API Keys [#using-api-keys] Pass your API key as a `Bearer` token in the `Authorization` header on every request. ```bash curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://app.across.to/api/swap/approval?originChainId=42161&destinationChainId=8453&inputToken=0xaf88d065e77c8cC2239327C5EDb3A432268e5831&outputToken=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&amount=10000000&tradeType=minOutput&depositor=0xYourAddress&integratorId=0xdead" ``` ```ts const res = await fetch( "https://app.across.to/api/swap/approval?originChainId=42161&destinationChainId=8453&inputToken=0xaf88d065e77c8cC2239327C5EDb3A432268e5831&outputToken=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&amount=10000000&tradeType=minOutput&depositor=0xYourAddress&integratorId=0xdead", { headers: { Authorization: "Bearer YOUR_API_KEY" }, } ); ``` ```python import requests response = requests.get( "https://app.across.to/api/swap/approval", params={ "originChainId": "42161", "destinationChainId": "8453", "inputToken": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", "outputToken": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "amount": "10000000", "tradeType": "minOutput", "depositor": "0xYourAddress", "integratorId": "0xdead", }, headers={"Authorization": "Bearer YOUR_API_KEY"}, ) ``` ```go req, _ := http.NewRequest("GET", "https://app.across.to/api/swap/approval?originChainId=42161&destinationChainId=8453&inputToken=0xaf88d065e77c8cC2239327C5EDb3A432268e5831&outputToken=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&amount=10000000&tradeType=minOutput&depositor=0xYourAddress&integratorId=0xdead", nil) req.Header.Set("Authorization", "Bearer YOUR_API_KEY") resp, _ := http.DefaultClient.Do(req) ``` ```rust let res = reqwest::Client::new() .get("https://app.across.to/api/swap/approval?originChainId=42161&destinationChainId=8453&inputToken=0xaf88d065e77c8cC2239327C5EDb3A432268e5831&outputToken=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&amount=10000000&tradeType=minOutput&depositor=0xYourAddress&integratorId=0xdead") .header("Authorization", "Bearer YOUR_API_KEY") .send() .await?; ``` ```ruby uri = URI("https://app.across.to/api/swap/approval?originChainId=42161&destinationChainId=8453&inputToken=0xaf88d065e77c8cC2239327C5EDb3A432268e5831&outputToken=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&amount=10000000&tradeType=minOutput&depositor=0xYourAddress&integratorId=0xdead") req = Net::HTTP::Get.new(uri) req["Authorization"] = "Bearer YOUR_API_KEY" res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) } ``` Endpoints [#endpoints] Browse the individual endpoint pages in the sidebar for full parameter details, response schemas, and code samples. # Actors in the System import { Callout } from 'fumadocs-ui/components/callout'; Across is a multi-actor system where each participant plays a distinct role in making crosschain transfers fast, secure, and capital-efficient. Understanding these roles helps you reason about the protocol's trust assumptions and economic incentives. User [#user] Users initiate crosschain transfers by submitting deposit orders to a SpokePool on the origin chain. They pay fees to relayers and liquidity providers in exchange for near-instant token delivery on the destination chain. Users interact with the protocol through: * The [Swap API](/introduction/swap-api) (most common) * [ERC-7683](/guides/concepts/erc-7683) `open()` calls * Direct `depositV3()` calls on SpokePool contracts Relayer [#relayer] Relayers are the actors that make \~2 second fills possible. When a user deposits on the origin chain, relayers **front their own capital** on the destination chain to fill the intent immediately, then wait for reimbursement through the settlement process. How Relayers Work [#how-relayers-work] 1. Monitor origin SpokePools for `V3FundsDeposited` events 2. Evaluate profitability (fees vs. gas costs and capital lock-up) 3. Call `fillV3Relay()` on the destination SpokePool using their own tokens 4. Request repayment on any Across-supported chain (not necessarily the origin chain) 5. Receive the user's `inputAmount` minus the LP fee after bundle settlement Relayer Risks [#relayer-risks] Relayer fees compensate for real costs and risks: | Risk | Description | | ------------------- | ------------------------------------------------------------ | | **Gas costs** | Submitting fill transactions on destination chains | | **Capital lock-up** | Funds are locked until bundle settlement (\~1.5 hours) | | **Software bugs** | Errors in relayer implementation could lead to invalid fills | | **Finality risk** | Chain reorganizations could revert confirmed fills | Permissionless Participation [#permissionless-participation] Running a relayer is **permissionless** — anyone can operate one. Risk Labs provides [open-source relayer software](https://github.com/across-protocol/relayer-v3), though developers can customize behavior or build alternative implementations. Exclusive Relayers [#exclusive-relayers] For some intents, a relayer may have **exclusive fill rights** during an exclusivity period. This is determined by the `exclusiveRelayer` and `exclusivityDeadline` fields in the deposit. After the exclusivity window expires, any relayer can fill. Liquidity Provider (LP) [#liquidity-provider-lp] LPs deposit assets into pools on Ethereum mainnet at [across.to/pool](https://across.to/pool). This capital serves two purposes: 1. **Repayment flexibility** — Relayers can request repayment on any chain. LP capital ensures the HubPool can rebalance funds across SpokePools to honor these requests 2. **Backup fulfillment** — In rare cases where no relayer fills a deposit, LP capital can be used to fulfill the transfer directly LP Risks [#lp-risks] | Risk | Description | | ----------------------- | ------------------------------------------------------------ | | **Capital deployment** | Deposited capital is actively used for rebalancing and fills | | **Liquidity risk** | High demand periods may temporarily prevent withdrawals | | **Smart contract risk** | Standard DeFi smart contract risk | LP Fee Model [#lp-fee-model] LPs earn fees based on pool utilization — the more the pool is used for crosschain rebalancing, the higher the LP fee percentage. See [Fees in the System](/introduction/fees) for details on the utilization-based pricing model. Dataworker [#dataworker] The dataworker is a **whitelisted** off-chain actor that coordinates settlement between all chains. It aggregates fills into **bundles** and proposes them to the HubPool on Ethereum. What the Dataworker Does [#what-the-dataworker-does] 1. **Determines block ranges** for each supported chain 2. **Validates fills** against deposits across all chains 3. **Aggregates** valid fills into payment lists 4. **Computes** crosschain transfer instructions for rebalancing 5. **Organizes** everything into merkle trees 6. **Proposes** the bundle to the HubPool with a bond Bundles [#bundles] A bundle is a set of merkle roots encoding: * Relayer refund instructions (which relayers to pay, how much, on which chain) * Token rebalancing instructions (how to move funds between SpokePools) * Slow fill instructions (for deposits that weren't filled by relayers) Bundles are proposed every **\~1.5 hours minimum**. After proposal, they enter a challenge period where anyone can dispute them. Settlement cost is **O(1)**, not O(N). No matter how many individual fills happen in a period, they're aggregated into a single bundle proposal. This is what makes Across gas-efficient at scale. Dataworker Rules [#dataworker-rules] Dataworker behavior is precisely defined by **UMIP-157** and **UMIP-179** — UMA Improvement Proposals that specify exactly how bundles must be constructed. Any deviation from these rules can be disputed. UMA Optimistic Oracle [#uma-optimistic-oracle] The UMA Optimistic Oracle provides the economic security layer for Across. It verifies that proposed bundles are valid through an **optimistic verification** mechanism. How It Works [#how-it-works] 1. The dataworker proposes a bundle and posts a bond 2. The bundle enters a **challenge period** 3. During this period, anyone can dispute the bundle by also posting a bond 4. If no dispute occurs, the bundle is finalized and executed 5. If disputed, UMA's **Data Verification Mechanism (DVM)** resolves it — UMA token holders vote on whether the bundle was valid Trust Assumption [#trust-assumption] The system only needs **a single honest actor** willing to dispute invalid proposals to remain secure. This is a much weaker trust assumption than requiring a majority of validators to be honest. See [Security Model](/introduction/security) for more details on the verification and dispute process. # API Keys & Integrator ID import { Callout } from 'fumadocs-ui/components/callout'; import { Tabs, Tab } from 'fumadocs-ui/components/tabs'; What Are They? [#what-are-they] Production use of Across requires two credentials: | Credential | What it is | How it's used | | ----------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | **API Key** | Authentication token for API requests | Passed in the `Authorization` header on every API call | | **Integrator ID** | Unique 2-byte hex identifier (e.g., `0xdead`) | Passed as a query parameter (`integratorId`) in API calls, or appended to calldata for direct contract integration | Both are required for production. Without them, your requests may be rate-limited or rejected. How to Request [#how-to-request] **[Get your API key and Integrator ID](/tools/integrator-id)** — register your project and receive your credentials instantly. Using with the Swap API [#using-with-the-swap-api] When calling the Swap API, pass **both** your API key in the `Authorization` header and your integrator ID as a query parameter: ```ts title="swap-api-auth.ts" const params = new URLSearchParams({ tradeType: "minOutput", originChainId: "42161", destinationChainId: "8453", inputToken: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", outputToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", amount: "1000000000", depositor: "0xYourWalletAddress", integratorId: "0xdead", // Your 2-byte hex integrator ID }); const response = await fetch( `https://app.across.to/api/swap/approval?${params}`, { headers: { Authorization: "Bearer YOUR_API_KEY", }, } ); ``` Using with Direct Contract Integration [#using-with-direct-contract-integration] If you're calling `depositV3` on the SpokePool directly (via the `suggested-fees` API), the credentials are passed differently: * **API key** — `Authorization` header on the `/suggested-fees` API call * **Integrator ID** — appended to the `depositV3` transaction **calldata** (not as a query parameter) Appending Integrator ID to Calldata [#appending-integrator-id-to-calldata] Append a **delimiter** (`1dc0de`) followed by your **integrator ID** to the end of the encoded `depositV3` calldata: | Component | Value | Description | | ----------------- | ------------------------------- | -------------------------------------------------------------- | | **Delimiter** | `1dc0de` | Fixed 3-byte hex string that marks the start of the identifier | | **Integrator ID** | Your assigned ID (e.g., `f001`) | Unique identifier provided by the Across team | ``` ...0000000000000000000000000000000000000000000000000000000000000000 // last param 1dc0def001 // ← delimiter (1dc0de) + integrator ID (f001) ``` **Do NOT** pass the delimiter + identifier to any `depositV3` parameter, including the `message` param. Only append it to the raw calldata of the transaction. **Do NOT** use `f001` as your identifier — it is an example only. Request your unique identifier from the Across team via your shared communication channel (Telegram, Slack). ```ts title="append-integrator-id.ts" import { encodeFunctionData, type Hex } from "viem"; const DELIMITER = "1dc0de"; const INTEGRATOR_ID = "f001"; // Replace with your assigned ID // Encode depositV3 calldata normally const calldata = encodeFunctionData({ abi: spokePoolAbi, functionName: "depositV3", args: [depositor, recipient, inputToken, outputToken, inputAmount, outputAmount, destinationChainId, exclusiveRelayer, quoteTimestamp, fillDeadline, exclusivityDeadline, message], }); // Append delimiter + integrator ID to the raw calldata const calldataWithId = `${calldata}${DELIMITER}${INTEGRATOR_ID}` as Hex; const hash = await walletClient.sendTransaction({ to: spokePoolAddress, data: calldataWithId, }); ``` Testnet vs Production [#testnet-vs-production] | Environment | Base URL | API Key Required? | | -------------- | ------------------------------- | ----------------- | | **Testnet** | `https://testnet.across.to/api` | No | | **Production** | `https://app.across.to/api` | Yes | **Testnet does not require an API key or integrator ID.** You can develop and test your integration freely on testnet without credentials. Only production requests need authentication. Rate Limits [#rate-limits] Without a valid API key, production requests are subject to strict rate limits. Authenticated requests receive higher rate limits appropriate for production traffic. If you're seeing `429 Too Many Requests` errors: * Verify your API key is included in the `Authorization` header * Check that the key is valid and not expired * Avoid polling `/swap/approval` or `/suggested-fees` more frequently than needed — these endpoints return fresh quotes, not cached data Integrator ID Format [#integrator-id-format] The integrator ID is a **2-byte hex string** prefixed with `0x` when used as a query parameter: ``` 0xdead ✓ Valid 0x0001 ✓ Valid 0xffff ✓ Valid dead ✗ Missing 0x prefix 0xdeadbeef ✗ Too long (4 bytes) ``` When appending to calldata for direct contract integration, use the raw hex without the `0x` prefix (e.g., `dead` not `0xdead`). # Audits Across Audit References [#across-audit-references] * [Across V3 Incremental Audit](https://blog.openzeppelin.com/across-v3-and-oval-incremental-audit) * [Across V3 Audit](https://blog.openzeppelin.com/across-v3-incremental-audit?hs_preview=lerUtAnm-157915368784) * [Across Token and Token Distributor Audit](https://blog.openzeppelin.com/across-token-and-token-distributor-audit/) * [Across V2 Audit](https://blog.openzeppelin.com/uma-across-v2-audit/) UMA Audit References [#uma-audit-references] * [UMA's Continuous Audit (Optimistic Oracle)](https://blog.openzeppelin.com/uma-continuous-audit/) * [UMA Audit – L2 Bridges](https://blog.openzeppelin.com/uma-audit-l2-bridges/) * [UMA Audit – Phase 4](https://blog.openzeppelin.com/uma-audit-phase-4/) * [UMA Audit – Phase 6](https://blog.openzeppelin.com/uma-audit-phase-6/) # The Bridge Across The $ACX Exchange portal is not live yet. Follow [@AcrossProtocol on X](https://x.com/AcrossProtocol) for the launch announcement and the latest details. Overview [#overview] "The Bridge Across" is a [governance proposal](https://forum.across.to/t/the-bridge-across/2097) from Risk Labs to move Across from a token-based DAO to a private U.S. C-corporation ("AcrossCo"). The goal is to explore new growth opportunities for Across. The proposal [passed its Snapshot vote](https://snapshot.box/#/s:acrossprotocol.eth/proposal/0x4a3b1bdaf7cf8df8a04827fbe9fbc8df36274e595585e7a4bb6744652044d9d5) and the work is underway. As part of the transition, $ACX holders will be offered a way to exchange their $ACX through a dedicated portal. Full terms will be shared ahead of launch. This page is a short summary with links to the primary sources. It is not legal, financial, or tax advice. Preparing [#preparing] Taking part will require holding $ACX in a wallet whose private keys you control, rather than on a centralized exchange. The [guide to moving your $ACX off exchanges](https://across.to/blog/how-to-move-your-acx-off-exchanges) walks through setting up a wallet, withdrawing, and confirming the tokens arrived. **Beware of scams.** The portal link will only ever be announced through Across' official channels. Do not connect your wallet to a "portal" you found in a DM, reply, or search ad, and never share your seed phrase. Links [#links] The proposal and the community discussion around it. Where the exchange portal currently stands. Getting your $ACX into a self-custody wallet. Stay updated [#stay-updated] Announcements, including the portal launch and the full terms, go out on [@AcrossProtocol on X](https://x.com/AcrossProtocol) first. # Bug Bounty Security of the platform is our highest priority. All smart contracts and off-chain code (i.e. most of the code within the `across-protocol` [repository](https://github.com/across-protocol)) are within scope and are publicly verifiable. Security researchers are eligible for a bug bounty for reporting undiscovered vulnerabilities. Bounty Program [#bounty-program] We encourage the community to audit our open source code; we also encourage the responsible disclosure of any issues. The bug bounty program is intended to recognize the value of working with the community of independent security researchers and sets out our definition of good faith in the context of finding and reporting vulnerabilities, as well as what you can expect from us in return. Across offers substantial rewards for discoveries that can prevent the loss of assets, the freezing of assets, or harm to users. To be eligible for a bounty, a bug must have not been previously known by the Across team or publicly disclosed by anyone. All Across smart contracts and interactions (including bots and front end code) are in scope. The amount of compensation will vary depending on bug severity. Reward amounts typically correspond to severity in the following manner. The reward currency can be discussed on a case by case basis. | Severity | Reward | | -------- | ---------------: | | Low | $250 | | Medium | $1,000 | | High | $10,000 | | Critical | up to $1,000,000 | Severity is calculated according to the [OWASP](https://owasp.org/www-project-risk-assessment-framework/) risk rating model based on Impact and Likelihood. Submissions [#submissions] Please email your submissions to [bugs@across.to](mailto:bugs@across.to). The submission must include clear and concise steps to reproduce the discovered vulnerability. The following layout of the bug bounty report is encouraged: * **Description:** Describe at a high level the bug with links to problematic code * **Attack:** Detailed instructions for exploiting the bug * **Mitigation:** How to resolve the bug * **Suggested risk rating:** The recommended severity of this bug Terms & Conditions [#terms--conditions] The [same terms and conditions](https://docs.umaproject.org/resources/audit-and-bug-bounty-programs) from the UMA bug bounty program apply here. # Direct Route Linking import { Callout } from 'fumadocs-ui/components/callout'; Across's frontend supports **direct route linking** — construct a URL with query parameters to drop users directly into a pre-filled bridge route. No API calls, no wallet connection required upfront. How It Works [#how-it-works] Append query parameters to the base URL to pre-select the origin chain, destination chain, and tokens: ``` https://app.across.to/bridge-and-swap?from=8453&to=42161&inputToken=BAL&outputToken=USDC ``` Direct route linking This opens the Across bridge with **Base → Arbitrum** selected and **BAL → USDC** as the token pair. | Parameter | Description | Example | | ------------- | -------------------- | ------- | | `from` | Origin chain ID | `8453` | | `to` | Destination chain ID | `42161` | | `inputToken` | Input token symbol | `BAL` | | `outputToken` | Output token symbol | `USDC` | All parameters are optional — use any combination. For the full parameter reference, examples, chain ID table, and use cases, see the [complete guide](/guides/user-guides/direct-route-linking). # Technical FAQ import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; import { Callout } from 'fumadocs-ui/components/callout';
Getting Started [#getting-started]
We recommend using the **Swap API** for most integrations — it's the most flexible and well-documented path. * **[Swap API](/introduction/swap-api)** — Full control over the bridging flow. Ideal for wallets, dApps, aggregators, and backend services. * **[App SDK](/guides/dev-guides/integrate-swap-api)** — TypeScript SDK that wraps the API. Good for quick prototyping. Start with the [step-by-step integration guide](/guides/dev-guides/integrate-swap-api) to get up and running.
**Yes.** API keys should never be exposed in client-side code — they can be extracted from browser network requests and abused. Move your Across API calls to a server-side route (e.g. a Next.js API route, Express endpoint, or serverless function) and pass the API key there via the `Authorization` header. Beyond security, API keys and Integrator IDs are important because they help us: * **Track your usage** — so we can provide you with analytics and support * **Monitor for anomalies** — if your integration gets attacked or experiences unusual traffic, we can detect it early and alert you * **Prioritize your requests** — authenticated requests receive higher rate limits suitable for production traffic [Get your API key and Integrator ID](/tools/integrator-id) if you haven't already.
**No.** Testnet endpoints do not require authentication. You can develop and test your integration freely without credentials. Only production requests need an API key. See [API Keys & Integrator ID](/introduction/api-keys) for details on environments and rate limits.
When using the legacy `/suggested-fees` API with direct `depositV3` SpokePool calls, credentials are passed differently than the Swap API: * **API key** — pass as a `Bearer` token in the `Authorization` header on the `/suggested-fees` request itself * **Integrator ID** — append to the `depositV3` transaction **calldata** (not as a query parameter) To append the Integrator ID, add a **delimiter** (`1dc0de`) followed by your **ID** to the end of the encoded calldata: ``` ...0000000000000000 // last depositV3 param 1dc0def001 // ← delimiter (1dc0de) + integrator ID (f001) ``` ```ts import { encodeFunctionData, type Hex } from "viem"; const DELIMITER = "1dc0de"; const INTEGRATOR_ID = "f001"; // Replace with your assigned ID const calldata = encodeFunctionData({ abi: spokePoolAbi, functionName: "depositV3", args: [depositor, recipient, inputToken, outputToken, inputAmount, outputAmount, destinationChainId, exclusiveRelayer, quoteTimestamp, fillDeadline, exclusivityDeadline, message], }); const calldataWithId = `${calldata}${DELIMITER}${INTEGRATOR_ID}` as Hex; ``` **Important:** Do NOT pass the delimiter + ID to any `depositV3` parameter (including `message`). Only append it to the raw calldata. Do NOT use `f001` — it's an example. [Get your own ID](/tools/integrator-id). Full details at [API Keys & Integrator ID — Legacy Suggested Fees](/introduction/api-keys). > **Warning:** The `/suggested-fees` API is legacy and will be sunset. New integrations should use the [Swap API (`/swap/approval`)](/api-reference/swap/approval/get) instead, which handles the Integrator ID as a simple query parameter — no calldata appending needed. See [Migrate from Suggested Fees to the Swap API](/guides/migration/suggested-fees-to-swap) for a step-by-step guide.
Token Behavior [#token-behavior]
Depending on the context, Across will sometimes send a user ETH and sometimes send a user WETH: | Input Asset | Recipient Type | Received | | ----------- | -------------- | -------- | | **ETH** | EOA (wallet) | **ETH** | | **ETH** | Contract | **WETH** | | **WETH** | EOA (wallet) | **ETH** | | **WETH** | Contract | **WETH** | This behavior is automatic — you don't need to handle wrapping/unwrapping yourself. See the [Swap API docs](/introduction/swap-api) for more details.
To deposit **ETH** (or the native token for a network), set `msg.value = inputAmount`. To deposit **WETH**, set `msg.value = 0`. In both cases, the `inputAmount` should be the same and the `inputToken` should be the **WETH address** for the current chain. ```ts // ETH deposit await contract.deposit({ ..., value: inputAmount }); // WETH deposit await contract.deposit({ ..., value: 0n }); ``` Find WETH addresses per chain via [/swap/tokens](/api-reference/swap/tokens/get).
Deposits & Refunds [#deposits--refunds]
Don't panic — your funds are safe. Here's what to do: 1. **Check the status** using the [Status Tracker](/tools/status-tracker) tool or the [deposit status API](/api-reference/deposit/status/get) 2. **If `pending`** — the transfer is still being processed. Most deposits fill in \~2 seconds, but it can take longer during congestion 3. **If `expired`** — a refund is being processed automatically. See [Refunds](/introduction/refunds) for timing (\~120-150 minutes) 4. **If stuck for >3 hours** — reach out via [Telegram](https://t.me/acrosstg), [in-app support](https://across.to), or Slack Note: refunds are only issued on mainnet deposits.
Recommended `fillDeadline` is **6 hours** from the time of deposit. This ensures the best experience for the user, especially in tricky situations where a deposit is being [slow filled](/guides/concepts/intent-lifecycle). A shorter `fillDeadline` risks the deposit expiring before it can be filled during periods of high congestion or relayer downtime. See [Refunds](/introduction/refunds) for what happens when a deposit expires.
Refund timing after a deposit expires: | Metric | Time after deposit | | ----------- | ------------------ | | **Minimum** | \~120 minutes | | **Typical** | \~135 minutes | | **Maximum** | \~150 minutes | Components: fill deadline (\~90 min) + bundle processing (\~30 min) + canonical bridge delay (\~5-30 min). If your refund hasn't arrived after **3 hours**, reach out to the team. Full details at [Refunds](/introduction/refunds).
Deposit Addresses [#deposit-addresses]
It still works. Deposit addresses are **amount agnostic** — the address is tied to the route (origin/destination chain, tokens, recipient, refund address), not to a specific amount. The sweeper bot re-quotes at sweep time using the actual balance that arrived at the address, so users can send more or less than originally quoted and the bridge will still execute. The output will reflect the amount actually sent and the fee bounds locked in at quote time. Amount agnostic does not mean unbounded: for [`POST /deposit-addresses`](/api-reference/deposit-addresses/post), the amount must still fall within the `limits` (`minInputAmount` / `maxInputAmount`) of the `supportedInputs[]` entry matching the origin chain and token you send. Amounts outside those bounds are refunded rather than swept. See [Persistent Deposit Addresses](/introduction/persistent-deposit-addresses) for more details.
No. Addresses returned by [`POST /deposit-addresses`](/api-reference/deposit-addresses/post) are **persistent and never expire** — `expiresAt` is always `null`. The same request parameters always resolve to the same address, so you can request it once and reuse it indefinitely. What bounds a transfer is the route, not the clock. A deposit is swept automatically when its origin chain and token match an entry in the response's `supportedInputs[]` **and** the amount falls inside that entry's `limits` (`minInputAmount` / `maxInputAmount`). A deposit outside those bounds can't be completed and takes the refund path — automatic to your `refundAddresses` if you supplied them, manual recovery via support if you didn't. `supportedInputs[]` changes as origin chains and limits are added, so re-POST before a reuse that depends on limits you read earlier: you get the same address back, with a current list. If a transfer that satisfied those bounds hasn't been swept, it is **not** because the address expired. Reach out to the team with your `depositAddress` and the transfer transaction hash via [Telegram](https://t.me/acrosstg), [in-app support](https://across.to), or Slack. Addresses issued by the deprecated `GET /swap/counterfactual` endpoint did carry a 24-hour TTL, and funds sent after expiry were not processed automatically. That limit never applied to `POST /deposit-addresses`. Migrating off the legacy endpoint depends on your destination. [`POST /deposit-addresses`](/api-reference/deposit-addresses/post) now covers 20 destination chains — see [Supported routes](/introduction/persistent-deposit-addresses#supported-routes) — so most integrations can move across. See [Persistent Deposit Addresses](/introduction/persistent-deposit-addresses) for the full flow.
All scenarios require manual recovery — reach out to the team with your `depositAddress`, the token address you sent, and the chain ID where funds were sent. | Scenario | Outcome | | -------------------------- | ------------------------ | | Correct token, wrong chain | Manual recovery required | | Wrong token, correct chain | Manual recovery required | | Wrong token, wrong chain | Manual recovery required | Full details at [Persistent Deposit Addresses](/introduction/persistent-deposit-addresses).
***

Still have questions?

Our team is here to help. Reach out on any of these channels.

# Key Features import { Callout } from 'fumadocs-ui/components/callout'; import { Card, Cards } from 'fumadocs-ui/components/card'; Integration methods [#integration-methods] Across supports three integration methods. The [**Swap API**](/introduction/swap-api) is the default and covers most use cases. | Method | What it is | Use when | | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ---------------------------------------------- | | [**Swap API**](/introduction/swap-api) *(primary)* | Quote a route, then execute the returned transaction from the user's wallet | All Standard integrations — the primary path | | [**Gasless**](/api-reference/swap/gasless/get) | The user signs a message; Across submits the transaction on-chain, no gas from the user | You want users to transact without holding gas | | [**Deposit address**](/introduction/persistent-deposit-addresses) | Send funds to a persistent infinite-TTL address to conduct a crosschain swap | automation, on-ramps, deposit flows | Feature Highlights [#feature-highlights] | Feature | Details | | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | **Sub-2-second fills** | Competitive relayer network delivers funds on the destination chain in \~2 seconds on mainnet | | **24 mainnet chains** | Solana, MegaETH, Plasma, Monad, Hyperliquid, and all major L2s | | **CCTP V2 / CCTPFast** | Native USDC settlement up to $10M per transaction. CCTPFast offers faster attestation where available | | **Sponsored routes** | Zero-fee USDT/USDT0 → USDC-SPOT/USDC-PERPS on HyperCore from Ethereum, Arbitrum, Monad, Polygon, Unichain. | | **Embedded crosschain actions** | Compose DeFi operations — swap, bridge, and execute destination-chain calls in a single transaction | | **OFT for USDT0** | Native USDT0 mint-and-burn settlement via LayerZero. More tokens coming soon | | **ERC-7683 standard** | Implements the crosschain intent standard for interoperability with other intent-based systems | | **ZK proofs** | Succinct/SP1 zero-knowledge proofs enable permissionless chain expansion without governance votes | API Comparison [#api-comparison] The three main APIs for initiating crosschain transfers are [`/swap/approval`](/api-reference/swap/approval/get), [`/swap/gasless`](/api-reference/swap/gasless/get), and [`/deposit-addresses`](/api-reference/deposit-addresses/post) — sign and submit a transaction, sign one gasless message, or send funds to a persistent address. They serve different use cases and have different capabilities. | Features | `/swap/approval` | `/swap/gasless` | `/deposit-addresses` | | -------------------------------- | -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | | **How it works** | Returns a transaction for the user to sign and submit on-chain | User signs one EIP-712 message (ERC-3009 / Permit2 permit); a submitter broadcasts it on-chain — no gas from the user | Returns a persistent deposit address to send tokens to — no signing or per-transfer quote | | **HTTP methods** | `GET` (simple bridge) / `POST` (embedded actions) | `GET` to quote → `POST /gasless/submit` with the signature | `POST` only | | **API key required** | Yes | Yes | Yes | | **Integrator ID required** | Yes | Yes | Yes | | **Supported origin chains** | All 24 mainnet chains | Any Across-supported EVM chain (plus HyperCore via `1337`) | 17 chains — discovered dynamically, read `supportedInputs[]` from the response (not hardcoded) | | **Supported destination chains** | All 24 mainnet chains | Any Across-supported EVM chain (plus HyperEVM `999`) | 20 chains — the 17 origin chains plus HyperCore (`1337`) | | **Supported tokens (origin)** | All bridgeable tokens (per `/swap/tokens`) | Tokens that support gasless authorization (ERC-3009 / Permit2 — e.g. USDC) | Route-specific — stablecoins, ETH/WETH and WBTC where the route carries them | | **Trade types** | `exactInput`, `minOutput`, `exactOutput` | `exactInput`, `exactOutput` | N/A — amount-agnostic (the amount isn't part of the request) | | **Embedded crosschain actions** | Yes (via `POST` with `crossChainMessage`) | No | No | | **Origin swaps** | Yes (automatic if input token is not directly bridgeable) | Yes (any-input routes, e.g. USDC → native ETH) | No | | **Refund mechanism** | Automatic via SpokePool to `refundAddress` | Origin or destination by route (`refundOnOrigin`); paid to `refundAddress` → `recipient` → `depositor` | Refunds to `refundAddresses` if a deposit can't complete | | **Ideal for** | Wallets, dApps, DEX aggregators — any flow with wallet signing | Onboarding users with no gas or native token; embedded wallets; HyperCore withdrawals | CEX onramps, bots, automation, recurring deposits — flows with no wallet signing | | **Expected fill time** | Less than 2 seconds | Less than 2 seconds | Provided per route via `indicativeFillTime` in the response | | **Deposit address TTL** | N/A | N/A | Never expires — `expiresAt` is always `null` | | **Status** | Generally available | Generally available | Gated ([request access](https://t.me/acrosstg)) | Speed [#speed] Across's infrastructure fills most transfers in **\~2 seconds** on all supported chains, so funds reach the recipient on the destination chain almost immediately. Chain Coverage [#chain-coverage] Across supports **24 mainnet chains** including: * **EVM L2s** — Arbitrum, Optimism, Base, Polygon and more * **Alt-L1s and new chains** — Solana, MegaETH, Plasma, Monad * **App-specific chains** — Hyperliquid (HyperCore + HyperEVM) * **Testnet support** — 8 testnet chains for development See the full list [here](/chains-and-contracts). Sponsored Routes [#sponsored-routes] Select routes have zero bridge fees, subsidized by the protocol or partners. **Currently sponsored:** * **USDT / USDT0 → USDC-SPOT, USDC-PERPS** on HyperCore from Ethereum, Arbitrum, Monad, Polygon, Unichain Sponsored routes change over time. Check the `/swap/approval` response for fee breakdowns to see if a route is currently sponsored. Embedded Crosschain Actions [#embedded-crosschain-actions] Bridge and execute any arbitrary action (like swap, mint, deposit etc.) in one transaction. Attach arbitrary contract calls to your crosschain transfer — the [MulticallHandler](/introduction/embedded-actions) executes them atomically on the destination chain. Use cases: * Swap + deposit into Aave * Bridge + add liquidity to a DEX * Transfer + stake on the destination chain # Fees in the System import { Callout } from 'fumadocs-ui/components/callout'; Every crosschain transfer through Across incurs fees that compensate the actors making the system work — liquidity providers and relayers. All fees are derived from the spread between the `inputAmount` (what the user deposits on the origin chain) and the `outputAmount` (what the user receives on the destination chain). Fee Breakdown [#fee-breakdown] The total fee a user pays is the difference between their deposit and what they receive: ``` totalFee = inputAmount - outputAmount ``` This total fee is split between two components: | Fee Type | Recipient | Purpose | | --------------- | ---------------------------- | ------------------------------------------------------------ | | **LP Fee** | Liquidity providers | Compensates LPs for capital utilization and rebalancing risk | | **Relayer Fee** | Relayer who fills the intent | Covers gas costs, capital opportunity cost, and risk | Use the `/suggested-fees` endpoint to get a fee quote before executing a transfer. The response breaks down each component. `/suggested-fees` is **legacy**. New integrations should read fees off the [`/swap/approval`](/api-reference/swap/approval/get) response instead. Already on it? See [Migrate from Suggested Fees to the Swap API](/guides/migration/suggested-fees-to-swap). LP Fees [#lp-fees] Across uses a **utilization-based pricing model** adapted from lending protocols like Aave. LP fees depend on how much of the available liquidity pool is being used. How It Works [#how-it-works] * When utilization is low, LP fees are minimal — plenty of capital is available * As utilization increases, fees rise along a curve to incentivize more LP deposits * Above a "kink" threshold, fees increase steeply to discourage draining the pool Key Variables [#key-variables] | Variable | Description | | --------- | --------------------------------------------------------- | | **U** | Current pool utilization (0 to 1) | | **U-bar** | Kink utilization — threshold where the fee curve steepens | | **R0** | Base interest rate | | **R1** | Slope below kink | | **R2** | Slope above kink (steep) | The annualized rate formula follows an Aave-style two-slope model: ``` R(U) = R0 + (min(U-bar, U) / U-bar) * R1 + (max(0, U - U-bar) / (1 - U-bar)) * R2 ``` Weekly rates are derived as `(1 + R_annual)^(1/52) - 1`, and the final LP fee is the weekly rate multiplied by the transaction size. **When are LP fees zero?** If the relayer takes repayment on the origin chain (same chain as the deposit), no crosschain rebalancing is needed, so LP fees are zero. Non-zero fees apply when repayment occurs on a different chain. Per-Route Parameters [#per-route-parameters] The rate parameters (R0, R1, R2, U-bar) vary by token and route. This means the LP fee for bridging USDC from Arbitrum to Base may differ from USDC from Ethereum to Optimism, reflecting different pool utilization levels. Relayer Fees [#relayer-fees] Relayers are compensated for three costs they incur when filling an intent: 1. Gas Fees [#1-gas-fees] The cost of submitting the fill transaction on the destination chain. This varies by chain — filling on Ethereum L1 costs more gas than filling on Arbitrum or Base. 2. Capital Opportunity Cost [#2-capital-opportunity-cost] Relayers lock their own capital to fill intents immediately, then wait for reimbursement through the bundle settlement process (\~1.5 hours). The fee compensates for the time value of that locked capital. 3. Capital at Risk [#3-capital-at-risk] Relayers bear risk from: * **Software bugs** — implementation errors in relayer software * **Chain reorganizations** — finality risk where a filled transaction could be reverted * **Settlement delays** — longer-than-expected reimbursement periods App Fees (Integrator Revenue) [#app-fees-integrator-revenue] App fees are an **optional fee you add on top of Across's protocol fees** — your integrator revenue, separate from the LP and relayer fees above. Set `appFee` (a decimal from `0` to `1`, e.g. `0.01` = 1%) and `appFeeRecipient` on your [Swap API](/introduction/swap-api) request. The fee is charged in the output token and paid to `appFeeRecipient` on the destination chain, and appears in the `/swap/approval` fee breakdown. `appFeeRecipient` is required whenever `appFee > 0`, and must be valid **on the destination chain** — an EVM `0x` address for EVM destinations, or a **base58 Solana pubkey for Solana destinations** (now supported). Behavior is otherwise identical across EVM and Solana. Querying Fees [#querying-fees] Use the `/suggested-fees` endpoint to get a full fee breakdown before executing a swap: ```ts title="query-fees.ts" const params = new URLSearchParams({ inputToken: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", // USDC on Arbitrum outputToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base originChainId: "42161", destinationChainId: "8453", amount: "1000000000", // 1000 USDC }); const res = await fetch( `https://app.across.to/api/suggested-fees?${params}` ); const fees = await res.json(); console.log("Total relay fee:", fees.totalRelayFee.total); console.log("LP fee pct:", fees.lpFee.pct); // 1e16 = 1%, 1e18 = 100% console.log("Relayer capital fee:", fees.relayerCapitalFee.total); console.log("Relayer gas fee:", fees.relayerGasFee.total); console.log("Expected fill time:", fees.expectedFillTimeSec, "seconds"); console.log("Is amount too low?", fees.isAmountTooLow); ``` Fee Response Fields [#fee-response-fields] | Field | Description | | --------------------- | --------------------------------------------------------- | | `totalRelayFee` | Combined LP + relayer fee | | `lpFee` | LP fee component (pct format: 1e16 = 1%) | | `relayerCapitalFee` | Relayer capital opportunity cost | | `relayerGasFee` | Relayer destination gas cost | | `isAmountTooLow` | `true` if the amount is below the minimum for this route | | `expectedFillTimeSec` | Estimated time for a relayer to fill | | `limits` | Min/max deposit amounts for instant and short-delay fills | **Do not cache fee responses.** Fees change with market conditions, pool utilization, and gas prices. Always fetch fresh fees before presenting a quote to users. # Hyperliquid Withdrawals import { Callout } from 'fumadocs-ui/components/callout'; import { Step, Steps } from 'fumadocs-ui/components/steps'; import { Tabs, Tab } from 'fumadocs-ui/components/tabs'; HyperCore is Hyperliquid's margin and matching engine. To move funds off it, the user signs EIP-712 typed signatures that Across submits on-chain. Across handles the HyperCore → HyperEVM lift and delivery to the destination chain. **API key with `swap-gasless` permission required.** [Get an API key + Integrator ID](/tools/integrator-id). Endpoints in order of integration [#endpoints-in-order-of-integration] | Method | Path | Purpose | | ------ | -------------------------------------------------- | ------------------------------------------------- | | `GET` | `/swap/gasless` | Quote — returns the steps the user must sign | | `POST` | `/gasless/submit` | Submit signed steps; relayer takes over from here | | `GET` | `/deposit/status?depositId=...&originChainId=1337` | Track the withdrawal to terminal status | Supported Hyperliquid Origin Tokens [#supported-hyperliquid-origin-tokens] | Symbol | `inputToken` value | Decimals | Balance lives in | | ------------------ | -------------------------------------------- | -------- | ---------------- | | `USDC-spot` | `0x2000000000000000000000000000000000000000` | 8 | HL spot | | `USDC-perps` | `0x2100000000000000000000000000000000000000` | 8 | HL perps margin | | `HYPE` | `0x2222222222222222222222222222222222222222` | 8 | HL spot | | `USDT` (USDT-SPOT) | `0x200000000000000000000000000000000000010C` | 8 | HL spot | The `0x2…` addresses are HyperCore sentinels — they are not HyperEVM ERC-20s. Destinations: any EVM chain Across supports. End-to-end integration flow [#end-to-end-integration-flow] Quote [#quote] ``` GET /swap/gasless ?inputToken=0x2100000000000000000000000000000000000000 # USDC-PERPS &originChainId=1337 # HyperCore &outputToken=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 # USDC on Base &destinationChainId=8453 # Base &amount=100000000 # 1 USDC (8 decimals on HC) &depositor= &recipient= &integratorId= ``` **Decimals trap.** `amount` is in the **input token's** decimals. HyperCore tokens use **8 decimals** (HyperEVM and standard EVM USDC use 6) — passing a 6-decimal value for an 8-decimal HyperCore input under-bridges by 100×. Returns `depositId`, the steps to sign in `swapTxns[]`, `quoteExpiryTimestamp`, `expectedOutputAmount`, `minOutputAmount`, and `fees` (the submission fee is at **`fees.submission`** — there is no top-level `submissionFees`). **Submission fee** — the amount (in the input token) paid to the submitter that broadcasts your signed permit on-chain in the gasless flow, reimbursing their gas plus margin. It's part of the signed witness (`submissionFees: { amount, recipient }`), so the user authorizes the exact fee and payee when signing — no separate gas payment from the user. See the [API reference](/api-reference/swap/gasless/get) for the full response shape. Sign every step [#sign-every-step] Iterate `swapTxns` and dispatch by `step.ecosystem`. The user signs **every** step — missing signatures are rejected at submit. Please note that even if a single signature is unsigned or signed incorrectly, the transaction will not go through. ```ts const sigs: Record = {}; for (const step of quote.swapTxns) { if (step.ecosystem === 'hypercore') { sigs[step.stepId] = await hlSign(step.typedData); // HL wallet } else if (step.ecosystem === 'evm-gasless') { sigs[step.stepId] = await evmSign(step.typedData); // EVM EOA — EIP-712 } } ``` **`evm-gasless` chainId gotcha.** Sign at `step.typedData.domain.chainId` (always `999` / HyperEVM), NOT the outer `step.chainId`. If you use a single EVM wallet for both ecosystems, make sure it is on chain `999` before signing every step. Wrong-chain signatures fail server-side `verifyTypedData`. Submit [#submit] ``` POST /gasless/submit { "swapTx": , "swapTxns": , "signaturesByStepId": { "hypercore-transfer": "0x…", "gasless-auth": "0x…" } } ``` Returns `{ depositId, messageId, id }` (`messageId` may be `null`). Submit is gated in two stages: **(1)** every signature must recover to the `depositor` (else `400 "Invalid signature: unable to verify signature"`); **(2)** the integrator key must be eligible for gasless submission (else `403`). It is accepted **optimistically** — a `200` is returned before on-chain execution, so an underfunded withdrawal still returns `200` and later surfaces as `deposit-failed`. See the [API reference](/api-reference/gasless/submit/post) for the full response shape. Track status [#track-status] Poll `/deposit/status` with the `depositId` and `originChainId=1337`: ``` GET /deposit/status?depositId=&originChainId=1337 ``` | Status | Meaning | | ----------------- | --------------------------------------------------------------------------------------------------------- | | `deposit-pending` | HyperCore → HyperEVM lift in progress | | `deposit-failed` | Lift failed (e.g. insufficient HyperCore balance); funds not moved (terminal) | | `filled` | Destination tokens delivered (terminal) | | `expired` | Destination tokens were not delivered within the `fillDeadline` and the user is now eligible for a refund | | `refunded` | Terminal state; user has received a refund | See the [API reference](/api-reference/deposit/status/get) for the full response shape. **Refunds land on HyperEVM, not HyperCore.** `refundToken.chainId` on bridging quotes is `999`. Set `refundOnOrigin: true` and surface the destination in your UI — users won't expect refunds to arrive on HyperEVM. Gas Fees [#gas-fees] HyperCore → HyperEVM mirror moves cost a small HyperEVM gas fee. * **USDC / USDT origin** — fee is paid in-kind or auto-deducted in HYPE. Users don't need to think about it. * **HYPE / non-stablecoin origin** — users must hold HYPE on HyperCore to cover gas. This lift cost is reported at **`fees.submission`** on every quote (a flat amount in the submission token, independent of size). `fees.total` / `fees.totalMax` cover any protocol/relayer spread — `0` for same-asset USDC routes. Minimal Example: HC USDC-PERPS → Base ETH [#minimal-example-hc-usdc-perps--base-eth] ```ts const quote = await fetch(`${BASE}/swap/gasless?inputToken=0x2100…&…`).then(r => r.json()); const sigs: Record = {}; for (const step of quote.swapTxns) { sigs[step.stepId] = step.ecosystem === 'hypercore' ? await hlSign(step.typedData) : await evmSign(step.typedData); } const { depositId } = await fetch(`${BASE}/gasless/submit`, { method: 'POST', body: JSON.stringify({ swapTx: quote.swapTx, swapTxns: quote.swapTxns, signaturesByStepId: sigs, }), }).then(r => r.json()); while (true) { const { status } = await fetch( `${BASE}/deposit/status?depositId=${depositId}&originChainId=1337`, ).then(r => r.json()); if (['filled', 'expired', 'refunded', 'deposit-failed'].includes(status)) break; await new Promise(r => setTimeout(r, 2000)); } ``` Caveats [#caveats] 1. **HL nonces are wallet-unique.** If your client races two quotes, sign + submit them in order — HL will reject a later nonce that arrives first as stale. 2. **Quote expiry is short** (typically 60–120s). After `quoteExpiryTimestamp`, re-quote; submitting a stale quote returns a witness-binding error. 3. **Don't mutate `swapTx` or `swapTxns` between quote and submit.** The submit endpoint re-validates byte-for-byte; any drift returns a deep-equal error. 4. **Trust `fees.submission.amount` from the quote** rather than computing the lift fee yourself. 5. **Very large amounts may exceed a route's available liquidity** and return `400 AMOUNT_TOO_HIGH` (the max is in the message). 6. **`recipient` ≠ `depositor` is fine** — but the `depositor` EOA must own the HC funds and sign every step. All validation errors return `400 InvalidParamError` with a `param` field telling you which input to fix. Most "re-quote" errors are TOCTOU freshness checks — retrying the same body will keep failing. References [#references] * [GET /swap/gasless](/api-reference/swap/gasless/get) — quote endpoint reference * [POST /gasless/submit](/api-reference/gasless/submit/post) — submit endpoint reference * [Working with Hypercore](/introduction/hypercore) — companion deposits guide * [Tracking Deposits](/introduction/tracking-deposits) — generic deposit polling for non-gasless flows * [Refunds](/introduction/refunds) — refund mechanics on Across # Working with Hypercore import { Callout } from 'fumadocs-ui/components/callout'; import { Card, Cards } from 'fumadocs-ui/components/card'; import { Step, Steps } from 'fumadocs-ui/components/steps'; HyperCore is Hyperliquid's high-performance trading layer with sub-second settlement. Across supports bridging assets directly to HyperCore via the Swap API, letting users go from most supported chains to Hyperliquid quickly. **API key required.** [Get your API key and Integrator ID](/tools/integrator-id) to authenticate your requests. Key Parameters [#key-parameters] When bridging to HyperCore, use these specific values: | Parameter | Value | Notes | | -------------------- | ----------------- | ------------------------------- | | `destinationChainId` | `1337` | HyperCore chain ID | | `outputToken` | USDC-SPOT address | The USDC-SPOT token on HyperEVM | HyperCore uses Chain ID **1337**. The Swap API handles the routing from HyperEVM to HyperCore automatically — you don't need to interact with HyperCore directly. How It Works [#how-it-works] Origin Deposit [#origin-deposit] The user deposits assets on the origin chain (e.g., USDT on Arbitrum) via the Swap API. This creates a crosschain intent with HyperEVM as the destination. HyperEVM Fill [#hyperevm-fill] A relayer fills the intent on HyperEVM, delivering the output token (USDC-SPOT) to the recipient address. HyperCore Credit [#hypercore-credit] The bridged funds are automatically credited to the user's HyperCore account. If the user doesn't have a HyperCore account yet, one is initialized automatically. **Automatic account initialization.** If the recipient address doesn't have a HyperCore account, one is created automatically during the bridging process. No manual setup required. Example: Bridge USDT from Arbitrum to HyperCore [#example-bridge-usdt-from-arbitrum-to-hypercore] ```ts title="bridge-to-hypercore.ts" import { createWalletClient, createPublicClient, http, parseUnits } from "viem"; import { arbitrum } from "viem/chains"; import { privateKeyToAccount } from "viem/accounts"; const account = privateKeyToAccount("0xYOUR_PRIVATE_KEY"); const walletClient = createWalletClient({ account, chain: arbitrum, transport: http(), }); const publicClient = createPublicClient({ chain: arbitrum, transport: http(), }); async function bridgeToHyperCore() { const params = new URLSearchParams({ tradeType: "minOutput", originChainId: "42161", // Arbitrum destinationChainId: "1337", // HyperCore inputToken: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", // USDT on Arbitrum outputToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC-SPOT on HyperEVM amount: parseUnits("500", 6).toString(), // 500 USDT depositor: account.address, integratorId: "0xdead", }); const response = await fetch( `https://app.across.to/api/swap/approval?${params}`, { headers: { Authorization: "Bearer YOUR_API_KEY", }, } ); const quote = await response.json(); if (!quote.swapTx) { throw new Error(`Quote failed: ${JSON.stringify(quote)}`); } console.log("Cross-swap type:", quote.crossSwapType); console.log("Expected fill time:", quote.expectedFillTime, "seconds"); // Execute approvals if (quote.approvalTxns?.length) { for (const approvalTx of quote.approvalTxns) { const hash = await walletClient.sendTransaction({ to: approvalTx.to, data: approvalTx.data, }); await publicClient.waitForTransactionReceipt({ hash }); console.log("Approval confirmed:", hash); } } // Execute the bridge transaction const hash = await walletClient.sendTransaction({ to: quote.swapTx.to, data: quote.swapTx.data, value: quote.swapTx.value ? BigInt(quote.swapTx.value) : 0n, gas: quote.swapTx.gas ? BigInt(quote.swapTx.gas) : undefined, }); console.log("Bridge tx:", hash); // Funds will be credited to HyperCore automatically } bridgeToHyperCore(); ``` Verify the correct USDC-SPOT token address on HyperEVM before production use. Use the `/swap/tokens` endpoint to look up the current address for destination chain `1337`. Withdrawing from HyperCore [#withdrawing-from-hypercore] Going the other direction — moving funds **off** HyperCore to another Across-supported chain — uses a different flow. HyperCore is not EVM-compatible, so users can't broadcast a transaction from there. Instead, they sign **2 EIP-712 payloads** on HyperEVM (chain id `999`) and Across drives the rest: the HyperCore → HyperEVM mirror move, the standard Across gasless deposit on HyperEVM (for non-passthrough routes), and the relayer fill on the destination. The endpoints involved are `GET /swap/gasless` (quote + signing steps) and `POST /gasless/submit` (submit signatures, get a `depositId`). **Users withdrawing HYPE (or non-stablecoin tokens) must hold HYPE on HyperCore to cover HyperCore → HyperEVM gas.** For USDC and USDT, the gas fee can be paid in the token itself or auto-deducted in HYPE — no special action needed. **Refunds for HyperCore withdrawals land on HyperEVM (chain id 999), not HyperCore.** Surface this to users explicitly — they will expect refunds to come back to HyperCore. Learn about HyperCore withdrawals in detail on the next page. # Welcome to Across import { Callout } from 'fumadocs-ui/components/callout'; import { Card, Cards } from 'fumadocs-ui/components/card'; import { Step, Steps } from 'fumadocs-ui/components/steps'; Tempo is now live on Across. Bridge to and from Tempo (Chain ID 4217) using the Swap API. What is Across? [#what-is-across] Across is a **crosschain interoperability protocol** that provides the fastest, cheapest, and most secure way to move assets across blockchains. With \~2 second fills on mainnet and support for 24 chains, Across powers crosschain swaps, bridges, and embedded actions through a single unified API. The [Swap API](/introduction/swap-api) is the single entry point: you request a route and it returns a transaction to execute. Across automatically selects the best settlement path under the hood — you don't need to choose. **API key required for production.** [Get your API key and Integrator ID](/tools/integrator-id) to authenticate your requests. Quick Start [#quick-start] Get a crosschain swap executing in three steps: Get a Quote [#get-a-quote] Call the Swap API with your transfer parameters to get executable calldata. ```ts title="get-quote.ts" const params = new URLSearchParams({ tradeType: "minOutput", originChainId: "42161", // Arbitrum destinationChainId: "8453", // Base inputToken: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", // USDC on Arbitrum outputToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base amount: "1000000000", // 1000 USDC (6 decimals) depositor: "0xYourWalletAddress", integratorId: "0xdead", // Your integrator ID }); const response = await fetch( `https://app.across.to/api/swap/approval?${params}`, { headers: { Authorization: "Bearer YOUR_API_KEY", }, } ); const quote = await response.json(); ``` See the [`GET /swap/approval` API reference](/api-reference/swap/approval/get) for every parameter and the full response shape. Approve Token Spending [#approve-token-spending] If the quote includes approval transactions, execute them first. ```ts title="approve.ts" import { createWalletClient, http } from "viem"; import { arbitrum } from "viem/chains"; import { privateKeyToAccount } from "viem/accounts"; const account = privateKeyToAccount("0xYOUR_PRIVATE_KEY"); const walletClient = createWalletClient({ account, chain: arbitrum, transport: http(), }); if (quote.approvalTxns?.length) { for (const approvalTx of quote.approvalTxns) { const hash = await walletClient.sendTransaction({ to: approvalTx.to, data: approvalTx.data, }); console.log("Approval tx:", hash); } } ``` Execute the Swap [#execute-the-swap] Send the main swap transaction using the calldata from the quote. ```ts title="execute.ts" const hash = await walletClient.sendTransaction({ to: quote.swapTx.to, data: quote.swapTx.data, value: quote.swapTx.value ? BigInt(quote.swapTx.value) : 0n, gas: quote.swapTx.gas ? BigInt(quote.swapTx.gas) : undefined, }); console.log("Swap tx:", hash); // Funds arrive on Base in ~2 seconds ``` Direct Route Linking [#direct-route-linking] Link users directly to a pre-filled bridge route on the Across UI — no API calls needed. Just construct a URL with query parameters for origin chain, destination chain, and tokens. ``` https://app.across.to/bridge-and-swap?from=8453&to=42161&inputToken=BAL&outputToken=USDC ``` [Learn more about Direct Route Linking to improve your UX →](/guides/user-guides/direct-route-linking) What's Next [#whats-next] Move native USDC and USDT0 across chains with a single Swap API call. Deep dive into the Swap API — parameters, response structure, and integration patterns. Bridge assets to Hyperliquid's sub-second settlement layer. # Persistent Deposit Addresses import { Callout } from 'fumadocs-ui/components/callout'; import { Step, Steps } from 'fumadocs-ui/components/steps'; `POST /deposit-addresses` returns a **persistent, never-expiring** deposit address that delivers a token to a recipient on the destination you specify. The same request parameters always resolve to the **same** address, so you can request it once and reuse it — the **amount is not part of the request**. Send any amount, any time, and it's swept to the destination automatically; no wallet signing or per-transfer quote is required. **API key with the `deposit-address` permission required** (Bearer auth). [Get an API key + Integrator ID](/tools/integrator-id). Supported routes [#supported-routes] Support is **route-specific** — it depends on the origin/destination pair and the tokens on each side, not on the chain alone. There is no single "is this chain supported" flag. | | Chains | | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Origin** (deposit from) | Ethereum, Optimism, BNB Smart Chain, Polygon, Unichain, Monad, World Chain, HyperEVM, MegaETH, Base, Plasma, Arbitrum, Avalanche, Ink, Linea, Tempo, Tron | | **Destination** (deliver to) | The 17 origin chains above, plus HyperCore (`1337`) | * **Tron** is the only non-EVM chain in the set. Its addresses use the `tron` namespace (base58, `T…`) and arrive as their own entry in `depositInstructions[]` — iterate that array instead of assuming a single EVM address. * **zkSync and Solana are not supported** on either side. **Don't hardcode origins.** The response's `supportedInputs[]` lists every origin chain the address accepts, the token to send on each, that route's min/max limits, and its indicative fees and fill time — read them from there. The list stays current as chains and tokens are added. Unsupported destinations and recipients return `400`. Integrate [#integrate] Request an address [#request-an-address] Send the destination (`token` + `recipient`) and a `refundAddresses` entry to refund to if a deposit can't be completed. The example below requests `USDC-SPOT` on HyperCore; swap in any supported destination token. ```bash curl -X POST https://across.to/api/deposit-addresses \ -H "Authorization: Bearer $ACROSS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "destination": { "token": { "chainId": 1337, "address": "0x2000000000000000000000000000000000000000" }, "recipient": "0x1111111111111111111111111111111111111111" }, "refundAddresses": [ { "namespace": "evm", "address": "0x2222222222222222222222222222222222222222" } ] }' ``` Read the deposit instruction [#read-the-deposit-instruction] The response returns `depositInstructions[]`. Each has a `depositAddress` (an `Account` — `{ namespace, address }`) and the `supportedInputs` it accepts: origin chain, input token, that route's min/max limits, plus an indicative fee breakdown and fill time. `expiresAt` is always `null`. ```jsonc { "destination": { "token": { "chainId": 1337, "address": "0x2000…0000", "symbol": "USDC-SPOT", "name": "USDC", "decimals": 8 }, "recipient": { "namespace": "evm", "address": "0x1111…1111" } }, "refundAddresses": [ { "namespace": "evm", "address": "0x2222…2222" } ], "expiresAt": null, "depositInstructions": [ { "depositAddress": { "namespace": "evm", "address": "0xAcc0…BD6E" }, "supportedInputs": [ { "originChainId": 1, "inputToken": { "chainId": 1, "address": "0xA0b8…eB48", "symbol": "USDC", "name": "USD Coin", "decimals": 6 }, "limits": { "minInputAmount": "5000000", "maxInputAmount": "10000000000000" }, "indicativeFees": { "proportionalPct": "100000000000000", "fixedFeeUsd": 0.246894 }, "indicativeFillTime": 30 } // …one entry per supported origin chain ] } ], "id": "75795-1781095882239-8e49a5353db9" } ``` Send funds [#send-funds] Send one of the tokens listed in `supportedInputs[]`, on the origin chain that entry names, to the `depositAddress`. It's swept to the destination token and recipient you requested. The address is **amount-agnostic and permanent** — reuse it for future transfers; re-POSTing the same parameters returns the same address. Track your deposit [#track-your-deposit] Two calls: list the deposits made through your deposit address, then fetch full details for the latest one. List deposits for the address [#list-deposits-for-the-address] Call [`GET /deposits`](/api-reference/deposits/get) with the `depositAddress` query param. It returns the transfers sent to that address, newest first, each enriched with the bridge deposit created for it. `depositTxnRef` is the hash of the transfer **you** sent to the deposit address. A transfer that hasn't been swept yet reports status `deposit-pending`; once the sweep is picked up, the item carries the bridge deposit's fill status (`pending` → `filled`). ```bash curl "https://across.to/api/deposits?depositAddress=0xAcc0dDdb8169FD06a34AADd52a39A4166a15BD6E" \ -H "Authorization: Bearer $ACROSS_API_KEY" ``` ```jsonc [ { "status": "filled", "depositTxnRef": "0x92ffc5d69d00e14e79254f68adab030dd38119996501a88cac0da76557703d11" // …other deposit fields } ] ``` Fetch the latest deposit [#fetch-the-latest-deposit] Pick the latest transfer — the first item in the response — and pass its `depositTxnRef` to [`GET /deposit`](/api-reference/deposit/get) for the full deposit record, including fill status. ```bash curl "https://across.to/api/deposit?depositTxnRef=0x92ffc5d69d00e14e79254f68adab030dd38119996501a88cac0da76557703d11" \ -H "Authorization: Bearer $ACROSS_API_KEY" ``` See the full schema, fields, and error responses in the [`POST /deposit-addresses` API reference](/api-reference/deposit-addresses/post). # Refunds import { Callout } from 'fumadocs-ui/components/callout'; import { Step, Steps } from 'fumadocs-ui/components/steps'; Refunds occur when no relayer fills a deposit before its `fillDeadline` expires. This is rare on mainnet — the competitive relayer network fills most deposits in \~2 seconds — but it's important to handle in production integrations. You can check the status of your bridge transaction using our **Status Tracker** tool here: *** When Refunds Happen [#when-refunds-happen] A deposit becomes eligible for a refund when: 1. The `fillDeadline` passes without any relayer filling the intent 2. No partial fills were executed Common causes include extremely large transfers, unusual token pairs, or temporary relayer downtime. Refund Flow [#refund-flow] Deposit Expires [#deposit-expires] The `fillDeadline` passes and the deposit status changes to `expired` in the `/deposit/status` API. The user's funds are still escrowed in the origin SpokePool. Bundle Processing [#bundle-processing] The Across Dataworker includes the expired deposit in the next settlement bundle. Bundles are proposed to the HubPool every \~1.5 hours and must pass a challenge period via UMA's Optimistic Oracle. Settlement [#settlement] After the challenge period, the bundle is finalized and the refund root is transmitted to the appropriate chain via canonical bridges. Refund Execution [#refund-execution] The refund is executed on-chain, returning the escrowed funds to the `refundAddress`. The deposit status changes to `refunded` in the API. **Refunds are not instant.** After a deposit expires, the refund goes through the bundle settlement process. With \~1.5-hour bundle intervals plus the challenge period and canonical bridge delays, refunds can take **several hours** to arrive. Do not tell users to expect immediate refunds. Refund Parameters [#refund-parameters] Configure refund behavior when calling the Swap API. | Parameter | Type | Default | Description | | ---------------- | --------- | ----------- | -------------------------------------------------------------------------- | | `refundAddress` | `string` | `depositor` | Address that receives the refund | | `refundOnOrigin` | `boolean` | `true` | If `true`, refund on origin chain. If `false`, refund on destination chain | ```ts title="swap-with-refund-config.ts" const params = new URLSearchParams({ tradeType: "minOutput", originChainId: "42161", destinationChainId: "8453", inputToken: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", outputToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", amount: "1000000000", depositor: "0xYourWalletAddress", // Refund configuration refundAddress: "0xYourWalletAddress", // Where to send the refund refundOnOrigin: "true", // Refund on Arbitrum (origin) }); const response = await fetch( `https://app.across.to/api/swap/approval?${params}` ); ``` Checking Refund Status [#checking-refund-status] Use the `/deposit/status` endpoint to monitor whether an expired deposit has been refunded. ```ts title="check-refund.ts" import { createPublicClient, http } from "viem"; import { arbitrum } from "viem/chains"; const BASE_URL = "https://app.across.to/api"; async function checkRefundStatus(originChainId: number, depositId: number) { const params = new URLSearchParams({ originChainId: originChainId.toString(), depositId: depositId.toString(), }); const res = await fetch(`${BASE_URL}/deposit/status?${params}`); const data = await res.json(); switch (data.status) { case "pending": console.log("Deposit still pending — waiting for fill"); break; case "filled": console.log("Deposit filled successfully — no refund needed"); break; case "expired": console.log("Deposit expired — refund is being processed"); console.log("Refund will arrive after bundle settlement (~hours)"); break; case "refunded": console.log("Refund complete — funds returned to refund address"); break; } return data.status; } // Poll until refunded async function waitForRefund(originChainId: number, depositId: number) { const intervalMs = 60_000; // Check every minute for refunds const maxAttempts = 360; // Up to 6 hours for (let i = 0; i < maxAttempts; i++) { const status = await checkRefundStatus(originChainId, depositId); if (status === "refunded") { return "refunded"; } if (status === "filled") { return "filled"; // Was filled after all — no refund needed } await new Promise((resolve) => setTimeout(resolve, intervalMs)); } throw new Error("Refund polling timed out"); } ``` For refund polling, use a **60-second interval** instead of the 10-second interval used for fill tracking. Refunds take hours, not seconds, so frequent polling is unnecessary. # Across for Stablecoins import { Callout } from 'fumadocs-ui/components/callout'; import { Tabs, Tab } from 'fumadocs-ui/components/tabs'; Across moves native stablecoins across every supported chain through one [Swap API](/introduction/swap-api) call — no wrapped tokens. You request a route; Across picks the best settlement path automatically and returns a transaction to execute. **API key required for production.** [Get your API key and Integrator ID](/tools/integrator-id) to authenticate your requests. Sponsored Routes [#sponsored-routes] Select stablecoin routes have zero bridge fees, subsidized by the protocol. **Currently sponsored:** * **USDT / USDT0 → USDC-SPOT, USDC-PERPS** on HyperCore from Ethereum, Arbitrum, Monad, Polygon, Unichain Sponsored routes change over time. To check if a route is currently sponsored, inspect the `fees` object in the `/swap/approval` response. Stablecoin Contract Addresses [#stablecoin-contract-addresses] **Always verify addresses** via the [`/swap/tokens`](/api-reference/swap/tokens/get) endpoint. Token addresses can change as new deployments roll out. | Chain | Address | | -------- | -------------------------------------------- | | Ethereum | `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48` | | Arbitrum | `0xaf88d065e77c8cC2239327C5EDb3A432268e5831` | | Base | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` | | Optimism | `0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85` | | Polygon | `0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359` | Use [`/swap/tokens?chainId=`](/api-reference/swap/tokens/get) for the complete list across all supported chains. | Chain | Address | | -------- | -------------------------------------------- | | Ethereum | `0xdAC17F958D2ee523a2206206994597C13D831ec7` | | Arbitrum | `0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9` | | Optimism | `0x94b008aA00579c1307B0EF2c499aD98a8ce58e58` | Use `/swap/tokens?chainId=` for the complete list. This is a subset of supported chains. Query `/swap/tokens` for the full matrix of stablecoins and chains. Example: Large USDC Transfer [#example-large-usdc-transfer] One `/swap/approval` call quotes the route and returns the transaction(s) to execute. The integration code is the same for every stablecoin route, regardless of size. ```ts title="usdc-transfer.ts" import { createWalletClient, createPublicClient, http } from "viem"; import { mainnet } from "viem/chains"; import { privateKeyToAccount } from "viem/accounts"; const account = privateKeyToAccount("0xYOUR_PRIVATE_KEY"); const walletClient = createWalletClient({ account, chain: mainnet, transport: http() }); const publicClient = createPublicClient({ chain: mainnet, transport: http() }); const params = new URLSearchParams({ tradeType: "minOutput", originChainId: "1", destinationChainId: "42161", inputToken: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum outputToken: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", // USDC on Arbitrum amount: "1000000000", // 1,000 USDC (6 decimals) depositor: account.address, }); const quote = await fetch( `https://app.across.to/api/swap/approval?${params}`, { headers: { Authorization: "Bearer YOUR_API_KEY" } }, ).then((r) => r.json()); // Execute approvals (if any), then the swap if (quote.approvalTxns?.length) { for (const approvalTx of quote.approvalTxns) { const hash = await walletClient.sendTransaction({ to: approvalTx.to, data: approvalTx.data }); await publicClient.waitForTransactionReceipt({ hash }); } } const hash = await walletClient.sendTransaction({ to: quote.swapTx.to, data: quote.swapTx.data, value: quote.swapTx.value ? BigInt(quote.swapTx.value) : 0n, }); console.log("Transfer tx:", hash); ``` See the [`GET /swap/approval` API reference](/api-reference/swap/approval/get) for every parameter and the full response shape. The integration code is identical for every stablecoin route — call `/swap/approval`, execute any approvals, then send the returned transaction. Across handles settlement behind the scenes. # Introduction to Swap API import { Callout } from 'fumadocs-ui/components/callout'; import { Card, Cards } from 'fumadocs-ui/components/card'; import { Tabs, Tab } from 'fumadocs-ui/components/tabs'; The Swap API is Across's **single entry point** for all crosschain operations — bridging, swapping, and embedded actions. One unified interface at `/swap/approval` quotes any route; Across selects the best settlement path automatically, so your integration stays the same regardless of token, amount, or chain. **API key required for production.** [Get your API key and Integrator ID](/tools/integrator-id) to authenticate your requests. Trade Types [#trade-types] The `tradeType` parameter controls how the swap amount is interpreted. **Spend exactly this amount, receive whatever the market gives.** Use when the user specifies how much they want to send. The output amount varies based on fees, slippage, and exchange rates. ``` tradeType=exactInput&amount=1000000000 ``` **Receive at least this amount, spend whatever is needed.** Use when the user has a minimum acceptable output. The API calculates the required input. This is the recommended default for most integrations. ``` tradeType=minOutput&amount=1000000000 ``` **Receive exactly this amount.** Use when the user needs a precise output. Stricter than `minOutput` — the API will fail if the exact amount can't be delivered. Set `strictTradeType=true` to enforce. ``` tradeType=exactOutput&amount=1000000000&strictTradeType=true ``` Base URLs [#base-urls] | Environment | Base URL | | ----------- | --------------------------- | | Production | `https://app.across.to/api` | Required Parameters [#required-parameters] | Parameter | Type | Description | | -------------------- | -------- | -------------------------------------------------- | | `tradeType` | `string` | `exactInput`, `minOutput`, or `exactOutput` | | `amount` | `string` | Amount in smallest unit (e.g., `1000000` = 1 USDC) | | `inputToken` | `string` | Token address on origin chain | | `outputToken` | `string` | Token address on destination chain | | `originChainId` | `number` | Origin chain ID | | `destinationChainId` | `number` | Destination chain ID | | `depositor` | `string` | Wallet address initiating the transfer | Optional Parameters [#optional-parameters] | Parameter | Type | Description | | ------------------------ | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | `recipient` | `string` | Destination address (defaults to `depositor`) | | `integratorId` | `string` | 2-byte hex identifier for your integration | | `slippage` | `string` | `"auto"` or a value between 0-1 (default: auto) | | `refundAddress` | `string` | Address for refunds if fill expires | | `refundOnOrigin` | `boolean` | Refund on origin chain instead of destination | | `appFee` | `string` | Integrator fee as decimal 0-1 (e.g., `"0.01"` = 1%). Charged in the output token on the destination chain | | `appFeeRecipient` | `string` | Address that receives the app fee, on the **destination chain** — EVM hex, or a base58 Solana pubkey for Solana destinations. Required if `appFee` is set | | `skipOriginTxEstimation` | `boolean` | Skip gas estimation for the origin transaction | | `strictTradeType` | `boolean` | Fail if exact trade type can't be satisfied | | `excludeSources` | `string` | Comma-separated swap sources to exclude | | `includeSources` | `string` | Comma-separated swap sources to include exclusively | See the [`GET /swap/approval` API reference](/api-reference/swap/approval/get) for the complete, always-current parameter list and response schema. Response Structure [#response-structure] The `/swap/approval` response contains everything needed to execute the crosschain transfer. ```json title="response-structure.json" { "crossSwapType": "bridgeableToBridgeable", "checks": { "allowance": { "token": "0x...", "spender": "0x...", "actual": "0", "expected": "1000000" }, "balance": { "token": "0x...", "actual": "5000000", "expected": "1000000" } }, "approvalTxns": [ { "chainId": 42161, "to": "0x...", "data": "0x..." } ], "steps": { "bridge": { "inputAmount": "1000000", "outputAmount": "998000" } }, "fees": { "total": "2000", "totalMax": "3000", "originGas": "50000" }, "swapTx": { "simulationSuccess": true, "chainId": 42161, "to": "0x...", "data": "0x...", "value": "0", "gas": "250000", "maxFeePerGas": "100000000", "maxPriorityFeePerGas": "1500000" }, "expectedFillTime": 2, "quoteExpiryTimestamp": 1700000000 } ``` **Key fields:** * **`crossSwapType`** — The routing path: `bridgeableToBridgeable`, `bridgeableToBridgeableIndirect`, `bridgeableToAny`, `anyToBridgeable`, or `anyToAny` * **`checks`** — Current allowance and balance status for the depositor * **`approvalTxns`** — Token approval transactions to execute before the swap (may be empty) * **`swapTx`** — The main swap transaction calldata * **`expectedFillTime`** — Estimated seconds until the destination fill * **`quoteExpiryTimestamp`** — Unix timestamp when this quote expires Full Integration Example [#full-integration-example] ```ts title="swap-viem.ts" import { createWalletClient, createPublicClient, http, parseUnits } from "viem"; import { arbitrum } from "viem/chains"; import { privateKeyToAccount } from "viem/accounts"; const account = privateKeyToAccount("0xYOUR_PRIVATE_KEY"); const walletClient = createWalletClient({ account, chain: arbitrum, transport: http(), }); const publicClient = createPublicClient({ chain: arbitrum, transport: http(), }); async function executeSwap() { // 1. Get quote const params = new URLSearchParams({ tradeType: "minOutput", originChainId: "42161", destinationChainId: "8453", inputToken: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", // USDC on Arbitrum outputToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base amount: parseUnits("100", 6).toString(), // 100 USDC depositor: account.address, integratorId: "0xdead", }); const res = await fetch( `https://app.across.to/api/swap/approval?${params}`, { headers: { Authorization: "Bearer YOUR_API_KEY", }, } ); const quote = await res.json(); if (!quote.swapTx) { throw new Error(`Quote failed: ${JSON.stringify(quote)}`); } // 2. Execute approval transactions if (quote.approvalTxns?.length) { for (const approvalTx of quote.approvalTxns) { const hash = await walletClient.sendTransaction({ to: approvalTx.to, data: approvalTx.data, }); await publicClient.waitForTransactionReceipt({ hash }); console.log("Approval confirmed:", hash); } } // 3. Execute swap const hash = await walletClient.sendTransaction({ to: quote.swapTx.to, data: quote.swapTx.data, value: quote.swapTx.value ? BigInt(quote.swapTx.value) : 0n, gas: quote.swapTx.gas ? BigInt(quote.swapTx.gas) : undefined, }); console.log("Swap tx:", hash); console.log("Expected fill:", quote.expectedFillTime, "seconds"); return hash; } executeSwap(); ``` ```ts title="swap-ethers.ts" import { ethers } from "ethers"; const provider = new ethers.JsonRpcProvider("https://arb1.arbitrum.io/rpc"); const wallet = new ethers.Wallet("0xYOUR_PRIVATE_KEY", provider); async function executeSwap() { // 1. Get quote const params = new URLSearchParams({ tradeType: "minOutput", originChainId: "42161", destinationChainId: "8453", inputToken: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", outputToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", amount: ethers.parseUnits("100", 6).toString(), depositor: wallet.address, integratorId: "0xdead", }); const res = await fetch( `https://app.across.to/api/swap/approval?${params}`, { headers: { Authorization: "Bearer YOUR_API_KEY", }, } ); const quote = await res.json(); if (!quote.swapTx) { throw new Error(`Quote failed: ${JSON.stringify(quote)}`); } // 2. Execute approval transactions if (quote.approvalTxns?.length) { for (const approvalTx of quote.approvalTxns) { const tx = await wallet.sendTransaction({ to: approvalTx.to, data: approvalTx.data, }); await tx.wait(); console.log("Approval confirmed:", tx.hash); } } // 3. Execute swap const tx = await wallet.sendTransaction({ to: quote.swapTx.to, data: quote.swapTx.data, value: quote.swapTx.value || 0, }); console.log("Swap tx:", tx.hash); console.log("Expected fill:", quote.expectedFillTime, "seconds"); return tx.hash; } executeSwap(); ``` **Integrator ID required for production.** You must obtain a 2-byte hex integrator ID (e.g., `0xdead`) before launching in production. [Get your API key and Integrator ID](/tools/integrator-id). Without it, your requests may be rate-limited or rejected. Helper Endpoints [#helper-endpoints] These endpoints help you discover supported routes, tokens, and fee estimates before calling `/swap/approval`. | Endpoint | Method | Description | | ------------------- | ------ | ---------------------------------------------------- | | `/swap/chains` | GET | List all supported chains | | `/swap/tokens` | GET | List all supported tokens per chain | | `/swap/sources` | GET | List swap providers (DEX sources) | | `/suggested-fees` | GET | *(legacy)* Get fee breakdown for a specific route | | `/limits` | GET | Get transfer limits (min, max, instant, short-delay) | | `/available-routes` | GET | Get all available origin → destination routes | Coming from `/suggested-fees` + direct `depositV3`? See [Migrate from Suggested Fees to the Swap API](/guides/migration/suggested-fees-to-swap). **Integrator ID required for production.** You must obtain a 2-byte hex integrator ID (e.g., `0xdead`) before launching in production. [Get your API key and Integrator ID](/tools/integrator-id). Without it, your requests may be rate-limited or rejected. **Do not cache** `/swap/approval` or `/suggested-fees` responses. Quotes expire quickly and fees change with market conditions. The only stateful endpoint is `/deposit/status`. # Tracking Deposits import { Callout } from 'fumadocs-ui/components/callout'; import { Tabs, Tab } from 'fumadocs-ui/components/tabs'; **API key required for production.** [Get your API key and Integrator ID](/tools/integrator-id) to authenticate your requests. After executing a swap transaction, use the `/deposit/status` endpoint to track whether the deposit has been filled, is still pending, or has expired. Status Values [#status-values] | Status | Description | | ---------- | --------------------------------------------------------- | | `pending` | Deposit submitted but not yet filled by a relayer | | `filled` | Relayer has filled the intent on the destination chain | | `expired` | Fill deadline passed without a fill (eligible for refund) | | `refunded` | Deposit was refunded after expiration | During the pre-fill window (while `status` is `pending` or `received`), the response also returns `originToken` and `destinationToken` — the token addresses taken from the deposit witness. Both are `null` once the deposit is filled. Query Methods [#query-methods] You can query deposit status in two ways: by `depositId` (extracted from the deposit event) or by the deposit transaction hash. Query with the `originChainId` and `depositId` from the `V3FundsDeposited` event. ``` GET /deposit/status?originChainId=42161&depositId=12345 ``` Query with the deposit transaction hash. The API will look up the deposit event from the transaction. ``` GET /deposit/status?depositTxnRef=0xabc123... ``` Extracting the Deposit ID [#extracting-the-deposit-id] When you submit a swap transaction, the origin SpokePool emits a `V3FundsDeposited` event. Extract the `depositId` from this event to use for status tracking. ```ts title="extract-deposit-id.ts" import { createPublicClient, http, parseAbiItem } from "viem"; import { arbitrum } from "viem/chains"; const publicClient = createPublicClient({ chain: arbitrum, transport: http(), }); const SPOKE_POOL_ADDRESS = "0xe35e9842fceaca96570b734083f4a58e8f7c5f2a"; async function getDepositId(txHash: `0x${string}`) { const receipt = await publicClient.getTransactionReceipt({ hash: txHash }); const depositEvent = parseAbiItem( "event V3FundsDeposited(address inputToken, address outputToken, uint256 inputAmount, uint256 outputAmount, uint256 indexed destinationChainId, uint32 indexed depositId, uint32 quoteTimestamp, uint32 fillDeadline, uint32 exclusivityDeadline, address indexed depositor, address recipient, address exclusiveRelayer, bytes message)" ); const logs = await publicClient.getLogs({ address: SPOKE_POOL_ADDRESS, event: depositEvent, fromBlock: receipt.blockNumber, toBlock: receipt.blockNumber, }); if (logs.length === 0) { throw new Error("No V3FundsDeposited event found in transaction"); } const depositId = logs[0].args.depositId; console.log("Deposit ID:", depositId); return depositId; } ``` Polling for Status [#polling-for-status] Poll the `/deposit/status` endpoint until the deposit reaches a terminal state (`filled`, `expired`, or `refunded`). ```ts title="poll-status.ts" const BASE_URL = "https://app.across.to/api"; async function pollDepositStatus( originChainId: number, depositId: number, intervalMs = 10_000, maxAttempts = 60 ): Promise { for (let i = 0; i < maxAttempts; i++) { const params = new URLSearchParams({ originChainId: originChainId.toString(), depositId: depositId.toString(), }); const res = await fetch(`${BASE_URL}/deposit/status?${params}`); const data = await res.json(); console.log(`[Attempt ${i + 1}] Status: ${data.status}`); if (data.status === "filled") { console.log("Deposit filled on destination chain"); return data.status; } if (data.status === "expired") { console.log("Deposit expired — eligible for refund"); return data.status; } if (data.status === "refunded") { console.log("Deposit refunded"); return data.status; } // Wait before next poll await new Promise((resolve) => setTimeout(resolve, intervalMs)); } throw new Error("Polling timed out"); } // Usage const status = await pollDepositStatus(42161, 12345); ``` Polling Recommendations [#polling-recommendations] **10-second polling interval.** The Across indexer has a latency of 1-15 seconds. Polling more frequently than every 10 seconds won't return faster results and will waste API calls. * **Mainnet fills** typically complete in \~2 seconds, but the indexer may take 1-15 seconds to reflect the status * **Testnet fills** take \~1 minute * Set a reasonable timeout (e.g., 10 minutes) to avoid polling indefinitely * For production integrations, consider using a webhook or event-based approach alongside polling Alternative: Query by Transaction Hash [#alternative-query-by-transaction-hash] If you don't want to extract the `depositId`, you can query directly with the transaction hash: ```ts title="poll-by-hash.ts" async function pollByTxHash( txHash: string, intervalMs = 10_000, maxAttempts = 60 ): Promise { for (let i = 0; i < maxAttempts; i++) { const params = new URLSearchParams({ depositTxnRef: txHash }); const res = await fetch(`${BASE_URL}/deposit/status?${params}`); const data = await res.json(); console.log(`[Attempt ${i + 1}] Status: ${data.status}`); if (["filled", "expired", "refunded"].includes(data.status)) { return data.status; } await new Promise((resolve) => setTimeout(resolve, intervalMs)); } throw new Error("Polling timed out"); } ``` # Why Across import { Card, Cards } from 'fumadocs-ui/components/card'; Integration Simplicity [#integration-simplicity] One API call bridges or swaps across any supported chain. `GET /swap/approval` returns ready-to-execute calldata — Across handles routing, settlement, and gas under the hood, so your integration stays the same for every token, amount, and route. Speed [#speed] Across delivers **sub-2-second fills** on mainnet — funds reach the recipient on the destination chain almost immediately, with no waiting on slow finality. The fastest way to feel it is to move something. Cost [#cost] Across is one of the cheapest ways to move assets crosschain: * **Market-based pricing** — every transfer is quoted competitively per route, not charged a flat bridge toll, so you get the lowest sustainable rate. * **Transparent, all-in fees** — each quote returns an explicit `fees` breakdown, so the total cost is known before you submit. No hidden spread, no surprise surcharges. * **Efficient infrastructure** — low operating overhead is passed through to the fee you see, not tacked on top. Security [#security] Across's security model combines: * **Optimistic verification** with UMA's Optimistic Oracle — bundles are accepted unless challenged * **Economic bonds** — proposers stake capital that can be slashed for invalid bundles * **Only one honest actor needed** — a single honest dispute is enough to reject a bad bundle * **V4 ZK proofs** — Succinct/SP1 zero-knowledge proofs provide cryptographic settlement verification Chain Coverage [#chain-coverage] **23+ mainnet chains** and **8 testnet chains**, including first-mover support for Solana, MegaETH, Plasma, Monad, and Hyperliquid. Check whether a specific chain or token is supported:
# AI Transaction Builder Build a Transaction with AI [#build-a-transaction-with-ai] # Chain Checker Check Chain Support [#check-chain-support] # Tools Available Tools [#available-tools] # Get API Key & Integrator ID Get Your API Key & Integrator ID [#get-your-api-key--integrator-id] # Status Tracker Track Your Deposit [#track-your-deposit] # Token Checker Check Token Support [#check-token-support] # Transaction Builder Build a Transaction [#build-a-transaction] # Get all details for a single deposit {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Get all deposits matching the given filters {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Direct Route Linking import { Callout } from 'fumadocs-ui/components/callout'; The Across frontend at `app.across.to` supports URL query parameters that pre-fill the bridge form with a specific route — origin chain, destination chain, input token, and output token. This lets you drop users directly into the exact transfer they need, no manual selection required. Direct route linking Direct route linking opens the **Across UI** with pre-filled fields. For programmatic bridging without a UI, use the [Swap API](/introduction/swap-api) instead. Base URL [#base-url] ``` https://app.across.to/bridge-and-swap ``` Append query parameters to pre-fill the bridge form. Query Parameters [#query-parameters] | Parameter | Type | Description | Example | | ------------- | ------ | ---------------------------------- | ------- | | `from` | number | Origin chain ID | `8453` | | `to` | number | Destination chain ID | `42161` | | `inputToken` | string | Input token symbol on origin chain | `BAL` | | `outputToken` | string | Output token symbol on destination | `USDC` | All parameters are optional — you can specify any combination. Omitted fields will show the default selection in the UI. Examples [#examples] Bridge USDC from Base to Arbitrum [#bridge-usdc-from-base-to-arbitrum] ``` https://app.across.to/bridge-and-swap?from=8453&to=42161&inputToken=USDC&outputToken=USDC ``` Swap BAL on Base to USDC on Arbitrum [#swap-bal-on-base-to-usdc-on-arbitrum] ``` https://app.across.to/bridge-and-swap?from=8453&to=42161&inputToken=BAL&outputToken=USDC ``` Pre-fill only the origin chain [#pre-fill-only-the-origin-chain] ``` https://app.across.to/bridge-and-swap?from=8453 ``` The user lands on the bridge page with Base already selected as the origin. They choose everything else. Bridge ETH from Ethereum to Optimism [#bridge-eth-from-ethereum-to-optimism] ``` https://app.across.to/bridge-and-swap?from=1&to=10&inputToken=ETH&outputToken=ETH ``` Common Chain IDs [#common-chain-ids] | Chain | Chain ID | | ---------- | -------- | | Ethereum | `1` | | Optimism | `10` | | Arbitrum | `42161` | | Base | `8453` | | Polygon | `137` | | ZKsync Era | `324` | | Linea | `59144` | | Scroll | `534352` | | Blast | `81457` | | MegaETH | `4326` | For the full list of supported chains and their IDs, see [Chains & Contracts](/chains-and-contracts). Use Cases [#use-cases] * **Partner integrations** — Link from your dApp directly to Across with the route pre-filled for your users * **Documentation and tutorials** — Provide clickable example links so readers can try a route instantly * **Marketing campaigns** — Share a specific route link in tweets, blog posts, or newsletters * **Wallet integrations** — Deep-link from wallet UIs into the Across bridge with the right tokens selected * **Support flows** — Send users a direct link to the exact route they need help with # User Guides import { Card, Cards } from 'fumadocs-ui/components/card'; Practical guides for using Across Protocol's frontend app and related tools. Link users directly to a pre-filled bridge route on the Across UI using URL query parameters. # BNB Smart Chain Migration Guide import { Callout } from 'fumadocs-ui/components/callout'; Across supports bridging USDC, USDT, and ETH to and from BNB Smart Chain (Chain ID 56). This guide covers the API changes required for developers, integrators, and relayers. BNB Smart Chain is the first chain using [Across V4's ZK settlement](/guides/concepts/across-v4). No testnet is available — test on mainnet with small amounts. API Changes [#api-changes] New outputAmount Field [#new-outputamount-field] The `/suggested-fees` endpoint now returns a calculated `outputAmount` field directly. **Stop calculating output amounts manually.** ```ts title="output-amount-change.ts" // Old approach (deprecated) — do NOT use const outputAmount = inputAmount * (1n * 10n**18n - BigInt(fees.totalRelayFee.pct)) / 10n**18n; // New approach (required) — use the API response directly const outputAmount = fees.outputAmount; ``` allowUnmatchedDecimals Parameter [#allowunmatcheddecimals-parameter] A new optional query parameter `allowUnmatchedDecimals` has been added to `/suggested-fees`. When omitted, the API returns a `4XX` error if input and output tokens have different decimal counts (e.g., USDC has 6 decimals on some chains, 18 on others). Set `allowUnmatchedDecimals=true` if you're intentionally bridging between tokens with different decimals. outputToken Requirements [#outputtoken-requirements] **For BSC routes, you must NOT set `outputToken` to `0x0`.** Integrators must explicitly specify the output token address for all BNB Smart Chain transfers. Deprecation Timeline [#deprecation-timeline] | Date | Change | | ----------------- | -------------------------------------------------------------- | | Now | `outputToken=0x0` still works on non-BSC routes | | **June 19, 2025** | Protocol contracts will prevent `outputToken=0x0` network-wide | **Action required**: Update all integrations to explicitly specify `outputToken` addresses before the June deadline. For Relayers and Indexers [#for-relayers-and-indexers] * Recognize chain ID `56` in your configurations * **Reject** any BSC deposits where `outputToken` is set to `0x0` * Prepare for complete `outputToken=0x0` removal by June 19, 2025 Key Details [#key-details] | Property | Value | | -------------------- | -------------------------------------- | | **Chain ID** | `56` | | **Supported tokens** | USDC, USDT, ETH | | **Deposit function** | `depositV3()` | | **Settlement** | Across V4 (ZK proofs via Succinct SP1) | | **Testnet** | Not available | | **App-SDK** | Supported | # Migration Guides import { Card, Cards } from 'fumadocs-ui/components/card'; Available Migrations [#available-migrations] Move from the legacy `/suggested-fees` + `depositV3` flow to the Swap API — and attribute volume with an Integrator ID. Prepare for USDC bridging on Solana — API changes, deposit flow, and relayer updates. Upgrade from Across V2 to V3 — new events, functions, fee structure, and contract changes. Transition from Bridged USDC to Native USDC via Circle's CCTP. Support non-EVM chains and deterministic relay hashes for prefills. Enable BNB Smart Chain (Chain ID 56) bridging for USDC, USDT, and ETH. # Solana Migration Guide import { Callout } from 'fumadocs-ui/components/callout'; import { Step, Steps } from 'fumadocs-ui/components/steps'; import { Tabs, Tab } from 'fumadocs-ui/components/tabs'; Across supports USDC bridging on Solana, connecting Ethereum Mainnet and other chains to Solana's ecosystem. This guide covers the API changes, deposit flow, and relayer updates required. **No testnet support for Solana.** Across does not support Solana devnet or testnet. You must test on Solana mainnet with small token amounts. The Across App-SDK does not yet support Solana. API Changes [#api-changes] recipient Parameter Required [#recipient-parameter-required] The `/suggested-fees` endpoint now **requires** the `recipient` parameter in Solana **Pubkey format** (base-58 encoded string) when the destination is Solana. Missing or invalid Solana public keys return a `400` error. ```ts title="solana-quote.ts" const params = new URLSearchParams({ inputToken: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", // USDC on Arbitrum outputToken: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC on Solana originChainId: "42161", destinationChainId: "34268394551451", // Solana chain ID amount: "10000000", // 10 USDC recipient: "YourSolanaPublicKeyBase58", // Required — Solana Pubkey format }); const res = await fetch( `https://app.across.to/api/suggested-fees?${params}` ); ``` Response Changes [#response-changes] Response fields display Solana addresses where applicable (base-58 format instead of hex). Transaction Size Limit [#transaction-size-limit] Solana transactions have a **maximum size limit of 1,232 bytes**. This affects deposits with attached messages (embedded actions) from EVM to Solana. If you need large payloads, contact the Across team. Deposit Flow [#deposit-flow] Fetch Quote [#fetch-quote] Call `/suggested-fees` with the Solana chain ID (`34268394551451`) as `destinationChainId` and include the `recipient` in Solana Pubkey format. Add your `integratorId`. Call deposit() on SVM SpokePool [#call-deposit-on-svm-spokepool] Use the deposit function on the SVM SpokePool contract. A [reference script](https://github.com/across-protocol/contracts/blob/master/scripts/svm/simpleDeposit.ts) demonstrates proper invocation with `integratorId`. Track the Deposit [#track-the-deposit] Use the `/deposit/status` endpoint to monitor the fill status. Relayer Updates [#relayer-updates] Relayers must update to the latest versions of: * `@across-protocol/sdk` * `@across-protocol/contracts` * `@solana/kit` (replaces `@solana/web3.js@^2.0.0`) For Solana transactions, relayers transition from `fillV3Relay` to `fillRelay`. Solana-specific fill functions are required, and relayers must monitor events via the `emit_cpi!()` method in Anchor. When filling to or from Solana, relayers **must always specify a valid address on the chain where they request repayment**. Failing to do so may result in **loss of funds**. Key Details [#key-details] | Property | Value | | -------------------- | -------------------------------------------------- | | **Solana Chain ID** | `34268394551451` | | **SVM SpokePool** | `DLv3NggMiSaef97YCkew5xKUHDh13tVGZ7tydt3ZeAru` | | **Supported tokens** | USDC | | **Testnet** | Not available — test on mainnet with small amounts | | **App-SDK** | Not yet supported for Solana | Early Solana support positively affects relayer nomination percentages. If you're running a relayer, supporting Solana fills early can improve your standing. # Migrate from /suggested-fees to the Swap API import { Callout } from 'fumadocs-ui/components/callout'; import { Step, Steps } from 'fumadocs-ui/components/steps'; import { Card, Cards } from 'fumadocs-ui/components/card'; `/suggested-fees` returns a **fee quote only**. You then build and send the SpokePool `depositV3` transaction yourself (encode the calldata, handle the ERC-20 approval, and append your integrator ID to the calldata). The **Swap API** ([`/swap/approval`](/api-reference/swap/approval/get)) collapses all of that into one call: it returns ready-to-execute calldata, any required approval transactions, and the full fee breakdown. **Why migrate** * Approval transactions come **pre-built** (`approvalTxns[]`); no manual ERC-20 `approve`. * No hand-encoded `depositV3` calldata; you send the returned `swapTx` as-is. * Your **Integrator ID is a clean query param** (`integratorId=0x…`); no calldata delimiter. * **Pre-flight `checks`** (allowance + balance) surface problems before you sign. * Swap **any token**, not just bridge the same token; origin/destination swaps are handled for you. * Settlement path is **selected automatically** per route. * Bigger transfer sizes, up to $10M depending on the token/chain. * Easier to maintain. * You can still track deposits via [`GET /deposit/status`](/api-reference/deposit/status/get) — that part is unchanged. * swap/chains and swap/tokens endpoints are available to dynamically discover supported chains and tokens. * sponsorship and app fees are supported via the Swap API. Migrate in four steps [#migrate-in-four-steps] Get an API key + Integrator ID [#get-an-api-key--integrator-id] Production requests need a Bearer API key, and an Integrator ID attributes your volume. [Request both via the integrator form](/tools/integrator-id). Swap the endpoint call [#swap-the-endpoint-call] Replace the `/suggested-fees` request with [`/swap/approval`](/api-reference/swap/approval/get). Add `tradeType=exactInput` and `depositor`, and pass `integratorId` as a **query parameter**. Delete the manual deposit code [#delete-the-manual-deposit-code] Drop the `depositV3` encoding, the calldata delimiter, and your hand-rolled approval. Execute the returned `approvalTxns[]` (if any), then send `swapTx` as-is. Track the deposit [#track-the-deposit] Unchanged. Poll [`GET /deposit/status`](/api-reference/deposit/status/get) until terminal. **Gotchas** * Production needs an API key (`Authorization: Bearer …`). * Once migrated, **stop appending `1dc0de` + your ID to calldata**; it becomes the `integratorId` query param. * Don't cache quotes; they expire. Respect `quoteExpiryTimestamp` and re-quote when stale. Integration changes [#integration-changes] One script, showing exactly what to remove (red) and what to add (green). The `/suggested-fees` call, the hand-encoded `depositV3`, the calldata delimiter, and the manual approval all go; the [`/swap/approval`](/api-reference/swap/approval/get) call and executing the returned transactions replace them. ```ts title="migrate.ts" // 1. Quote const params = new URLSearchParams({ originChainId, destinationChainId, inputToken, outputToken, amount, recipient, tradeType: "exactInput", // [!code ++] depositor: account.address, // [!code ++] integratorId: "0xdead", // your ID as a query param, no calldata delimiter // [!code ++] }); const quote = await fetch( `https://app.across.to/api/suggested-fees?${params}`, // [!code --] `https://app.across.to/api/swap/approval?${params}`, // [!code ++] { headers: { Authorization: "Bearer YOUR_API_KEY" } }, ).then((r) => r.json()); // 2. Deposit const calldata = encodeFunctionData({ // [!code --] abi: spokePoolAbi, // [!code --] functionName: "depositV3", // [!code --] args: [depositor, recipient, inputToken, outputToken, inputAmount, outputAmount, // [!code --] destinationChainId, quote.exclusiveRelayer, quote.timestamp, // [!code --] quote.fillDeadline, quote.exclusivityDeadline, "0x"], // [!code --] }); // [!code --] const data = `${calldata}1dc0de${INTEGRATOR_ID}`; // append integrator ID to raw calldata // [!code --] await approveErc20(inputToken, quote.spokePoolAddress, inputAmount); // [!code --] await walletClient.sendTransaction({ to: quote.spokePoolAddress, data }); // [!code --] for (const tx of quote.approvalTxns ?? []) { // [!code ++] await walletClient.sendTransaction({ to: tx.to, data: tx.data }); // [!code ++] } // [!code ++] await walletClient.sendTransaction({ // [!code ++] to: quote.swapTx.to, // [!code ++] data: quote.swapTx.data, // [!code ++] value: quote.swapTx.value ? BigInt(quote.swapTx.value) : 0n, // [!code ++] }); // [!code ++] ``` Parameter mapping [#parameter-mapping] | Artifact | `/suggested-fees` | `/swap/approval` | | ------------------ | ---------------------------------- | ---------------------------------------- | | Origin chain | `originChainId` | `originChainId` | | Destination chain | `destinationChainId` | `destinationChainId` | | Input token | `inputToken` | `inputToken` | | Output token | `outputToken` | `outputToken` | | Amount | `amount` | `amount` + `tradeType: exactInput` | | Recipient | `recipient` | `recipient` | | Destination call | `message` | embedded actions (POST `/swap/approval`) | | Depositor | *(implicit signer)* | `depositor` (required) | | Integrator ID | calldata delimiter (`1dc0de` + ID) | `integratorId` (query param) | | Quote timestamp | `timestamp` | *(handled internally)* | | Relayer override | `relayer` | *(none)* | | Unmatched decimals | `allowUnmatchedDecimals` | *(handled)* | Response mapping [#response-mapping] | Artifact | `/suggested-fees` | `/swap/approval` | | ---------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------ | | Total relay fee | `totalRelayFee` / `relayFeeTotal` | `fees.total` | | Fee components | `relayerCapitalFee` / `relayerGasFee` / `lpFee` | `fees.total.details` | | Max fee | *(none)* | `fees.totalMax` | | Output amount | `outputAmount` | `expectedOutputAmount` / `minOutputAmount` | | Fill-time estimate | `estimatedFillTimeSec` | `expectedFillTime` | | Limits / too-low check | `limits` / `isAmountTooLow` | `checks` + error codes | | Deposit settings | `fillDeadline`, `exclusiveRelayer`, `exclusivityDeadline`, `quoteBlock`, `spokePoolAddress` | baked into `swapTx` | | Executable transaction | *(you build `depositV3`)* | `swapTx` | | Token approvals | *(you call `approve`)* | `approvalTxns` | | Route type | *(none)* | `crossSwapType` | | Quote identifier | `id` | `id` | Not ready to fully migrate? At least attribute your volume [#not-ready-to-fully-migrate-at-least-attribute-your-volume] If you're staying on the direct `depositV3` flow for now, you should **still send an Integrator ID** so your volume is attributed. On the legacy path the *only* way to do that is to append it to the raw transaction calldata using a fixed delimiter. Append the **delimiter `1dc0de`** followed by your **integrator ID** to the **end of the encoded `depositV3` calldata**: | Component | Value | Description | | ----------------- | ------------------------------ | -------------------------------------------------------------- | | **Delimiter** | `1dc0de` | Fixed 3-byte hex string that marks the start of the identifier | | **Integrator ID** | Your assigned ID (e.g. `f001`) | Unique identifier provided by the Across team | ```ts title="append-integrator-id.ts" import { encodeFunctionData, type Hex } from "viem"; const DELIMITER = "1dc0de"; const INTEGRATOR_ID = "f001"; // Replace with your assigned ID // Encode depositV3 calldata normally... const calldata = encodeFunctionData({ abi: spokePoolAbi, functionName: "depositV3", args: [depositor, recipient, inputToken, outputToken, inputAmount, outputAmount, destinationChainId, exclusiveRelayer, quoteTimestamp, fillDeadline, exclusivityDeadline, message], }); // ...then append the delimiter + integrator ID to the RAW calldata. const calldataWithId = `${calldata}${DELIMITER}${INTEGRATOR_ID}` as Hex; await walletClient.sendTransaction({ to: spokePoolAddress, data: calldataWithId }); ``` **Do NOT** pass the delimiter + ID to any `depositV3` parameter, including `message`. It goes only on the end of the raw transaction calldata. **Do NOT** use `f001`; it's an example. Request your unique ID from the Across team via your shared channel (Telegram, Slack), [the integrator form](/tools/integrator-id). After you migrate to `/swap/approval`, **all of this goes away**: the Integrator ID is just the `integratorId=0x…` query parameter on the request. The delimiter is the interim attribution path; the query param is the destination. Next steps [#next-steps] Trade types, parameters, response shape, and a full integration example. The complete parameter list and response schema. Poll deposit status to terminal; the same for both flows. # Migration from V2 to V3 import { Callout } from 'fumadocs-ui/components/callout'; import { Tabs, Tab } from 'fumadocs-ui/components/tabs'; Across V3 introduces crosschain token swaps, a simplified contract interface, and a restructured fee model. This guide covers all breaking changes for composable bridging, events, relayers, and dApp developers. Summary of Changes [#summary-of-changes] | Component | V2 | V3 | | ------------------------- | ----------------------- | ------------------------- | | **Composable callback** | `handleAcrossMessage()` | `handleV3AcrossMessage()` | | **Deposit event** | `FundsDeposited` | `V3FundsDeposited` | | **Fill event** | `FilledRelay` | `FilledV3Relay` | | **Relayer fill function** | N/A | `fillV3Relay()` | | **Speed-up function** | `speedUpDeposit()` | `speedUpV3Deposit()` | | **Partial fills** | Supported | Not supported | | **LP fee in fill** | Required from relayer | Calculated at refund time | | **Fee API structure** | Flat fields | Nested objects | API Changes [#api-changes] Host Update [#host-update] Migrate from `across.to/api` to `app.across.to/api`. The original endpoint will be sunset. Fee Structure [#fee-structure] V2 used flat fee fields: ``` capitalFeePct + relayGasFeePct = relayFeePct lpFeePct (separate) totalBridgeFee = relayFeePct + lpFeePct ``` These fields (`capitalFeePct`, `capitalFeeTotal`, `relayGasFeePct`, `relayGasFeeTotal`, `lpFeePct`) will be removed. V3 uses structured fee objects: ```json { "totalRelayFee": { "pct": "...", "total": "..." }, "relayerCapitalFee": { "pct": "...", "total": "..." }, "relayerGasFee": { "pct": "...", "total": "..." }, "lpFee": { "pct": "...", "total": "..." } } ``` The `relayFeePct` field now includes `lpFeePct`. The standalone `lpFeePct` returns `"0"`. Smart Contract Changes [#smart-contract-changes] Composable Bridging [#composable-bridging] Recipient contracts must replace `handleAcrossMessage` with `handleV3AcrossMessage`: ```solidity title="V3 callback" function handleV3AcrossMessage( address tokenSent, uint256 amount, address relayer, bytes memory message ) external; ``` The `bool fillCompleted` parameter is removed — partial fills are no longer possible in V3. During the transition, implement support for **both** function signatures. Deprecate `handleAcrossMessage()` after the migration is complete. Deposit Function [#deposit-function] ```solidity title="depositV3" function depositV3( address depositor, address recipient, address inputToken, address outputToken, uint256 inputAmount, uint256 outputAmount, uint256 destinationChainId, address exclusiveRelayer, uint32 quoteTimestamp, uint32 fillDeadline, uint32 exclusivityDeadline, bytes calldata message ) external payable; ``` **Default values for basic usage:** * `fillDeadline`: `MAX_UINT` (no expiration) * `outputToken`: `0x0` (equivalent token on destination) * `exclusivityDeadline`: `0` * `exclusiveRelayer`: `0x0` * `outputAmount`: `inputAmount * (1 - totalRelayFeePct)` ```solidity title="deposit (deprecated)" function deposit( address recipient, address originToken, uint256 amount, uint256 destinationChainId, int64 relayerFeePct, uint32 quoteTimestamp, bytes memory message, uint256 maxCount ) external payable; ``` Speed-Up Function [#speed-up-function] V3 changes from fee-based to output-amount-based speed-ups: ```solidity title="speedUpV3Deposit" function speedUpV3Deposit( address depositor, uint32 depositId, uint256 updatedOutputAmount, address updatedRecipient, bytes calldata updatedMessage, bytes calldata depositorSignature ) external; ``` All three mutable fields (output amount, recipient, message) can be modified in one transaction. Requires the depositor's signature. Event Changes [#event-changes] Deposit Events [#deposit-events] The `V3FundsDeposited` event replaces `FundsDeposited`. Key additions: `outputToken`, `outputAmount`, `fillDeadline`, `exclusivityDeadline`, `exclusiveRelayer`. Fill Events [#fill-events] The `FilledV3Relay` event replaces `FilledRelay`. Key additions: `repaymentChainId` at the event level, `V3RelayExecutionEventInfo` struct with `fillType` enum (`FastFill`, `ReplacedSlowFill`, `SlowFill`). **V3 matching rules:** Events must match on all common parameters except `outputToken`. If the deposit specifies `outputToken = 0x0`, the fill must use the "equivalent" token on the destination chain. Relayer Changes [#relayer-changes] All V3 deposits are filled exclusively via `fillV3Relay()`: ```solidity title="fillV3Relay" function fillV3Relay( V3RelayData calldata relayData, uint256 repaymentChainId ) ``` Key differences from V2: * **LP fee**: Relayers no longer set `realizedLpFeePct` — LP fees are calculated at refund time using UMIP formulas * **Output token**: When `V3FundsDeposited.outputToken == 0x0`, the relayer must substitute the equivalent token on the destination chain * **No partial fills**: Each intent is filled completely or not at all # Create persistent deposit addresses {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Retrieve current transfer limits of the system {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} import { Callout } from 'fumadocs-ui/components/callout'; **Legacy:** The `/limits` API is no longer actively maintained. New integrations should use the [Swap API](/api-reference/swap/approval/get) instead. # Retrieve suggested fee quote for a deposit. {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} import { Callout } from 'fumadocs-ui/components/callout'; **Legacy:** The `/suggested-fees` API is no longer actively maintained. New integrations should use the [Swap API](/api-reference/swap/approval/get) instead. See [Migrate from Suggested Fees to the Swap API](/guides/migration/suggested-fees-to-swap) for a step-by-step guide. # Retrieve available routes for transfers {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} import { Callout } from 'fumadocs-ui/components/callout'; **Legacy:** The `/available-routes` API is no longer actively maintained. New integrations should use the [Swap API](/api-reference/swap/approval/get) instead. # Deposit ETH into Aave import { Callout } from 'fumadocs-ui/components/callout'; This example swaps tokens crosschain to ETH, then deposits the received ETH into Aave's WETH Gateway — all in a single user transaction. The key mechanism is `populateCallValueDynamically: true`, which forwards the entire swapped ETH balance as the contract call's `msg.value`. Action Configuration [#action-configuration] ```json title="action.json" { "actions": [ { "target": "0x5283BEcEd7ADF6D003225C13896E536f2D4264FF", "functionSignature": "function depositETH(address, address onBehalfOf, uint16 referralCode)", "args": [ { "value": "0x0000000000000000000000000000000000000000", "populateDynamically": false }, { "value": "0x718648C8c531F91b528A7757dD2bE813c3940608", "populateDynamically": false }, { "value": "0", "populateDynamically": false } ], "value": "0", "isNativeTransfer": false, "populateCallValueDynamically": true } ] } ``` How It Works [#how-it-works] * **`target`** — Aave's WETH Gateway contract (`0x5283BEcEd7ADF6D003225C13896E536f2D4264FF`) * **`functionSignature`** — `depositETH(address, address onBehalfOf, uint16 referralCode)` * **First arg** — Aave lending pool address (zero address in this case, resolved by the gateway) * **Second arg** — `onBehalfOf` — the address that receives the aToken deposit position * **Third arg** — Aave referral code (`0` for none) * **`populateCallValueDynamically: true`** — The entire ETH balance from the swap is automatically sent as `msg.value` to the `depositETH` call. No need to specify a static `value` Full Example [#full-example] ```ts title="swap-and-deposit-aave.ts" import { createWalletClient, createPublicClient, http, parseUnits } from "viem"; import { arbitrum } from "viem/chains"; import { privateKeyToAccount } from "viem/accounts"; const account = privateKeyToAccount("0xYOUR_PRIVATE_KEY"); const walletClient = createWalletClient({ account, chain: arbitrum, transport: http(), }); const publicClient = createPublicClient({ chain: arbitrum, transport: http(), }); const MULTICALL_HANDLER = "0x924a9f036260DdD5808007E1AA95f08eD08aA569"; const AAVE_WETH_GATEWAY = "0x5283BEcEd7ADF6D003225C13896E536f2D4264FF"; const DEPOSIT_BENEFICIARY = "0x718648C8c531F91b528A7757dD2bE813c3940608"; async function swapAndDepositToAave() { const params = new URLSearchParams({ tradeType: "exactInput", originChainId: "42161", // Arbitrum destinationChainId: "1", // Ethereum inputToken: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", // USDC on Arbitrum outputToken: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", // WETH on Ethereum amount: parseUnits("500", 6).toString(), depositor: account.address, recipient: MULTICALL_HANDLER, }); const response = await fetch( `https://app.across.to/api/swap/approval?${params}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ actions: [ { target: AAVE_WETH_GATEWAY, functionSignature: "function depositETH(address, address onBehalfOf, uint16 referralCode)", args: [ { value: "0x0000000000000000000000000000000000000000", populateDynamically: false, }, { value: DEPOSIT_BENEFICIARY, populateDynamically: false, }, { value: "0", populateDynamically: false, }, ], value: "0", isNativeTransfer: false, populateCallValueDynamically: true, }, ], }), } ); const quote = await response.json(); if (quote.approvalTxns?.length) { for (const approvalTx of quote.approvalTxns) { const hash = await walletClient.sendTransaction({ to: approvalTx.to, data: approvalTx.data, }); await publicClient.waitForTransactionReceipt({ hash }); } } const hash = await walletClient.sendTransaction({ to: quote.swapTx.to, data: quote.swapTx.data, value: quote.swapTx.value ? BigInt(quote.swapTx.value) : 0n, }); console.log("Swap + Aave deposit tx:", hash); } swapAndDepositToAave(); ``` `populateCallValueDynamically: true` tells the MulticallHandler to use the **entire ETH balance** received from the swap as `msg.value` for the `depositETH` call. This is the key mechanism for forwarding native ETH into DeFi protocols. # Add Liquidity to Across HubPool import { Callout } from 'fumadocs-ui/components/callout'; This example swaps tokens crosschain to ETH, then adds that ETH as liquidity to the Across HubPool — all in a single transaction. It demonstrates using `populateDynamically: true` with native ETH balance, where the same balance serves as both a function parameter and the transaction's `msg.value`. Action Configuration [#action-configuration] ```json title="action.json" { "actions": [ { "target": "0xc186fA914353c44b2E33eBE05f21846F1048bEda", "functionSignature": "function addLiquidity(address l1Token, uint256 l1TokenAmount)", "args": [ { "value": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "populateDynamically": false }, { "value": "0", "populateDynamically": true, "balanceSourceToken": "0x0000000000000000000000000000000000000000" } ], "value": "0", "isNativeTransfer": false, "populateCallValueDynamically": true } ] } ``` How It Works [#how-it-works] * **`target`** — The Across HubPool contract on Ethereum (`0xc186fA914353c44b2E33eBE05f21846F1048bEda`) * **`functionSignature`** — `addLiquidity(address l1Token, uint256 l1TokenAmount)` * **First arg (l1Token)** — WETH address on Ethereum, static * **Second arg (l1TokenAmount)** — Dynamically populated from the native ETH balance. `balanceSourceToken` is set to the zero address, which references native ETH * **`populateCallValueDynamically: true`** — Forwards the entire ETH balance as `msg.value` When an argument is populated dynamically from the native ETH balance (`balanceSourceToken: "0x0000..."`), the transaction's `msg.value` is **automatically and implicitly** set to the same amount. This means both the function parameter and the call value use the same balance. Full Example [#full-example] ```ts title="swap-and-add-liquidity.ts" import { createWalletClient, createPublicClient, http, parseUnits } from "viem"; import { arbitrum } from "viem/chains"; import { privateKeyToAccount } from "viem/accounts"; const account = privateKeyToAccount("0xYOUR_PRIVATE_KEY"); const walletClient = createWalletClient({ account, chain: arbitrum, transport: http(), }); const publicClient = createPublicClient({ chain: arbitrum, transport: http(), }); const MULTICALL_HANDLER_ETH = "0x924a9f036260DdD5808007E1AA95f08eD08aA569"; const HUBPOOL = "0xc186fA914353c44b2E33eBE05f21846F1048bEda"; const WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; async function swapAndAddLiquidity() { const params = new URLSearchParams({ tradeType: "exactInput", originChainId: "42161", // Arbitrum destinationChainId: "1", // Ethereum inputToken: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", // USDC on Arbitrum outputToken: WETH, amount: parseUnits("1000", 6).toString(), depositor: account.address, recipient: MULTICALL_HANDLER_ETH, }); const response = await fetch( `https://app.across.to/api/swap/approval?${params}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ actions: [ { target: HUBPOOL, functionSignature: "function addLiquidity(address l1Token, uint256 l1TokenAmount)", args: [ { value: WETH, populateDynamically: false, }, { value: "0", populateDynamically: true, balanceSourceToken: "0x0000000000000000000000000000000000000000", }, ], value: "0", isNativeTransfer: false, populateCallValueDynamically: true, }, ], }), } ); const quote = await response.json(); if (quote.approvalTxns?.length) { for (const approvalTx of quote.approvalTxns) { const hash = await walletClient.sendTransaction({ to: approvalTx.to, data: approvalTx.data, }); await publicClient.waitForTransactionReceipt({ hash }); } } const hash = await walletClient.sendTransaction({ to: quote.swapTx.to, data: quote.swapTx.data, value: quote.swapTx.value ? BigInt(quote.swapTx.value) : 0n, }); console.log("Swap + HubPool liquidity tx:", hash); } swapAndAddLiquidity(); ``` # Embedded Crosschain Actions import { Callout } from 'fumadocs-ui/components/callout'; import { Card, Cards } from 'fumadocs-ui/components/card'; import { Step, Steps } from 'fumadocs-ui/components/steps'; Embedded crosschain actions let you compose arbitrary destination-chain operations — token transfers, DeFi deposits, contract calls — into a single crosschain transaction via the Swap API. Instead of bridging first and then executing separately, the user submits one transaction and everything happens atomically on the destination. **API key required for production.** [Get your API key and Integrator ID](/tools/integrator-id) to authenticate your requests. How It Works [#how-it-works] Submit Intent with Actions [#submit-intent-with-actions] Call `POST /swap/approval` with an `actions` array in the request body. Each action defines a contract call or native transfer to execute after the swap lands on the destination chain. Relayer Includes Message [#relayer-includes-message] The relayer fills the intent on the destination chain and includes the same `message` field to ensure repayment. This message encodes your actions. SpokePool Executes Actions [#spokepool-executes-actions] When the `message` field is non-empty and the recipient is a contract, the destination SpokePool calls `handleV3AcrossMessage()` on the `MulticallHandler` contract. If any action uses dynamic balances, it calls `makeCallWithBalance()` to inject runtime token amounts. Action Object Schema [#action-object-schema] Each action in the `actions` array has this structure: ```json title="action-schema.json" { "target": "0x...", "functionSignature": "function transfer(address to, uint256 value)", "args": [ { "value": "0x...", "populateDynamically": false }, { "value": "0", "populateDynamically": true, "balanceSourceToken": "0x..." } ], "value": "0", "isNativeTransfer": false, "populateCallValueDynamically": false } ``` Field Reference [#field-reference] | Field | Type | Description | | ------------------------------ | --------- | ------------------------------------------------------------------------------------------------------------------ | | `target` | `string` | Contract or recipient address on the destination chain | | `functionSignature` | `string` | Solidity function signature (e.g., `"function transfer(address,uint256)"`). Empty string `""` for native transfers | | `args` | `array` | Ordered array of function arguments | | `args[].value` | `string` | Argument value. For dynamic args, this is ignored at runtime | | `args[].populateDynamically` | `boolean` | If `true`, the actual token balance at execution time replaces this value | | `args[].balanceSourceToken` | `string` | Token address whose balance to inject. Use `0x0000000000000000000000000000000000000000` for native ETH | | `value` | `string` | Static `msg.value` in wei to send with the call | | `isNativeTransfer` | `boolean` | If `true`, this is a simple ETH transfer — `functionSignature` must be `""` and `args` must be `[]` | | `populateCallValueDynamically` | `boolean` | If `true`, the entire native balance from the swap is used as `msg.value` | Deciding How to Configure an Action [#deciding-how-to-configure-an-action] Determine the Action Type [#determine-the-action-type] * **Native ETH transfer**: Set `isNativeTransfer: true`, leave `functionSignature` as `""` and `args` as `[]` * **Contract call**: Set `isNativeTransfer: false` and provide the full `functionSignature` * **Contract call with ETH value**: Choose between a static `value` or set `populateCallValueDynamically: true` to forward the entire swapped ETH balance Define Function Parameters [#define-function-parameters] For each argument: * **Static value** (e.g., a recipient address): set `populateDynamically: false` and provide the `value` * **Dynamic value** (e.g., the swapped token amount): set `populateDynamically: true` and specify `balanceSourceToken` with the token address whose balance should be injected Handle Native ETH as Value [#handle-native-eth-as-value] When a dynamic argument references native ETH (`balanceSourceToken: "0x0000..."`), the transaction's `msg.value` is automatically set to the same amount. You don't need to set `populateCallValueDynamically` separately in this case. **Avoid `exactOutput` with dynamic balances.** When using `tradeType: exactOutput`, some routes may return more tokens than expected. If your embedded action requires an exact amount, use static values instead of `populateDynamically: true`. API Request Format [#api-request-format] Embedded actions use the `POST` method on `/swap/approval`. Query parameters remain the same as `GET`, but the actions go in the request body: ```ts title="post-with-actions.ts" const params = new URLSearchParams({ tradeType: "exactInput", originChainId: "42161", destinationChainId: "1", inputToken: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", outputToken: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", amount: "1000000000", depositor: "0xYourWalletAddress", recipient: "0x924a9f036260DdD5808007E1AA95f08eD08aA569", // MulticallHandler }); const response = await fetch( `https://app.across.to/api/swap/approval?${params}`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: "Bearer YOUR_API_KEY", }, body: JSON.stringify({ actions: [ { target: "0x...", functionSignature: "function transfer(address,uint256)", args: [ { value: "0xRecipient", populateDynamically: false }, { value: "0", populateDynamically: true, balanceSourceToken: "0x..." }, ], value: "0", isNativeTransfer: false, populateCallValueDynamically: false, }, ], }), } ); ``` The `recipient` parameter should be set to the **MulticallHandler** contract address on the destination chain. This is the contract that executes your actions. See [contract addresses](/api-reference) for each chain. Examples [#examples] Transfer swapped tokens to a different address on the destination chain. Swap to ETH and deposit directly into Aave's lending pool. Swap to ETH and add liquidity to the Across HubPool in one transaction. Send native ETH to an address after a crosschain swap. Handle tuple and struct parameters in function signatures. # Simple Native ETH Transfer import { Callout } from 'fumadocs-ui/components/callout'; This is the simplest embedded action — swap tokens crosschain to ETH, then transfer the ETH to a recipient address. Setting `isNativeTransfer: true` tells the MulticallHandler this is a plain value transfer with no contract function call. Action Configuration [#action-configuration] ```json title="action.json" { "actions": [ { "target": "0xA4d353BBc130cbeF1811f27ac70989F9d568CeAB", "functionSignature": "", "args": [], "value": "0", "isNativeTransfer": true, "populateCallValueDynamically": true } ] } ``` How It Works [#how-it-works] * **`target`** — The address that receives the ETH * **`functionSignature`** — Must be an empty string `""` for native transfers * **`args`** — Must be an empty array `[]` for native transfers * **`isNativeTransfer: true`** — Signals a simple ETH transfer, not a contract call * **`populateCallValueDynamically: true`** — The entire ETH balance from the swap is sent to the `target` address When both `isNativeTransfer` and `populateCallValueDynamically` are `true`, the entire ETH balance from the swap is sent directly to the `target` address. The actual amount is determined at execution time. Full Example [#full-example] ```ts title="swap-and-transfer-eth.ts" import { createWalletClient, createPublicClient, http, parseUnits } from "viem"; import { arbitrum } from "viem/chains"; import { privateKeyToAccount } from "viem/accounts"; const account = privateKeyToAccount("0xYOUR_PRIVATE_KEY"); const walletClient = createWalletClient({ account, chain: arbitrum, transport: http(), }); const publicClient = createPublicClient({ chain: arbitrum, transport: http(), }); const MULTICALL_HANDLER_ETH = "0x924a9f036260DdD5808007E1AA95f08eD08aA569"; const ETH_RECIPIENT = "0xA4d353BBc130cbeF1811f27ac70989F9d568CeAB"; async function swapAndTransferEth() { const params = new URLSearchParams({ tradeType: "exactInput", originChainId: "42161", // Arbitrum destinationChainId: "1", // Ethereum inputToken: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", // USDC on Arbitrum outputToken: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", // WETH on Ethereum amount: parseUnits("200", 6).toString(), depositor: account.address, recipient: MULTICALL_HANDLER_ETH, }); const response = await fetch( `https://app.across.to/api/swap/approval?${params}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ actions: [ { target: ETH_RECIPIENT, functionSignature: "", args: [], value: "0", isNativeTransfer: true, populateCallValueDynamically: true, }, ], }), } ); const quote = await response.json(); if (quote.approvalTxns?.length) { for (const approvalTx of quote.approvalTxns) { const hash = await walletClient.sendTransaction({ to: approvalTx.to, data: approvalTx.data, }); await publicClient.waitForTransactionReceipt({ hash }); } } const hash = await walletClient.sendTransaction({ to: quote.swapTx.to, data: quote.swapTx.data, value: quote.swapTx.value ? BigInt(quote.swapTx.value) : 0n, }); console.log("Swap + ETH transfer tx:", hash); } swapAndTransferEth(); ``` # Handling Nested Parameters import { Callout } from 'fumadocs-ui/components/callout'; Some destination-chain contracts accept complex parameters — arrays of structs, nested tuples, or compound types. The Swap API handles these by treating nested arguments collectively: you define them as arrays or tuples in the `value` property and apply `populateDynamically` and `balanceSourceToken` to the entire group, not individual elements. How Nested Parameters Work [#how-nested-parameters-work] Each argument in the `args` array supports three properties: | Property | Type | Description | | --------------------- | --------- | --------------------------------------------------------------------------------- | | `value` | `string` | The parameter data — can be a single value or a JSON-encoded array for tuples | | `populateDynamically` | `boolean` | If `true`, the runtime balance replaces the value for the entire nested structure | | `balanceSourceToken` | `string` | Token address for dynamic population (required when `populateDynamically: true`) | For nested types, the `value` contains an inner array with all struct fields in the exact order defined by the ABI. Example: ERC-4337 handleOps [#example-erc-4337-handleops] This example calls `handleOps` on an EntryPoint contract, which accepts an array of `UserOperation` structs: ```json title="nested-action.json" { "actions": [ { "target": "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", "functionSignature": "function handleOps((address,uint256,bytes,bytes,bytes32,uint256,bytes32,bytes,bytes)[],address)", "args": [ { "value": [ [ "0xSenderAddress", "0", "0x", "0xCallData", "0x0000000000000000000000000000000000000000000000000000000000000000", "100000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x", "0xSignature" ] ], "populateDynamically": false }, { "value": "0xBeneficiaryAddress", "populateDynamically": false } ], "value": "0", "isNativeTransfer": false, "populateCallValueDynamically": false } ] } ``` Breaking It Down [#breaking-it-down] The function signature is: ``` function handleOps((address,uint256,bytes,bytes,bytes32,uint256,bytes32,bytes,bytes)[],address) ``` This accepts: 1. An **array of UserOperation tuples** — each tuple contains 9 fields (sender, nonce, initCode, callData, accountGasLimits, preVerificationGas, gasFees, paymasterAndData, signature) 2. A **beneficiary address** In the `args` array: * **First arg** — The outer array `[...]` contains one inner array `[...]` representing a single UserOperation. Each element in the inner array corresponds to a tuple field, in exact ABI order * **Second arg** — The beneficiary address, a simple static value Key Rules [#key-rules] **Order matters.** Values in the nested array must match the tuple definition exactly as specified in the ABI. Getting the order wrong will cause the transaction to revert. * **Collective application** — `populateDynamically` and `balanceSourceToken` apply to the entire nested structure, not individual fields within a tuple * **Type flexibility** — Nested parameters can mix addresses, uints, bytes, and other types as long as they maintain proper ABI ordering * **Array of structs** — Wrap each struct as an inner array, then wrap all structs in an outer array Encoding Verification [#encoding-verification] If you're unsure about the encoding, you can verify with viem's `encodeAbiParameters`: ```ts title="verify-encoding.ts" import { encodeAbiParameters, parseAbiParameters } from "viem"; // Verify your tuple encoding matches what the contract expects const encoded = encodeAbiParameters( parseAbiParameters( "(address,uint256,bytes,bytes,bytes32,uint256,bytes32,bytes,bytes)[],address" ), [ [ [ "0xSenderAddress", 0n, "0x", "0xCallData", "0x0000000000000000000000000000000000000000000000000000000000000000", 100000n, "0x0000000000000000000000000000000000000000000000000000000000000000", "0x", "0xSignature", ], ], "0xBeneficiaryAddress", ] ); console.log("Encoded:", encoded); ``` # Transfer ERC-20 Tokens After Swap import { Callout } from 'fumadocs-ui/components/callout'; This example demonstrates a common post-swap operation: transferring the newly acquired ERC-20 tokens to a different address on the destination chain. The swap lands tokens in the MulticallHandler, which then executes the transfer. Action Configuration [#action-configuration] The action targets the output token's ERC-20 contract and calls `transfer()`. The amount is dynamically populated with the actual swap output balance. ```json title="action.json" { "actions": [ { "target": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", "functionSignature": "function transfer(address to, uint256 value)", "args": [ { "value": "0x718648C8c531F91b528A7757dD2bE813c3940608", "populateDynamically": false }, { "value": "0", "populateDynamically": true, "balanceSourceToken": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831" } ], "value": "0", "isNativeTransfer": false, "populateCallValueDynamically": false } ] } ``` How It Works [#how-it-works] * **`target`** — The ERC-20 token contract on the destination chain (USDC on Arbitrum in this example) * **`functionSignature`** — Standard ERC-20 `transfer(address, uint256)` function * **First arg (recipient)** — Static address where tokens should be sent. `populateDynamically: false` means this value is used as-is * **Second arg (amount)** — Set to `populateDynamically: true` with `balanceSourceToken` pointing to the same token contract. At execution time, the MulticallHandler replaces `"0"` with the actual token balance received from the swap Full Example [#full-example] ```ts title="transfer-erc20-after-swap.ts" import { createWalletClient, createPublicClient, http, parseUnits } from "viem"; import { arbitrum } from "viem/chains"; import { privateKeyToAccount } from "viem/accounts"; const account = privateKeyToAccount("0xYOUR_PRIVATE_KEY"); const walletClient = createWalletClient({ account, chain: arbitrum, transport: http(), }); const publicClient = createPublicClient({ chain: arbitrum, transport: http(), }); // MulticallHandler on Arbitrum const MULTICALL_HANDLER = "0x924a9f036260DdD5808007E1AA95f08eD08aA569"; // USDC on Arbitrum const USDC_ARBITRUM = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"; // Final recipient for the tokens const FINAL_RECIPIENT = "0x718648C8c531F91b528A7757dD2bE813c3940608"; async function swapAndTransfer() { const params = new URLSearchParams({ tradeType: "exactInput", originChainId: "8453", // Base destinationChainId: "42161", // Arbitrum inputToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base outputToken: USDC_ARBITRUM, amount: parseUnits("100", 6).toString(), depositor: account.address, recipient: MULTICALL_HANDLER, }); const response = await fetch( `https://app.across.to/api/swap/approval?${params}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ actions: [ { target: USDC_ARBITRUM, functionSignature: "function transfer(address to, uint256 value)", args: [ { value: FINAL_RECIPIENT, populateDynamically: false }, { value: "0", populateDynamically: true, balanceSourceToken: USDC_ARBITRUM, }, ], value: "0", isNativeTransfer: false, populateCallValueDynamically: false, }, ], }), } ); const quote = await response.json(); // Execute approvals if (quote.approvalTxns?.length) { for (const approvalTx of quote.approvalTxns) { const hash = await walletClient.sendTransaction({ to: approvalTx.to, data: approvalTx.data, }); await publicClient.waitForTransactionReceipt({ hash }); } } // Execute swap const hash = await walletClient.sendTransaction({ to: quote.swapTx.to, data: quote.swapTx.data, value: quote.swapTx.value ? BigInt(quote.swapTx.value) : 0n, }); console.log("Swap + transfer tx:", hash); } swapAndTransfer(); ``` The `recipient` in the query params is the **MulticallHandler** contract, not the final token recipient. The MulticallHandler receives the tokens, then executes your action to transfer them to the actual recipient. # Disputing Root Bundles import { Callout } from 'fumadocs-ui/components/callout'; import { Step, Steps } from 'fumadocs-ui/components/steps'; import { Tabs, Tab } from 'fumadocs-ui/components/tabs'; Anyone can dispute a root bundle proposal if they believe it is invalid. Disputes require posting a bond, and the outcome is resolved by UMA's Data Verification Mechanism (DVM) — a decentralized voting system where UMA token holders determine whether the proposal was valid. Why Disputes Matter [#why-disputes-matter] The bond mechanism serves two purposes: 1. **Prevents fraud** — Invalid proposals that would move funds incorrectly can be challenged before execution 2. **Prevents denial-of-service** — The bond requirement makes it expensive to file frivolous disputes that block valid proposals If a dispute succeeds (the proposal was invalid), the proposer loses their bond to the disputer. If a dispute fails (the proposal was valid), the disputer loses their bond to the proposer. Manual Dispute Process [#manual-dispute-process] Check Bond Requirements [#check-bond-requirements] Query the HubPool contract to determine the required bond token and amount. ```ts title="check-bond.ts" import { createPublicClient, http, parseAbi } from "viem"; import { mainnet } from "viem/chains"; const publicClient = createPublicClient({ chain: mainnet, transport: http(), }); const HUBPOOL = "0xc186fA914353c44b2E33eBE05f21846F1048bEda"; const hubPoolAbi = parseAbi([ "function bondToken() view returns (address)", "function bondAmount() view returns (uint256)", ]); const bondToken = await publicClient.readContract({ address: HUBPOOL, abi: hubPoolAbi, functionName: "bondToken", }); const bondAmount = await publicClient.readContract({ address: HUBPOOL, abi: hubPoolAbi, functionName: "bondAmount", }); console.log("Bond token:", bondToken); console.log("Bond amount:", bondAmount); // Typically ~0.45 ABT ``` Mint Across Bond Token (ABT) [#mint-across-bond-token-abt] ABT functions like WETH — deposit ETH to mint ABT. ```ts title="mint-abt.ts" import { parseAbi, parseEther } from "viem"; // bondToken address from step 1 const ABT_ADDRESS = bondToken; const mintHash = await walletClient.writeContract({ address: ABT_ADDRESS, abi: parseAbi(["function deposit() payable"]), functionName: "deposit", value: parseEther("0.5"), // Slightly more than required bond }); await publicClient.waitForTransactionReceipt({ hash: mintHash }); console.log("ABT minted:", mintHash); ``` Approve HubPool [#approve-hubpool] Grant the HubPool contract permission to transfer your ABT bond. ```ts title="approve-hubpool.ts" const approveHash = await walletClient.writeContract({ address: ABT_ADDRESS, abi: parseAbi(["function approve(address spender, uint256 amount)"]), functionName: "approve", args: [HUBPOOL, bondAmount], }); await publicClient.waitForTransactionReceipt({ hash: approveHash }); console.log("Approval confirmed:", approveHash); ``` Submit Dispute [#submit-dispute] Call `disputeRootBundle()` on the HubPool contract. ```ts title="dispute.ts" const disputeHash = await walletClient.writeContract({ address: HUBPOOL, abi: parseAbi(["function disputeRootBundle()"]), functionName: "disputeRootBundle", }); console.log("Dispute submitted:", disputeHash); ``` The dispute is now escalated to UMA's DVM for resolution. Automated Disputes [#automated-disputes] The Across relayer repository includes automated dispute tooling that handles bond minting, approval, and submission. ```bash title="setup" git clone https://github.com/across-protocol/relayer-v3.git cd relayer-v3 yarn install && yarn build ``` Configure your `.env` file with a mnemonic for the EOA that will submit the dispute: ```bash title=".env" SECRET=your_mnemonic_phrase_here ``` Ensure the account is funded with: * At least **0.45 ETH** (to mint ABT for the bond) * Additional ETH for transaction gas costs ```bash title="run-dispute" yarn dispute ``` The script will: 1. Check if there's a pending proposal to dispute 2. Mint ABT if needed 3. Approve the HubPool 4. Prompt you for confirmation with a transaction hash 5. Submit the dispute transaction **Only dispute if you are confident the proposal is invalid.** If the DVM votes that the proposal was valid, you lose your bond (\~0.45 ABT / ETH). Run validation checks before disputing — see [Validating Root Bundles](/introduction/security/validating-root-bundles). After a Dispute [#after-a-dispute] Once a dispute is filed: 1. The pending bundle proposal is **paused** — it cannot be executed 2. The dispute is escalated to UMA's DVM 3. UMA token holders **vote** on whether the proposal was valid per UMIP-157 / UMIP-179 rules 4. The vote resolves: * **Proposal invalid** → Proposer's bond goes to the disputer. A new bundle must be proposed. * **Proposal valid** → Disputer's bond goes to the proposer. The bundle is executed. # Security Model and Verification import { Callout } from 'fumadocs-ui/components/callout'; import { Card, Cards } from 'fumadocs-ui/components/card'; import { Step, Steps } from 'fumadocs-ui/components/steps'; Across uses **optimistic verification** to secure crosschain transfers. Rather than requiring every transaction to be proven before execution, the protocol assumes proposals are valid unless challenged — and provides strong economic incentives to catch any invalid proposals. Core Security Framework [#core-security-framework] The security model rests on two foundations: 1. **Across smart contracts** — The on-chain rules governing deposits, fills, and settlement across all supported chains 2. **UMIP-157 and UMIP-179** — UMA Improvement Proposals that precisely define the constraints and rules for bundle construction and verification All actors in the system — relayers, dataworkers, and validators — must behave in accordance with these rules. Any deviation can be detected and disputed. How Optimistic Verification Works [#how-optimistic-verification-works] Bundle Proposal [#bundle-proposal] The dataworker aggregates fills from all chains into a **root bundle** — a set of merkle roots encoding relayer refunds, token rebalancing instructions, and slow fill data. The dataworker proposes this bundle to the HubPool on Ethereum and posts a **bond** (denominated in ABT, the Across Bond Token). Challenge Period [#challenge-period] After proposal, the bundle enters a challenge period. During this window, anyone can inspect the proposed bundle and verify it against the on-chain data. Dispute or Finalization [#dispute-or-finalization] * **No dispute** — The bundle is accepted as valid and executed. The dataworker's bond is returned. * **Dispute filed** — A challenger posts their own bond and the dispute is escalated to UMA's Data Verification Mechanism (DVM) for resolution. DVM Resolution (if disputed) [#dvm-resolution-if-disputed] UMA token holders vote on whether the proposed bundle was valid. The losing party forfeits their bond to the winning party. The 1-of-N Trust Assumption [#the-1-of-n-trust-assumption] The system only requires **a single honest actor** to dispute invalid proposals to remain secure. This is fundamentally different from validator-set models that require a majority or supermajority to be honest. In practice, multiple independent parties monitor proposals: * The Across dataworker itself validates before proposing * Independent validators run verification software * Community members can monitor and dispute The economic cost of attacking Across is the bond posted with each proposal. If an invalid proposal is disputed and the DVM votes against it, the proposer loses their bond. This creates a strong deterrent against malicious proposals. Bonds and Economic Security [#bonds-and-economic-security] Across Bond Token (ABT) [#across-bond-token-abt] The Across Bond Token is a custom ERC-20 that functions similarly to WETH. Users deposit Ether to mint ABT and can redeem it for the underlying Ether at any time. ABT has an additional property: it **restricts which addresses may propose root bundles** to the HubPool. This adds a permissioning layer on top of the economic bond requirement. Bond Requirements [#bond-requirements] | Action | Bond Required | | ----------------------- | ------------- | | Proposing a root bundle | \~0.45 ABT | | Disputing a root bundle | \~0.45 ABT | The bond amount is configurable via the HubPool contract's `bondAmount()` function. What Root Bundles Contain [#what-root-bundles-contain] Root bundles instruct the Across system on how to transfer funds between smart contracts on different chains. Each bundle contains: | Component | Purpose | | ----------------------- | ----------------------------------------------------------------------- | | **Relayer refund root** | Merkle root of all relayer repayments for the period | | **Rebalance root** | Instructions for moving tokens between SpokePools via canonical bridges | | **Slow fill root** | Instructions for fulfilling deposits that weren't filled by relayers | | **Block ranges** | The range of blocks on each chain covered by this bundle | Learn More [#learn-more] How to challenge an invalid proposal — manually or with automated tooling. How root bundles are validated and what the verification process checks. # Validating Root Bundles import { Callout } from 'fumadocs-ui/components/callout'; import { Step, Steps } from 'fumadocs-ui/components/steps'; Root bundles instruct the Across system on how to transfer funds between smart contracts on different chains to refund relayers and fulfill user deposits. They are **optimistically validated** — proposed by the dataworker and accepted unless challenged — and ultimately secured by UMA's Oracle. What Gets Validated [#what-gets-validated] A valid root bundle must correctly encode all activity that occurred across every supported chain during the covered block range. Validators check that the proposed merkle roots accurately represent: | Check | Description | | ---------------------------- | ----------------------------------------------------------------------------------------------------- | | **Fill validity** | Every relayer fill included in the bundle corresponds to a real user deposit with matching parameters | | **Deposit matching** | Fills match deposits on input/output tokens, amounts, chains, and recipients | | **No double-counting** | Each fill is counted exactly once — no duplicate refunds | | **Block range correctness** | The claimed block ranges cover the right period on each chain | | **Rebalancing instructions** | Crosschain token transfers correctly balance the SpokePools | | **Slow fill accuracy** | Unfilled deposits are correctly identified and included | | **Fee calculations** | LP fees and relayer repayment amounts are computed correctly per UMIP-157 / UMIP-179 | How Validation Works [#how-validation-works] Read the Proposal [#read-the-proposal] When a dataworker proposes a bundle to the HubPool, the proposal includes: * Merkle roots for relayer refunds, rebalancing, and slow fills * Block ranges for each chain * The bond posted by the proposer Reconstruct the Expected Bundle [#reconstruct-the-expected-bundle] An independent validator replays all on-chain events (deposits, fills) across every supported chain for the proposed block ranges. Using the same rules defined in UMIP-157 and UMIP-179, they reconstruct what the merkle roots **should** be. Compare Roots [#compare-roots] The validator compares their independently computed roots against the proposed roots. If they match, the proposal is valid. If they differ, it indicates an error or malicious proposal. Dispute if Invalid [#dispute-if-invalid] If the roots don't match, the validator files a dispute on the HubPool. See [Disputing Root Bundles](/introduction/security/disputing-root-bundles) for the dispute process. Running a Validator [#running-a-validator] The Across dataworker software can run in **validation mode**, where it independently reconstructs bundles and compares them against proposals without submitting proposals itself. ```bash title="run-validator" git clone https://github.com/across-protocol/relayer-v3.git cd relayer-v3 yarn install && yarn build ``` Configure `.env` with RPC providers for all supported chains: ```bash title=".env" SECRET=your_mnemonic_phrase_here RPC_PROVIDER_INFURA_1=your_eth_rpc RPC_PROVIDER_INFURA_42161=your_arb_rpc RPC_PROVIDER_INFURA_8453=your_base_rpc RPC_PROVIDER_INFURA_10=your_op_rpc # ... RPCs for all supported chains ``` Validators need **archive RPC access** for all supported chains. Standard RPC endpoints may not support the historical queries needed to reconstruct bundles accurately. Rules Governing Bundle Construction [#rules-governing-bundle-construction] The precise rules for what constitutes a valid bundle are defined in two UMA Improvement Proposals: UMIP-157 [#umip-157] The original specification for Across V2 bundle construction, covering: * How block ranges are determined per chain * How fills are validated against deposits * How relayer refunds are calculated * How rebalancing instructions are generated * Merkle tree construction UMIP-179 [#umip-179] An extension to UMIP-157 that adds: * Updated rules for Across V3 intent structure * Additional validation rules for new features (exclusivity, embedded actions) * Updated fee calculation logic Anyone can validate bundles — the rules are public and the data is on-chain. The more independent validators monitoring proposals, the stronger the security guarantee. You only need **one honest validator** to catch an invalid proposal. # Relayer Nomination Relayer nomination grants a single relayer temporary exclusive rights to fulfill a specific deposit. The protocol assigns exclusivity through the `exclusiveRelayer` and `exclusivityDeadline` parameters at deposit creation, emitted via the `V3FundsDeposited` event. The designated relayer maintains fill rights until the deadline on the destination chain. After expiration, any relayer may complete unfilled deposits. Why Nomination? [#why-nomination] Without nomination, relayers race to fill orders — creating collision risks and escalating gas price pressure. Exclusivity retains the speed gains that Across has unlocked for users while reducing uncertainty and risk for relayers. Additional benefits include: * **Performance evaluation** — the protocol can measure and score relayer reliability * **Order flow preferences** — a pathway toward rewarding top-performing relayers Participation Requirements [#participation-requirements] 1. **Apply** — submit a pull request to the [Exclusive Relayer Configuration repository](https://github.com/across-protocol/exclusive-relayer-configs). 2. **Build a track record** — new participants must demonstrate capability through step-in fills (completing deposits abandoned by other nominated relayers) and sub-10 second fill times on open order flow. 3. **Receive scores** — selected relayers receive biweekly performance scores based on three metrics: | Metric | Description | | --------------- | ------------------------------------------------------------------ | | Completion Rate | Percentage of assigned fills successfully executed | | Fill Speed | Transaction timing benchmarked against non-exclusive baseline | | Step-in Fills | Ability to complete deposits abandoned by other nominated relayers | Implementation Status [#implementation-status] The system is rolling out incrementally across select Across API routes. Most frontend flow currently has assigned exclusive relayers. Third-party integrators are expected to implement relayer exclusivity during Q1-Q2 2025. # Running a Relayer The Across V3 Relayer is a Node.js-based system that enables users to operate their own relay infrastructure. The complete source code is available in the [relayer-v3 GitHub repository](https://github.com/across-protocol/relayer-v3). System Requirements [#system-requirements] | Component | Minimum | | --------- | ---------------------------- | | CPU | 64-bit Dual Core @ 2+ GHz | | RAM | 4 GB | | OS | UNIX-like (GNU/Linux, macOS) | Installation [#installation] Clone the repository and set up your environment: ```bash git clone https://github.com/across-protocol/relayer-v3.git cd relayer-v3 touch .env chmod 0600 .env ``` Your `.env` file stores sensitive configuration. Restrict permissions to protect private keys or mnemonics. Core Configuration [#core-configuration] Essential Environment Variables [#essential-environment-variables] | Variable | Description | | ------------------------------- | ------------------------------------------------ | | `SECRET` | Path to private key or mnemonic file | | `RPC_PROVIDER_*` | RPC endpoints for supported chains | | `RPC_PROVIDERS` | Provider preference ordering | | `SEND_RELAYS` | Enable transaction submission (default: `false`) | | `MAX_RELAYER_DEPOSIT_LOOK_BACK` | Lookback window in seconds | Gas Configuration [#gas-configuration] | Variable | Description | | -------------------------- | ----------------------------- | | `MAX_FEE_PER_GAS_SCALER_*` | Per-chain gas fee multipliers | | `PRIORITY_FEE_SCALER_*` | Priority fee adjustments | Operational Workflow [#operational-workflow] 1. **Simulate first** — run with `SEND_RELAYS=false` to review transactions before going live: ```bash SEND_RELAYS=false yarn relay ``` 2. **Review** the simulated transactions and token approvals. 3. **Go live** once you're satisfied: ```bash SEND_RELAYS=true yarn relay ``` The relayer identifies unfilled deposits across chains and attempts to fill them when your token balances permit. Advanced Features [#advanced-features] Redis Caching [#redis-caching] Redis caching significantly improves performance by storing RPC responses. Configure it via the `REDIS_URL` environment variable. Inventory Management [#inventory-management] Relayers can configure automatic rebalancing across chains using a JSON configuration that specifies: * Target allocation percentages per token and chain * WETH wrapping/unwrapping thresholds * Intelligent repayment chain selection Supported Tokens [#supported-tokens] WETH, USDC, DAI, USDT, WBTC, UMA, ACX, BAL, SNX, and POOL. Refund Mechanism [#refund-mechanism] Relayers receive compensation through **root bundles** — a set of three Merkle roots containing all information necessary to refund relayers who fulfilled a deposit during a given block range. Root bundles are published every 1.5 hours minimum. Relayers choose their repayment chain, which affects how liquidity provider fees are distributed. Security Considerations [#security-considerations] The relay bot requires in-memory access to private keys for transaction signing. Follow these best practices: * Deploy in isolated environments * Use key-based authentication for remote access * Restrict filesystem permissions on secret files * Maintain secure offline backups of key material * Limit exposure of systems with raw disk access # Crosschain Deposit into Aave import { Callout } from 'fumadocs-ui/components/callout'; import { Step, Steps } from 'fumadocs-ui/components/steps'; In this guide you'll build a script that takes USDC on Arbitrum and, in a **single user transaction**, bridges it to Ethereum, swaps it to ETH, and deposits the ETH into Aave's lending pool. The user signs once on Arbitrum and ends up with an aWETH position on Ethereum. This is powered by **embedded crosschain actions** — instructions attached to the Swap API request that the MulticallHandler contract executes on the destination chain after the swap lands. Prerequisites [#prerequisites] **API key required for production.** [Get your API key and Integrator ID](/tools/integrator-id) to authenticate your requests. * Node.js 20+ * A wallet with at least 100 USDC on Arbitrum (+ gas) * Basic familiarity with [the Swap API](/introduction/swap-api) — if you haven't read the [basic integration guide](/guides/dev-guides/integrate-swap-api), start there Install dependencies: ```bash title="terminal" npm init -y npm install viem ``` How It Works [#how-it-works] Here's the high-level flow: 1. **User deposits** USDC on Arbitrum via the Swap API 2. **Relayer fills** the intent on Ethereum, delivering ETH to the MulticallHandler contract 3. **MulticallHandler executes** our embedded action — calling `depositETH()` on Aave's WETH Gateway 4. **User receives** an aWETH position on Ethereum, credited to their address The key insight: the `recipient` of the swap is set to the **MulticallHandler** contract (not the user). The MulticallHandler receives the ETH and then forwards it into Aave on the user's behalf. *** Step 1 — Discover Chains and Tokens [#step-1--discover-chains-and-tokens] Just like the basic guide, we start by querying the Across API for supported chains and tokens. We need Arbitrum as origin and Ethereum as destination, with USDC on Arbitrum and WETH on Ethereum. ```ts title="aave-deposit.ts" const BASE_URL = "https://app.across.to/api"; async function getChains() { const res = await fetch(`${BASE_URL}/swap/chains`); return res.json(); } async function getTokens(chainId: number) { const res = await fetch(`${BASE_URL}/swap/tokens?chainId=${chainId}`); return res.json(); } function findChain(chains: any[], name: string) { const chain = chains.find( (c: any) => c.name.toLowerCase() === name.toLowerCase() ); if (!chain) throw new Error(`Chain "${name}" not found`); return chain; } function findToken(tokens: any[], symbol: string) { const token = tokens.find( (t: any) => t.symbol.toUpperCase() === symbol.toUpperCase() ); if (!token) throw new Error(`Token "${symbol}" not found`); return token; } ``` We'll call these in our main function to get the chain IDs and token addresses dynamically. Step 2 — Define the Aave Embedded Action [#step-2--define-the-aave-embedded-action] The embedded action tells the MulticallHandler what to do after receiving the ETH from the swap. We're calling Aave's WETH Gateway `depositETH()` function. ```ts title="aave-deposit.ts" // Aave V3 WETH Gateway on Ethereum Mainnet const AAVE_WETH_GATEWAY = "0x893411580e590D62dDBca8a703d61Cc4A8c7b2b9"; // MulticallHandler on Ethereum — this is the swap recipient const MULTICALL_HANDLER_ETH = "0x924a9f036260DdD5808007E1AA95f08eD08aA569"; function buildAaveDepositAction(onBehalfOf: string) { return { // The contract to call on Ethereum target: AAVE_WETH_GATEWAY, // Aave's depositETH function functionSignature: "function depositETH(address, address onBehalfOf, uint16 referralCode)", args: [ { // First param: lending pool address (zero = resolved by gateway) value: "0x0000000000000000000000000000000000000000", populateDynamically: false, }, { // Second param: who gets the aWETH position value: onBehalfOf, populateDynamically: false, }, { // Third param: referral code (0 = none) value: "0", populateDynamically: false, }, ], // No static ETH value — we use the dynamic balance instead value: "0", // This is a contract call, not a plain ETH transfer isNativeTransfer: false, // KEY: Forward the entire ETH balance from the swap as msg.value populateCallValueDynamically: true, }; } ``` Let's break down each field: | Field | Value | Why | | ------------------------------ | -------------------------------------- | -------------------------------------------------------------------- | | `target` | Aave WETH Gateway | The contract that accepts ETH and deposits into the lending pool | | `functionSignature` | `depositETH(address, address, uint16)` | The exact function signature — the ABI encoding depends on this | | `args[0]` | Zero address | Aave resolves the lending pool address from the gateway | | `args[1]` | User's address | The `onBehalfOf` param — who receives the aWETH | | `args[2]` | `"0"` | No referral code | | `populateCallValueDynamically` | `true` | The MulticallHandler sends the **entire ETH balance** as `msg.value` | **`populateCallValueDynamically: true` is the key mechanism.** It tells the MulticallHandler to use the entire ETH balance received from the swap as the `msg.value` for the `depositETH` call. Without this, the call would have `msg.value = 0` and deposit nothing. Step 3 — Build the Swap API Request [#step-3--build-the-swap-api-request] Embedded actions use the `POST` method on `/swap/approval`. The query parameters are the same as a normal swap, but we add the `actions` array in the request body. The critical difference from a basic bridge: **the `recipient` is the MulticallHandler contract**, not the user. The MulticallHandler receives the swapped ETH and then executes our Aave deposit action. ```ts title="aave-deposit.ts" import { parseUnits } from "viem"; async function getQuoteWithActions(params: { originChainId: number; destinationChainId: number; inputToken: string; outputToken: string; amount: string; depositor: string; actions: any[]; }) { const searchParams = new URLSearchParams({ tradeType: "exactInput", originChainId: params.originChainId.toString(), destinationChainId: params.destinationChainId.toString(), inputToken: params.inputToken, outputToken: params.outputToken, amount: params.amount, depositor: params.depositor, // IMPORTANT: recipient is the MulticallHandler, NOT the user recipient: MULTICALL_HANDLER_ETH, }); const res = await fetch( `${BASE_URL}/swap/approval?${searchParams}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ actions: params.actions }), } ); const quote = await res.json(); if (!quote.swapTx) { throw new Error(`Quote failed: ${JSON.stringify(quote, null, 2)}`); } return quote; } ``` **Don't forget to set `recipient` to the MulticallHandler.** If you set the recipient to the user's address, the swapped ETH goes directly to the user and the embedded action never runs. Step 4 — Execute Approvals [#step-4--execute-approvals] Same as the basic guide — if the user hasn't approved the SpokePool to spend their USDC, the quote will include approval transactions. ```ts title="aave-deposit.ts" async function executeApprovals( quote: any, walletClient: any, publicClient: any ) { if (!quote.approvalTxns?.length) { console.log(" No approvals needed."); return; } for (const approvalTx of quote.approvalTxns) { const hash = await walletClient.sendTransaction({ to: approvalTx.to as `0x${string}`, data: approvalTx.data as `0x${string}`, }); console.log(` Approval tx: ${hash}`); await publicClient.waitForTransactionReceipt({ hash }); console.log(` Confirmed.`); } } ``` Step 5 — Execute the Bridge + Aave Deposit [#step-5--execute-the-bridge--aave-deposit] Send the main swap transaction. From the user's perspective, this is a single transaction on Arbitrum. Behind the scenes, a relayer delivers ETH to the MulticallHandler on Ethereum, which calls `depositETH()` on Aave. ```ts title="aave-deposit.ts" async function executeBridge( quote: any, walletClient: any, publicClient: any ) { const hash = await walletClient.sendTransaction({ to: quote.swapTx.to as `0x${string}`, data: quote.swapTx.data as `0x${string}`, value: quote.swapTx.value ? BigInt(quote.swapTx.value) : 0n, gas: quote.swapTx.gas ? BigInt(quote.swapTx.gas) : undefined, }); console.log(` Bridge tx: ${hash}`); const receipt = await publicClient.waitForTransactionReceipt({ hash }); console.log(` Confirmed in block ${receipt.blockNumber}`); return hash; } ``` Putting It All Together [#putting-it-all-together] ```ts title="aave-deposit.ts" import { createWalletClient, createPublicClient, http, parseUnits, } from "viem"; import { arbitrum } from "viem/chains"; import { privateKeyToAccount } from "viem/accounts"; const BASE_URL = "https://app.across.to/api"; const AAVE_WETH_GATEWAY = "0x893411580e590D62dDBca8a703d61Cc4A8c7b2b9"; const MULTICALL_HANDLER_ETH = "0x924a9f036260DdD5808007E1AA95f08eD08aA569"; // ── Helpers ────────────────────────────────────────────── async function getChains() { const res = await fetch(`${BASE_URL}/swap/chains`); return res.json(); } async function getTokens(chainId: number) { const res = await fetch(`${BASE_URL}/swap/tokens?chainId=${chainId}`); return res.json(); } function findChain(chains: any[], name: string) { const chain = chains.find( (c: any) => c.name.toLowerCase() === name.toLowerCase() ); if (!chain) throw new Error(`Chain "${name}" not found`); return chain; } function findToken(tokens: any[], symbol: string) { const token = tokens.find( (t: any) => t.symbol.toUpperCase() === symbol.toUpperCase() ); if (!token) throw new Error(`Token "${symbol}" not found`); return token; } function buildAaveDepositAction(onBehalfOf: string) { return { target: AAVE_WETH_GATEWAY, functionSignature: "function depositETH(address, address onBehalfOf, uint16 referralCode)", args: [ { value: "0x0000000000000000000000000000000000000000", populateDynamically: false, }, { value: onBehalfOf, populateDynamically: false, }, { value: "0", populateDynamically: false, }, ], value: "0", isNativeTransfer: false, populateCallValueDynamically: true, }; } // ── Main ───────────────────────────────────────────────── async function main() { const account = privateKeyToAccount( process.env.PRIVATE_KEY as `0x${string}` ); const walletClient = createWalletClient({ account, chain: arbitrum, transport: http(), }); const publicClient = createPublicClient({ chain: arbitrum, transport: http(), }); console.log(`Wallet: ${account.address}\n`); // ── Step 1: Discover chains and tokens ─────────────── console.log("Step 1: Discovering chains and tokens..."); const chains = await getChains(); const origin = findChain(chains, "Arbitrum"); const destination = findChain(chains, "Ethereum"); const originTokens = await getTokens(origin.chainId); const destTokens = await getTokens(destination.chainId); const inputToken = findToken(originTokens, "USDC"); const outputToken = findToken(destTokens, "WETH"); console.log(` Route: ${inputToken.symbol} on ${origin.name} → ${outputToken.symbol} on ${destination.name}`); console.log(` Input: ${inputToken.address}`); console.log(` Output: ${outputToken.address}\n`); // ── Step 2: Build the Aave action ──────────────────── console.log("Step 2: Building Aave deposit action..."); const action = buildAaveDepositAction(account.address); console.log(` Target: ${action.target} (Aave WETH Gateway)`); console.log(` OnBehalfOf: ${account.address}`); console.log(` Recipient: ${MULTICALL_HANDLER_ETH} (MulticallHandler)\n`); // ── Step 3: Get quote with embedded action ─────────── console.log("Step 3: Fetching quote for 100 USDC → ETH → Aave..."); const amount = parseUnits("100", inputToken.decimals).toString(); const searchParams = new URLSearchParams({ tradeType: "exactInput", originChainId: origin.chainId.toString(), destinationChainId: destination.chainId.toString(), inputToken: inputToken.address, outputToken: outputToken.address, amount, depositor: account.address, recipient: MULTICALL_HANDLER_ETH, }); const quoteRes = await fetch( `${BASE_URL}/swap/approval?${searchParams}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ actions: [action] }), } ); const quote = await quoteRes.json(); if (!quote.swapTx) { throw new Error(`Quote failed: ${JSON.stringify(quote, null, 2)}`); } console.log(` Cross-swap type: ${quote.crossSwapType}`); console.log(` Expected fill: ${quote.expectedFillTime}s`); console.log(` Simulation passed: ${quote.swapTx.simulationSuccess}\n`); // ── Step 4: Execute approvals ──────────────────────── console.log("Step 4: Handling approvals..."); if (quote.approvalTxns?.length) { for (const approvalTx of quote.approvalTxns) { const hash = await walletClient.sendTransaction({ to: approvalTx.to as `0x${string}`, data: approvalTx.data as `0x${string}`, }); console.log(` Approval tx: ${hash}`); await publicClient.waitForTransactionReceipt({ hash }); console.log(` Confirmed.`); } } else { console.log(" No approvals needed."); } console.log(); // ── Step 5: Execute bridge + Aave deposit ──────────── console.log("Step 5: Executing bridge + Aave deposit..."); const hash = await walletClient.sendTransaction({ to: quote.swapTx.to as `0x${string}`, data: quote.swapTx.data as `0x${string}`, value: quote.swapTx.value ? BigInt(quote.swapTx.value) : 0n, gas: quote.swapTx.gas ? BigInt(quote.swapTx.gas) : undefined, }); console.log(` Bridge tx: ${hash}`); const receipt = await publicClient.waitForTransactionReceipt({ hash }); console.log(` Confirmed in block ${receipt.blockNumber}`); console.log(`\n View on Arbiscan: ${origin.explorerUrl}/tx/${hash}`); console.log(` Your aWETH position on Ethereum will appear in ~${quote.expectedFillTime} seconds.`); } main().catch(console.error); ``` Running It [#running-it] ```bash title="terminal" PRIVATE_KEY=0xYOUR_PRIVATE_KEY npx tsx aave-deposit.ts ``` Expected output: ``` Wallet: 0xYourAddress Step 1: Discovering chains and tokens... Route: USDC on Arbitrum → WETH on Ethereum Input: 0xaf88d065e77c8cC2239327C5EDb3A432268e5831 Output: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 Step 2: Building Aave deposit action... Target: 0x893411580e590D62dDBca8a703d61Cc4A8c7b2b9 (Aave WETH Gateway) OnBehalfOf: 0xYourAddress Recipient: 0x924a9f036260DdD5808007E1AA95f08eD08aA569 (MulticallHandler) Step 3: Fetching quote for 100 USDC → ETH → Aave... Cross-swap type: anyToBridgeable Expected fill: 2s Simulation passed: true Step 4: Handling approvals... Approval tx: 0xabc123... Confirmed. Step 5: Executing bridge + Aave deposit... Bridge tx: 0xdef456... Confirmed in block 284719304 View on Arbiscan: https://arbiscan.io/tx/0xdef456... Your aWETH position on Ethereum will appear in ~2 seconds. ``` What Happens Under the Hood [#what-happens-under-the-hood] 1. Your USDC is deposited into the Arbitrum SpokePool 2. A relayer detects the intent and fills it on Ethereum — delivering ETH to the MulticallHandler contract 3. The MulticallHandler reads the embedded action and calls `depositETH()` on Aave's WETH Gateway, forwarding all the ETH as `msg.value` 4. Aave deposits the ETH and mints aWETH to your address (the `onBehalfOf` param) 5. \~1.5 hours later, the relayer is reimbursed through Across's settlement process Next Steps [#next-steps] * **Try other DeFi protocols** — the same pattern works for any contract that accepts ETH or ERC-20 tokens. Change the `target`, `functionSignature`, and `args`. * **Use dynamic token amounts** — set `populateDynamically: true` with `balanceSourceToken` to forward the exact swapped balance for ERC-20 actions. See [Transfer ERC-20 Tokens](/introduction/embedded-actions/transfer-erc20). * **Chain multiple actions** — add more objects to the `actions` array to execute a sequence of operations on the destination. * **Track the deposit** — poll `/deposit/status` to confirm everything landed. See [Tracking Deposits](/introduction/tracking-deposits). # Developer Guides import { Card, Cards } from 'fumadocs-ui/components/card'; Available Guides [#available-guides] Build a complete crosschain bridge from scratch — discover chains, look up tokens, get a quote, and execute a USDC transfer from Arbitrum to Base using viem. Bridge USDC from Arbitrum to Ethereum, swap it to ETH, and deposit directly into Aave's lending pool — all in a single user transaction using embedded actions. # Integrate Across Swap API import { Callout } from 'fumadocs-ui/components/callout'; import { Step, Steps } from 'fumadocs-ui/components/steps'; In this guide you'll build a complete crosschain bridge that sends **1 USDC from Arbitrum to Base** using the Across Swap API and viem. We'll start from zero — discovering supported chains, looking up token addresses, fetching a quote, and executing the transaction. **API key required for production.** [Get your API key and Integrator ID](/tools/integrator-id) to authenticate your requests. Prerequisites [#prerequisites] * Node.js 20+ * A wallet with at least 2 USDC on Arbitrum (1 USDC to bridge + gas) * An Arbitrum RPC URL (the public `https://arb1.arbitrum.io/rpc` works for testing) Install dependencies: ```bash title="terminal" npm init -y npm install viem ``` Create a single file for the integration: ```bash title="terminal" touch bridge.ts ``` The Plan [#the-plan] We'll go through five steps, each calling one API endpoint and building on the previous result: 1. **Discover chains** — call `/swap/chains` to confirm Arbitrum and Base are supported 2. **Look up tokens** — call `/swap/tokens` to find the USDC addresses on both chains 3. **Get a quote** — call `/swap/approval` with the token addresses and amount 4. **Execute approvals** — send any required token approval transactions 5. **Execute the bridge** — send the swap transaction and track the result Let's build it. *** Step 1 — Discover Supported Chains [#step-1--discover-supported-chains] The `/swap/chains` endpoint returns every chain Across supports, with its chain ID, name, and public RPC URL. ```ts title="bridge.ts" const BASE_URL = "https://app.across.to/api"; // Step 1: Discover chains async function getChains() { const res = await fetch(`${BASE_URL}/swap/chains`); const chains = await res.json(); return chains; } ``` The response is an array of chain objects: ```json title="sample response (abbreviated)" [ { "chainId": 42161, "name": "Arbitrum", "publicRpcUrl": "https://arb1.arbitrum.io/rpc", "explorerUrl": "https://arbiscan.io" }, { "chainId": 8453, "name": "Base", "publicRpcUrl": "https://mainnet.base.org", "explorerUrl": "https://basescan.org" } ] ``` We can use this to find our origin and destination chains programmatically: ```ts title="bridge.ts" async function findChain(chains: any[], name: string) { const chain = chains.find( (c: any) => c.name.toLowerCase() === name.toLowerCase() ); if (!chain) throw new Error(`Chain "${name}" not found`); return chain; } ``` In a real integration, you'd cache the chain list and present it as a dropdown for users to select their origin and destination chains. Step 2 — Look Up Token Addresses [#step-2--look-up-token-addresses] The `/swap/tokens` endpoint returns all supported tokens for a given chain, including their contract addresses and decimal counts. ```ts title="bridge.ts" // Step 2: Look up tokens on a specific chain async function getTokens(chainId: number) { const res = await fetch(`${BASE_URL}/swap/tokens?chainId=${chainId}`); const tokens = await res.json(); return tokens; } ``` The response is an array of token objects: ```json title="sample response (abbreviated)" [ { "chainId": 42161, "address": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", "name": "USD Coin", "symbol": "USDC", "decimals": 6, "priceUsd": "0.999903" }, { "chainId": 42161, "address": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", "name": "Wrapped Ether", "symbol": "WETH", "decimals": 18, "priceUsd": "1937.75" } ] ``` Find USDC on both chains: ```ts title="bridge.ts" function findToken(tokens: any[], symbol: string) { const token = tokens.find( (t: any) => t.symbol.toUpperCase() === symbol.toUpperCase() ); if (!token) throw new Error(`Token "${symbol}" not found`); return token; } ``` **Always use the addresses from the API response** — don't hardcode token addresses. Token addresses differ between chains (USDC on Arbitrum is `0xaf88d065e77c8cC2239327C5EDb3A432268e5831`, but on Base it's `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`). The API is the source of truth. Step 3 — Get a Quote [#step-3--get-a-quote] Now we have everything needed to call `/swap/approval`. This endpoint returns executable calldata for the crosschain transfer — approval transactions, the main swap transaction, expected fees, and fill time. ```ts title="bridge.ts" import { parseUnits } from "viem"; // Step 3: Get a quote from the Swap API async function getQuote(params: { originChainId: number; destinationChainId: number; inputToken: string; outputToken: string; amount: string; depositor: string; }) { const searchParams = new URLSearchParams({ tradeType: "minOutput", originChainId: params.originChainId.toString(), destinationChainId: params.destinationChainId.toString(), inputToken: params.inputToken, outputToken: params.outputToken, amount: params.amount, depositor: params.depositor, }); const res = await fetch( `${BASE_URL}/swap/approval?${searchParams}` ); const quote = await res.json(); if (!quote.swapTx) { throw new Error(`Quote failed: ${JSON.stringify(quote)}`); } return quote; } ``` **What the response tells you:** | Field | What It Means | | ------------------ | ------------------------------------------------------------------------------------- | | `checks.allowance` | Whether the depositor has approved enough tokens for the SpokePool to spend | | `checks.balance` | Whether the depositor has enough tokens | | `approvalTxns` | Array of approval transactions to execute first (may be empty if already approved) | | `swapTx` | The main bridge transaction — `to`, `data`, `value`, `gas` | | `expectedFillTime` | Estimated seconds until the relayer fills on the destination (\~2 seconds on mainnet) | | `fees` | Total fees, including origin gas | We use `tradeType: "minOutput"` which means "I want to receive **at least** this amount on the destination." The API calculates the required input. This is the recommended default for most integrations. Step 4 — Execute Approvals [#step-4--execute-approvals] Before sending the bridge transaction, we need to approve the SpokePool contract to spend our USDC. The quote tells us exactly which approval transactions to send. ```ts title="bridge.ts" import { createWalletClient, createPublicClient, http, } from "viem"; import { arbitrum } from "viem/chains"; import { privateKeyToAccount } from "viem/accounts"; // Step 4: Execute approval transactions async function executeApprovals( quote: any, walletClient: any, publicClient: any ) { if (!quote.approvalTxns?.length) { console.log("No approvals needed — already approved"); return; } console.log(`Executing ${quote.approvalTxns.length} approval(s)...`); for (const approvalTx of quote.approvalTxns) { const hash = await walletClient.sendTransaction({ to: approvalTx.to as `0x${string}`, data: approvalTx.data as `0x${string}`, }); console.log(` Approval tx sent: ${hash}`); const receipt = await publicClient.waitForTransactionReceipt({ hash }); console.log(` Confirmed in block ${receipt.blockNumber}`); } } ``` Approvals only need to happen once per token/spender pair. On subsequent bridges with the same token, `approvalTxns` will be an empty array and this step is skipped automatically. Step 5 — Execute the Bridge [#step-5--execute-the-bridge] Finally, send the main swap transaction. This deposits your USDC into the origin SpokePool, and a relayer will deliver USDC on Base in \~2 seconds. ```ts title="bridge.ts" // Step 5: Execute the bridge transaction async function executeBridge( quote: any, walletClient: any, publicClient: any ) { console.log("Sending bridge transaction..."); console.log(` Expected fill time: ${quote.expectedFillTime}s`); const hash = await walletClient.sendTransaction({ to: quote.swapTx.to as `0x${string}`, data: quote.swapTx.data as `0x${string}`, value: quote.swapTx.value ? BigInt(quote.swapTx.value) : 0n, gas: quote.swapTx.gas ? BigInt(quote.swapTx.gas) : undefined, }); console.log(` Bridge tx sent: ${hash}`); const receipt = await publicClient.waitForTransactionReceipt({ hash }); console.log(` Confirmed in block ${receipt.blockNumber}`); return hash; } ``` Putting It All Together [#putting-it-all-together] Here's the complete script that chains all five steps: ```ts title="bridge.ts" import { createWalletClient, createPublicClient, http, parseUnits, } from "viem"; import { arbitrum } from "viem/chains"; import { privateKeyToAccount } from "viem/accounts"; const BASE_URL = "https://app.across.to/api"; // ── Helpers ────────────────────────────────────────────── async function getChains() { const res = await fetch(`${BASE_URL}/swap/chains`); return res.json(); } async function getTokens(chainId: number) { const res = await fetch(`${BASE_URL}/swap/tokens?chainId=${chainId}`); return res.json(); } function findChain(chains: any[], name: string) { const chain = chains.find( (c: any) => c.name.toLowerCase() === name.toLowerCase() ); if (!chain) throw new Error(`Chain "${name}" not found`); return chain; } function findToken(tokens: any[], symbol: string) { const token = tokens.find( (t: any) => t.symbol.toUpperCase() === symbol.toUpperCase() ); if (!token) throw new Error(`Token "${symbol}" not found`); return token; } // ── Main ───────────────────────────────────────────────── async function main() { // Setup wallet const account = privateKeyToAccount( process.env.PRIVATE_KEY as `0x${string}` ); const walletClient = createWalletClient({ account, chain: arbitrum, transport: http(), }); const publicClient = createPublicClient({ chain: arbitrum, transport: http(), }); console.log(`Wallet: ${account.address}\n`); // ── Step 1: Discover chains ────────────────────────── console.log("Step 1: Discovering chains..."); const chains = await getChains(); const origin = findChain(chains, "Arbitrum"); const destination = findChain(chains, "Base"); console.log(` Origin: ${origin.name} (${origin.chainId})`); console.log(` Destination: ${destination.name} (${destination.chainId})\n`); // ── Step 2: Look up tokens ─────────────────────────── console.log("Step 2: Looking up USDC on both chains..."); const originTokens = await getTokens(origin.chainId); const destTokens = await getTokens(destination.chainId); const inputToken = findToken(originTokens, "USDC"); const outputToken = findToken(destTokens, "USDC"); console.log(` Input: ${inputToken.symbol} (${inputToken.address}) — ${inputToken.decimals} decimals`); console.log(` Output: ${outputToken.symbol} (${outputToken.address}) — ${outputToken.decimals} decimals\n`); // ── Step 3: Get a quote ────────────────────────────── console.log("Step 3: Fetching quote for 1 USDC..."); const amount = parseUnits("1", inputToken.decimals).toString(); const searchParams = new URLSearchParams({ tradeType: "minOutput", originChainId: origin.chainId.toString(), destinationChainId: destination.chainId.toString(), inputToken: inputToken.address, outputToken: outputToken.address, amount, depositor: account.address, }); const quoteRes = await fetch(`${BASE_URL}/swap/approval?${searchParams}`); const quote = await quoteRes.json(); if (!quote.swapTx) { throw new Error(`Quote failed: ${JSON.stringify(quote, null, 2)}`); } console.log(` Cross-swap type: ${quote.crossSwapType}`); console.log(` Expected fill: ${quote.expectedFillTime}s`); console.log(` Approvals needed: ${quote.approvalTxns?.length ?? 0}`); console.log(` Simulation passed: ${quote.swapTx.simulationSuccess}\n`); // ── Step 4: Execute approvals ──────────────────────── if (quote.approvalTxns?.length) { console.log("Step 4: Executing approvals..."); for (const approvalTx of quote.approvalTxns) { const hash = await walletClient.sendTransaction({ to: approvalTx.to as `0x${string}`, data: approvalTx.data as `0x${string}`, }); console.log(` Approval tx: ${hash}`); await publicClient.waitForTransactionReceipt({ hash }); console.log(` Confirmed.`); } console.log(); } else { console.log("Step 4: No approvals needed — skipping.\n"); } // ── Step 5: Execute bridge ─────────────────────────── console.log("Step 5: Sending bridge transaction..."); const hash = await walletClient.sendTransaction({ to: quote.swapTx.to as `0x${string}`, data: quote.swapTx.data as `0x${string}`, value: quote.swapTx.value ? BigInt(quote.swapTx.value) : 0n, gas: quote.swapTx.gas ? BigInt(quote.swapTx.gas) : undefined, }); console.log(` Bridge tx: ${hash}`); const receipt = await publicClient.waitForTransactionReceipt({ hash }); console.log(` Confirmed in block ${receipt.blockNumber}`); console.log(`\n View on Arbiscan: ${origin.explorerUrl}/tx/${hash}`); console.log(` Your USDC will arrive on Base in ~${quote.expectedFillTime} seconds.`); } main().catch(console.error); ``` Running It [#running-it] ```bash title="terminal" PRIVATE_KEY=0xYOUR_PRIVATE_KEY npx tsx bridge.ts ``` You should see output like: ``` Wallet: 0xYourAddress Step 1: Discovering chains... Origin: Arbitrum (42161) Destination: Base (8453) Step 2: Looking up USDC on both chains... Input: USDC (0xaf88d065e77c8cC2239327C5EDb3A432268e5831) — 6 decimals Output: USDC (0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913) — 6 decimals Step 3: Fetching quote for 1 USDC... Cross-swap type: bridgeableToBridgeable Expected fill: 2s Approvals needed: 1 Simulation passed: true Step 4: Executing approvals... Approval tx: 0xabc123... Confirmed. Step 5: Sending bridge transaction... Bridge tx: 0xdef456... Confirmed in block 284719304 View on Arbiscan: https://arbiscan.io/tx/0xdef456... Your USDC will arrive on Base in ~2 seconds. ``` Next Steps [#next-steps] * **Track the deposit** — poll `/deposit/status` to confirm the fill. See [Tracking Deposits](/introduction/tracking-deposits). * **Add an integrator ID** — required for production. See the [Swap API reference](/introduction/swap-api#integrator-id). * **Handle errors** — check `quote.swapTx.simulationSuccess` before sending, and handle `checks.balance` to show a "not enough funds" message. * **Add embedded actions** — execute operations on the destination chain after the bridge. See [Crosschain Deposit into Aave](/guides/dev-guides/crosschain-aave-deposit). # What is Across V4? import { Callout } from 'fumadocs-ui/components/callout'; import { Step, Steps } from 'fumadocs-ui/components/steps'; Across V4 integrates **zero-knowledge proofs** with Succinct's SP1 proving network to decouple settlement from chain-specific verification logic. This enables **permissionless expansion to new chains** without custom bridge adapters or audits for each new deployment. V4 only changes the **settlement layer** (Phase 3 of the [intent lifecycle](/guides/concepts/intent-lifecycle)). The user experience and relayer network remain the same — users still get \~2 second fills. Why V4? [#why-v4] In V3, adding a new chain requires deploying chain-specific adapters for the canonical bridge between Ethereum and that chain. Each adapter needs custom code and an audit. V4 replaces this with a universal verification mechanism: ZK proofs of Ethereum state. | | V3 Settlement | V4 Settlement | | --------------------- | -------------------------------------- | ------------------------------------------ | | **New chain support** | Custom adapter + audit | Deploy UniversalSpokePool (no custom code) | | **Verification** | Optimistic Oracle + canonical bridges | ZK proofs of Ethereum state | | **Trust model** | Tied to canonical bridge of each chain | Universal — proves Ethereum state directly | V4 Architecture [#v4-architecture] On Ethereum L1 [#on-ethereum-l1] * **UMA's Optimistic Oracle** — Verifies repayment bundles (unchanged from V3) * **HubPoolStore** — Stores message hashes in storage slots and emits `StoredCallData` events, anchoring settlement data to L1 Off-Chain Infrastructure [#off-chain-infrastructure] * **Finalizer** — Monitors `StoredCallData` events from HubPoolStore. After Ethereum reaches finality (12–15 minutes), initiates ZK proof generation * **ZK API** — Stateful service managing proof lifecycle through four statuses: `WaitingForFinality`, `Generating`, `Success`, `Errored` On Destination Chain [#on-destination-chain] * **SP1Helios** — A ZK light client contract that verifies proofs and stores proven L1 state. Built on Succinct's SP1 proving system * **UniversalSpokePool** — Executes verified messages. Calls `relayRootBundle` to process settlement data and `executeRelayerRefundLeaf()` to process relayer repayments Settlement Flow [#settlement-flow] Bundle Finalized on L1 [#bundle-finalized-on-l1] After the optimistic challenge period passes, the confirmed bundle is written to the HubPoolStore contract. A `StoredCallData` event is emitted containing the message hash. Await Ethereum Finality [#await-ethereum-finality] The Finalizer waits for Ethereum to finalize the block containing the `StoredCallData` event (12–15 minutes). This ensures the proof is based on irreversible state. ZK Proof Generation [#zk-proof-generation] The Finalizer submits a proof request to the ZK API, which: 1. Gathers proof inputs: finalized beacon headers, sync committee signatures (512 validators), and a Merkle proof of the HubPoolStore storage slot 2. Runs a pre-flight check on a local RISC-V virtual machine 3. Sends the validated job to Succinct's SP1 proving network Proof Submission [#proof-submission] Once the proof is generated, the Finalizer submits two transactions on the destination chain: 1. **`SP1Helios.update()`** — Provides the ZK proof, updating the light client's view of Ethereum state 2. **`UniversalSpokePool.executeMessage()`** — Executes the verified settlement message Relayer Refunds Processed [#relayer-refunds-processed] With the message verified, the UniversalSpokePool processes `relayRootBundle` and executes `executeRelayerRefundLeaf()` to repay relayers on the destination chain. Benefits [#benefits] **For users** — More chains supported, more routes available, faster token availability on new networks. **For builders** — A single integration that automatically works on new chains as they're added. No per-chain audits needed. **For relayers** — Repayments on origin chains with flexible rebalancing. New chains mean new fill opportunities. **For chains** — Near-frictionless integration with Across. New chains can be added permissionlessly once a UniversalSpokePool is deployed. # What are Crosschain Intents? import { Callout } from 'fumadocs-ui/components/callout'; import { Step, Steps } from 'fumadocs-ui/components/steps'; An **intent** is an order where users declare a desired **outcome** rather than specifying the execution steps. Traditional bridges require users to specify the exact path: lock tokens here, mint tokens there, wait for finality. Intents flip this model — users say "I want 100 USDC on Base" and a competitive network of solvers figures out the best way to make it happen. Across specializes exclusively in **crosschain** intents — outcomes that span multiple blockchains. Intents vs. Traditional Bridges [#intents-vs-traditional-bridges] | | Traditional Bridge | Intent-Based (Across) | | ------------------ | -------------------------------- | ---------------------------- | | **User specifies** | Exact execution path | Desired outcome | | **Execution** | Protocol-defined, single path | Solver-optimized, flexible | | **Speed** | Minutes to days (finality-bound) | \~2 seconds (solver-fronted) | | **Complexity** | User bears routing complexity | Solver bears complexity | | **Capital model** | Lock-and-mint or liquidity pools | Competitive solver capital | Three Phases of Crosschain Intents [#three-phases-of-crosschain-intents] Across's intent model has evolved through three phases since 2021: Phase 1 — Same-Asset Transfer (2021) [#phase-1--same-asset-transfer-2021] The foundational use case. A user's intent specifies moving the **same asset** from Chain A to Chain B. For example: "Send my USDC from Arbitrum to Base." This has been Across's core offering since launch. Phase 2 — Transfer + Execution [#phase-2--transfer--execution] A user's intent specifies moving an asset from Chain A to Chain B **and then executing a transaction on Chain B**. For example: "Bridge my USDC to Ethereum and deposit it into Aave." This is enabled through [embedded crosschain actions](/introduction/embedded-actions) — instructions attached to the intent that execute on the destination chain after the fill. Phase 3 — Swap + Execution [#phase-3--swap--execution] A user's intent specifies **swapping** asset X on Chain A for a minimum amount of asset Y on Chain B, optionally followed by a transaction on Chain B. For example: "Swap my ETH on Arbitrum for at least 2000 USDC on Base and transfer it to a specific address." This is the current state, powered by [Across Settlement](/guides/concepts/intents-architecture) infrastructure. Why Intents Win [#why-intents-win] **For users**: No need to understand bridges, finality, or routing. Declare the outcome and get it in \~2 seconds. **For developers**: A single API call handles all the complexity. No need to integrate multiple bridges or manage routing logic. **For solvers/relayers**: A competitive marketplace where speed and capital efficiency determine profits. ERC-7683: Standardizing Intents [#erc-7683-standardizing-intents] Across is developing **ERC-7683**, a crosschain intent order standard that defines a unified format for how intents are submitted, bid on, escrowed, and settled. The goal is to make crosschain intents interoperable across the ecosystem — any solver network can fill any standardized intent. See [ERC-7683 in Production](/guides/concepts/erc-7683) for implementation details. # ERC-7683 in Production import { Callout } from 'fumadocs-ui/components/callout'; import { Step, Steps } from 'fumadocs-ui/components/steps'; import { Tabs, Tab } from 'fumadocs-ui/components/tabs'; [ERC-7683](https://erc7683.org) is a standard for crosschain intent orders. Instead of users specifying complex execution paths, they declare their desired outcome — and a competitive network of relayers handles fulfillment. Across implements ERC-7683 through the **AcrossOriginSettler** contract. AcrossOriginSettler [#acrossoriginsettler] The AcrossOriginSettler is the on-chain entry point for ERC-7683 intents. It validates intent parameters and creates deposits on SpokePool contracts (via `depositV3()` or `unsafeDeposit()`). Deployments [#deployments] | Chain | Address | | -------- | -------------------------------------------- | | Base | `0x4afb570AC68BfFc26Bb02FdA3D801728B0f93C9E` | | Arbitrum | `0xB0B07055F214Ce59ccB968663d3435B9f3294998` | The open() Function [#the-open-function] The core function takes three parameters: | Parameter | Type | Description | | --------------- | --------- | ----------------------------------------------------- | | `fillDeadline` | `uint256` | Unix timestamp — deadline for the intent to be filled | | `orderDataType` | `bytes32` | EIP-712 typehash specifying the data format | | `orderData` | `bytes` | ABI-encoded parameters defining the desired outcome | The `orderDataType` for Across intents is: ``` 0x9df4b782e7bbc178b3b93bfe8aafb909e84e39484d7f3c59f400f1b4691f85e2 ``` The `orderData` encodes all the transfer details: input/output tokens, amounts, chains, recipient, and more. Generating Order Data [#generating-order-data] Use viem's `encodeAbiParameters()` to construct the `orderData`: ```ts title="generate-order-data.ts" import { encodeAbiParameters, parseAbiParameters } from "viem"; const orderData = encodeAbiParameters( parseAbiParameters([ "address outputToken", "uint256 outputAmount", "uint256 destinationChainId", "address recipient", "address exclusiveRelayer", "uint32 exclusivityDeadline", "bytes message", ]), [ "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base 1000000n, // 1 USDC (6 decimals) 8453n, // Base "0xYourRecipientAddress", "0x0000000000000000000000000000000000000000", // No exclusive relayer 0, // No exclusivity "0x", // No message ] ); ``` Use the intent generator tool to build order data interactively: **crosschain-intents-generator.vercel.app** This tool lets you configure all parameters visually and outputs the encoded `orderData` and `fillDeadline` ready to pass to `open()`. Full Integration Example [#full-integration-example] Approve Token Spending [#approve-token-spending] Approve the AcrossOriginSettler contract to spend your input token. ```ts title="approve.ts" import { createWalletClient, createPublicClient, http, parseAbi } from "viem"; import { base } from "viem/chains"; import { privateKeyToAccount } from "viem/accounts"; const account = privateKeyToAccount("0xYOUR_PRIVATE_KEY"); const walletClient = createWalletClient({ account, chain: base, transport: http(), }); const publicClient = createPublicClient({ chain: base, transport: http(), }); const USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; const ORIGIN_SETTLER_BASE = "0x4afb570AC68BfFc26Bb02FdA3D801728B0f93C9E"; // Approve USDC spending const approveHash = await walletClient.writeContract({ address: USDC_BASE, abi: parseAbi(["function approve(address spender, uint256 amount)"]), functionName: "approve", args: [ORIGIN_SETTLER_BASE, 1000000000n], // 1000 USDC max }); await publicClient.waitForTransactionReceipt({ hash: approveHash }); console.log("Approval confirmed:", approveHash); ``` Generate Order Data [#generate-order-data] Encode the intent parameters. ```ts title="order-data.ts" import { encodeAbiParameters, parseAbiParameters } from "viem"; const fillDeadline = Math.floor(Date.now() / 1000) + 3600; // 1 hour from now const orderDataType = "0x9df4b782e7bbc178b3b93bfe8aafb909e84e39484d7f3c59f400f1b4691f85e2"; const orderData = encodeAbiParameters( parseAbiParameters([ "address outputToken", "uint256 outputAmount", "uint256 destinationChainId", "address recipient", "address exclusiveRelayer", "uint32 exclusivityDeadline", "bytes message", ]), [ "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", // USDC on Arbitrum 100000000n, // 100 USDC 42161n, // Arbitrum account.address, "0x0000000000000000000000000000000000000000", 0, "0x", ] ); ``` Call open() [#call-open] Submit the intent to the AcrossOriginSettler. ```ts title="submit-intent.ts" import { parseAbi } from "viem"; const openHash = await walletClient.writeContract({ address: ORIGIN_SETTLER_BASE, abi: parseAbi([ "function open(uint256 fillDeadline, bytes32 orderDataType, bytes orderData)", ]), functionName: "open", args: [BigInt(fillDeadline), orderDataType, orderData], }); console.log("Intent submitted:", openHash); const receipt = await publicClient.waitForTransactionReceipt({ hash: openHash, }); console.log("Confirmed in block:", receipt.blockNumber); ``` Track the Fill [#track-the-fill] Use the `/deposit/status` endpoint to verify the intent was filled on the destination chain. See [Tracking Deposits](/introduction/tracking-deposits) for details. ERC-7683 is ideal when you want **direct on-chain composability** — calling `open()` from another smart contract, building custom settlement flows, or integrating with account abstraction. For most server-side or frontend integrations, the [Swap API](/introduction/swap-api) is simpler since it handles encoding, gas estimation, and approval transactions for you. # Concepts import { Card, Cards } from 'fumadocs-ui/components/card'; Learn the key ideas behind how Across works — from intents to settlement to the protocol's hub-and-spoke architecture. How intent-based bridging differs from traditional bridges and why it matters. Hub-and-spoke model with HubPool on Ethereum and SpokePools on each chain. Step-by-step flow from deposit to fill to settlement. What's new in V4 — ZK proofs, permissionless chain expansion, and more. The crosschain intent order standard in production on Across. # Intent Lifecycle in Across import { Callout } from 'fumadocs-ui/components/callout'; import { Step, Steps } from 'fumadocs-ui/components/steps'; Every crosschain intent in Across goes through three phases: **initiation** (user deposits on origin), **fill** (relayer delivers on destination), and **settlement** (protocol verifies and repays). Understanding this lifecycle helps you build reliable integrations and debug issues. Phase 1: Initiation [#phase-1-initiation] depositV3 Called on Origin SpokePool [#depositv3-called-on-origin-spokepool] The user (or a contract on behalf of the user) calls `depositV3()` on the origin chain's SpokePool. This can be triggered directly or through the Swap API's generated calldata. The call specifies the destination chain, output token, output amount, recipient, fill deadline, and optional message. Funds Escrowed [#funds-escrowed] The user's input tokens are pulled into the SpokePool and escrowed. These funds remain locked until the intent is fulfilled and settled. Escrowing is essential because crosschain intents lack atomicity — the relayer needs assurance that the user's funds exist before fronting capital on the destination. V3FundsDeposited Event Emitted [#v3fundsdeposited-event-emitted] The SpokePool emits a `V3FundsDeposited` event containing all intent parameters. Relayers subscribe to this event to discover intents they can fill. Phase 2: Fill [#phase-2-fill] Relayer Calls fillV3Relay on Destination [#relayer-calls-fillv3relay-on-destination] A relayer calls `fillV3Relay()` on the destination chain's SpokePool, using their own tokens. The relayer specifies which chain they want to receive repayment on — this affects the LP fee (smallest when the relayer accepts tokens on the deposit chain). Intent Marked as Filled [#intent-marked-as-filled] The intent transitions to **filled** status on the destination SpokePool. This prevents duplicate fills by other relayers. Each intent can only be filled once. FilledV3Relay Event Emitted [#filledv3relay-event-emitted] The SpokePool emits a `FilledV3Relay` event. Use this event (or the `/deposit/status` API) to track when a user's intent has been fulfilled. Exclusivity [#exclusivity] Intents can have an **exclusivity period** where a particular relayer address has the sole right to perform the fill. The `exclusiveRelayer` and `exclusivityDeadline` fields control this. After the deadline passes, any relayer can fill. Phase 3: Settlement [#phase-3-settlement] Settlement is the process of verifying fills and repaying relayers. It happens in the background — users don't need to wait for it. Block Ranges Determined [#block-ranges-determined] The Dataworker determines the range of blocks to cover on each supported chain for this settlement period. Fills Validated Against Deposits [#fills-validated-against-deposits] Every `FilledV3Relay` event is matched against its corresponding `V3FundsDeposited` event on the origin chain. The Dataworker verifies that all parameters match. Aggregated into Payment Lists [#aggregated-into-payment-lists] Valid fills are aggregated into consolidated payment lists per chain — which relayers to pay, how much, and on which chain. Crosschain Transfer Instructions [#crosschain-transfer-instructions] The Dataworker computes rebalancing instructions — how to move funds between SpokePools via canonical bridges to ensure each pool has sufficient liquidity. Merkle Tree Proposal [#merkle-tree-proposal] Everything is organized into merkle trees and proposed to the HubPool on Ethereum with a bond. The proposal enters a challenge period. Bundle Execution [#bundle-execution] After the challenge period passes without dispute, the bundle is finalized. Root bundles are transmitted to SpokePools via canonical bridges. Payment Execution [#payment-execution] Relayer refunds and rebalancing transfers are executed on each chain. Any eligible party can trigger the execution — it's permissionless. Settlement cost is **O(1)**, not **O(N)**. No matter how many fills happen in a period, they're all aggregated into a single bundle. Bundles are proposed every \~1.5 hours minimum. Intent Structure [#intent-structure] Basic Fields [#basic-fields] | Field | Type | Description | | -------------------- | --------- | ---------------------------------------------------------------------------------------------------- | | `recipient` | `address` | Destination address for funds | | `inputToken` | `address` | Token deposited on origin chain | | `inputAmount` | `uint256` | Amount deposited | | `outputToken` | `address` | Token to receive on destination | | `outputAmount` | `uint256` | Amount to receive | | `destinationChainId` | `uint256` | Destination chain ID | | `fillDeadline` | `uint32` | Unix timestamp — deadline for fill | | `message` | `bytes` | Custom calldata for contract recipients (enables [embedded actions](/introduction/embedded-actions)) | Advanced Fields [#advanced-fields] | Field | Type | Description | | --------------------- | --------- | ---------------------------------------------------------------- | | `exclusiveRelayer` | `address` | Relayer with exclusive fill rights during the exclusivity period | | `exclusivityDeadline` | `uint32` | Unix timestamp — when exclusivity expires | # Intent Architecture in Across import { Callout } from 'fumadocs-ui/components/callout'; Across implements crosschain intents through a **three-layer architecture**. Each layer is modular and can evolve independently, allowing the protocol to support new chains, new auction mechanisms, and more efficient settlement without changing the core design. The Three Layers [#the-three-layers] 1. Request-for-Quote (RFQ) Mechanism [#1-request-for-quote-rfq-mechanism] The RFQ layer houses user intents. Users submit their desired outcome — input token, output token, chains, amounts — and receive quotes from the relayer network. The current implementation uses **fixed fees** with relayer competition strictly based on **speed**. The [Swap API](/introduction/swap-api) is the primary RFQ interface. RFQ systems exist externally to the core protocol. Across can support any auction mechanism that produces transactions or signed orders recognized by the SpokePool infrastructure. 2. Competitive Relayer Network [#2-competitive-relayer-network] Relayers claim orders and execute them by fronting their own capital on the destination chain. Key properties: * **Permissionless** — anyone can run a relayer * **Competitive** — relayers compete on speed to fill intents * **Flexible repayment** — relayers choose which chain to receive repayment on, affecting LP fee distribution * **Capital-efficient** — relayers reuse capital across fills, getting repaid every \~1.5 hours Risk Labs provides an [open-source relayer implementation](https://github.com/across-protocol/relayer-v3), but relayers can customize behavior or build alternatives. 3. Settlement Layer [#3-settlement-layer] The settlement layer verifies that fills are valid and repays relayers. This is where Across's key innovations live: **Aggregated verification** — Instead of verifying each fill individually (O(N) cost), the Dataworker aggregates all fills into a single bundle and proposes it to the HubPool. This makes settlement cost **O(1)** regardless of how many fills occurred — an order of magnitude in gas savings vs. other approaches. **Optimistic verification** — Bundles are proposed with a bond and accepted unless challenged within a window. Only a single honest actor needs to dispute an invalid proposal. Hub-and-Spoke Model [#hub-and-spoke-model] The settlement layer uses a hub-and-spoke architecture: | Component | Location | Role | | ------------------------- | --------------------- | ----------------------------------------------------------- | | **HubPool** | Ethereum L1 | Coordinates settlement, holds LP capital, processes bundles | | **SpokePools** | Every supported chain | Handle deposits (origin) and fills (destination) | | **Dataworker** | Off-chain | Aggregates fills into bundles, proposes to HubPool | | **UMA Optimistic Oracle** | Ethereum L1 | Secures bundle verification with economic bonds | | **Canonical Bridges** | Between L1 and L2s | Transfer funds for rebalancing after bundle finalization | How Settlement Flows [#how-settlement-flows] 1. Relayers fill intents on destination SpokePools using their own capital 2. The Dataworker aggregates all fills within a time window (\~1.5 hours) 3. Fills are validated against deposits, organized into merkle trees 4. The bundle is proposed to the HubPool with a bond 5. After the challenge period, the bundle is finalized 6. Relayer refunds are executed on the chains where relayers requested repayment 7. Token rebalancing instructions move funds between SpokePools via canonical bridges Relayer Repayment Flexibility [#relayer-repayment-flexibility] A key architectural advantage: relayers can request repayment on **any supported chain**, not just the origin or destination. The Hub-and-Spoke model enables this by: 1. **Passive LPs** extend short-term loans — their capital covers the gap between fill and reimbursement 2. **Canonical bridges** handle rebalancing — after settlement, the HubPool routes funds to the correct SpokePools 3. **Relayer capital costs drop** — relayers can optimize their treasury across chains without needing balanced liquidity everywhere The modular design means each layer can evolve independently. New RFQ mechanisms, new relayer strategies, and new settlement approaches (like [Across V4's ZK proofs](/guides/concepts/across-v4)) can be added without disrupting the rest of the system. # Track the lifecycle of a deposit {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # CCTP Migration for API Users import { Callout } from 'fumadocs-ui/components/callout'; After CCTP migration, API users must update token addresses, adopt new endpoint parameters, and migrate to `depositV3()` for routes that require separate input and output tokens. API Host [#api-host] Migrate to the new endpoint: **`app.across.to/api`**. Adopt the new `/suggested-fees` response format which deprecates redundant fields. Token Address Changes [#token-address-changes] When CCTP activates on a chain, Bridged USDC addresses are replaced with **Native USDC addresses** in API responses. For example, on Polygon: * **Before**: Bridged USDC `0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174` * **After**: Native USDC (new address in API responses) New destination routes emerge offering Native USDC as an option alongside existing Bridged USDC routes. New Endpoint Parameters [#new-endpoint-parameters] The `/suggested-fees` and `/limits` endpoints now accept: | Parameter | Type | Description | | ------------- | -------- | ---------------------------------- | | `inputToken` | `string` | Token address on origin chain | | `outputToken` | `string` | Token address on destination chain | These replace the legacy `token` parameter (which becomes optional when `inputToken`/`outputToken` are provided). Fee Behavior [#fee-behavior] For the foreseeable future, fees for Native USDC output and Bridged USDC output routes are **equivalent**. However, limits may differ significantly since they're based on relayer capacity for each token. Smart Contract Migration [#smart-contract-migration] deposit() → depositV3() [#deposit--depositv3] The legacy `deposit()` function cannot specify separate input and output tokens, making it incompatible with Native-to-Bridged USDC routes. Use `depositV3()` instead: ```solidity // depositV3 supports separate inputToken and outputToken function depositV3( address depositor, address recipient, address inputToken, // Native USDC address outputToken, // Bridged USDC (or Native USDC) uint256 inputAmount, uint256 outputAmount, uint256 destinationChainId, ... ) external payable; ``` Bridged USDC as Origin Token [#bridged-usdc-as-origin-token] Routes originating from Bridged USDC **no longer function** at the API and SpokePool contract levels. If users hold Bridged USDC, they must first swap to Native USDC. The `SwapAndBridge` contract facilitates converting Bridged USDC to Native USDC before bridging, with existing audit documentation available. # Migration to CCTP import { Callout } from 'fumadocs-ui/components/callout'; import { Card, Cards } from 'fumadocs-ui/components/card'; import { Step, Steps } from 'fumadocs-ui/components/steps'; Across is transitioning from Bridged USDC to **Native USDC** via Circle's Crosschain Transfer Protocol (CCTP). Rather than a simple pass-through, Across uses its relayer network for fast fills while leveraging CCTP for protocol-level rebalancing — delivering faster and cheaper USDC bridging. What Changes [#what-changes] | | Before CCTP | After CCTP | | ------------------------- | ----------------- | ------------------------------------------- | | **L2 input token** | Bridged USDC only | Native USDC only | | **Deposit routes** | Bridged → Bridged | Native → Native, Native → Bridged | | **Bridged USDC as input** | Supported | Not supported at API/contract level | | **Supported chains** | — | Ethereum, Polygon, Optimism, Arbitrum, Base | After migration, **Bridged USDC can no longer be used as an `inputToken`**. Users holding Bridged USDC can swap it to Native USDC on the origin chain before bridging. Why CCTP? [#why-cctp] Capital Efficiency [#capital-efficiency] When Across proposes sending L2 tokens to the L1 HubPool, those funds are frozen until released after the liveness window. CCTP's faster speed enables more frequent USDC recycling, reducing required LP capital reserves. Lower Fees [#lower-fees] By minimizing how long USDC remains in canonical bridges, average utilization decreases. This directly reduces the LP fee component of bridge fees. Security Alignment [#security-alignment] CCTP is Circle's official Native USDC bridge — the issuer itself operates it. Using CCTP does not add additional trusted actors to the Across system, maintaining the canonical bridge security philosophy. Migration Process [#migration-process] Pause USDC Operations [#pause-usdc-operations] USDC deposits and withdrawals are paused for the target chain. Return Bridged USDC [#return-bridged-usdc] All Bridged USDC is returned to the HubPool on Ethereum. Upgrade Routes [#upgrade-routes] The HubPool's `PoolRebalanceRoutes` are upgraded from Bridged USDC to Native USDC addresses. Upgrade Contracts [#upgrade-contracts] SpokePool and L1 Adapter are upgraded to use CCTP via the `CircleCCTPAdapter`. Resume Operations [#resume-operations] Deposits resume. The SpokePool holds only Native USDC from this point forward. **CCTP latency**: CCTP completes in minutes, compared to 7 days for optimistic rollup canonical bridges or several hours for ZK-rollups. This is what enables the capital efficiency improvement. Who Is Affected [#who-is-affected] Must handle routes where inputToken and outputToken differ (Native USDC → Bridged USDC). Must update to new API host, use new fee response format, and update token addresses. # CCTP Migration for Relayers import { Callout } from 'fumadocs-ui/components/callout'; After CCTP migration, deposits will use `inputToken = Native USDC` and `outputToken = Bridged USDC` on some routes. Relayers must handle scenarios where input and output tokens differ. Key Changes [#key-changes] Different Input and Output Tokens [#different-input-and-output-tokens] The Across V3 `depositV3` function supports deposits where the `outputToken` differs from the `inputToken`. After CCTP migration, this becomes a production reality — relayers must fill deposits where the user deposited Native USDC but expects Bridged USDC on the destination. Token Constraints [#token-constraints] | Rule | Detail | | --------------------- | ----------------------------------------------------------------- | | `inputToken` | Must be natively supported by Across (Native USDC post-migration) | | `outputToken` | Can be any ERC-20 on the destination chain | | Bridged USDC as input | **Not supported** — cannot serve as `inputToken` | Price Computation [#price-computation] Relayers should: 1. Compute the price of the `outputToken` relative to the `inputToken` 2. Normalize both amounts to determine fill profitability 3. Avoid unprofitable fills where the output token is worth more than the input Inventory Requirements [#inventory-requirements] Maintain inventory in **both** Bridged and Native USDC on CCTP-enabled chains (Ethereum, Polygon, Optimism, Arbitrum, Base). Some routes will require Bridged USDC for fills while others require Native USDC. While temporarily hardcoding support for the specific USDC-to-Bridged-USDC route is acceptable, implementations should progress toward supporting **arbitrary output tokens** for future crosschain swap routes. Prerequisites [#prerequisites] * Must be upgraded from Across V2 to V3 before CCTP migration * Must be familiar with the V3 fee structure (see [V2 to V3 migration](/guides/migration/v2-to-v3)) # Breaking Changes for API Users import { Callout } from 'fumadocs-ui/components/callout'; API users and integrators must update their deposit transaction construction to include new parameters returned by the `/suggested-fees` endpoint. Implementation began **January 23, 2025**. Full deprecation of old functions by **April 1, 2025**. Required API Changes [#required-api-changes] The `/api/suggested-fees` endpoint now returns three parameters that **must** be included in your `deposit` or `depositV3` transactions: | Parameter | Description | | --------------------- | -------------------------------------------------- | | `exclusiveRelayer` | Address of the relayer with exclusive fill rights | | `exclusivityDeadline` | Unix timestamp when exclusivity expires | | `fillDeadline` | Unix timestamp by which the deposit must be filled | These values must be passed through to the deposit transaction exactly as returned by the API. ```ts title="use-api-params.ts" const fees = await fetch( `https://app.across.to/api/suggested-fees?${params}` ).then((r) => r.json()); // These MUST be included in your deposit transaction const { exclusiveRelayer, exclusivityDeadline, fillDeadline } = fees; ``` Deprecated Functions [#deprecated-functions] Two deposit functions are being phased out: | Function | Status | | --------------------------------- | --------------------------------------- | | `deposit()` (original version) | Deprecated — removed from contract ABIs | | `depositFor()` (original version) | Deprecated — removed from contract ABIs | These functions are **not guaranteed to work after April 1, 2025**. Migrate to the new `deposit` function (distinct from the deprecated version) which provides all the functionality of `depositV3` with future support for non-EVM chain deposits. Recommended: Use /deposit/status [#recommended-use-depositstatus] Instead of independently indexing Across events, adopt the `/deposit/status` endpoint for tracking. It has approximately 60-second latency and is simpler to integrate than event indexing. # Non-EVM and Prefills Migration import { Callout } from 'fumadocs-ui/components/callout'; import { Card, Cards } from 'fumadocs-ui/components/card'; This migration introduces two major features: **non-EVM chain support** (extending Across beyond EVM-only) and **prefills** (deterministic relay hashes enabling fills that occur simultaneously with or before deposits). Contract migration occurred **January 23, 2025**. Breaking changes took effect on that date. What Changed [#what-changed] Non-EVM Expansion [#non-evm-expansion] Multiple SpokePool functions and events now use `bytes32` addresses instead of `address`, facilitating cross-compatibility between EVM and non-EVM deployments (like Solana). Prefills [#prefills] A new `unsafeDeposit` function enables **prefills** — fills that occur simultaneously with or before deposits. Deterministic relay hashes ensure deposit inclusion timing on origin chains doesn't affect destination fill correctness. Event Renaming [#event-renaming] Four SpokePool events are renamed and updated: | Old Event | New Event | | --------------------------- | ------------------------- | | `V3FundsDeposited` | `FundsDeposited` | | `RequestedSpeedUpV3Deposit` | `RequestedSpeedUpDeposit` | | `FilledV3Relay` | `FilledRelay` | | `RequestedV3SlowFill` | `RequestedSlowFill` | All replacement events convert `address` types to `bytes32` and change `depositId` from `uint32` to `uint256`. Repayment Flexibility [#repayment-flexibility] The new `fillRelay` function allows relayers to designate a **repayment address** different from the fill-executing address. Who Is Affected [#who-is-affected] Must update to track 4 new SpokePool events. Old events are deprecated. Must include exclusiveRelayer, exclusivityDeadline, and fillDeadline from API responses in deposit transactions. Must handle new event structures and migrate to fillRelay. Testnet environment with sample deposits and fills for migration testing. Recommended (Non-Breaking) Changes [#recommended-non-breaking-changes] * Adopt `/deposit/status` endpoint instead of independently indexing Across events (\~60 second latency) * Use the new `deposit` method (not `depositV3`) for future non-EVM destination support * Relayers: migrate toward `fillRelay` for repayment address selection and non-EVM fill capabilities # Breaking Changes for Indexers import { Callout } from 'fumadocs-ui/components/callout'; Indexers of Across data must update to support the new event types. The recommendation is to consume **both old and new events** during migration to prevent timing issues — each SpokePool upgrades at different times. Effective **January 23, 2025**. Failing to update will cause indexer failures as SpokePools emit only the new events. Key Changes [#key-changes] Address Type Conversion [#address-type-conversion] All `address` types are converted to `bytes32` to accommodate longer address types from non-EVM chains (like Solana). Deposit ID Expansion [#deposit-id-expansion] `depositId` changes from `uint32` to `uint256` to support larger identifier spaces. Message Optimization [#message-optimization] `FilledRelay` and `RequestedSlowFill` events now contain only the `messageHash` instead of the full `message` to reduce gas costs. Event Mapping [#event-mapping] | Deprecated Event | Replacement Event | Key Changes | | --------------------------- | ------------------------- | ----------------------------------------------------------------------------------- | | `V3FundsDeposited` | `FundsDeposited` | `address` → `bytes32`; `depositId`: `uint32` → `uint256` | | `RequestedSpeedUpV3Deposit` | `RequestedSpeedUpDeposit` | `address` → `bytes32`; `depositId`: `uint32` → `uint256` | | `FilledV3Relay` | `FilledRelay` | `address` → `bytes32`; `message` → `messageHash`; `depositId`: `uint32` → `uint256` | | `RequestedV3SlowFill` | `RequestedSlowFill` | `address` → `bytes32`; `message` → `messageHash`; `depositId`: `uint32` → `uint256` | Migration Strategy [#migration-strategy] Query **both** old and new event types during the transition period. Each SpokePool upgrades independently, but the protocol guarantees no single fill or deposit will emit duplicate events across old and new formats. ```ts title="dual-event-indexing.ts" // Index both old and new deposit events during migration const oldDepositEvent = parseAbiItem( "event V3FundsDeposited(address inputToken, address outputToken, uint256 inputAmount, uint256 outputAmount, uint256 indexed destinationChainId, uint32 indexed depositId, uint32 quoteTimestamp, uint32 fillDeadline, uint32 exclusivityDeadline, address indexed depositor, address recipient, address exclusiveRelayer, bytes message)" ); const newDepositEvent = parseAbiItem( "event FundsDeposited(bytes32 inputToken, bytes32 outputToken, uint256 inputAmount, uint256 outputAmount, uint256 indexed destinationChainId, uint256 indexed depositId, uint32 quoteTimestamp, uint32 fillDeadline, uint32 exclusivityDeadline, bytes32 indexed depositor, bytes32 recipient, bytes32 exclusiveRelayer, bytes message)" ); // Subscribe to both during the transition ``` # Breaking Changes for Relayers import { Callout } from 'fumadocs-ui/components/callout'; Relayers must update their event subscriptions and fill functions to support the new event formats introduced in the non-EVM migration. Changes took effect **January 23, 2025**. Query both old and new event types during the transition. Event Changes [#event-changes] Three events that relayers depend on are being deprecated and replaced: | Deprecated Event | Replacement Event | | --------------------------- | ------------------------- | | `V3FundsDeposited` | `FundsDeposited` | | `RequestedSpeedUpV3Deposit` | `RequestedSpeedUpDeposit` | | `FilledV3Relay` | `FilledRelay` | Key Differences [#key-differences] * **Address types**: All `address` fields → `bytes32` to support non-EVM chains * **Gas optimization**: `FilledRelay` contains only the `messageHash` instead of the full message * **Deposit ID**: `uint32` → `uint256` Migration Strategy [#migration-strategy] Query **both** old and new event types to ensure a smooth transition. Each SpokePool upgrades at a different time, but the protocol guarantees no single fill or deposit will emit duplicate events. ```ts title="dual-event-monitoring.ts" // Monitor both old and new deposit events // Old: V3FundsDeposited (address types, uint32 depositId) // New: FundsDeposited (bytes32 types, uint256 depositId) // Process whichever event you receive — they won't overlap ``` New fillRelay Function [#new-fillrelay-function] The new `fillRelay` function replaces `fillV3Relay` and adds: * **Repayment address selection** — Relayers can designate a repayment address different from the fill-executing address * **Non-EVM fill capabilities** — Supports filling on non-EVM chains Migrating to `fillRelay` is **recommended** but not immediately required. `fillV3Relay` continues to work during the transition period. However, `fillRelay` is required for Solana and future non-EVM chain fills. # Testnet Environment import { Callout } from 'fumadocs-ui/components/callout'; A testnet environment is available on Sepolia for testing the non-EVM migration changes. Two SpokePools are deployed with sample deposits and fills. **No bundles will be run on this test deployment.** All WETH you deposit will be permanently locked. This environment is for testing event indexing and fill logic only. SpokePool 1 [#spokepool-1] | Property | Value | | -------------- | -------------------------------------------- | | **Address** | `0x1755DD08108095C48509F5C91e071e0cb62eb27a` | | **WETH Token** | `0x43f133FE6fDFA17c417695c476447dc2a449Ba5B` | | **Chain ID** | `11155111` | SpokePool 2 [#spokepool-2] | Property | Value | | -------------- | -------------------------------------------- | | **Address** | `0xeF89171DBD5DEe4D74842C62689EC0804aF5807B` | | **WETH Token** | `0x5b3acAADE6c53eFCD113dCa116F71312f2df666d` | | **Chain ID** | `11155112` | Sample Transfers [#sample-transfers] SpokePool 1 → SpokePool 2 [#spokepool-1--spokepool-2] | Transaction | Hash | | ----------- | -------------------------------------------------------------------- | | **Deposit** | `0xf260507cbff7f46fa8fe5fa35c20d839393123a9a396b371c37ba48ff6364806` | | **Fill** | `0xb1294420167b4468ef1ec8aef8994678c92b0be01afb3dc5e37a5ab7d8978615` | SpokePool 2 → SpokePool 1 [#spokepool-2--spokepool-1] | Transaction | Hash | | ----------- | -------------------------------------------------------------------- | | **Deposit** | `0x14c5b18b838387e4ea613ce95c0c43b4823cd62997cefad301264dc4b83ce0ba` | | **Fill** | `0x27474ba120315af04ce69c1296b9da3487972f4b5e40138add4e581c689fde6a` | Creating Test Transfers [#creating-test-transfers] You can create your own deposits and fills by: 1. Minting WETH on either SpokePool using the WETH contract's `deposit()` function (send ETH to receive WETH) 2. Calling the SpokePool deposit function with the minted WETH 3. Filling the deposit on the other SpokePool Contract artifacts are available at the [GitHub release v3.1.0-beta.4](https://github.com/across-protocol/contracts/releases). # Submit a signed gasless swap {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} import { Callout } from 'fumadocs-ui/components/callout'; **Origin support.** Gasless quoting and submission are currently supported for **HyperCore (Hyperliquid) as the origin chain** (`originChainId: 1337`) — **support for additional origin chains is coming soon**. See the [Hyperliquid Withdrawals Guide](/introduction/hypercore-withdrawals) for a working example. # Get supported tokens for swap operations {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Get swap approval data {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Build embedded crosschain swap actions with the Swap API {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Get a gasless swap quote {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} import { Callout } from 'fumadocs-ui/components/callout'; **Origin support.** Gasless quoting and submission are currently supported for **HyperCore (Hyperliquid) as the origin chain** (`originChainId: 1337`) — **support for additional origin chains is coming soon**. See the [Hyperliquid Withdrawals Guide](/introduction/hypercore-withdrawals) for a working example. # Get supported sources for swap operations {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Get supported chains for swap operations {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}