Skip to main content

Executor Protocol

What is an Executor?

An Executor is, in one word, an “actor.” It is the component that actually moves funds, checks status, and recovers when something goes wrong. Think of it as an international courier system: The key point is that all of these Executors follow a single common interface. Whether it is the courier driver or the customs officer, they all file the same report form — which lets the system manage them in a uniform way.
Why a common interface? The contract is duck-typed: anyone who implements these 6 lifecycle methods is recognized as an Executor, regardless of which transport it drives.
The executor contract is six methods:
submit and recover can take a DB session that the dispatcher threads in. This lets an executor that needs to read or write the DB (e.g. nonce reservation, intent re-derivation) participate in the orchestrator’s transaction instead of opening its own. Executors that don’t need it simply ignore it.

The 6 Methods of the Executor Contract

Here is the full execution lifecycle shown as a single sequence:

Each method in detail

1. preflight — “Can you do this?”

Pre-execution validation. A return value of None means pass; returning an ExecutionResult means a problem was found. For a CEX withdrawal, for example:
  • Is the withdrawal address on the whitelist?
  • Is withdrawal currently available? (not under maintenance)
  • Is the balance sufficient?
  • Is the amount within the min/max limits?
Why problems must be caught in preflight Finding a problem after submit() is already too late. “The address was wrong” means nothing after the funds have left. Preflight is the “safety check before pulling the trigger.”

2. prepare — “Get ready”

Packages what is going to be done into a prepared-action object. Nothing is executed yet. For CEX: serializes a payload like { exchange: "upbit", asset: "XRP", amount: "100", address: "..." }.
For CCTP: constructs the EVM transaction (calldata, nonce, gas) for the burn call.

3. submit — “Execute!”

Actually executes the prepared action. For CEX, it calls the withdrawal API; for on-chain, it broadcasts the signed transaction. The signed payload arrives inside the prepared action (the orchestrator runs the signer between prepare and submit). A submit that needs the DB — for example re-deriving the signed recipient or reserving an EVM nonce — can run inside the orchestrator’s transaction via the session the dispatcher threads in.

4. observe — “Check the result”

Checks the status of an asynchronous operation. Returns OBSERVING if still in progress, COMPLETED when done, and FAILED on failure.
Why observe is a separate method A blockchain TX or exchange withdrawal does not finish the moment you submit it. Withdrawals take minutes to hours, and on-chain TXs require confirmations. That is why “execution” and “confirmation” are separate.

5. recover — “Something seems off — try to recover”

Called when the system is in an UNKNOWN state. Example: the withdrawal API was called but timed out. Funds may or may not have gone out. recover investigates and classifies the situation as COMPLETED or FAILED. Like submit, it can run reconciliation inside the orchestrator’s transaction via the DB session the dispatcher threads in.
UNKNOWN is the most dangerous state UNKNOWN is scarier than FAILED. With FAILED you at least have the certain information “it did not happen.” UNKNOWN means “we don’t know whether it happened or not.”

6. health — “Are you alive?”

Returns a small status dict (or nothing) describing whether the executor’s downstream dependency is reachable. It is polled at bootstrap and by the executor-health worker, and feeds the approval-time availability gate described below. Unlike the other five methods, health takes no execution context — it is a standalone liveness probe.

Execution Context — All the information needed to execute

Think of this as the “work order” handed to an Executor. It contains everything needed to carry out the task. Key fields grouped by role:
provider_context is the key A node’s output is propagated as the next node’s input. For example:
  • Withdrawal Action returns exchange_withdrawal_id
  • Withdrawal Observe receives it and polls the status, then returns txid
  • Deposit Observe uses that txid to match the incoming deposit
It is like passing a baton in a relay race.

Local vs Remote Executor

Registration decides Local vs Remote — not the key. Whichever path wins is decided when the executor is registered: a Local executor wraps an in-process handler that runs directly, or a Remote executor proxies to an HTTP service. The registry is a flat lookup keyed by the exact executor key; resolving a binding is a single exact-key lookup with no prefix parsing.
A non-Python lane such as Solana is illustrative — it sketches how a non-Python transport would plug in over the Remote path. The Remote path exists today, but no Solana executor ships; the live executors are the CEX, CCTP/CCIP/Gateway/USDT0/EVM, and Hyperliquid lanes.

Local Executor

Executes directly in-process. It delegates each lifecycle call to the underlying execution logic.
Why separate the wrapper from the logic The system interface concerns (carrying the executor key, registry registration, health checks) are kept apart from the actual execution logic. The execution logic stays pure business logic, while the wrapper handles the system concerns.

Remote Executor

Proxies each lifecycle call to an external service over HTTP JSON. Each method maps to an HTTP endpoint (/preflight, /submit, /observe, …); a no-content response on preflight means “passed,” otherwise the JSON body is the execution result.
Why Remote is needed Not everything can be done in Python. Chains like Solana have a much more mature TypeScript/Rust ecosystem, and certain DEX aggregators have dedicated SDKs. Thanks to the Remote path, execution logic can be implemented in any language — just expose the same executor contract over HTTP.

Executor Registry

All Executors are registered in a central registry under an exact key, and a binding is resolved by exact-key lookup. The key format follows the exec.{lane}.{role} pattern:
  • exec.cex.withdrawal_action
  • exec.cex.withdrawal_observe
  • exec.cex.deposit_observe
  • exec.cctp.burn
  • exec.cctp.mint

Prepared Action and Signing

The prepared action that prepare produces is a “specification of what will be done” — it is an execution plan, not the execution itself. A prepared action carries, conceptually:
Why prepare and submit are separated Signing is not the Executor’s job. Private key management is handled by a separate signer. This separation provides:
  1. Security: Executors never need access to the private key
  2. Flexibility: Signing methods (HSM, KMS, MPC wallet, etc.) can be swapped out
  3. Auditing: The “what will be done” is recorded at the prepare stage; signing and execution come later
  4. Replay prevention: The payload hash can verify that what was prepared matches what was submitted
Analogy: first draft the contract (prepare) → stamp it (sign) → file it (submit). The drafter and the seal keeper can be different people.
For CEX withdrawals, signing is not required — exchange API key authentication is handled inside the adapter. For CCTP, signing is required — blockchain transactions require a private key signature.

Health State and Binding Resolution Policy

3-layer defense model

An Executor/Signer’s health state can be checked at 3 points:

Why not filter at Resolution?

  1. Health is transient. An executor that is down at compile time may be up by dispatch time
  2. Resolution expresses intent. A plan’s binding decides “which executor to use,” not “is it alive right now”
  3. The Approval-time gate is sufficient. The availability gate checks health just before approval, preventing execution from starting with a down executor
  4. Filtering at Resolution degrades UX. A transient down state causing a resolution error would force the operator to recreate the movement
The gap between Resolution and Approval A warning is logged at Resolution time when the health state is down or unknown. This gives operators visibility but does not block behavior.

What if health changes after approval?

If an executor goes down after approval:
  1. The Dispatcher fails at the executor’s preflight or submit
  2. The Node transitions to FAILED state
  3. The Recovery worker attempts recovery
This is the normal failure/recovery path. Adding a health guard to the Dispatcher is an optimization, not a correctness requirement.