> ## Documentation Index
> Fetch the complete documentation index at: https://jephalabs.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Executor Protocol

> How QTG dispatches, observes, and recovers individual execution steps across CEX and on-chain transports.

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

| Role                   | Courier analogy                             | Real example                          |
| ---------------------- | ------------------------------------------- | ------------------------------------- |
| **Withdrawal Action**  | The courier driver who picks up the package | Submits a withdrawal from an exchange |
| **Withdrawal Observe** | The package tracking system                 | Polls the withdrawal status           |
| **Deposit Observe**    | The delivery confirmation agent             | Confirms the deposit arrived          |
| **CCTP Burn**          | The currency exchange burning won           | Burns USDC on the source chain        |
| **CCTP Mint**          | The destination bank issuing new bills      | Mints USDC on the destination chain   |

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.

<Tip>
  **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.
</Tip>

The executor contract is six methods:

| Method      | Question it answers                                                   |
| ----------- | --------------------------------------------------------------------- |
| `preflight` | "Can you do this?" — pre-execution validation                         |
| `prepare`   | "Get ready" — package the action, sign nothing yet                    |
| `submit`    | "Execute!" — call the API / broadcast the tx                          |
| `observe`   | "Check the result" — poll asynchronous status                         |
| `recover`   | "Something seems off — try to recover" — resolve an `UNKNOWN` outcome |
| `health`    | "Are you alive?" — standalone liveness probe                          |

<Note>
  `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.
</Note>

***

## The 6 Methods of the Executor Contract

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

```mermaid theme={null}
sequenceDiagram
    participant O as Orchestrator
    participant E as Executor
    participant S as Signer (optional)
    participant X as External System

    O->>E: preflight(ctx)
    Note right of E: "Can you do this?"
    alt problem found
        E-->>O: ExecutionResult(FAILED)
    else no problem
        E-->>O: None (= pass)
    end

    O->>E: prepare(ctx)
    Note right of E: "Get ready to do it"
    E-->>O: ExecutionResult + PreparedAction

    opt signing_required = true
        O->>S: sign(payload)
        S-->>O: SignResult(signature)
    end

    O->>E: submit(ctx, prepared_action, sign_result=...)
    Note right of E: "Execute!"
    E->>X: actual API call / TX broadcast
    X-->>E: response
    E-->>O: ExecutionResult(COMPLETED | UNKNOWN)

    loop status check (polling)
        O->>E: observe(ctx)
        E->>X: status query
        X-->>E: current status
        alt complete
            E-->>O: ExecutionResult(COMPLETED)
        else in progress
            E-->>O: ExecutionResult(OBSERVING)
        end
    end

    opt when UNKNOWN state occurs
        O->>E: recover(ctx)
        Note right of E: "Attempt recovery"
        E-->>O: COMPLETED | FAILED | UNKNOWN
    end
```

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

<Warning>
  **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."**
</Warning>

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

<Info>
  **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.
</Info>

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

<Warning>
  **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."**
</Warning>

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

```mermaid theme={null}
classDiagram
    class ExecutionContext {
        +str protocol_version
        +str request_id
        +str request_node_id
        +str compiled_plan_hash
        +str template_key
        +int template_version
        +str node_key
        +str node_kind
        +str action_type
        +int attempt_no
        +dict intent
        +dict input_params
        +dict resolved_bindings
        +dict risk_controls
        +list prior_artifacts
        +dict node_config
        +dict provider_context
        +dict timeout_policy
    }
```

Key fields grouped by role:

| Group                | Field               | Description                                        |
| -------------------- | ------------------- | -------------------------------------------------- |
| **What**             | `intent`            | Original intent: source/destination/amount, etc.   |
| **Parameters**       | `input_params`      | Concrete parameters needed for execution           |
| **Node config**      | `node_config`       | Configuration values for this specific node (step) |
| **Previous results** | `provider_context`  | Accumulated data from upstream nodes               |
| **Security**         | `risk_controls`     | Risk control values: hardcap, max\_fee, etc.       |
| **Auth**             | `resolved_bindings` | Signer bindings, etc.                              |
| **Retry**            | `attempt_no`        | Which attempt number this is                       |

<Tip>
  **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.
</Tip>

***

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

```mermaid theme={null}
flowchart TD
    REG["Registration<br/>(bootstrap)"] -->|"register Local"| L[Local Executor]
    REG -->|"register Remote"| RM[Remote Executor]
    L -->|"key → instance"| MAP["Executor Registry<br/>flat key → executor map"]
    RM -->|"key → instance"| MAP
    MAP -->|"exact-key lookup"| RESOLVED["resolved executor"]

    L --> H[In-process Handler<br/>direct execution]
    RM -->|HTTP POST /preflight| RS[Remote Service<br/>other language/process]
    RM -->|HTTP POST /submit| RS
    RM -->|HTTP POST /observe| RS

    style L fill:#e8f5e9
    style RM fill:#e3f2fd
    style H fill:#c8e6c9
    style RS fill:#bbdefb
    style MAP fill:#fff3e0
```

<Note>
  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.
</Note>

### Local Executor

Executes directly in-process. It delegates each lifecycle call to the underlying execution logic.

<Info>
  **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.
</Info>

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

<Tip>
  **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.
</Tip>

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

```mermaid theme={null}
sequenceDiagram
    participant O as Orchestrator
    participant E as Executor
    participant S as Signer

    O->>E: prepare
    E-->>O: PreparedAction<br/>(payload, signing required)

    Note over O: validates payload hash

    O->>S: sign
    Note right of S: signs with private key<br/>in external HSM / KMS
    S-->>O: signature

    O->>E: submit (with signature)
    E-->>O: ExecutionResult
```

A prepared action carries, conceptually:

| Field            | Meaning                                                         |
| ---------------- | --------------------------------------------------------------- |
| Action type      | The action being performed (e.g. `cctp_burn`, `cex_withdrawal`) |
| Payload          | The actual data to execute (serialized)                         |
| Payload hash     | Integrity hash linking what was prepared to what is submitted   |
| Signing required | Whether the action needs an external signature                  |
| Expiry           | When the prepared action goes stale                             |

<Warning>
  **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.
</Warning>

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**:

| Stage                             | When                | Health checked? | Reason                                                      |
| --------------------------------- | ------------------- | :-------------: | ----------------------------------------------------------- |
| **Resolution** (Plan compilation) | binding is resolved |    No filter    | Health is transient — compile time and dispatch time differ |
| **Approval** (Movement approval)  | operator approves   |  **Main gate**  | Rejects approval if the binding's health is `down`          |
| **Dispatch** (Node execution)     | node runs           |     No check    | Already validated at Approval; failures go through recovery |

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

<Warning>
  **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.
</Warning>

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