Bridge Integration Guide

This guide contains technical instructions for using our API to provide bridge quotes to your users via bridge experience alongside your application. It is a simple two step integration process:

  1. Request quotes from Across API

  2. Use data returned to call functions on Across smart contracts (initiate a deposit)

Request a Quote

The process for initiating a deposit begins with determining the fee that needs to be paid to the relayer. To do this, you can use the suggested-fees endpoint: app.across.to/api/suggested-fees with the following query parameters:

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

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

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

  • amount: 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.

Example for a user transferring 1000 USDC from Ethereum to Optimism:

curl "https://app.across.to/api/suggested-fees?token=0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48&originChainId=1&destinationChainId=10&amount=1000000000"

There are two elements of the response that you'll need to create the deposit:

  • totalRelayFee.total: this is the amount of the deposit that the user will need to pay to have it relayed. It is intended to capture all fees, including gas fees, relayer fees, and system fees.

  • timestamp: this is the quote timestamp. Specifying this timestamp onchain ensures that the system fees don't shift under the user while the intent is in-flight.

To determine how long a fill is expected to take and to ensure that Across can process a deposit of a given size, you'll want to refer the following elements of the response:

  • fillSpeedType: one of instant, shortDelay or slow corresponding to an instant fill (within seconds), short delay (within 5-15 minutes) or slow (~3 hours)

  • estimateFillTimeSec = the estimated fill time, for the specified route and asset, in seconds

The above values depend on the amount in the request relative to the following limit elements of the response:

  1. maxDepositInstant: if the user's amount is less than or equal to this amount, there is known to be enough relayer liquidity on the destination chain to fill them instantly, within seconds

  2. maxDepositShortDelay: if the user's deposit amount is larger than maxDepositInstant, and less than or equal to this amount, there is known to be enough relayer liquidity that can be moved to the destination chain to fill the user within 30 minutes

  3. maxDeposit: if the user's deposit amount is larger than maxDepositShortDelay, and less than or equal to this amount, there is enough liquidity in Across to fill them via a slow fill, which could take up to 3 hours. In most cases maxDeposit will be equal to maxDepositShortDelay to reduce the likelihood of slow fills. If the user's deposit amount is larger than this, Across cannot fulfill the user's intent

  4. reccommendedDepositInstant: this is used for certain integrations to limit the input size, and is currently hardcoded to 2 ETH/WETH and 5,000 USDC

You can find details on the Across API here.

Initiating a Deposit (User Intent)

Deposits are initiated by interacting with contracts called SpokePools. There is one SpokePool deployed on each chain supported by Across. Each SpokePool has minor modifications to work with each chain, but maintains the same core interface and implementation. For example, on Ethereum the SpokePool contract is named Ethereum_SpokePool.sol, and on Optimism the contract is named Optimism_SpokePool.sol.

Calling depositV3

Before making the call, you'll need the SpokePool address. This can be retrieved from the suggested-fees response for convenience, but we suggest manually verifying these and hardcoding them per chain in your application for security. You can find the addresses here.

Once you have the SpokePool address, you'll need to approve the SpokePool to spend tokens from the user's EOA or the contract that will be calling the SpokePool . The approval amount must be >= the amount value. If sending ETH, no approval is necessary.

The deposit call can come from an intermediary contract or directly from the user's EOA. This is the function that needs to be called:

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;

Here is an example of how the parameters could be populated in Javascript:

const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";

// Bridge to the same address on the destination chain.
// Note: If the depositor is not an EOA then the depositor's address
// might not be valid on the destination chain.
const depositor = <USER_ADDRESS>;
const recipient = depositor;

// Test WETH deposit.
const inputToken = <ORIGIN_CHAIN_WETH_ADDRESS>;

// The 0 address is resolved automatically to the equivalent supported
// token on the the destination chain. Any other input/output token
// combination should be advertised by the Across API available-routes
// endpoint.
const outputToken = ZERO_ADDRESS;

// The outputAmount is set as the inputAmount - relay fees.
// totalRelayFee.total is returned by the Across API suggested-fees
// endpoint.
const outputAmount = inputAmount.sub(BigNumber.from(response.totalRelayFee.total));

// fillDeadline: A fill deadline of 5 hours. Can be up to
// SpokePool.getCurrentTime() + SpokePool.fillDeadlineBuffer() seconds.
const fillDeadlineBuffer = 18000;
const fillDeadline = Math.round(Date.now() / 1000) + fillDeadlineBuffer;

// timestamp is returned by the Across API suggested-fees endpoint.
// This should be _at least 2_ mainnet blocks behind the current time
// for best service from relayers.
const quoteTimestamp = response.timestamp;

// Exclusive relayer and exclusivity deadline should be taken from the
// Across API suggested-fees response.
const exclusivityDeadline = response.exclusivityDeadline;
const exclusiveRelayer = response.exclusiveRelayer;

// No message will be executed post-fill on the destination chain.
// See `Across+ Integration` for more information.
const message = "0x";

spokePool.depositV3(
  depositor,
  recipient,
  inputToken,
  outputToken,
  inputAmount,
  outputAmount,
  destinationChainId,
  exclusiveRelayer,
  quoteTimestamp,
  fillDeadline,
  exclusivityDeadline,
  message
)

and in Solidity (see also the Javascript descriptions above):

spokePool.depositV3(
  depositor, // User's address on the origin chain.
  recipient, // Receiving address on the destination chain.
  weth, // inputToken. This is the WETH address on the origin chain.
  address(0), // outputToken: Auto-resolve the destination equivalent token.
  inputAmount,
  inputAmount - totalRelayFee, // outputAmount
  destinationChainId,
  exclusiveRelayer, // exclusiveRelayer from suggested-fees, or 0x0 to disable
  quoteTimestamp, // suggested-fees timestamp, or block.timestamp - 36
  block.timestamp + spokePool.fillDeadlineBuffer(),
  exclusivityDeadline, // exclusivityDeadline from suggested-fees, or 0 to disable
  "", // message (empty)
);

Append Unique Identifier to CallData

In order to track the origination source for deposits for analytics, debugging and support purposes we request all integrators to append a delimiter of 1dc0de and a unique identifier provided by our team to the deposit transaction call data.

Here is an example of depositV3 call data with 1dc0de delimiter and f001 unique identifier appended.

Do NOT pass delimiter + identifier to any depositV3 param, including the message param. Only append to call data of the transaction.

Do NOT use f001 identifier in your implementation, see below on how to request your unique ID.

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 message)

0x7b939232000000000000000000000000c30c7ea910a71ce06ae840868b0c7e47616ba4c9000000000000000000000000c30c7ea910a71ce06ae840868b0c7e47616ba4c9000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000000000000000000000000000000000009502f9000000000000000000000000000000000000000000000000000000000094fd2f84000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000066799bfb000000000000000000000000000000000000000000000000000000006679f0f40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000001dc0def001

Request your unique identifier in our shared communication channel (TG, Slack, etc), if already set up, or reach out to [email protected]. Please do not use any other identifier other than the one provided to you.

Updating Deposits

Relayer fees are a function of the spread between the outputAmount and inputAmount parameters of a depositV3. If this spread is too low, it may not be profitable for relayers to fill. While a deposit is not filled, the outputAmount can be increased by calling speedUpDepositV3 , which requires a signature from the depositor address. If fees are set according to the above instructions, this functionality should not be needed.

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: chain Id 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.

We also provide a deeper explanation to implement your service to track events in Tracking Events.

Last updated