Transfer Tokens between EOAs

In this tutorial, you will use Chainlink CCIP to transfer tokens directly from your EOA (Externally Owned Account) to an account on a different blockchain. First, you will pay for CCIP fees on the source blockchain using LINK. Then, you will run the same example paying for CCIP fees in native gas, such as ETH on Ethereum or AVAX on Avalanche.

Before you begin

  1. Install Node.js 18. Optionally, you can use the nvm package to switch between Node.js versions with nvm use 18.

    node -v
    
    $ node -v
    v18.7.0
    
  2. Your EOA (Externally Owned Account) must have both AVAX and LINK tokens on Avalanche Fuji to pay for the gas fees and CCIP fees.

  3. Check the CCIP Directory to confirm that the tokens you will transfer are supported for your lane. In this example, you will transfer tokens from Avalanche Fuji to Ethereum Sepolia so check the list of supported tokens here. Alternatively, you can use the Get Supported Tokens tutorial to retrieve the list of supported tokens programmatically.

  4. Learn how to acquire CCIP test tokens. After following this guide, your EOA (Externally Owned Account) should have CCIP-BnM tokens, and CCIP-BnM should appear in the list of your tokens in MetaMask.

  5. In a terminal, clone the ccip-tools-ts repository and change to the ccip-tools-ts directory.

    git clone https://github.com/smartcontractkit/ccip-tools-ts && \
    cd ccip-tools-ts
    
  6. Run npm install to install the dependencies.

    npm install
    
  7. To make sure that the installation is correct and the ccip-tools CLI commands are available, run the following command:

    ./dist/ccip-tools-ts --help
    
  8. Inside the project's root folder, i.e., ccip-tools-ts, create a .env file and add two environment variables to store the RPC URLs:

    • AVALANCHE_FUJI_RPC_URL: Set this to a URL for the Avalanche Fuji testnet. You can sign up for a personal endpoint from Alchemy, Infura, or another node provider.

    • ETHEREUM_SEPOLIA_RPC_URL: Set this to a URL for the Ethereum Sepolia testnet. You can sign up for a personal endpoint from Alchemy, Infura, or another node provider.

  9. Commands of the ccip-tools that need to send transactions try to get the private key from a USER_KEY environment variable. For simplicity (not a recommended practice), if you are using a testnet wallet that only contains test tokens, you can export the USER_KEY environment variable into the current terminal session by running the following command:

    export USER_KEY=<YOUR_TESTNET_WALLET_PRIVATE_KEY>
    

Tutorial

Transfer tokens and pay in native

In this example, you will transfer CCIP-BnM tokens from your EOA on Avalanche Fuji to an account on Ethereum Sepolia. The destination account could be an EOA (Externally Owned Account) or a smart contract. The example shows how to transfer CCIP-BnM tokens, but you can reuse the same example to transfer other tokens as long as they are supported for your lane.

For this example, CCIP fees are paid in Avalanche Fuji's native AVAX. To learn how to pay CCIP fees in LINK, read the Pay in LINK section.

To transfer tokens and pay in native, use the following command:

./src/index.ts send <source> <router> <dest> \
    --receiver <destinationAccount> \
    --transfer-tokens <tokenAddress>=<amount> \
    --gas-limit 0

Complete the following steps in your terminal:

  1. Send 0.001 CCIP-BnM from your EOA on Avalanche Fuji to another account on Ethereum Sepolia:

    ./src/index.ts send 43113 0xF694E193200268f9a4868e4Aa017A0118C9a8177 11155111 \
        --receiver 0x27d7A69C878F9c8f51f4e53703abCE9bAcd2D9bf \
        --transfer-tokens 0xD21341536c5cF5EB1bcb58f6723cE26e8D8E90e4=0.001 \
        --gas-limit 0
    

    Command arguments:

    ArgumentExplanation
    ./src/index.ts sendThis executes the send command of the ccip-tools.
    43113This specifies the source blockchain, in this case, Avalanche Fuji.
    0xF694E193200268f9a4868e4Aa017A0118C9a8177This specifies the router address on the source blockchain, in this case, Avalanche Fuji.
    11155111This specifies the destination blockchain, which is Ethereum Sepolia in this case.
    --receiverThis specifies the receiver flag followed by the account address on the destination blockchain. Skip this argument if you want to use the same address as the source account.
    YOUR_ACCOUNTThis is the account address on the destination blockchain that is supposed to receive the tokens. You can replace this with your account address.
    --transfer-tokensThis specifies the transfer-tokens flag, followed by the token address and the amount of tokens to transfer, separated by =, as in --transfer-tokens <tokenAddress>=<amount>.
    0xD21341536c5cF5EB1bcb58f6723cE26e8D8E90e4This is the CCIP-BnM token contract address on Avalanche Fuji. The contract addresses for each network can be found on the CCIP Directory.
    0.001This is the amount of CCIP-BnM tokens to be transferred. In this example, 0.001 CCIP-BnM are transferred. The CCIP-BnM token has 18 decimals, so 0.001 would be 1000000000000000 in 18-decimal format.
    --gas-limit 0This specifies the gas limit for the transaction. This is optional and defaults to 200000, which is the default value in the ramp config. Set it to 0 when the transaction is directed to an Externally Owned Account (EOA).
  2. After you execute the command, you should see the following logs:

    $ ./src/index.ts send 43113 0xF694E193200268f9a4868e4Aa017A0118C9a8177 11155111 \
        --receiver 0x27d7A69C878F9c8f51f4e53703abCE9bAcd2D9bf \
        --transfer-tokens 0xD21341536c5cF5EB1bcb58f6723cE26e8D8E90e4=0.001 \
        --gas-limit 0
    
    Approving 1000000000000000n 0xD21341536c5cF5EB1bcb58f6723cE26e8D8E90e4 for 0xF694E193200268f9a4868e4Aa017A0118C9a8177 = 0x1ea6d165cf627fd4f6856520fc9afa2e08c4e04f79fd78bf7f4ef7da94692503
    Sending message to 0x27d7A69C878F9c8f51f4e53703abCE9bAcd2D9bf @ ethereum-testnet-sepolia , tx_hash = 0x0a00f9240b6860e34a0664ad0aa8f8e86877d70b97e9787e08e270bea564edce
    Lane:
    ┌────────────────┬──────────────────────────────────────────────┬────────────────────────────┐
    │ (index)        │ source                                       │ dest                       │
    ├────────────────┼──────────────────────────────────────────────┼────────────────────────────┤
    │ name           │ 'avalanche-testnet-fuji'                     │ 'ethereum-testnet-sepolia' │
    │ chainId        │ 43113                                        │ 11155111                   │
    │ chainSelector  │ 14767482510784806043n                        │ 16015286601757825753n      │
    │ onRamp/version │ '0x75b9a75Ee1fFef6BE7c4F842a041De7c6153CF4E' │ '1.5.0'                    │
    └────────────────┴──────────────────────────────────────────────┴────────────────────────────┘
    Request (source):
    ┌─────────────────┬──────────────────────────────────────────────────────────────────────┐
    │ (index)         │ Values                                                               │
    ├─────────────────┼──────────────────────────────────────────────────────────────────────┤
    │ messageId       │ '0xd902134a69bff565005c354996386479f9b1204b1810f49e27abc8c413c64312' │
    │ origin          │ '0x8C244f0B2164E6A3BED74ab429B0ebd661Bb14CA'                         │
    │ sender          │ '0x8C244f0B2164E6A3BED74ab429B0ebd661Bb14CA'                         │
    │ receiver        │ '0x27d7A69C878F9c8f51f4e53703abCE9bAcd2D9bf'                         │
    │ sequenceNumber  │ 3861                                                                 │
    │ nonce           │ 18                                                                   │
    │ gasLimit        │ 0                                                                    │
    │ transactionHash │ '0x0a00f9240b6860e34a0664ad0aa8f8e86877d70b97e9787e08e270bea564edce' │
    │ logIndex        │ 11                                                                   │
    │ blockNumber     │ 41235059                                                             │
    │ timestamp       │ '2025-06-02 16:25:12 (7s ago)'                                       │
    │ finalized       │ true                                                                 │
    │ fee             │ '0.019124641265363576 WAVAX'                                         │
    │ tokens          │ '0.001 CCIP-BnM'                                                     │
    │ data            │ '0x'                                                                 │
    └─────────────────┴──────────────────────────────────────────────────────────────────────┘
    
  3. Analyze the logs:

    • The script interacts with the CCIP-BnM token contract, authorizing the router contract to deduct 0.001 CCIP-BnM from your Externally Owned Account (EOA) balance.
    • The script initiates a transaction through the router to transfer 0.001 CCIP-BnM tokens to your destination account on Ethereum Sepolia. It also returns the CCIP message ID.
    • The script continuously monitors the destination blockchain (Ethereum Sepolia) to track the progress and completion of the cross-chain transaction.
  4. While the script is waiting for the cross-chain transaction to proceed, open the CCIP explorer and search your cross-chain transaction using the message ID. Notice that the status is not finalized yet.

  5. After several minutes (the waiting time depends on the finality of the source blockchain), the transaction should be finalized on the source chain. Once finalized, the corresponding transaction is executed on the destination chain by the DON. After execution, the status will be shown as SUCCESS in the CCIP explorer.

  6. Open the CCIP explorer and use the message ID to find your cross-chain transaction.

    Chainlink CCIP Explorer transaction details
  7. The data field is empty because only tokens are transferred. The gas limit is set to 0 because, although the default value in the ramp config is 200,000, you can override it by passing the --gas-limit flag. Setting it to 0 is appropriate when the transaction is directed to an Externally Owned Account (EOA). With an empty data field, no function calls on a smart contract are expected on the destination chain.

What's next

Get the latest Chainlink content straight to your inbox.