> ## Documentation Index
> Fetch the complete documentation index at: https://docs.layerzero.network/llms.txt
> Use this file to discover all available pages before exploring further.

# EVM Integration

> Complete TypeScript example for cross-chain EVM transfers using the Value Transfer API.

This example demonstrates a complete cross-chain transfer from Base to Optimism using TypeScript and viem.

## Prerequisites

* Node.js 18+
* An API key from LayerZero
* A funded wallet on Base (source chain)

## Installation

<Tabs>
  <Tab title="pnpm">
    ```bash wrap theme={null}

        pnpm add viem dotenv

    ```
  </Tab>

  <Tab title="npm">
    ```bash wrap theme={null}

        npm install viem dotenv

    ```
  </Tab>

  <Tab title="yarn">
    ```bash wrap theme={null}

        yarn add viem dotenv

    ```
  </Tab>
</Tabs>

## Environment setup

Create a `.env` file in your project root:

```bash wrap theme={null}

    VT_API_KEY=your_api_key_here
    EVM_PRIVATE_KEY=0xyour_private_key_here

```

***

<Steps>
  <Step title="Validate transfer path">
    Before requesting a quote, verify that the destination token is reachable from your source token.

    Query the tokens endpoint with filters to check if your destination exists in the list of transferrable tokens:

    <Tabs>
      <Tab title="TypeScript (viem)">
        ```typescript wrap theme={null}
        const response = await fetch(
          'https://transfer.layerzero-api.com/v1/tokens?' +
            new URLSearchParams({
              transferrableFromChainKey: 'base',
              transferrableFromTokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE',
            }),
        );

        const {tokens} = await response.json();

        // Check if destination token exists in reachable tokens
        const isSupported = tokens.some(
          (t) => t.chainKey === 'optimism' && t.address === '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE',
        );

        if (!isSupported) {
          throw new Error('Transfer path not supported');
        }

        console.log(`Found ${tokens.length} reachable destinations`);
        ```
      </Tab>
    </Tabs>

    <Tip>
      **Why validate first?** Validating the transfer path before requesting quotes prevents unnecessary API calls and provides immediate feedback if a route doesn't exist.
    </Tip>
  </Step>

  <Step title="Get quotes">
    Request a quote for your cross-chain transfer. The API returns available routes with fees, estimated duration, and the steps needed to execute.

    <Tabs>
      <Tab title="TypeScript (viem)">
        ```typescript wrap theme={null}
        const quoteResponse = await fetch('https://transfer.layerzero-api.com/v1/quotes', {
          method: 'POST',
          headers: {
            'x-api-key': 'YOUR_API_KEY',
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            srcChainKey: 'base',
            dstChainKey: 'optimism',
            srcTokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE',
            dstTokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE',
            srcWalletAddress: '0xYourWallet',
            dstWalletAddress: '0xYourWallet',
            amount: '100000000000000', // 0.0001 ETH in wei
            options: {
              amountType: 'EXACT_SRC_AMOUNT',
              feeTolerance: {type: 'PERCENT', amount: 2},
            },
          }),
        });

        const {quotes} = await quoteResponse.json();
        const quote = quotes[0];

        console.log('Quote ID:', quote.id);
        console.log('Route:', quote.routeSteps[0].type);
        console.log('Fee:', quote.feeUsd, 'USD');
        ```
      </Tab>
    </Tabs>

    **Quote response structure:**

    For native token transfers (like ETH), the response contains a single bridge step. For ERC20 transfers (like USDC), the response contains two steps — an approve followed by a bridge. See [Executing userSteps safely](/v2/developers/value-transfer-api/api-reference/quotes#executing-usersteps-safely) for details on the two-step flow.

    ```typescript wrap theme={null}

        {
          id: '0x0000...7a', // hex quote ID
          routeSteps: [
            {
              type: 'STARGATE_V2_TAXI',
              srcChainKey: 'base',
              description: 'Stargate',
            },
          ],
          feeUsd: '0.42',
          feePercent: '0.42',
          srcAmount: '100000000000000',
          dstAmount: '99580000000000',
          duration: {estimated: '60000'},
          userSteps: [
            // Native transfers: single bridge step
            // ERC20 transfers: approve step + bridge step (execute both in order)
            {
              type: 'TRANSACTION',
              description: 'bridge',
              chainKey: 'base',
              chainType: 'EVM',
              signerAddress: '0xYourWallet',
              transaction: {
                encoded: {
                  chainId: 8453,
                  data: '0xc7c7f5b3...',
                  from: '0xYourWallet',
                  to: '0x27a16dc786820B16E5c9028b75B99F6f604b5d26',
                  value: '100024887844265667',
                },
              },
            },
          ],
        }

    ```
  </Step>

  <Step title="Execute user steps">
    Process each user step in the quote. The quote contains an array of steps—execute them in order.

    <Danger>
      **Never approve the LZMulticall (Wrapper) as a token spender**

      LZMulticall executes bridge transactions. It is not the right spender, and approving it will lose you tokens. The correct spender is the **TransferDelegate**, and the API's approve step already has this set in the calldata. Execute every `userStep` as returned.

      See [Contracts Overview](/v2/developers/value-transfer-api/contracts/overview) for details on the contract architecture.
    </Danger>

    <Tabs>
      <Tab title="TypeScript (viem)">
        ```typescript wrap theme={null}
        import {createWalletClient, createPublicClient, http} from 'viem';
        import {privateKeyToAccount} from 'viem/accounts';
        import {base} from 'viem/chains';

        const account = privateKeyToAccount(process.env.EVM_PRIVATE_KEY);
        const wallet = createWalletClient({account, chain: base, transport: http()});
        const client = createPublicClient({chain: base, transport: http()});

        let txHash;

        for (const step of quote.userSteps) {
          if (step.type === 'TRANSACTION') {
            const tx = step.transaction.encoded;
            txHash = await wallet.sendTransaction({
              account,
              to: tx.to,
              data: tx.data,
              value: BigInt(tx.value ?? '0'),
            });
            await client.waitForTransactionReceipt({hash: txHash});
            console.log('Transaction sent:', txHash);
          } else if (step.type === 'SIGNATURE') {
            const typed = step.signature.typedData;
            const signature = await wallet.signTypedData({
              account,
              domain: typed.domain,
              types: typed.types,
              primaryType: typed.primaryType,
              message: typed.message,
            });

            await fetch('https://transfer.layerzero-api.com/v1/submit-signature', {
              method: 'POST',
              headers: {
                'x-api-key': 'YOUR_API_KEY',
                'Content-Type': 'application/json',
              },
              body: JSON.stringify({
                quoteId: quote.id,
                signatures: [signature],
              }),
            });
            console.log('Signature submitted');
          }
        }
        ```
      </Tab>
    </Tabs>

    <Info>
      **Step types:** Most EVM routes use `TRANSACTION` steps. Intent-based routes (like Aori) may include `SIGNATURE` steps for EIP-712 signed messages.
    </Info>
  </Step>

  <Step title="Track transfer status">
    Poll the status endpoint until the transfer completes. The API returns the current status and explorer link.

    <Tabs>
      <Tab title="TypeScript (viem)">
        ```typescript wrap theme={null}
        async function pollStatus(quoteId: string, txHash?: string) {
          const deadline = Date.now() + 5 * 60_000; // 5 minute timeout

          while (Date.now() < deadline) {
            const query = txHash ? `?txHash=${txHash}` : '';
            const response = await fetch(
              `https://transfer.layerzero-api.com/v1/status/${encodeURIComponent(quoteId)}${query}`,
              {headers: {'x-api-key': 'YOUR_API_KEY'}},
            );

            const {status, explorerUrl} = await response.json();
            console.log('Status:', status);

            if (status === 'SUCCEEDED') {
              console.log('Transfer complete!');
              console.log('Explorer:', explorerUrl);
              return status;
            }

            if (status === 'FAILED' || status === 'UNKNOWN') {
              throw new Error(`Transfer ${status.toLowerCase()}`);
            }

            await new Promise((r) => setTimeout(r, 4000));
          }

          throw new Error('Transfer timed out');
        }

        await pollStatus(quote.id, txHash);
        ```
      </Tab>
    </Tabs>

    **Status values:**

    | Status       | Description                                  |
    | ------------ | -------------------------------------------- |
    | `PENDING`    | Transfer initiated, waiting for confirmation |
    | `PROCESSING` | Transfer in progress across chains           |
    | `SUCCEEDED`  | Transfer completed successfully              |
    | `FAILED`     | Transfer failed                              |
    | `UNKNOWN`    | Status cannot be determined                  |
  </Step>
</Steps>

***

## Complete example

```typescript wrap theme={null}
import {createWalletClient, createPublicClient, http, type Hex} from 'viem';
import {privateKeyToAccount} from 'viem/accounts';
import {base} from 'viem/chains';
import * as dotenv from 'dotenv';
dotenv.config();

const API = 'https://transfer.layerzero-api.com/v1';
const API_KEY = process.env.VT_API_KEY!;
const PRIVATE_KEY = process.env.EVM_PRIVATE_KEY as Hex;

const account = privateKeyToAccount(PRIVATE_KEY);
const wallet = createWalletClient({account, chain: base, transport: http()});
const client = createPublicClient({chain: base, transport: http()});

async function main() {
  // Step 1: Validate transfer path
  const tokensRes = await fetch(
    `${API}/tokens?` +
      new URLSearchParams({
        transferrableFromChainKey: 'base',
        transferrableFromTokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE',
      }),
  );
  const {tokens} = await tokensRes.json();
  const isSupported = tokens.some(
    (t) => t.chainKey === 'optimism' && t.address === '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE',
  );
  if (!isSupported) throw new Error('Transfer path not supported');
  console.log(`Found ${tokens.length} reachable destinations`);

  // Step 2: Get quote
  const quoteRes = await fetch(`${API}/quotes`, {
    method: 'POST',
    headers: {'x-api-key': API_KEY, 'Content-Type': 'application/json'},
    body: JSON.stringify({
      srcChainKey: 'base',
      dstChainKey: 'optimism',
      srcTokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE',
      dstTokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE',
      srcWalletAddress: account.address,
      dstWalletAddress: account.address,
      amount: '100000000000000',
      options: {amountType: 'EXACT_SRC_AMOUNT', feeTolerance: {type: 'PERCENT', amount: 2}},
    }),
  });

  const {quotes} = await quoteRes.json();
  const quote = quotes[0];
  if (!quote) throw new Error('No quote available');

  console.log('Quote received:', quote.id);
  console.log('Fee:', quote.feeUsd, 'USD');

  // Step 3: Execute user steps
  let txHash: Hex | undefined;
  for (const step of quote.userSteps) {
    if (step.type === 'SIGNATURE') {
      const typed = step.signature.typedData;
      const signature = await wallet.signTypedData({
        account,
        domain: typed.domain,
        types: typed.types,
        primaryType: typed.primaryType,
        message: typed.message,
      });
      await fetch(`${API}/submit-signature`, {
        method: 'POST',
        headers: {'x-api-key': API_KEY, 'Content-Type': 'application/json'},
        body: JSON.stringify({quoteId: quote.id, signatures: [signature]}),
      });
      console.log('Signature submitted');
    } else if (step.type === 'TRANSACTION') {
      const tx = step.transaction.encoded;
      txHash = await wallet.sendTransaction({
        account,
        to: tx.to,
        data: tx.data,
        value: BigInt(tx.value ?? '0'),
      });
      await client.waitForTransactionReceipt({hash: txHash});
      console.log('Transaction sent:', txHash);
    }
  }

  // Step 4: Poll status
  const deadline = Date.now() + 5 * 60_000;
  while (Date.now() < deadline) {
    const statusRes = await fetch(
      `${API}/status/${encodeURIComponent(quote.id)}${txHash ? `?txHash=${txHash}` : ''}`,
      {headers: {'x-api-key': API_KEY}},
    );
    const {status, explorerUrl} = await statusRes.json();
    console.log('Status:', status);
    if (status === 'SUCCEEDED') {
      console.log('Explorer:', explorerUrl);
      break;
    }
    if (status === 'FAILED' || status === 'UNKNOWN') throw new Error(`Transfer ${status.toLowerCase()}`);
    await new Promise((r) => setTimeout(r, 4000));
  }
}

main().catch(console.error);
```

## Next steps

* [Solana Example](/v2/developers/value-transfer-api/examples/solana) — Transfer tokens from Solana
* [API Reference](/v2/developers/value-transfer-api/api-reference/overview) — Explore all endpoints
