Skip to content

Across Protocol + Biconomy MEE Integration Guide

๐ŸŒ‰ Cross-Chain USDC Supply to AAVE with Gasless Fusion Orchestration

This guide demonstrates how to use Across Protocol together with Biconomy's Modular Execution Environment (MEE) to perform a cross-chain supply of USDC into AAVE, completely gasless using Fusion Orchestration.

๐ŸŽฏ Overview

The integration enables:

  • ๐Ÿ”€ Cross-chain bridging from Optimism to Base using Across Protocol
  • ๐Ÿฆ Automated AAVE supply on the destination chain
  • โ›ฝ Gasless execution through Fusion Orchestration
  • โœ๏ธ Single signature UX for the entire flow
  • โšก Ultra-fast bridging with Across's optimistic design

๐Ÿ—๏ธ Architecture

Key Components

  1. ๐ŸŒ Across Protocol: Ultra-fast optimistic bridge with capital-efficient design
  2. ๐Ÿ”ง Biconomy MEE: Orchestrates the entire transaction flow
  3. ๐ŸŽญ Fusion Mode: Enables gasless execution with external wallets
  4. ๐Ÿค Companion Account: Temporary smart account for orchestration

Flow Diagram

User EOA (Optimism) ๐Ÿ’ฐ
    โ†“ [Sign Trigger] โœ๏ธ
Companion Account ๐Ÿค–
    โ†“ [Bridge via Across] ๐ŸŒ‰
Base Network โ›“๏ธ
    โ†“ [Supply to AAVE] ๐Ÿฆ
aUSDC โ†’ User EOA (Base) ๐Ÿ’Ž

๐Ÿ“ Implementation Guide

1. Setup and Dependencies

First, create the Across Quote Service file. Copy and paste this entire code block into a new file called across-quote-service.ts:

// across-quote-service.ts
import type { Address, Hex } from 'viem';
import { getAddress, formatUnits, parseUnits } from 'viem';
 
/**
 * -------------------------------------------------------------
 *  Across Protocol Quote Service โ€” typed with viem
 * -------------------------------------------------------------
 */
 
export type SuggestedFeesParameters = {
  inputToken: Address;
  outputToken: Address;
  originChainId: number;
  destinationChainId: number;
  amount: bigint;
  depositor?: Address;
  recipient?: Address;
  message?: Hex;
  relayerAddress?: Address;
  referrer?: Address;
};
 
export type Fee = {
  pct: bigint;
  total: bigint;
};
 
export type Limits = {
  minDeposit: bigint;
  maxDeposit: bigint;
  maxDepositInstant: bigint;
  maxDepositShortDelay: bigint;
  recommendedDepositInstant: bigint;
};
 
export type SuggestedFeesReturnType = {
  totalRelayFee: Fee;
  relayerCapitalFee: Fee;
  relayerGasFee: Fee;
  lpFee: Fee;
  timestamp: bigint;
  isAmountTooLow: boolean;
  quoteBlock: bigint;
  spokePoolAddress: Address;
  fillDeadline: bigint;
  limits: Limits;
};
 
export type GetSuggestedFeesErrorType = Error;
 
/**
 * Gets suggested fees for Across Protocol bridge transfer
 * 
 * @example
 * const fees = await getAcrossSuggestedFees({
 *   inputToken: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
 *   outputToken: '0x4200000000000000000000000000000000000006',
 *   originChainId: 1,
 *   destinationChainId: 10,
 *   amount: parseEther('1')
 * })
 */
export async function getAcrossSuggestedFees(
  parameters: SuggestedFeesParameters
): Promise<SuggestedFeesReturnType> {
  const {
    inputToken,
    outputToken,
    originChainId,
    destinationChainId,
    amount,
    depositor,
    recipient,
    message,
    relayerAddress,
    referrer
  } = parameters;
 
  const url = new URL('https://app.across.to/api/suggested-fees');
  
  // Validate and format addresses
  url.searchParams.append('inputToken', getAddress(inputToken));
  url.searchParams.append('outputToken', getAddress(outputToken));
  url.searchParams.append('originChainId', originChainId.toString());
  url.searchParams.append('destinationChainId', destinationChainId.toString());
  url.searchParams.append('amount', amount.toString());
  
  if (depositor) {
    url.searchParams.append('depositor', getAddress(depositor));
  }
  if (recipient) {
    url.searchParams.append('recipient', getAddress(recipient));
  }
  if (message) {
    url.searchParams.append('message', message);
  }
  if (relayerAddress) {
    url.searchParams.append('relayerAddress', getAddress(relayerAddress));
  }
  if (referrer) {
    url.searchParams.append('referrer', getAddress(referrer));
  }
 
  const response = await fetch(url.toString(), {
    method: 'GET',
    headers: {
      'Accept': 'application/json',
    },
  });
 
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }
 
  const data = await response.json() as {
    totalRelayFee: { pct: string; total: string };
    relayerCapitalFee: { pct: string; total: string };
    relayerGasFee: { pct: string; total: string };
    lpFee: { pct: string; total: string };
    timestamp: string;
    isAmountTooLow: boolean;
    quoteBlock: string;
    spokePoolAddress: string;
    expectedFillTimeSec: string;
    fillDeadline: string;
    limits: {
      minDeposit: string;
      maxDeposit: string;
      maxDepositInstant: string;
      maxDepositShortDelay: string;
      recommendedDepositInstant: string;
    };
  };
  
  // Parse response to bigint types
  return {
    totalRelayFee: {
      pct: BigInt(data.totalRelayFee.pct),
      total: BigInt(data.totalRelayFee.total)
    },
    relayerCapitalFee: {
      pct: BigInt(data.relayerCapitalFee.pct),
      total: BigInt(data.relayerCapitalFee.total)
    },
    relayerGasFee: {
      pct: BigInt(data.relayerGasFee.pct),
      total: BigInt(data.relayerGasFee.total)
    },
    lpFee: {
      pct: BigInt(data.lpFee.pct),
      total: BigInt(data.lpFee.total)
    },
    timestamp: BigInt(data.timestamp),
    isAmountTooLow: data.isAmountTooLow,
    quoteBlock: BigInt(data.quoteBlock),
    spokePoolAddress: getAddress(data.spokePoolAddress),
    fillDeadline: BigInt(data.fillDeadline),
    limits: {
      minDeposit: BigInt(data.limits.minDeposit),
      maxDeposit: BigInt(data.limits.maxDeposit),
      maxDepositInstant: BigInt(data.limits.maxDepositInstant),
      maxDepositShortDelay: BigInt(data.limits.maxDepositShortDelay),
      recommendedDepositInstant: BigInt(data.limits.recommendedDepositInstant)
    }
  };
}
 
/**
 * Calculates total fees from suggested fees response
 * 
 * @example
 * const result = calculateAcrossFees({
 *   fees,
 *   amount: parseEther('1')
 * })
 * 
 * console.log(result.totalFees) // 0.005n (example)
 * console.log(result.outputAmount) // 0.995n (example)
 */
export type CalculateAcrossFeesParameters = {
  fees: SuggestedFeesReturnType;
  amount: bigint;
};
 
export type CalculateAcrossFeesReturnType = {
  totalFees: bigint;
  relayerFees: bigint;
  lpFees: bigint;
  outputAmount: bigint;
};
 
export function calculateAcrossFees(
  parameters: CalculateAcrossFeesParameters
): CalculateAcrossFeesReturnType {
  const { fees, amount } = parameters;
  
  // Calculate fees based on percentages (denominated in 1e18)
  const PRECISION = 10n ** 18n;
  
  const relayerFees = (amount * fees.totalRelayFee.pct) / PRECISION;
  const lpFees = (amount * fees.lpFee.pct) / PRECISION;
  const totalFees = relayerFees + lpFees;
  const outputAmount = amount - totalFees;
  
  return {
    totalFees,
    relayerFees,
    lpFees,
    outputAmount
  };
}
 
/**
 * Formats Across fee percentage to human readable format
 * 
 * @example
 * formatAcrossFeePercentage(376607094864283n) // "0.0376607094864283"
 */
export function formatAcrossFeePercentage(pct: bigint): string {
  return formatUnits(pct, 16); // Across uses 1e18 for 100%, so 1e16 for 1%
}

Now, import the dependencies in your main file:

import { 
  createMeeClient, 
  toMultichainNexusAccount,
  createChainAddressMap,
  runtimeERC20BalanceOf,
  greaterThanOrEqualTo,
  type Trigger 
} from "@biconomy/abstractjs"
import { 
  getAcrossSuggestedFees, 
  calculateAcrossFees 
} from "./across-quote-service" // ๐Ÿ‘ˆ Import the service you just created
import { base, optimism } from "viem/chains"
import { privateKeyToAccount } from "viem/accounts"
import { encodeFunctionData, http, parseAbi, parseUnits, zeroAddress } from "viem"

2. ๐Ÿ“ Configure Token and Protocol Addresses

// ๐Ÿ’ต USDC addresses on different chains
const usdcAddresses = createChainAddressMap([
  [base.id, '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'],
  [optimism.id, '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85']
])
 
// ๐Ÿฆ AAVE Pool addresses
const aavePoolAddresses = createChainAddressMap([
  [base.id, '0xA238Dd80C259a72e81d7e4664a9801593F98d1c5'],
  [optimism.id, '0x794a61358D6845594F94dc1DB02A252b5b4814aD']
])
 
// ๐Ÿ’Ž aUSDC (AAVE interest-bearing USDC)
const aUSDCAddresses = createChainAddressMap([
  [base.id, '0x4e65fE4DbA92790696d040ac24Aa414708F5c0AB'],
  [optimism.id, '0x625E7708f30cA75bfd92586e17077590C60eb4cD']
])
 
// ๐Ÿš€ Across SpokePool addresses
const acrossSpokePool = createChainAddressMap([
  [base.id, '0x09aea4b2242abc8bb4bb78d537a67a245a7bec64'],
  [optimism.id, '0x6f26Bf09B1C792e3228e5467807a900A503c0281']
])

3. ๐Ÿš€ Initialize MEE Client and Orchestrator

// Create orchestrator account
const eoa = privateKeyToAccount(PRIVATE_KEY)
const orchestrator = await toMultichainNexusAccount({
  chains: [optimism, base],
  transports: [http(), http('https://base.llamarpc.com')],
  signer: eoa
})
 
// Initialize MEE client
const meeClient = await createMeeClient({
  account: orchestrator,
  apiKey: 'your_mee_api_key'
})

4. ๐Ÿ” Get Across Bridge Quote & Build Transaction

โš ๏ธ Important: Make sure you've created the across-quote-service.ts file from Step 1 before proceeding!

Unlike other bridges, with Across we build the transaction ourselves:

// Get suggested fees from Across
const fees = await getAcrossSuggestedFees({
  amount: inputAmount,
  originChainId: optimism.id,
  inputToken: usdcAddresses[optimism.id],
  destinationChainId: base.id,
  outputToken: usdcAddresses[base.id]
})
 
// Calculate output amount after fees
const { outputAmount } = calculateAcrossFees({
  fees,
  amount: inputAmount
})
 
// Define Across SpokePool ABI
const acrossSpokePoolAbi = parseAbi([
  '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'
])
 
// Build the deposit transaction
const transactionRequest = {
  to: acrossSpokePool[optimism.id],
  chainId: optimism.id,
  data: encodeFunctionData({
    abi: acrossSpokePoolAbi,
    functionName: 'depositV3',
    args: [
      orchestrator.addressOn(optimism.id)!,      // depositor
      orchestrator.addressOn(base.id)!,          // recipient
      usdcAddresses[optimism.id],                // inputToken
      usdcAddresses[base.id],                    // outputToken
      inputAmount,                               // inputAmount
      outputAmount,                              // outputAmount (after fees)
      BigInt(base.id),                          // destinationChainId
      zeroAddress,                              // exclusiveRelayer
      Number(fees.timestamp),                    // quoteTimestamp
      Number(fees.fillDeadline),                 // fillDeadline
      0,                                        // exclusivityDeadline
      '0x'                                      // message
    ]
  })
}

5. ๐Ÿ› ๏ธ Build Orchestration Instructions

Step 1: Define Trigger ๐ŸŽฌ

The trigger initiates the orchestration by pulling tokens from the EOA:

const trigger: Trigger = {
  chainId: optimism.id,
  tokenAddress: usdcAddresses[optimism.id],
  amount: inputAmount,
}

Step 2: Approve Across Protocol โœ…

const approveAcross = await orchestrator.buildComposable({
  type: 'approve',
  data: {
    amount: inputAmount,
    chainId: optimism.id,
    spender: transactionRequest.to,
    tokenAddress: usdcAddresses[optimism.id]
  }
})

Step 3: Execute Bridge Transaction ๐ŸŒ‰

const callAcrossInstruction = await orchestrator.buildComposable({
  type: 'rawCalldata',
  data: {
    to: transactionRequest.to,
    calldata: transactionRequest.data,
    chainId: transactionRequest.chainId,
  }
})

Step 4: Approve AAVE (with runtime balance) ๐Ÿ”„

Using runtime balance ensures we approve exactly what arrived:

const approveAAVEInstruction = await orchestrator.buildComposable({
  type: 'approve',
  data: {
    amount: runtimeERC20BalanceOf({
      targetAddress: orchestrator.addressOn(base.id)!,
      tokenAddress: usdcAddresses[base.id],
      constraints: [greaterThanOrEqualTo(1n)]
    }),
    chainId: base.id,
    spender: aavePoolAddresses[base.id],
    tokenAddress: usdcAddresses[base.id]
  }
})

Step 5: Supply to AAVE ๐Ÿฆ

const aaveSupplyAbi = parseAbi([
  'function supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode)'
])
 
const callAAVEInstruction = await orchestrator.buildComposable({
  type: 'default',
  data: {
    abi: aaveSupplyAbi,
    chainId: base.id,
    functionName: 'supply',
    to: aavePoolAddresses[base.id],
    args: [
      usdcAddresses[base.id],
      runtimeERC20BalanceOf({
        targetAddress: orchestrator.addressOn(base.id)!,
        tokenAddress: usdcAddresses[base.id],
        constraints: [greaterThanOrEqualTo(1n)]
      }),
      orchestrator.addressOn(base.id)!,
      0
    ]
  }
})

Step 6: Withdraw aUSDC to EOA ๐Ÿ’Ž

const withdrawInstruction = await orchestrator.buildComposable({
  type: 'withdrawal',
  data: {
    amount: runtimeERC20BalanceOf({
      targetAddress: orchestrator.addressOn(base.id)!,
      tokenAddress: aUSDCAddresses[base.id],
      constraints: [greaterThanOrEqualTo(1n)]
    }),
    chainId: base.id,
    tokenAddress: aUSDCAddresses[base.id],
  }
})

6. ๐ŸŽฏ Execute Fusion Orchestration

const fusionQuote = await meeClient.getFusionQuote({
  trigger,
  instructions: [
    approveAcross,
    callAcrossInstruction,
    approveAAVEInstruction,
    callAAVEInstruction,
    withdrawInstruction
  ],
  cleanUps: [{
    chainId: base.id,
    recipientAddress: eoa.address,
    tokenAddress: usdcAddresses[base.id]
  }],
  feeToken: { 
    address: usdcAddresses[optimism.id], 
    chainId: optimism.id 
  },
  lowerBoundTimestamp: nowInSeconds,
  upperBoundTimestamp: nowInSeconds + 60
})
 
const { hash } = await meeClient.executeFusionQuote({ fusionQuote })
console.log(getMeeScanLink(hash))

๐Ÿง  Key Concepts

๐ŸŽฏ Across Optimistic Design

Across uses an optimistic architecture that:

  • โšก Lightning fast: Relayers front capital for instant fills
  • ๐Ÿ’ฐ Capital efficient: Uses a single liquidity pool across all chains
  • ๐Ÿ”’ Secure: UMA's optimistic oracle validates all transfers

๐Ÿ“Š Runtime Balance Constraints

The runtimeERC20BalanceOf function ensures instructions use the exact amount that arrives:

  • โœ… Handles bridge fees automatically
  • โœ… Ensures proper sequencing
  • โœ… Avoids failed transactions

๐Ÿ”— Constraints and Execution Order

Instructions execute only when their constraints are met:

  1. ๐Ÿฆ AAVE approval waits for bridged funds
  2. ๐Ÿ’ธ AAVE supply waits for approval
  3. ๐Ÿ“ค Withdrawal waits for aUSDC

๐Ÿ›ก๏ธ Cleanup Mechanism

If any step fails, cleanup instructions ensure funds are returned:

cleanUps: [{
  chainId: base.id,
  recipientAddress: eoa.address,
  tokenAddress: usdcAddresses[base.id]
}]

โ›ฝ Gas Payment

The orchestration is gasless because:

  • ๐Ÿ’ฐ Gas paid using bridged USDC
  • ๐Ÿ”ง MEE handles gas abstraction
  • โœ๏ธ Users only sign once

๐ŸŽญ Across Quote Service

Quote Parameters

interface SuggestedFeesParameters {
  inputToken: Address           // Source token address
  outputToken: Address         // Destination token address
  originChainId: number       // Source chain ID
  destinationChainId: number  // Destination chain ID
  amount: bigint             // Amount to bridge
  
  // Optional parameters
  depositor?: Address        // Override depositor address
  recipient?: Address        // Override recipient address
  message?: Hex             // Cross-chain message
  referrer?: Address        // For referral tracking
}

Fee Structure

interface SuggestedFeesReturnType {
  totalRelayFee: Fee         // Total relayer compensation
  relayerCapitalFee: Fee     // Fee for fronting capital
  relayerGasFee: Fee        // Gas reimbursement
  lpFee: Fee                // Liquidity provider fee
  timestamp: bigint         // Quote timestamp
  fillDeadline: bigint      // Deadline for fill
  limits: {                 // Deposit limits
    minDeposit: bigint
    maxDeposit: bigint
    maxDepositInstant: bigint
    recommendedDepositInstant: bigint
  }
}

๐Ÿ’ก Best Practices

  1. ๐Ÿ”„ Always use runtime balances for cross-chain operations
  2. ๐Ÿ›ก๏ธ Include cleanup instructions for failure scenarios
  3. โฐ Set reasonable time bounds (60 seconds recommended)
  4. ๐Ÿงช Test on testnets first before mainnet deployment
  5. ๐Ÿ“Š Monitor transactions using MEE Scan
  6. ๐Ÿ’ธ Check deposit limits before bridging large amounts
  7. ๐Ÿ”‘ Secure your API keys in environment variables
  8. โš ๏ธ Validate fee amounts to ensure they're reasonable

๐Ÿšจ Error Handling

try {
  const fees = await getAcrossSuggestedFees(parameters)
  
  // Check if amount is too low
  if (fees.isAmountTooLow) {
    throw new Error('โŒ Amount below minimum deposit')
  }
  
  // Check deposit limits
  if (inputAmount > fees.limits.maxDepositInstant) {
    console.warn('โš ๏ธ Amount exceeds instant deposit limit')
  }
  
  // ... orchestration logic
} catch (error) {
  if (error.message.includes('HTTP error')) {
    console.error('โŒ Failed to get Across quote')
  }
  console.error('๐Ÿšจ Orchestration failed:', error)
}

๐Ÿš€ Advanced Features

๐Ÿ“จ Cross-Chain Messaging

Include messages with your bridge:

const fees = await getAcrossSuggestedFees({
  // ... other params
  message: '0x1234...', // Encoded message
})
 
// Include message in deposit
args: [
  // ... other args
  message // Pass to depositV3
]

๐Ÿ’ฐ Referral Tracking

Track referrals for analytics:

const fees = await getAcrossSuggestedFees({
  // ... other params
  referrer: '0xYourReferrerAddress',
})

๐ŸŽฏ Exclusive Relayers

Specify exclusive relayers for priority fills:

args: [
  // ... other args
  '0xRelayerAddress',    // exclusiveRelayer
  // ... 
  300,                   // exclusivityDeadline (5 min)
]

๐Ÿ”„ Multi-Chain Cleanups

cleanUps: [
  { 
    chainId: optimism.id, 
    recipientAddress: eoa.address, 
    tokenAddress: usdcOptimism 
  },
  { 
    chainId: base.id, 
    recipientAddress: eoa.address, 
    tokenAddress: usdcBase 
  }
]

๐Ÿ“Š Fee Analysis

// Format fees for display
import { formatAcrossFeePercentage } from './across-quote-service'
 
console.log(`
  ๐Ÿ’ฐ Total Fee: ${formatAcrossFeePercentage(fees.totalRelayFee.pct)}%
  โ›ฝ Gas Fee: ${formatAcrossFeePercentage(fees.relayerGasFee.pct)}%
  ๐Ÿ’ต LP Fee: ${formatAcrossFeePercentage(fees.lpFee.pct)}%
`)

๐Ÿ“Š Monitoring & Analytics

Track your orchestrations:

const meeScanUrl = getMeeScanLink(hash)
console.log(`๐Ÿ” Track transaction: ${meeScanUrl}`)

Monitor Across fills:

// Check fill status on Across Explorer
const acrossExplorerUrl = `https://app.across.to/transactions?deposit=${transactionHash}`
console.log(`๐ŸŒ‰ Track on Across: ${acrossExplorerUrl}`)

๐ŸŒ Supported Chains & Features

Across supports ultra-fast bridging across major chains:

  • โ›“๏ธ Chains: Ethereum, Arbitrum, Optimism, Base, Polygon, zkSync, and more
  • ๐Ÿ’ฑ Features: Optimistic filling, single liquidity pool, UMA oracle validation
  • โฑ๏ธ Speed: Usually completes in under 2 minutes
  • ๐Ÿ’ธ Capital Efficiency: Best rates due to unified liquidity

๐Ÿ Conclusion

This integration combines Across's ultra-fast optimistic bridging with Biconomy MEE's orchestration capabilities to deliver:

  • โšก Lightning-fast cross-chain transfers (under 2 minutes)
  • โ›ฝ Completely gasless experience
  • โœ๏ธ Single signature for complex flows
  • ๐Ÿ›ก๏ธ Built-in failure protection
  • ๐Ÿ’ฐ Capital-efficient pricing

The result is a seamless DeFi experience that abstracts away all the complexity of cross-chain operations while providing the fastest possible bridging.

๐Ÿ“š Resources