Bridge Integration Guide

This is a step-by-step framework agnostic guide to help you integrate Across Protocol into your applications using @across-protocol/app-sdk.

The App-SDK makes it extremely easy to add a bridge experience to any app (wallet, DEX, staking, lending/borrowing, etc). It abstracts away a lot of nuances about fetching bridge quotes and executing deposits by simply following a 2 step process:

  1. Request quotes using getQuote()

  2. Initiate deposit using executeQuote()

Along with the above mentioned functions, there are several utilities that we can use to ensure that the entire process of integrating a bridge takes less than an hour. Let's dive in!


Pre-Requisites

  1. A frontend framework of your choice (Next.js, Vue, Svelte, Vanilla JS/TS, etc).

  2. Basic understanding of intents architecture in Across.

  3. A web3 wallet kit of your choice.


Initialize the SDK

  1. We will kickoff by initializing the @across-protocol/app-sdk. To do so, install the SDK:

pnpm i @across-protocol/app-sdk viem

We will be using some utilities provided by Viem to ensure a quick integration.

  1. Now, set up the AcrossClient , add in your integrator ID and configure the chains you want to support.

import { createAcrossClient } from "@across-protocol/app-sdk";
import { mainnet, optimism, arbitrum } from "viem/chains";

const client = createAcrossClient({
  integratorId: "your-integrator-ID", 
  chains: [mainnet, optimism, arbitrum],
});

In case you don't have an integrator ID, please fill this form.


Retrieve a Quote

A Quote refers to the user’s expressed intent for a crosschain transaction. Rather than detailing the exact execution path, the user defines the desired outcome, all encapsulated in a standardized order structure. Here are the parameters you must define while retrieving a quote:

  • originChainId: chainId where the user's deposit is originating

  • destinationChainId: chainId where the user intends to receive their funds

  • inputToken: address of the token that the user is depositing on the origin chain

  • outputToken : address of the token that the user intends to receive on the destination chain

  • inputAmount: the raw amount the user is transferring. By raw amount, this means it should be represented exactly as it is in Solidity, meaning 1 USDC would be 1e6 or 1 ETH would be 1e18.

Here is an example:

import { parseEther } from "viem";

// WETH from Arbitrum -> Optimism
const quote = await client.getQuote({
  originChainId: arbitrum.id,
  destinationChainId: optimism.id,
  inputToken: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", // WETH arb
  outputToken: "0x4200000000000000000000000000000000000006", // WETH opt
  inputAmount: parseEther("1"),
});

When executed successfully, the output will be something like this:

{
  "deposit": {
    "destinationChainId": 10,
    "destinationSpokePoolAddress": "0x6f26Bf09B1C792e3228e5467807a900A503c0281",
    "exclusiveRelayer": "0x15652636f3898F550b257B89926d5566821c32E1",
    "exclusivityDeadline": 9,
    "fillDeadline": 1742312575,
    "inputAmount": "1000000000000000000n",
    "inputToken": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
    "message": "0x",
    "originChainId": 42161,
    "outputAmount": "999873046291859064n",
    "outputToken": "0x4200000000000000000000000000000000000006",
    "quoteTimestamp": 1742300663,
    "recipient": null,
    "spokePoolAddress": "0xe35e9842fceaCA96570B734083f4a58e8F7C5f2A",
  },
  "estimatedFillTimeSec": 2,
  "fees": {
    "lpFee": {
      "pct": "48018079727047n",
      "total": "48018079727047n"
    },
    "relayerCapitalFee": {
      "pct": "78750000000001n",
      "total": "78750000000001n"
    },
    "relayerGasFee": {
      "pct": "185628413888n",
      "total": "185628413888n"
    },
    "totalRelayFee": {
      "pct": "126953708140936n",
      "total": "126953708140936n"
    }
  },
  "isAmountTooLow": false,
  "limits": {
    "maxDeposit": "1218075461911387487542n",
    "maxDepositInstant": "240880099587949741088n",
    "maxDepositShortDelay": "1218075461911387487542n",
    "minDeposit": "263464345370141n",
    "recommendedDepositInstant": "240880099587949741088n"
  }
}

Important fields to note here are:

  1. deposit.outputAmount : This is the amount of outputToken the user will receive after deducting the totalRelayFee from the inputAmount .

  2. estimatedFillTimeSec : This is the estimated time for the deposit to be filled and user to receive funds on the destination chain.

  3. fees.totalRelayFee : This is the total fees (lpFee + capitalFee + destinationGasFee) that the user will be charged for conducting the bridge transaction.

  4. isAmountTooLow : This is a warning for when the inputAmount is too low for any relayer to fill. You can use this to show an error on the UI and request user to increase inputAmount.

  5. limits : To make the UX seamless, we present you with minimum and maximum deposit limits of the system so you can help the user achieve maximum efficiency with their fill times on Across.

Get Supported Chains Easily

The Across App-SDK comes with the getSupportedChains() function that allows developers to get all the necessary data about the origin and destination chain in less than a second.

With this, you can build dropdown menus and simply pass the information fetched by getSupportedChains() to build a complete user experience in seconds with minimal effort.

To fetch supported chains, simply call the function with the SDK initialized like we had discussed above:

const fetchChains = async () => {
  const chains = await client.getSupportedChains({});
  console.log("supported chains are: ", chains);
  return chains;  
};

fetchChains();

You can use the same function to get the details of a singular chain as well:

const fetchChain = async (chainId: number) => {
  const chains = await client.getSupportedChains({chainId:chainId});
  console.log("supported chains are: ", chains);
  return chain;  
};

fetchChain(chainId)

Upon running the above command with chainId = 1 , You will see inputTokens and outputTokens supported by Across on Ethereum. Along with this, there will be several other details that will allow you to build a seamless UX:

{
  "chainId": 1,
  "explorerUrl": "https://etherscan.io",
  "inputTokens": [{...}, {...}, {...}],
  "logoUrl": "https://raw.githubusercontent.com/across-protocol/frontend/master/scripts/chain-configs/mainnet/assets/logo.svg",
  "name": "Ethereum",
  "outputTokens": [{...}, {...}, {...}],
  "publicRpcUrl": "https://mainnet.gateway.tenderly.co",
  "spokePool": "0x5c7BCd6E7De5423a257D81B442095A1a6ced35C5",
  "spokePoolBlock": 17117454
}

inputTokens and outputTokens are array of objects. These objects contain details like address, decimal, logoUrl, name and symbol related to all supported tokens.


Execute a Quote

Once the quote is received, you will need the following parameters to execute it successfully:

  1. deposit : This is the deposit to execute. You can get this from the return value of the useQuote()function that we discussed above.

  2. walletClient : This is the wallet client to use for the deposit. We recommend using wagmi for this.

  3. onProgress : This is a handler for the execution progress.

Quote execution has 3 steps:

  1. Approve: Approving the SpokePool contract to transfer the specified amount of tokens from your account.

  2. Deposit: Depositing the input token on the origin chain.

  3. Fill: Waiting for the deposit to be filled on the destination chain.

using the onProgress handler you can track which step the current quote execution is on and debug if needed.

Here is how you can call the executeQuote() function in the Across App-SDK:

import { useWalletClient } from "wagmi";

const wallet = useWalletClient();

await client.executeQuote({
  walletClient: wallet,
  deposit: quote.deposit, // returned by `getQuote`
  onProgress: (progress) => {
    if (progress.step === "approve" && progress.status === "txSuccess") {
      // if approving an ERC20, you have access to the approval receipt
      const { txReceipt } = progress;
    }
    if (progress.step === "deposit" && progress.status === "txSuccess") {
      // once deposit is successful you have access to depositId and the           receipt
      const { depositId, txReceipt } = progress;
    }
    if (progress.step === "fill" && progress.status === "txSuccess") {
      // if the fill is successful, you have access the following data
      const { fillTxTimestamp, txReceipt, actionSuccess } = progress;
      // actionSuccess is a boolean flag, telling us if your cross chain messages were successful
    }
  },
});

Upon successful execution, you will be able to see funds on the destinationChain in the requested outputToken .

Congratulations! You have now successfully integrated Across into your application.

You can find a complete App-SDK integration here for your reference.


Tracking Deposits

To track the lifecycle of a deposit you can use the app.across.to/api/deposit/status endpoint with the following parameters:

  • originChainId: chainId where the deposit originated from

  • depositId: The deposit id that is emitted from the DepositV3 function call as a V3FundsDeposited event

The recommended solution for tracking all Across deposits originating from your integration, for a single user, is to store the user's depositId and originChainId from each transaction originating from your app, and then get the status of each via the above endpoint.

In case you face any errors or need support, please feel free to reach out to us.

Last updated