> ## 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.

# Build and Run Executors

> Deploy, configure, and operate a custom LayerZero V2 Executor on EVM testnets with setup, verification, replay, and production readiness guidance.

Executors provide Execution as a Service for LayerZero messages. A custom Executor receives a fee on the source chain, waits until the destination chain can accept the verified packet, and then calls the destination Endpoint to deliver the message.

This guide shows how to independently bring up an EVM Executor for a two-chain testnet path, then explains the checks and operational controls required before running one in production.

<Warning>
  The reference implementations linked in this guide are third-party examples,
  not LayerZero-owned production services. Use them to learn the protocol flow,
  then harden your own deployment with the production checklist below.
</Warning>

## What You Will Build

By the end of this guide, you should have:

* an Executor fee contract deployed on each source chain you want to support;
* an OApp configured to pay your Executor instead of the default Executor;
* an off-chain process watching source-chain `PacketSent` events;
* fee validation that proves your Executor was paid for the exact packet;
* destination-chain verification and execution logic; and
* a repeatable smoke test that sends a message in both directions.

The commands and addresses below use an EVM-to-EVM testnet path. For non-EVM chains, keep the same protocol lifecycle, but replace the EVM packet address codec, event provider, and execution client with chain-specific implementations.

## Executor Lifecycle

A complete Executor has two off-chain workflows: commit and execute.

1. Watch the source chain for `PacketSent` on `EndpointV2`.
2. Validate that the same transaction paid your Executor through the trusted send library.
3. Decode the packet and route it by destination endpoint ID.
4. Wait for destination-chain DVN verification.
5. Call `ReceiveUln302.commitVerification(packetHeader, payloadHash)` when the packet is verifiable.
6. Poll `EndpointV2View.executable(origin, receiver)` until the packet is executable.
7. Call `EndpointV2.lzReceive(origin, receiver, guid, message, extraData)`.
8. Persist the result so restarts do not duplicate or lose work.

The source-chain Executor contract handles fee quoting and fee collection. The off-chain worker handles observation, verification, commit, and execution.

## Prerequisites

Install:

* Node.js `>=18`
* npm, pnpm, or Yarn
* [Foundry](https://book.getfoundry.sh/getting-started/installation)
* `git`
* `jq`
* RPC URLs for both chains
* a funded signer on both chains

Prepare environment variables:

```bash theme={null}
export PRIVATE_KEY="0x..."
export RPC_URL_A="https://..."
export RPC_URL_B="https://..."
```

<Warning>
  Use a dedicated Executor signer. Do not run an Executor with a production
  owner key, treasury key, or deployer key unless you have intentionally
  designed that operational model.
</Warning>

## Choose Your Chains

Use the deployment pages or metadata API to collect the current Endpoint, SendUln302, ReceiveUln302, EndpointV2View, and endpoint ID values for each chain.

```bash theme={null}
curl -s https://metadata.layerzero-api.com/v1/metadata \
  | jq '.[] | select(.chainKey == "sepolia" or .chainKey == "hyperliquid-testnet")'
```

For example, this guide has been validated with:

| Chain                    |   Chain ID | Endpoint ID | Deployment page                                                                          |
| ------------------------ | ---------: | ----------: | ---------------------------------------------------------------------------------------- |
| Ethereum Sepolia Testnet | `11155111` |     `40161` | [/v2/deployments/chains/sepolia](/v2/deployments/chains/sepolia)                         |
| HyperEVM Testnet         |      `998` |     `40362` | [/v2/deployments/chains/hyperliquid-testnet](/v2/deployments/chains/hyperliquid-testnet) |

<Tip>
  Sepolia testnet pricing can be expensive because LayerZero testnet endpoints
  use real mainnet pricefeeds for crosschain transfers. For repeated tests,
  choose testnets with cheaper blockspace when possible.
</Tip>

## Bootstrap A Reference Implementation

The Paladin reference implementation is a useful EVM starting point because it already separates the source-chain event provider, destination-chain executor, packet codec, and sample contracts.

```bash theme={null}
git clone https://github.com/0xpaladinsecurity/zexecutor.git
cd zexecutor

npm install
git submodule update --init --recursive

npm test
forge build --root ./contracts
```

If a fresh checkout fails while fetching `contracts/lib/forge-std`, add the missing submodule entry and retry:

```bash theme={null}
git config -f .gitmodules submodule.contracts/lib/forge-std.path contracts/lib/forge-std
git config -f .gitmodules submodule.contracts/lib/forge-std.url https://github.com/foundry-rs/forge-std
git submodule sync --recursive
git submodule update --init --recursive
```

<Info>
  The reference is intentionally minimal. Before operating it beyond a testnet
  smoke test, add durable state, replay checkpoints, rate limiting, metrics, and
  classified retry handling.
</Info>

## Deploy The On-Chain Executor

Deploy an Executor fee contract on every source chain that should pay your Executor. The contract must implement `ILayerZeroExecutor`:

```solidity theme={null}
interface ILayerZeroExecutor {
    function assignJob(
        uint32 _dstEid,
        address _sender,
        uint256 _calldataSize,
        bytes calldata _options
    ) external payable returns (uint256 price);

    function getFee(
        uint32 _dstEid,
        address _sender,
        uint256 _calldataSize,
        bytes calldata _options
    ) external view returns (uint256 price);
}
```

For a smoke test, a static-fee contract is enough. For production, the fee must be calculated from the destination gas limit, gas price, calldata size, native token price, margin, and any native drop or value instructions in `_options`.

After deploying your Executor contracts, configure your OApp's send library so messages to the remote EID use your Executor:

```solidity theme={null}
ExecutorConfig memory executorConfig = ExecutorConfig({
    maxMessageSize: 1024,
    executor: address(customExecutor)
});

SetConfigParam[] memory sendConfigs = new SetConfigParam[](1);
sendConfigs[0] = SetConfigParam({
    eid: remoteEid,
    configType: 1, // CONFIG_TYPE_EXECUTOR
    config: abi.encode(executorConfig)
});

endpoint.setConfig(address(oapp), sendLib, sendConfigs);
```

Also configure the DVN set used by your test OApp. The Executor can only commit after the receive library has enough DVN verification for the packet.

## Configure OApp Peers

Your test OApps must be deployed on both chains and configured as peers before `_lzSend` can route packets.

For EVM-to-EVM tests, set each remote peer as a left-padded `bytes32` value:

```solidity theme={null}
oappOnChainA.setPeer(
    chainBEid,
    bytes32(uint256(uint160(address(oappOnChainB))))
);

oappOnChainB.setPeer(
    chainAEid,
    bytes32(uint256(uint160(address(oappOnChainA))))
);
```

Use the same owner account that controls the OApp configuration. If your test OApp configures DVNs and Executor settings inside `setPeer`, confirm that both the send library and receive library configs are written for each pathway before quoting a message.

## Create Executor Configuration

Your off-chain Executor needs one config entry per chain. Use current metadata for LayerZero protocol addresses and your own deployed Executor contract for `executor`.

```json theme={null}
{
  "chains": {
    "11155111": {
      "name": "Ethereum Sepolia Testnet",
      "rpc": "https://ethereum-sepolia-rpc.publicnode.com",
      "endpoint": "0x6EDCE65403992e310A62460808c4b910D972f10f",
      "endpointView": "0x982Ca8b3532236C5e77Ff215791dD454e07E21F7",
      "trustedSendLib": "0xcc1ae8Cf5D3904Cef3360A9532B477529b177cCE",
      "trustedReceiveLib": "0xdAf00F5eE2158dD58E0d3857851c432E34A3A851",
      "executor": "0xYourExecutorOnSepolia",
      "eid": 40161,
      "bootstrapLookbackBlocks": 0
    },
    "998": {
      "name": "HyperEVM Testnet",
      "rpc": "https://rpc.hyperliquid-testnet.xyz/evm",
      "endpoint": "0xf9e1815F151024bDE4B7C10BAC10e8Ba9F6b53E1",
      "endpointView": "0x386A3922470581155c42282801231762E7343802",
      "trustedSendLib": "0x43E505ba192aaC7BABdC1A796c87844171011684",
      "trustedReceiveLib": "0x012f6eaE2A0Bf5916f48b5F37C62Bcfb7C1ffdA1",
      "executor": "0xYourExecutorOnHyperEVM",
      "eid": 40362,
      "bootstrapLookbackBlocks": 0
    }
  }
}
```

Use `bootstrapLookbackBlocks` only if your worker persists checkpoints and can rate-limit historical replay. Endpoint-wide log scans can be noisy because the Endpoint emits traffic for all applications and executors.

## Validate Executor Payment

Do not decide that your Executor was paid by checking whether a transaction contains any `ExecutorFeePaid` event. A single transaction can send multiple packets, and only one of them may have paid your Executor.

Use this receipt-walking algorithm for each `PacketSent` log:

1. Fetch the transaction receipt.
2. Start from the specific `PacketSent.logIndex`.
3. Walk backward through logs in that transaction.
4. Stop if you hit another `PacketSent`; that earlier packet owns earlier fee events.
5. Ignore any event not emitted by the configured `trustedSendLib`.
6. Decode `ExecutorFeePaid(address executor, uint256 fee)`.
7. Accept the packet only if `executor == yourExecutorAddress`.

Pseudocode:

```ts theme={null}
for (const txLog of receipt.logsBefore(packetSentLog).reverse()) {
  if (txLog.topic0 === PACKET_SENT_TOPIC) return undefined;
  if (txLog.topic0 !== EXECUTOR_FEE_PAID_TOPIC) continue;
  if (txLog.address.toLowerCase() !== trustedSendLib.toLowerCase()) continue;

  const [executor, fee] = decodeExecutorFeePaid(txLog.data);
  if (executor.toLowerCase() !== configuredExecutor.toLowerCase()) continue;

  return { packet, fee };
}
```

Add tests for:

* multiple `PacketSent` logs in one transaction;
* `ExecutorFeePaid` emitted by an untrusted address;
* `ExecutorFeePaid` for another Executor;
* no `ExecutorFeePaid`; and
* zero or insufficient fee.

## Commit Verification

After accepting a packet, derive:

* `origin`: `{ srcEid, sender, nonce }`
* `receiver`: destination OApp address
* `packetHeader`
* `payloadHash`

First check endpoint-level verifiability:

```solidity theme={null}
EndpointV2View.verifiable(
    origin,
    receiver,
    receiveLib,
    payloadHash
);
```

Then check receive-library verifiability before committing:

```solidity theme={null}
UlnConfig memory config = ReceiveUln302.getUlnConfig(receiver, origin.srcEid);

ReceiveUln302.verifiable(
    config,
    keccak256(packetHeader),
    payloadHash
);
```

When the receive library reports the packet is verifiable, call:

```solidity theme={null}
ReceiveUln302.commitVerification(packetHeader, payloadHash);
```

<Tip>
  A packet can look close to ready at the Endpoint level while still not being
  committable at the receive library. Check both layers to avoid unnecessary
  commit reverts.
</Tip>

## Execute The Message

After commit, query:

```solidity theme={null}
EndpointV2View.executable(origin, receiver);
```

Handle execution state as:

| State                      | Meaning                                            | Action                              |
| -------------------------- | -------------------------------------------------- | ----------------------------------- |
| `NotExecutable`            | Packet is not committed or is blocked by ordering. | Requeue with backoff.               |
| `VerifiedButNotExecutable` | Packet is verified, but not executable yet.        | Requeue and inspect nonce ordering. |
| `Executable`               | Endpoint can deliver the message.                  | Call `lzReceive`.                   |
| `Executed`                 | Message was already delivered.                     | Mark complete.                      |

When executable:

```solidity theme={null}
EndpointV2.lzReceive(
    origin,
    receiver,
    guid,
    message,
    extraData
);
```

Use the message options to set destination gas and value. A production Executor should parse options and enforce that the collected fee covers the requested execution resources.

## Run The Off-Chain Worker

For the reference implementation, run:

```bash theme={null}
PRIVATE_KEY="$PRIVATE_KEY" npm run dev -- -c ./executor.testnet.config.json
```

A clean startup should:

* load each configured chain;
* register one provider per source EID;
* register one executor per destination EID; and
* begin watching `PacketSent`.

If you enable historical replay, start with a small block window and confirm your RPC provider supports the requested `eth_getLogs` range:

```json theme={null}
{
  "bootstrapLookbackBlocks": 100,
  "bootstrapBlockRange": 50
}
```

For production, replay should resume from the last persisted checkpoint, not from an arbitrary fixed lookback on every restart.

## Smoke Test Both Directions

Before treating the Executor as usable, run a two-way smoke test.

1. Quote a message from chain A to chain B.
2. Send a small payload from chain A to chain B.
3. Confirm the source transaction emitted `PacketSent` and `ExecutorFeePaid`.
4. Confirm your worker committed verification on chain B.
5. Confirm your worker executed `lzReceive` on chain B.
6. Query the destination OApp and confirm the payload was stored or handled.
7. Repeat from chain B to chain A.

If you are using the Paladin reference scripts, the commands look like:

```bash theme={null}
# Quote chain A -> chain B.
forge script QuoteMessage \
  --sig "run(address,uint32,bytes)" \
  "$APP_ON_CHAIN_A" \
  "$CHAIN_B_EID" \
  0x68656c6c6f2d6578656375746f72 \
  --rpc-url "$RPC_URL_A" \
  --root ./contracts

# Send chain A -> chain B. Use a value above the quoted native fee.
forge script SendMessage \
  --sig "run(address,uint32,bytes,uint256)" \
  "$APP_ON_CHAIN_A" \
  "$CHAIN_B_EID" \
  0x68656c6c6f2d6578656375746f72 \
  "$NATIVE_FEE_WITH_BUFFER" \
  --rpc-url "$RPC_URL_A" \
  --broadcast \
  --root ./contracts

# Query destination app state after execution.
forge script GetExecutedMessages \
  --sig "run(address)" \
  "$APP_ON_CHAIN_B" \
  --rpc-url "$RPC_URL_B" \
  --root ./contracts
```

For a test OApp that stores received messages, the success condition is:

```bash theme={null}
# Chain B after A -> B
EXECUTION_LENGTH: 1
EXECUTION_0: <payload>

# Chain A after B -> A
EXECUTION_LENGTH: 1
EXECUTION_0: <payload>
```

Record the source send tx, destination commit tx, and destination execution tx for each direction. These three hashes are the fastest way to debug later failures.

## Production Runtime Checklist

A production Executor needs more than the minimal reference loop.

### Persistent State

Persist one row per packet:

| Field           | Purpose                                                        |
| --------------- | -------------------------------------------------------------- |
| `srcEid`        | Source endpoint ID.                                            |
| `dstEid`        | Destination endpoint ID.                                       |
| `guid`          | Idempotency key for the packet.                                |
| `srcTxHash`     | Source transaction that emitted `PacketSent`.                  |
| `srcLogIndex`   | Exact source log index.                                        |
| `receiver`      | Destination OApp.                                              |
| `payloadHash`   | Payload hash used for verification.                            |
| `status`        | Observed, verifiable, committed, executable, executed, failed. |
| `commitTxHash`  | Destination commit transaction, if sent.                       |
| `executeTxHash` | Destination execution transaction, if sent.                    |
| `retryCount`    | Retry accounting.                                              |
| `lastError`     | Last classified error.                                         |

On restart, load pending packets from storage before scanning new blocks.

### Checkpointed Event Ingestion

Do not rely on `watchEvent` alone. RPC subscriptions can miss logs during disconnects or process restarts.

Use:

* per-chain block checkpoints;
* bounded `eth_getLogs` windows;
* packet GUID and tx/log-index deduplication;
* rate limits per RPC provider;
* retries with exponential backoff; and
* alerts when the scanner falls behind.

### Retry Classification

Classify errors before retrying:

| Error class                 | Retry? | Example response                    |
| --------------------------- | ------ | ----------------------------------- |
| DVN not ready               | Yes    | Requeue with normal backoff.        |
| Endpoint not executable     | Yes    | Requeue and inspect nonce ordering. |
| Already committed           | Yes    | Move to execution check.            |
| Already executed            | No     | Mark complete.                      |
| RPC rate limited            | Yes    | Back off and reduce scan range.     |
| Wrong config address        | No     | Alert operator.                     |
| Insufficient signer balance | No     | Alert and pause sends.              |

### Signer Operations

Monitor:

* native token balance on every destination chain;
* account nonce and stuck transactions;
* RPC latency and error rate;
* gas price spikes;
* oldest pending packet age; and
* queue depth by destination EID.

### Observability

Expose health and metrics:

* last source block scanned;
* last packet observed;
* packets committed;
* packets executed;
* retries by class;
* failures by class;
* signer balances;
* pending queue depth; and
* RPC provider status.

## Troubleshooting

| Symptom                                      | Likely cause                                             | Fix                                                            |
| -------------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------------- |
| No packets accepted                          | OApp is still configured for another Executor.           | Check `ExecutorConfig.executor` for the source pathway.        |
| Fee event found but packet rejected          | Fee event belongs to another packet or another Executor. | Walk receipt logs backward from the specific `PacketSent`.     |
| `commitVerification` reverts                 | DVNs have not satisfied receive-library verification.    | Check `ReceiveUln302.verifiable` before committing.            |
| `executable` stays `NotExecutable`           | Packet is not committed or ordered execution is blocked. | Check commit tx and nonce ordering.                            |
| Packet is delivered twice in logs            | Worker is not deduplicating queue entries.               | Deduplicate by `srcEid`, `dstEid`, and `guid`.                 |
| Worker misses packets after restart          | No durable checkpoint or catch-up scan.                  | Persist checkpoints and replay bounded block ranges.           |
| RPC returns block range or rate-limit errors | Historical scan is too broad.                            | Lower block range, throttle, or use an indexed provider.       |
| Signer transactions stop landing             | Low balance or stuck nonce.                              | Monitor balances and nonce state; replace or cancel stuck txs. |

## Non-EVM Chains

The EVM packet codec assumes sender and receiver values can be converted from `bytes32` to EVM addresses. For Solana, Aptos, Sui, or other non-EVM chains, implement chain-specific versions of:

* event provider;
* packet/address codec;
* destination execution client;
* transaction construction;
* signer management; and
* gas or fee accounting.

The protocol lifecycle remains the same: observe, validate payment, wait for verification, commit when needed, and execute when the destination Endpoint allows delivery.

## Summary

The minimal reference Executor is enough to prove the LayerZero V2 flow on testnet. To make it independently operable, you need:

* current metadata-derived chain config;
* an Executor contract deployed on each source chain;
* OApp send config pointing to your Executor;
* strict per-packet fee validation;
* current receive-library and endpoint verifiability checks;
* a two-way smoke test; and
* durable runtime state, checkpointed replay, retries, and monitoring before production.

## Important Security And Operational Considerations

### Can a custom Executor forge or alter LayerZero messages?

No. A custom Executor cannot make the destination Endpoint accept an unverified packet. Message authenticity is still enforced by the configured MessageLib and DVNs.

The primary risk is not message forgery. The primary risk is that a poorly implemented Executor can fail to deliver valid messages, execute messages without being properly paid, overpay for gas, or expose its signer infrastructure.

### What happens if I validate `ExecutorFeePaid` incorrectly?

You may execute packets that did not pay your Executor.

This most commonly happens when an implementation checks whether the transaction contains any `ExecutorFeePaid` event instead of tying the payment to the exact `PacketSent` log. Batched sends can emit multiple `PacketSent` events in one transaction, and each packet must be matched to its own fee event.

Mitigation:

* walk receipt logs backward from the exact `PacketSent.logIndex`;
* require the event emitter to equal the trusted send library;
* require the decoded `executor` to equal your Executor contract; and
* stop when a previous `PacketSent` is reached.

### What happens if I use the wrong Endpoint, SendUln, ReceiveUln, or DVN address?

Messages can become stuck or fail in non-obvious ways. A packet may be observed on the source chain, but never become committable or executable on the destination chain because the worker is checking the wrong contract or the OApp pathway is configured differently than the worker expects.

Mitigation:

* source addresses from the LayerZero metadata endpoint or deployment pages;
* store the exact metadata snapshot used for a deployment;
* validate every configured address at startup; and
* include a two-way smoke test after every config change.

### What happens if my worker misses events?

Valid packets can remain undelivered. RPC subscriptions and HTTP polling can miss logs during restarts, disconnects, rate limits, or provider outages.

Mitigation:

* persist per-chain scan checkpoints;
* replay bounded block ranges on startup;
* deduplicate by packet GUID and tx/log index;
* alert when the scanner falls behind; and
* use an indexed or production-grade RPC provider for production traffic.

### What happens if I do not persist packet state?

The worker can lose pending packets during a restart, retry the same packet indefinitely, or submit duplicate commit and execution transactions.

Mitigation:

* persist packet status transitions;
* treat packet GUID as the primary idempotency key;
* record commit and execution transaction hashes; and
* resume pending work from storage before processing new logs.

### What are the signer risks?

The Executor signer needs native gas on every destination chain it executes on. If the signer is overfunded, reused as an owner key, or stored on an insecure host, compromise of the Executor process can become a key-management incident.

Mitigation:

* use a dedicated low-privilege Executor signer;
* keep owner, treasury, and Executor keys separate;
* monitor signer balances and nonce state;
* rotate keys through an operational runbook; and
* fund only the amount required for expected execution volume plus buffer.

### Can a bad gas or fee model lose money?

Yes. If your `getFee` and `assignJob` logic undercharges relative to destination gas costs, your Executor can pay more to execute packets than it collected on the source chain. If it over-trusts user-provided options, it can also accept work that is too expensive or impossible to execute.

Mitigation:

* parse message options before quoting;
* cap supported gas and value options;
* price destination native gas and token conversion conservatively;
* include an operator margin; and
* pause or reprice pathways when gas markets move sharply.

### Is a stalled Executor a security issue?

It can be. A stalled Executor does not let invalid messages through, but it can create liveness failures for applications that rely on automatic delivery. If a chain is running its own Executor because another off-chain service was turned off, the Executor becomes part of that chain's application availability path.

Mitigation:

* alert on oldest pending packet age;
* alert on failed commit or execution attempts;
* maintain enough destination gas;
* document manual execution fallback; and
* run a tested recovery process for RPC outages and stuck nonces.
