Skip to content

Relay Protocol + Biconomy MEE Integration Guide

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

This guide demonstrates how to use Relay 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 Relay Protocol
  • ๐Ÿฆ Automated AAVE supply on the destination chain
  • โ›ฝ Gasless execution through Fusion Orchestration
  • โœ๏ธ Single signature UX for the entire flow
  • ๐Ÿš€ Fast and reliable bridging with Relay's optimized routes

๐Ÿ—๏ธ Architecture

Key Components

  1. ๐ŸŒ Relay Protocol: Fast cross-chain bridging with intent-based architecture
  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 Relay] ๐ŸŒ‰
Base Network โ›“๏ธ
    โ†“ [Supply to AAVE] ๐Ÿฆ
aUSDC โ†’ User EOA (Base) ๐Ÿ’Ž

๐Ÿ“ Implementation Guide

1. Setup and Dependencies

First, create the Relay Service file. Copy and paste this entire code block into a new file called relay-service.ts:

// relay-service.ts
import type { Address, Hex } from 'viem'
 
/**
 * -------------------------------------------------------------
 *  Relay Protocol Service โ€” typed with viem
 * -------------------------------------------------------------
 */
 
export type TradeType = 'EXACT_INPUT' | 'EXACT_OUTPUT'
 
export type Transaction = {
  to: Address
  value: string
  data: Hex
}
 
export type AppFee = {
  recipient: Address
  fee: string
}
 
export type GetQuoteParameters = {
  user: Address
  recipient: Address
  originChainId: number
  destinationChainId: number
  originCurrency: Address
  destinationCurrency: Address
  amount: string
  tradeType: TradeType
  txs?: Transaction[]
  txsGasLimit?: number
  referrer?: string
  referrerAddress?: Address
  refundTo?: Address
  refundOnOrigin?: boolean
  topupGas?: boolean
  useReceiver?: boolean
  useProtocol?: boolean
  useExternalLiquidity?: boolean
  usePermit?: boolean
  useDepositAddress?: boolean
  slippageTolerance?: string
  appFees?: AppFee[]
  gasLimitForDepositSpecifiedTxs?: number
  userOperationGasOverhead?: number
  forceSolverExecution?: boolean
  includedSwapSources?: string[]
  excludedSwapSources?: string[]
}
 
export type Currency = {
  chainId: number
  address: Address
  symbol: string
  name: string
  decimals: number
  metadata: {
    logoURI: string
    verified: boolean
    isNative: boolean
  }
}
 
export type CurrencyAmount = {
  currency: Currency
  amount: string
  amountFormatted: string
  amountUsd: string
  minimumAmount: string
}
 
export type TransactionData = {
  from: Address
  to: Address
  data: Hex
  value: string
  maxFeePerGas: string
  maxPriorityFeePerGas: string
  chainId: number
}
 
export type CheckEndpoint = {
  endpoint: string
  method: 'GET' | 'POST' | 'PUT' | 'DELETE'
}
 
export type StepItem = {
  status: 'incomplete' | 'complete' | 'failed'
  data: TransactionData
  check: CheckEndpoint
}
 
export type Step = {
  id: string
  action: string
  description: string
  kind: 'transaction' | 'signature' | 'permit'
  requestId: Hex
  items: StepItem[]
}
 
export type Fees = {
  gas: CurrencyAmount
  relayer: CurrencyAmount
  relayerGas: CurrencyAmount
  relayerService: CurrencyAmount
  app: CurrencyAmount
}
 
export type Impact = {
  usd: string
  percent: string
}
 
export type SlippageTolerance = {
  origin: {
    usd: string
    value: string
    percent: string
  }
  destination: {
    usd: string
    value: string
    percent: string
  }
}
 
export type Details = {
  operation: string
  sender: string
  recipient: string
  currencyIn: CurrencyAmount
  currencyOut: CurrencyAmount
  currencyGasTopup: CurrencyAmount
  totalImpact: Impact
  swapImpact: Impact
  rate: string
  slippageTolerance: SlippageTolerance
  timeEstimate: number
  userBalance: string
}
 
export type RelayerTransaction = {
  steps: Step[]
  fees: Fees
  details: Details
}
 
export type GetQuoteReturnType = RelayerTransaction
export type GetQuoteErrorType = Error
 
export async function getRelayQuote(
  parameters: GetQuoteParameters
): Promise<GetQuoteReturnType> {
  const response = await fetch('https://api.relay.link/quote', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(parameters),
  })
 
  if (!response.ok) {
    const error = await response.text()
    throw new Error(`Relayer API error: ${response.status} - ${error}`)
  }
 
  return response.json() as Promise<GetQuoteReturnType>
}

Now, import the dependencies in your main file:

import { 
  createMeeClient, 
  toMultichainNexusAccount,
  createChainAddressMap,
  runtimeERC20BalanceOf,
  greaterThanOrEqualTo,
  type Trigger 
} from "@biconomy/abstractjs"
import { getRelayQuote } from "./relay-service" // ๐Ÿ‘ˆ Import the service you just created
import { base, optimism } from "viem/chains"
import { privateKeyToAccount } from "viem/accounts"
import { http, parseAbi, parseUnits } 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']
])

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 Relay Bridge Quote

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

Relay provides fast, intent-based bridging:

const relayQuote = await getRelayQuote({
  amount: inputAmount.toString(),
  originChainId: optimism.id,
  originCurrency: usdcAddresses[optimism.id],
  destinationChainId: base.id,
  destinationCurrency: usdcAddresses[base.id],
  recipient: orchestrator.addressOn(base.id)!,
  tradeType: 'EXACT_INPUT',
  user: orchestrator.addressOn(optimism.id)!
})
 
// Extract transaction data from Relay quote
const txStep = relayQuote.steps.find(step => step.kind === 'transaction')
const transactionRequest = txStep?.items.at(0)?.data
 
if (!transactionRequest) {
  throw Error("No transaction parsed from Relay")
}

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 Relay Protocol โœ…

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

Step 3: Execute Bridge Transaction ๐ŸŒ‰

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

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: [
    approveRelay, 
    callRelayInstruction,
    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

๐ŸŽฏ Relay Intent-Based Architecture

Relay uses an intent-based system that:

  • โšก Fast execution: Intents are fulfilled by relayers instantly
  • ๐Ÿ’ฐ Competitive pricing: Relayers compete to fill your intent
  • ๐Ÿ”’ Guaranteed delivery: Funds are secured until successful completion

๐Ÿ“Š Runtime Balance Constraints

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

  • โœ… Handles any 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

๐ŸŽญ Relay Quote Service

Quote Parameters

interface GetQuoteParameters {
  user: Address                // Sender address
  recipient: Address           // Receiver address
  originChainId: number       // Source chain ID
  destinationChainId: number  // Destination chain ID
  originCurrency: Address     // Source token
  destinationCurrency: Address // Destination token
  amount: string              // Amount to bridge
  tradeType: TradeType        // 'EXACT_INPUT' or 'EXACT_OUTPUT'
  
  // Optional parameters
  slippageTolerance?: string  // Max slippage (e.g., "0.005" for 0.5%)
  referrerAddress?: Address   // For revenue sharing
  appFees?: AppFee[]         // Custom fees
  usePermit?: boolean        // Use permit for gasless approvals
}

Response Structure

interface RelayerTransaction {
  steps: Step[]              // Execution steps
  fees: {                    // Fee breakdown
    gas: CurrencyAmount
    relayer: CurrencyAmount
    relayerService: CurrencyAmount
    app: CurrencyAmount
  }
  details: {                 // Transaction details
    currencyIn: CurrencyAmount
    currencyOut: CurrencyAmount
    rate: string
    timeEstimate: number
    slippageTolerance: SlippageTolerance
  }
}

๐Ÿ’ก 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. ๐Ÿ’ฐ Consider slippage tolerance for volatile assets
  7. ๐Ÿ”‘ Secure your API keys in environment variables

๐Ÿšจ Error Handling

try {
  const relayQuote = await getRelayQuote(parameters)
  const txStep = relayQuote.steps.find(step => step.kind === 'transaction')
  
  if (!txStep?.items.at(0)?.data) {
    throw new Error('โŒ No valid transaction in Relay quote')
  }
  // ... orchestration logic
} catch (error) {
  if (error.message.includes('Relayer API error')) {
    console.error('โŒ Bridge route not available')
    // Handle specific Relay errors
  }
  console.error('๐Ÿšจ Orchestration failed:', error)
}

๐Ÿš€ Advanced Features

๐Ÿ’ฐ Revenue Sharing

Earn fees by setting referrer address:

const quote = await getRelayQuote({
  // ... other params
  referrerAddress: '0xYourAddress',
  appFees: [{
    recipient: '0xYourAddress',
    fee: '30' // 0.3% in basis points
  }]
})

๐ŸŽฏ Exact Output Bridging

Bridge to receive exact amount on destination:

const quote = await getRelayQuote({
  // ... other params
  tradeType: 'EXACT_OUTPUT',
  amount: desiredOutputAmount.toString()
})

๐Ÿ”„ Multi-Chain Cleanups

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

โšก Gas Optimization Options

const quote = await getRelayQuote({
  // ... other params
  topupGas: true,              // Top up gas on destination
  usePermit: true,             // Use permit for gasless approval
  useExternalLiquidity: true,  // Access external liquidity sources
})

๐Ÿ“Š Monitoring & Analytics

Track your orchestrations:

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

Monitor Relay bridge status:

// Check step status using the check endpoint
const checkUrl = txStep.items[0].check.endpoint
const status = await fetch(checkUrl).then(r => r.json())

๐ŸŒ Supported Chains & Features

Relay supports fast bridging across major chains:

  • โ›“๏ธ Chains: Ethereum, Arbitrum, Optimism, Base, Polygon, etc.
  • ๐Ÿ’ฑ Features: Intent-based filling, MEV protection, guaranteed delivery
  • โฑ๏ธ Speed: Usually completes in under 60 seconds

๐Ÿ Conclusion

This integration combines Relay's fast intent-based bridging with Biconomy MEE's orchestration capabilities to deliver:

  • ๐Ÿš€ Lightning-fast cross-chain transfers
  • โ›ฝ Completely gasless experience
  • โœ๏ธ Single signature for complex flows
  • ๐Ÿ›ก๏ธ Built-in failure protection

The result is a seamless DeFi experience that abstracts away all the complexity of cross-chain operations while maintaining speed and reliability.

๐Ÿ“š Resources