Use OFT Transfer API Usage Guide with LayerZero V2. Developer tools for building and debugging omnichain applications. LayerZero enables crosschain messaging.
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
The LayerZero OFT API abstracts the complexity of crosschain 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.
# Required for API accessOFT_API_KEY=your-api-key-here# Required for executing transactionsPRIVATE_KEY=your-private-key-here
Create a .env file in your project root:
# Required for API accessOFT_API_KEY=your-api-key-here# Required for executing transactions (base58 format)SOLANA_PRIVATE_KEY=your-solana-private-key-here# Optional: Custom RPC endpointSOLANA_RPC_URL=https://api.mainnet-beta.solana.com
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.
Chain names are used directly in API requests without any conversion needed:
import {Chain} from '@layerzerolabs/lz-definitions';// Use chain constants in API requestsconst response = await axios.get(`${API_BASE_URL}/list`, { params: {chainNames: `${Chain.SOLANA},${Chain.BSC}`},});
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/listParameters:
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 chainsconst 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`);
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 chainconst contractAddress = tokenData.deployments['abstract'].address;
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/transferAuthentication 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 deploymentsconst 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 deploymentsconst fromChain = Chain.ABSTRACT;const toChain = Chain.BSC;const contractAddress = tokenData.deployments[fromChain].address;// Step 3: Get transfer calldataconst 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},});
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.
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 crosschain 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 * */// Configurationconst 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 /listconst 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 crosschain transaction tracking. * LayerZero scan shows the complete crosschain 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 crosschain 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 crosschain 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 scriptsexport {discoverTokenDeployments, transferOFT, getRpcUrl, getOftAddress};// Run main function if this script is executed directlyif (require.main === module) { main();}
✗ npx ts-node scripts/testOFTAPI.tsLayerZero OFT API - Ethers.js Example==========================================🔍 Token Discovery: Searching for PENGU deployments... ✓ PENGU found on: abstract, bsc, ethereum, solanaOFT contract: 0x9ebe3a824ca958e4b3da772d2065518f009cba62🚀 OFT Transfer: Route: abstract → bscWallet Information: Address: 0xed422098669cBB60CAAf26E01485bAFdbAF9eBEA Balance: 0.009736997308229952 native tokensTransaction Preparation: Requesting transaction data from LayerZero API... ✓ Transaction data preparedOFT Transfer: Sending transfer transaction... ✓ Transaction confirmed🎉 Transaction Results: LayerZero Scan: https://layerzeroscan.com/tx/0x49c44f1ff5ab82ceaeee6c780e991863d75ad544cb6123583c2e335b314b77abExample completed successfully!
After running the Solana example, you should see:
✗ npx ts-node scripts/testSolanaOFTAPI.tsLayerZero OFT API - Solana to EVM Example==========================================🔍 Token Discovery: Searching for PENGU deployments... ✓ PENGU found on: abstract, bsc, ethereum, solana🚀 OFT Transfer: Route: solana → bscWallet Information: Address: 7BgBvyjrZX1YKz4oh9mjb8XScatufuNqPH7YLyRWCATp SOL Balance: 0.0421 SOLTransaction Preparation: Requesting transaction data from LayerZero API... ✓ Transaction data received from APITransaction Execution: Deserializing Solana transaction... Signing and sending transaction... Waiting for confirmation... ✓ Transaction confirmed on Solana🎉 Transfer Initiated: Solana Transaction: https://solscan.io/tx/2xK8vKv7... LayerZero Scan: https://layerzeroscan.com/tx/2xK8vKv7...PENGU transfer example completed successfully!
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:
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 feesconst nativeBalance = await wallet.getBalance();console.log(`Native balance: ${ethers.utils.formatEther(nativeBalance)}`);// Check token balanceconst 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 balanceconst transferAmount = '1000000000000000000'; // 1 token with 18 decimalsif (tokenBalance.lt(transferAmount)) { console.error('Insufficient token balance for transfer');}
Error:Unsupported source chain: chainNameSolution: Ensure the chain is configured in your RPC_URLS mapping and supported by the API.By using the OFT API, you agree to the OFT API Terms of Use.
Was this page helpful?
⌘I
Assistant
Responses are generated using AI and may contain mistakes.