Skip to main content
Version: 1.1.1

Manual Execution

When automatic execution fails or is delayed on the destination chain, you can manually execute the message. The SDK provides two paths depending on what data you have.

Two Approaches

ApproachWhat you needData sourceWhen to use
By message IDMessage ID + dest RPCCCIP APIDefault — covers most scenarios with minimal setup
By transaction hashSource tx hash + source RPC + dest RPCOn-chain (RPC)When operating without the API (apiClient: null)

When Manual Execution Is Needed

  • Receiver contract reverts — destination contract throws an error
  • Insufficient gas limitgasLimit in extraArgs was too low
  • Rate limiter blocked — token transfer exceeded rate limits
  • Execution timeout — automatic execution window expired

Before executing, ensure:

  1. The message has been committed on the destination chain
  2. Source chain finality has passed
  3. You have a funded wallet on the destination chain

By Message ID

The CCIP API handles proof calculation, off-chain token data (USDC attestations, LBTC attestations), and OffRamp discovery. You only need the message ID and a destination chain wallet.

TypeScript
import { EVMChain } from '@chainlink/ccip-sdk'

const dest = await EVMChain.fromUrl('https://rpc.fuji.avax.network')

const execution = await dest.execute({
messageId: '0x1234...abcd', // 32-byte CCIP message ID
wallet: destWallet, // ethers Signer or viemWallet(client)
})

console.log('Execution tx:', execution.log.transactionHash)
console.log('Block:', execution.log.blockNumber)

Requires the CCIP API (enabled by default). Throws CCIPApiClientNotAvailableError if you created the chain with apiClient: null.

Where to Get the Message ID

  • From sendMessage return: request.message.messageId
  • From getMessagesInTx: requests[0].message.messageId
  • From the CCIP Explorer
  • From the getMessageById API call

By Transaction Hash

Use this when operating without the CCIP API, or when you need full control over each step.

Step 1: Get the Message

TypeScript
import { EVMChain, discoverOffRamp } from '@chainlink/ccip-sdk'

const source = await EVMChain.fromUrl('https://rpc.sepolia.org')
const dest = await EVMChain.fromUrl('https://rpc.fuji.avax.network')

// Decode CCIP messages from the source transaction
const requests = await source.getMessagesInTx('0xSourceTxHash...')
const request = requests[0] // first message; use [n] if multiple messages in tx
console.log('Message ID:', request.message.messageId)

If the transaction contains multiple CCIP messages, select the one you want by index.

Step 2: Check Execution Status

TypeScript
import { discoverOffRamp, ExecutionState } from '@chainlink/ccip-sdk'

const offRamp = await discoverOffRamp(source, dest, request.lane.onRamp)

for await (const execution of dest.getExecutionReceipts({
offRamp,
messageId: request.message.messageId,
})) {
if (execution.receipt.state === ExecutionState.Success) {
console.log('Already executed — nothing to do')
process.exit(0)
}
if (execution.receipt.state === ExecutionState.Failed) {
console.log('Previous execution failed — proceeding with manual execution')
}
}

Step 3: Get Verifications

TypeScript
let verifications
try {
verifications = await dest.getVerifications({ offRamp, request })
} catch {
// CCIPCommitNotFoundError (transient) — message not yet committed
console.log('Not yet committed — wait and retry')
process.exit(1)
}

Step 4: Build Execution Input

getExecutionInput runs on the source chain. It fetches all messages in the commit batch, calculates the Merkle proof, and fetches off-chain token data (USDC/LBTC attestations) automatically.

TypeScript
const input = await source.getExecutionInput({ request, verifications })

Step 5: Execute

TypeScript
const execution = await dest.execute({
offRamp,
input,
wallet: destWallet,
})

console.log('Execution tx:', execution.log.transactionHash)

Complete Examples

By Message ID

TypeScript
import { ethers } from 'ethers'
import { EVMChain, CCIPError } from '@chainlink/ccip-sdk'

async function executeByMessageId(
destRpc: string,
messageId: string,
wallet: ethers.Signer
) {
const dest = await EVMChain.fromUrl(destRpc)

try {
const execution = await dest.execute({ messageId, wallet })
console.log('Execution tx:', execution.log.transactionHash)
return execution
} catch (error) {
if (CCIPError.isCCIPError(error)) {
console.error(`${error.code}: ${error.message}`)
if (error.isTransient) console.log('Transient — safe to retry')
if (error.recovery) console.log('Recovery:', error.recovery)
}
throw error
}
}

By Transaction Hash (No API)

TypeScript
import { ethers } from 'ethers'
import {
EVMChain,
discoverOffRamp,
ExecutionState,
CCIPError,
} from '@chainlink/ccip-sdk'

async function executeByTxHash(
sourceRpc: string,
destRpc: string,
sourceTxHash: string,
wallet: ethers.Signer,
messageIndex = 0
) {
const source = await EVMChain.fromUrl(sourceRpc, { apiClient: null })
const dest = await EVMChain.fromUrl(destRpc, { apiClient: null })

// 1. Get the message
const requests = await source.getMessagesInTx(sourceTxHash)
const request = requests[messageIndex]
console.log('Message ID:', request.message.messageId)

// 2. Discover OffRamp (memoized)
const offRamp = await discoverOffRamp(source, dest, request.lane.onRamp)

// 3. Check if already executed
for await (const execution of dest.getExecutionReceipts({
offRamp,
messageId: request.message.messageId,
})) {
if (execution.receipt.state === ExecutionState.Success) {
console.log('Already executed')
return { status: 'already_executed' as const }
}
}

// 4. Get verifications (throws CCIPCommitNotFoundError if not yet committed)
let verifications
try {
verifications = await dest.getVerifications({ offRamp, request })
} catch (error) {
if (CCIPError.isCCIPError(error) && error.isTransient) {
console.log('Not yet committed — retry later')
return { status: 'pending_commit' as const }
}
throw error
}

// 5. Build execution input (Merkle proof + off-chain token data)
const input = await source.getExecutionInput({ request, verifications })

// 6. Execute on destination
const execution = await dest.execute({ offRamp, input, wallet })
console.log('Execution tx:', execution.log.transactionHash)
return { status: 'executed' as const, txHash: execution.log.transactionHash }
}

Gas Limit Overrides

If execution reverts due to insufficient gas, override the gas limit:

TypeScript
const execution = await dest.execute({
offRamp,
input,
wallet: destWallet,
gasLimit: 500_000, // Override gas for ccipReceive call
tokensGasLimit: 100_000, // Override gas for tokenPool.releaseOrMint (EVM only)
})

The gasLimit override applies to the receiver contract's ccipReceive call. The tokensGasLimit override applies to each token pool operation.

Off-Chain Token Data

getExecutionInput automatically handles off-chain token data for all token types:

Token typeWhat the SDK does
USDC (CCTP)Fetches Circle attestation from Circle API
LBTCFetches attestation from Lombard API
Standard tokensNo off-chain data needed (empty)

Both the message ID path (dest.execute({ messageId })) and the transaction hash path (source.getExecutionInput()) handle this automatically.

Unsigned Transactions

Use generateUnsignedExecute to separate transaction construction from signing. This lets you build execution transactions on a backend with reliable RPCs and return them to clients for signing — or feed them into multi-sig wallets, offline signing workflows, or any system where signing happens separately.

By Message ID

TypeScript
const unsignedTx = await dest.generateUnsignedExecute({
messageId: '0x1234...abcd',
payer: walletAddress, // address of the wallet that will sign
})

// Single transaction — send to the OffRamp contract
const tx = unsignedTx.transactions[0]
const hash = await walletClient.sendTransaction(tx)

By Transaction Hash

TypeScript
const input = await source.getExecutionInput({ request, verifications })

const unsignedTx = await dest.generateUnsignedExecute({
offRamp,
input,
payer: walletAddress,
gasLimit: 500_000, // optional override
})

const tx = unsignedTx.transactions[0]
const hash = await walletClient.sendTransaction(tx)

Unlike generateUnsignedSendMessage (which may return multiple transactions for token approvals), generateUnsignedExecute always returns a single transaction.

Using the CLI

Bash
# By message ID (needs dest RPC only)
ccip-cli manual-exec 0xMessageId \
--rpc https://rpc.fuji.avax.network \
--wallet $PRIVATE_KEY

# By transaction hash (needs source + dest RPCs)
ccip-cli manual-exec 0xSourceTxHash \
--rpc https://rpc.sepolia.org \
--rpc https://rpc.fuji.avax.network \
--wallet $PRIVATE_KEY

# Override gas limit
ccip-cli manual-exec 0xMessageId \
--rpc https://rpc.fuji.avax.network \
--wallet $PRIVATE_KEY \
--gas-limit 500000

# Select specific message in multi-message transaction
ccip-cli manual-exec 0xSourceTxHash \
--rpc https://rpc.sepolia.org \
--rpc https://rpc.fuji.avax.network \
--wallet $PRIVATE_KEY \
--log-index 1

The CLI auto-detects whether the argument is a message ID or transaction hash.

Method Reference

MethodCalled onData sourcePurpose
execute({ messageId, wallet })DestinationCCIP APIExecute by message ID (simplest)
execute({ offRamp, input, wallet })DestinationPre-computed inputExecute with pre-fetched proof
generateUnsignedExecute({ messageId, payer })DestinationCCIP APIUnsigned execution tx by message ID
generateUnsignedExecute({ offRamp, input, payer })DestinationPre-computed inputUnsigned execution tx with pre-fetched proof
getExecutionInput({ request, verifications })SourceRPCBuild Merkle proof + fetch off-chain token data
getVerifications({ offRamp, request })DestinationRPCGet commit report or verifier results
getExecutionReceipts({ offRamp, messageId })DestinationRPCCheck current execution state
discoverOffRamp(source, dest, onRamp)RPCFind OffRamp address (memoized)

Errors

ErrorTransientMeaning
CCIPCommitNotFoundErrorYesMessage not yet committed — wait and retry
CCIPMessageBatchIncompleteErrorYesNot all batch messages found — retry with larger range
CCIPUsdcAttestationErrorYesUSDC attestation not ready — retry
CCIPLbtcAttestationErrorYesLBTC attestation not ready — retry
CCIPExecTxRevertedErrorNoExecution reverted — check gas limit, receiver contract
CCIPMerkleRootMismatchErrorNoProof mismatch — verify all messages in batch
CCIPOffRampNotFoundErrorNoNo OffRamp for this lane — verify lane exists
CCIPApiClientNotAvailableErrorNoAPI disabled — use transaction hash path instead