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
- ๐ Relay Protocol: Fast cross-chain bridging with intent-based architecture
- ๐ง Biconomy MEE: Orchestrates the entire transaction flow
- ๐ญ Fusion Mode: Enables gasless execution with external wallets
- ๐ค 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:
- ๐ฆ AAVE approval waits for bridged funds
- ๐ธ AAVE supply waits for approval
- ๐ค 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
- ๐ Always use runtime balances for cross-chain operations
- ๐ก๏ธ Include cleanup instructions for failure scenarios
- โฐ Set reasonable time bounds (60 seconds recommended)
- ๐งช Test on testnets first before mainnet deployment
- ๐ Monitor transactions using MEE Scan
- ๐ฐ Consider slippage tolerance for volatile assets
- ๐ 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
- ๐ Relay Protocol Documentation
- ๐ง Relay API Reference
- ๐ฌ Relay Discord