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

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 LINK tokens. To learn how to pay CCIP fees in native AVAX, read the Pay in native section. To transfer tokens and pay in LINK, use the following command:

./src/index.ts send <source> <router> <dest> \
    --receiver <destinationAccount> \
    --fee-token <feeTokenAddress> \
    --transfer-tokens <tokenAddress>=<amount>
    --gas-limit 0
  • source: Chain ID or network name.

    For example, 43113 for Avalanche Fuji or 11155111 for Ethereum Sepolia. You can also use the network name, such as avalanche-testnet-fuji or ethereum-testnet-sepolia.

    You can find the supported network names and chain IDs that can be used for source in the selectors.ts file of the ccip-tools repository.

  • router: Router contract address on the source network.

  • dest: Chain ID or network name.

    For example, 43113 for Avalanche Fuji or 11155111 for Ethereum Sepolia. You can also use the network name, such as avalanche-testnet-fuji or ethereum-testnet-sepolia.

    You can find the supported network names and chain IDs that can be used for dest in the selectors.ts file of the ccip-tools repository.

  • destinationAccount: Address of the destination account on the destination network. Skip this argument to use the same address as the source account.

  • feeTokenAddress: Token address used to pay CCIP fees. Supported tokens for paying fees include LINK, the native gas token of the source blockchain (such as ETH for Ethereum), and the wrapped native gas token (such as WETH for Ethereum).

  • tokenAddress: Address of the token to be transferred.

  • amount: Amount of the token to be transferred.

  • gasLimit: Gas limit for the transaction. This is optional and defaults to 200,000, which is the default value in the ramp config. You can set it to 0 when the transaction is directed to an Externally Owned Account (EOA).

Details such as the router contract address, LINK token address, and wrapped native gas token (like WETH) address can be found in the CCIP Directory by searching for the relevant network.

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 \
        --fee-token 0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846 \
        --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.
    --fee-tokenThis specifies the fee-token flag followed by the fee token address on the source blockchain.
    0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846Since you will pay for CCIP fees in LINK, this is the LINK token contract address on Avalanche Fuji. The LINK contract address can be found on the Link Token contracts page.
    --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. Once you execute the command, you should see the following logs:

    $ ./src/index.ts send 43113 0xF694E193200268f9a4868e4Aa017A0118C9a8177 11155111 \
        --receiver 0x27d7A69C878F9c8f51f4e53703abCE9bAcd2D9bf \
        --fee-token 0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846 \
        --transfer-tokens 0xD21341536c5cF5EB1bcb58f6723cE26e8D8E90e4=0.001 \
        --gas-limit 0
    
    Approving 1000000000000000n 0xD21341536c5cF5EB1bcb58f6723cE26e8D8E90e4 for 0xF694E193200268f9a4868e4Aa017A0118C9a8177 = 0xa3fd8053a74b71f34c4c280f10fdcba51ea105093998f8349db14485473da912
    Approving 23112499163862214n 0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846 for 0xF694E193200268f9a4868e4Aa017A0118C9a8177 = 0xb21d2822c7211a6bd39310ae78f4b59ef54ed6271fea21c949d9be78a30f12a7
    Sending message to 0x27d7A69C878F9c8f51f4e53703abCE9bAcd2D9bf @ ethereum-testnet-sepolia , tx_hash = 0x70858cfeadcfbd1404a65dd4116b549801bde3d184eb324f895529286e15249a
    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       │ '0x934f57925b5d8fbc763c2a06dfe2d003676816f8ea67392d3e7888a45469d4c1' │
    │ origin          │ '0x8C244f0B2164E6A3BED74ab429B0ebd661Bb14CA'                         │
    │ sender          │ '0x8C244f0B2164E6A3BED74ab429B0ebd661Bb14CA'                         │
    │ receiver        │ '0x27d7A69C878F9c8f51f4e53703abCE9bAcd2D9bf'                         │
    │ sequenceNumber  │ 3859                                                                 │
    │ nonce           │ 17                                                                   │
    │ gasLimit        │ 0                                                                    │
    │ transactionHash │ '0x70858cfeadcfbd1404a65dd4116b549801bde3d184eb324f895529286e15249a' │
    │ logIndex        │ 6                                                                    │
    │ blockNumber     │ 41234897                                                             │
    │ timestamp       │ '2025-06-02 16:19:51 (10s ago)'                                      │
    │ finalized       │ true                                                                 │
    │ fee             │ '0.023112499163862214 LINK'                                          │
    │ tokens          │ '0.001 CCIP-BnM'                                                     │
    │ data            │ '0x'                                                                 │
    └─────────────────┴──────────────────────────────────────────────────────────────────────┘
    
  3. Analyze the logs:

    • The script communicates with the router to calculate the transaction fees required to transfer tokens, which amounts to 40,903,083,926,519,498 Juels (equivalent to 0.04 LINK).
    • The script engages with the Link token contract, authorizing the router contract to spend 40,903,083,926,519,498 Juels for the fees and 1000000000000000 (0.001 CCIP-BnM) from your Externally Owned Account (EOA) balance.
    • The script initiates a transaction through the router to transfer 1000000000000000 (0.001 CCIP-BnM) to your account on Ethereum Sepolia. It also returns the CCIP message ID.
  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.

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.