Skip to main content
Version: Endpoint V2

LayerZero OFT Transfer API Usage Guide

A developer guide to using the LayerZero OFT Transfer API to simply orchestrate OFT transfers between chains.

Overview

The LayerZero OFT Transfer API provides a simple way to fetch the calldata necessary to call the send implementation on any known OFT deployment. This guide demonstrates complete integration examples for both EVM and Solana chains, showing how the same API endpoints work across different blockchain environments.

Key Benefits:

  • Universal API: Same endpoints work for EVM ↔ EVM, Solana ↔ EVM, and EVM ↔ Solana transfers
  • Chain-agnostic discovery: Find tokens across all supported chains with a single API call
  • Pre-built transaction data: Get ready-to-execute transaction data for any OFT transfer between chains
  • Built-in validation and error handling
  • LayerZero transaction tracking

Cross-Chain Architecture

The LayerZero OFT API abstracts the complexity of cross-chain transfers by providing the same interface regardless of source and destination chains.

Whether you're transferring from Ethereum to Solana or Solana to BSC, you use the same API endpoints with chain-specific transaction execution.

Prerequisites

Installation

npm install ethers axios dotenv @layerzerolabs/lz-definitions

Environment Setup

Create a .env file in your project root:

# Required for API access
OFT_API_KEY=your-api-key-here

# Required for executing transactions
PRIVATE_KEY=your-private-key-here

Understanding Chain Names

The LayerZero OFT API uses chain names to identify different blockchain networks. These chain names are standardized across the LayerZero ecosystem and can be imported from the @layerzerolabs/lz-definitions package for type safety.

Available Chain Names

You can import chain names as constants to avoid typos and get TypeScript autocomplete:

import {Chain} from '@layerzerolabs/lz-definitions';

// Examples of available chains
[Chain.ETHEREUM][Chain.BSC][Chain.ARBITRUM][Chain.ABSTRACT][Chain.BASE][Chain.OPTIMISM][ // "ethereum" // "bsc" // "arbitrum" // "abstract" // "base" // "optimism"
Chain.POLYGON
]; // "polygon"
[Chain.SOLANA]; // "solana"

Using Chain Names in API Calls

Chain names are used directly in API requests without any conversion needed:

import {Chain} from '@layerzerolabs/lz-definitions';

// Use chain constants in API requests
const response = await axios.get(`${API_BASE_URL}/list`, {
params: {chainNames: `${Chain.SOLANA},${Chain.BSC}`},
});

LayerZero OFT API Endpoints

The OFT API provides two main endpoints for token operations:

1. Token Discovery API (/list)

Purpose: Discover available OFT tokens across chains and get their canonical contract addresses. Use this to find where a token is deployed and whether it's an OFT.

Endpoint: GET https://metadata.layerzero-api.com/v1/metadata/experiment/ofts/list

Parameters:

  • chainNames (optional, string): Comma-separated list of chain names to search across
  • symbols (optional, string): Comma-separated list of token symbols to filter by

Common Usage Patterns:

import {Chain} from '@layerzerolabs/lz-definitions';

// 1. Find all deployments of a specific token (recommended approach)
const response = await axios.get(`${API_BASE_URL}/list`, {
params: {symbols: 'PENGU'}, // Discovers PENGU on all available chains
});

// 2. Search for tokens on specific chains
const response = await axios.get(`${API_BASE_URL}/list`, {
params: {
chainNames: `${Chain.ABSTRACT},${Chain.BSC}`,
symbols: 'PENGU,USDC',
},
});

// 3. List all available OFTs (no filters)
const response = await axios.get(`${API_BASE_URL}/list`);

Response Structure:

{
"USDT0": [
{
"name": "USDT0",
"sharedDecimals": 6,
"endpointVersion": "v2",
"deployments": {
"ethereum": {
"address": "0x6c96de32cea08842dcc4058c14d3aaad7fa41dee",
"localDecimals": 6,
"type": "OFT_ADAPTER",
"innerToken": "0xdac17f958d2ee523a2206206994597c13d831ec7",
"approvalRequired": true
},
"solana": {
"address": "So11111111111111111111111111111111111111112",
"localDecimals": 6,
"type": "OFT",
"approvalRequired": false
}
}
}
]
}

Key Response Fields:

  • name: Token display name
  • sharedDecimals: Number of decimals used across all chains for LayerZero transfers
  • endpointVersion: LayerZero endpoint version ("v2")
  • deployments: Object with chain names as keys, containing deployment details for each chain
  • address: The OFT contract address on that specific chain (Program ID for Solana)
  • localDecimals: Number of decimals the token uses on that specific chain
  • type: Contract type ("OFT_ADAPTER" for wrapped tokens, "OFT" for native OFTs)
  • innerToken: The underlying ERC20 token address (for OFT_ADAPTER types, not applicable to Solana)
  • approvalRequired: Whether token approval is required before transfers (always false for Solana)
Understanding Decimals

For detailed explanations of sharedDecimals and localDecimals concepts, including the decimal conversion process and overflow considerations, see the OFT Technical Reference.

Using the Response:

const response = await axios.get(`${API_BASE_URL}/list`, {
params: {symbols: 'PENGU'},
});

const tokenData = response.data['PENGU'][0];
const availableChains = Object.keys(tokenData.deployments);
console.log(`PENGU available on: ${availableChains.join(', ')}`);

// Get contract address for a specific chain
const contractAddress = tokenData.deployments['abstract'].address;

2. Transfer Transaction API (/transfer)

Purpose: Generate pre-built transaction data for executing OFT transfers from a source to destination network. The API returns chain-specific transaction data that can be executed using the appropriate blockchain SDK.

Endpoint: GET https://metadata.layerzero-api.com/v1/metadata/experiment/ofts/transfer

Authentication Required:

headers: { 'x-layerzero-api-key': API_KEY }

Parameters:

  • srcChainName (string): Source chain name (e.g., "solana", "ethereum", "bsc")
  • dstChainName (string): Destination chain name (e.g., "ethereum", "bsc", "solana")
  • srcAddress (string): Source chain OFT contract address or Program ID
  • amount (string): Transfer amount in token's smallest unit
  • from (string): Sender wallet address (public key for Solana)
  • to (string): Recipient wallet address on destination chain
  • validate (boolean): Pre-validate balances and parameters
  • options (string, optional): Structured LayerZero execution options as JSON string

Complete Example Workflow:

import {Chain} from '@layerzerolabs/lz-definitions';

// Step 1: Discover token deployments
const listResponse = await axios.get(`${API_BASE_URL}/list`, {
params: {symbols: 'PENGU'},
});
const tokenData = listResponse.data['PENGU'][0];

// Step 2: Choose your transfer route from available deployments
const fromChain = Chain.ABSTRACT;
const toChain = Chain.BSC;
const contractAddress = tokenData.deployments[fromChain].address;

// Step 3: Get transfer calldata
const transferResponse = await axios.get(`${API_BASE_URL}/transfer`, {
params: {
srcChainName: fromChain, // "abstract"
dstChainName: toChain, // "bsc"
srcAddress: contractAddress, // Contract address from /list
amount: '1000000000000000000', // 1 token (18 decimals)
from: wallet.address,
to: wallet.address,
validate: true,
},
headers: {'x-layerzero-api-key': API_KEY},
});

Response Structure:

{
"transactionData": {
"populatedTransaction": {
"to": "0x...",
"data": "0x...",
"value": "0x...",
"gasLimit": "0x..."
},
"approvalTransaction": {
"to": "0x...",
"data": "0x...",
"gasLimit": "0x..."
}
}
}

Key Response Fields:

  • populatedTransaction: The main transfer transaction ready to be sent via wallet.sendTransaction()
  • approvalTransaction: Token approval transaction (if required for OFT adapters)
  • Both transactions contain pre-built calldata and gas estimates

Executing the Transactions:

const {transactionData} = transferResponse.data;

// Step 4: Configure RPC for the source chain to execute transactions
const RPC_URLS = {
[Chain.ABSTRACT]: 'https://api.mainnet.abs.xyz',
[Chain.BSC]: 'https://bsc.drpc.org',
// Add other chains as needed
};

const provider = new ethers.providers.JsonRpcProvider(RPC_URLS[fromChain]);
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);

// Step 5: Execute approval if needed (for OFT adapters)
if (transactionData.approvalTransaction) {
const approvalTx = await wallet.sendTransaction(transactionData.approvalTransaction);
await approvalTx.wait();
}

// Step 6: Execute the transfer
const transferTx = await wallet.sendTransaction(transactionData.populatedTransaction);
await transferTx.wait();

(Optional) Add extraOptions

For advanced use cases, you can include LayerZero execution options to extend the base OFT functionality. The options parameter allows you to specify additional gas limits, native token drops, and compose message settings.

Example with extraOptions:

const transferResponse = await axios.get(`${API_BASE_URL}/transfer`, {
params: {
srcChainName: fromChain,
dstChainName: toChain,
srcAddress: contractAddress,
amount: '1000000000000000000',
from: wallet.address,
to: wallet.address,
validate: true,
// Optional: Add extra execution options
options: JSON.stringify({
executor: {
lzReceive: {
gasLimit: 300000, // Extra gas for complex lzReceive logic
},
nativeDrops: [
{
amount: '1000000000000000', // 0.001 ETH in wei
receiver: '0xd8538fa8fdd5872e68c4040449f64452ae536fa6',
},
],
},
}),
},
headers: {'x-layerzero-api-key': API_KEY},
});

How extraOptions work:

  • lzReceive gas limit: The gas you specify here is added to the base gas limit already set by the OFT deployer. For example, if the OFT enforces 65,000 gas and you add 35,000, the total execution will have 100,000 gas available.

  • nativeDrops: Allows you to send native chain currency (ETH, MATIC, BNB, etc.) to any receiver wallet address alongside your token transfer. The amount is specified in wei and sent directly to the specified receiver address.

  • composeOptions: Used specifically for omnichain composers when your OFT transfer triggers additional smart contract logic on the destination chain. See the EVM Composer Overview for implementation details.

  • For detailed information about LayerZero message options, see Message Options and Message Execution Options.

Chain Name Reference

Here are common chain names available in the @layerzerolabs/lz-definitions package:

Chain ConstantChain NameNetwork
Chain.ETHEREUMethereumEthereum Mainnet
Chain.BSCbscBNB Smart Chain
Chain.ARBITRUMarbitrumArbitrum One
Chain.OPTIMISMoptimismOptimism
Chain.BASEbaseBase
Chain.POLYGONpolygonPolygon
Chain.ABSTRACTabstractAbstract

Usage Tips:

  • Import Chain constants to avoid typos and get autocomplete
  • Use /list API without chain filters to discover all supported chains
  • If you see a chain missing, make sure your @layerzerolabs/lz-definitions package is updated to the latest version
  • Check the LayerZero API reference for the complete list of supported chains

Complete Transfer Examples

Example: Send $PENGU from BSC to Abstract

import {Chain} from '@layerzerolabs/lz-definitions';
import {ethers} from 'ethers';
import axios from 'axios';
import 'dotenv/config';

/**
* LayerZero OFT Transfer API - Ethers.js Integration Example
*
* This example demonstrates how to integrate the LayerZero OFT Transfer API
* to execute OFT transfers using ethers.js and TypeScript.
*
* How it works:
* 1. **Token Discovery**: Uses the LayerZero API to discover tokens
* across multiple chains and get their canonical contract addresses
*
* 2. **Fetching Transaction Data**: Requests pre-built transaction data from the
* LayerZero API instead of manually encoding contract calls
*
* 3. **Executing the Transaction**: Executes the transaction using the provider wallet
*
* 4. **LayerZero Scan Tracking**: Provides LayerZero scan links to track the
* complete cross-chain journey of transfers
*
* Key Benefits:
* - No need to understand complex LayerZero contract interfaces
* - Built-in validation and error handling from the API
* - Automatic gas estimation and fee calculation
* - Handling for both OFTs and OFT Adapters
* - Real-time transaction tracking across chains
*
*/

// Configuration
const API_KEY = process.env.OFT_API_KEY!;
const API_BASE_URL = 'https://metadata.layerzero-api.com/v1/metadata/experiment/ofts';

// RPC configuration using chain names from LayerZero API
// RPCs are required only for sending the /transfer transaction, not for /list
const RPC_URLS: Record<string, string> = {
[Chain.BSC]: 'https://bsc.drpc.org',
[Chain.ABSTRACT]: 'https://api.mainnet.abs.xyz',
};

/**
* Get the appropriate RPC URL for a chain name.
* Chain names come directly from LayerZero API discovery.
*/
function getRpcUrl(chainName: string): string {
const url = RPC_URLS[chainName];
if (!url) {
throw new Error(`RPC URL not configured for chain: ${chainName}`);
}
return url;
}

/**
* Discover where a token is available across chains.
* This is the typical user flow - start with a token symbol and see where it's deployed.
*/
async function discoverTokenDeployments(symbol: string) {
try {
console.log('🔍 Token Discovery:');
console.log(` Searching for ${symbol} deployments...`);

const response = await axios.get(`${API_BASE_URL}/list`, {
params: {symbols: symbol},
});

const tokenData = response.data[symbol]?.[0];
if (!tokenData) {
throw new Error(`Token ${symbol} not found`);
}

const availableChains = Object.keys(tokenData.deployments);
console.log(`${symbol} found on: ${availableChains.join(', ')}`);
console.log();

return response.data;
} catch (error: any) {
console.error('Error discovering token:', error.response?.data || error.message);
throw error;
}
}

/**
* Extract the OFT contract address for a token on a specific chain.
* Prevents address lookup errors and handles chain-specific deployments.
*/
function getOftAddress(tokens: any, symbol: string, chainName: string): string {
const tokenData = tokens[symbol]?.[0];
if (!tokenData) {
throw new Error(`Token ${symbol} not found`);
}

const address = tokenData.deployments[chainName]?.address;
if (!address) {
throw new Error(`Token ${symbol} not available on ${chainName}`);
}

return address;
}

/**
* Generate LayerZero scan links for cross-chain transaction tracking.
* LayerZero scan shows the complete cross-chain journey, unlike regular block explorers.
*/
function getLayerZeroScanLink(hash: string): string {
// For simplicity, always use mainnet scan since we're working with mainnet chains
return `https://layerzeroscan.com/tx/${hash}`;
}

/**
* Execute an OFT token transfer using the LayerZero API.
* Uses chain names discovered from the LayerZero API.
*/
async function transferOFT(fromChain: string, toChain: string, oftAddress: string, amount: string) {
try {
console.log('🚀 OFT Transfer:');
console.log(` Route: ${fromChain}${toChain}`);
console.log();

// Get RPC URL for the source chain
const rpcUrl = getRpcUrl(fromChain);

// Initialize wallet on the source chain
const provider = new ethers.providers.JsonRpcProvider(rpcUrl);
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);

// Check wallet balance
const balance = await wallet.getBalance();

console.log('Wallet Information:');
console.log(` Address: ${wallet.address}`);
console.log(` Balance: ${ethers.utils.formatEther(balance)} native tokens`);
console.log();

// Request pre-built transaction data from LayerZero API
console.log('Transaction Preparation:');
console.log(' Requesting transaction data from LayerZero API...');

const response = await axios.get(`${API_BASE_URL}/transfer`, {
params: {
srcChainName: fromChain,
dstChainName: toChain,
srcAddress: oftAddress, // Source chain OFT contract address
amount, // Amount in token's smallest unit (wei)
from: wallet.address,
to: wallet.address, // Same address on destination chain
validate: true, // Pre-validate balances and parameters
},
headers: {'x-layerzero-api-key': API_KEY},
});

const {transactionData} = response.data;
console.log(' ✓ Transaction data prepared');
console.log();

// Handle token approval if required (for OFT adapters wrapping existing ERC20s)
if (transactionData.approvalTransaction) {
console.log('Token Approval:');
console.log(' Sending approval transaction...');
const approvalTx = await wallet.sendTransaction(transactionData.approvalTransaction);
await approvalTx.wait();
console.log(' ✓ Approval confirmed');
console.log();
}

// Execute the omnichain transfer transaction (includes LayerZero messaging fees)
console.log('OFT Transfer:');
console.log(' Sending transfer transaction...');
const transferTx = await wallet.sendTransaction(transactionData.populatedTransaction);
await transferTx.wait();
console.log(' ✓ Transaction confirmed');
console.log();

// Provide tracking link for the complete cross-chain journey
const scanLink = getLayerZeroScanLink(transferTx.hash);

console.log('🎉 Transaction Results:');
console.log(` LayerZero Scan: ${scanLink}`);
console.log();

return transferTx.hash;
} catch (error: any) {
// Extract meaningful error messages from API responses
if (error.response?.data) {
throw new Error(`API Error: ${JSON.stringify(error.response.data)}`);
}
throw error;
}
}

/**
* Example: OFT transfer
*
* Demonstrates the complete integration flow:
* 1. Use explicit LayerZero chain names for chain identification
* 2. Dynamically discover contract addresses
* 3. Execute transfers with automatic approval handling
* 4. Provide LayerZero transaction tracking
*/
async function main() {
try {
console.log('LayerZero OFT API - Ethers.js Example');
console.log('==========================================\n');

// Discover where PENGU is available and choose transfer route
const tokens = await discoverTokenDeployments('PENGU');

// Choose source and destination chains from available deployments
const fromChain = 'abstract';
const toChain = 'bsc';

// Extract the source chain OFT contract address (always use FROM chain address)
const oftAddress = getOftAddress(tokens, 'PENGU', fromChain);
console.log(`OFT contract: ${oftAddress}`);
console.log();

// Execute the cross-chain transfer using chain names from API
await transferOFT(
fromChain, // Source chain name
toChain, // Destination chain name
oftAddress, // Source chain OFT contract address
'1000000000000', // Amount in token's smallest unit
);

console.log('Example completed successfully!');
} catch (error: any) {
// Handle errors gracefully with actionable feedback
if (error.response?.data) {
console.error('API Error:', error.response.data);
} else if (error.message) {
console.error('Error:', error.message);
} else {
console.error('Unknown error:', error);
}
}
}

// Export functions for use in other scripts
export {discoverTokenDeployments, transferOFT, getRpcUrl, getOftAddress};

// Run main function if this script is executed directly
if (require.main === module) {
main();
}

Expected Output

After running the EVM example, you should see:

✗ npx ts-node scripts/testOFTAPI.ts

LayerZero OFT API - Ethers.js Example
==========================================

🔍 Token Discovery:
Searching for PENGU deployments...
✓ PENGU found on: abstract, bsc, ethereum, solana

OFT contract: 0x9ebe3a824ca958e4b3da772d2065518f009cba62

🚀 OFT Transfer:
Route: abstract → bsc

Wallet Information:
Address: 0xed422098669cBB60CAAf26E01485bAFdbAF9eBEA
Balance: 0.009736997308229952 native tokens

Transaction Preparation:
Requesting transaction data from LayerZero API...
✓ Transaction data prepared

OFT Transfer:
Sending transfer transaction...
✓ Transaction confirmed

🎉 Transaction Results:
LayerZero Scan: https://layerzeroscan.com/tx/0x49c44f1ff5ab82ceaeee6c780e991863d75ad544cb6123583c2e335b314b77ab

Example completed successfully!

Common Issues & Solutions

Amount Validation Errors

Error:

API Error: {"code":4000,"message":"Amount Invalid. Config: {\"sharedDecimals\":6,\"localDecimals\":18,\"currentAmount\":\"1000\",\"minAmount\":\"1000000000000\"}"}

Cause: This error occurs due to the decimal conversion rate between localDecimals and sharedDecimals. The OFT standard enforces that transfer amounts must be greater than or equal to the decimal conversion rate to prevent precision loss when transferring tokens between blockchains.

The minAmount in the error response represents the decimal conversion rate: 10^(localDecimals - sharedDecimals). In this example: 10^(18-6) = 10^12 = 1000000000000.

Solution: Ensure your amount (in minor units) is greater than or equal to the decimal conversion rate:

// ❌ Wrong - amount smaller than conversion rate
const amount = '1000'; // Less than 10**(localDecimals - sharedDecimals)

// ✅ Correct - amount meets minimum conversion rate requirement
const amount = '1000000000000'; // Exactly 10**(localDecimals - sharedDecimals) (minimum)
const amount = '5000000000000000000'; // 5 tokens (5*10**localDecimals)

For detailed explanation of how sharedDecimals and localDecimals work together to enforce minimum transfer amounts, see the OFT Technical Reference.

Insufficient Balance

Error:

API Error: {"code":4000,"message":"Insufficient OFT balance: 114000000000000 < 100000000000000000000000"}

Cause: This error occurs when the API validates that your wallet doesn't have enough OFT tokens to perform the requested transfer. The error message shows your current balance vs. the requested transfer amount (both in the token's smallest unit).

In this example: 114000000000000 (current balance) < 100000000000000000000000 (requested amount).

Solution: Check both native token (for fees) and token balances:

// Check native balance for gas fees
const nativeBalance = await wallet.getBalance();
console.log(`Native balance: ${ethers.utils.formatEther(nativeBalance)}`);

// Check token balance
const tokenContract = new ethers.Contract(
tokenAddress,
['function balanceOf(address) view returns (uint256)'],
provider,
);
const tokenBalance = await tokenContract.balanceOf(wallet.address);
console.log(`Token balance: ${tokenBalance.toString()}`);

// Ensure your transfer amount is <= your token balance
const transferAmount = '1000000000000000000'; // 1 token with 18 decimals
if (tokenBalance.lt(transferAmount)) {
console.error('Insufficient token balance for transfer');
}

Network Not Supported

Error: Unsupported source chain: chainName

Solution: Ensure the chain is configured in your RPC_URLS mapping and supported by the API.


By using the OFT API, you agree to the LayerZero API Terms of Service.