nautechsystems/nautilus_trader · error · anyhow::Error

Persisted swap intent has no client order ID

Error message

Persisted swap intent has no client order ID

What it means

During connect(), reconcile_unresolved_execution rebuilds any active swap intent via restore_swap_plan (crates/adapters/blockchain/src/execution/client.rs:822), which requires the persisted intent's client_order_id column to re-associate the on-chain transaction with the Nautilus order that spawned it. A NULL client_order_id on a swap intent is a durable-state integrity violation: the reconciliation cannot look up the order in the restored cache, so it refuses rather than guessing. Swap intents are always written with the originating ClientOrderId, so this state implies an old-schema row, a partial write, or manual database edits.

Source

Thrown at crates/adapters/blockchain/src/execution/client.rs:822

            wallet_balance: Arc::clone(&self.wallet_balance),
            account_id: self.core.account_id,
            wallet_address: self.wallet_address,
            chain_id: self.chain.chain_id,
            max_fee_per_gas_wei: self.config.max_fee_per_gas_wei,
            base_fee_buffer_bps: self.config.base_fee_buffer_bps,
            gas_limit: self.config.gas_limit,
            gas_buffer_bps: self.config.gas_buffer_bps,
            receipt_timeout: receipt_timeout(self.transaction_limits.receipt_timeout_secs),
            receipt_max_polls: receipt_max_polls(self.transaction_limits.receipt_timeout_secs),
        })
    }

    fn restore_swap_plan(&self, intent: &ExecutionIntentRow) -> anyhow::Result<SwapPlan> {
        let client_order_id = ClientOrderId::new_checked(
            intent
                .client_order_id
                .as_deref()
                .ok_or_else(|| anyhow::anyhow!("Persisted swap intent has no client order ID"))?,
        )?;
        let order = self
            .core
            .cache()
            .try_order_owned(&client_order_id)
            .with_context(|| {
                format!(
                    "Cannot reconcile swap intent {} because order {client_order_id} is not restored",
                    intent.id
                )
            })?;
        let instrument_id = InstrumentId::from_str(
            intent
                .instrument_id
                .as_deref()
                .ok_or_else(|| anyhow::anyhow!("Persisted swap intent has no instrument ID"))?,
        )?;
        anyhow::ensure!(

View on GitHub (pinned to 2114cf6f76)

Solutions

  1. Inspect the row: SELECT id, status, purpose, client_order_id FROM execution_intent WHERE id = <id from error>.
  2. Verify the on-chain outcome of the intent's transaction hash, then resolve the row - mark it recoverable/terminal or delete it - so subsequent connects can proceed.
  3. Ensure the writer deployment is on the current EXECUTION_SCHEMA_VERSION so new intents always persist the client order id.
  4. Never hand-edit intent columns; use the client's own status-transition paths.

Example fix

-- before: poisoned swap intent blocks every connect()
SELECT client_order_id FROM execution_intent WHERE id = 42;
-- client_order_id | NULL

-- after: verify on-chain state, then clear the intent for reconciliation
SELECT transaction_hash FROM execution_transaction_hash WHERE intent_id = 42; -- check on explorer
UPDATE execution_intent SET status = 'recoverable' WHERE id = 42;
Defensive patterns

Strategy: try-catch

Validate before calling

-- Pre-flight: find active swap intents with a NULL client_order_id before restarting
SELECT id, status, client_order_id
FROM execution_intent
WHERE status IN ('prepared', 'signed', 'submitted')
  AND purpose = 'swap'
  AND client_order_id IS NULL;

Type guard

fn is_missing_client_order_id(e: &anyhow::Error) -> bool {
    e.to_string().contains("Persisted swap intent has no client order ID")
}

Try / catch

if let Err(e) = client.connect().await {
    if is_missing_client_order_id(&e) {
        // durable-state corruption: halt boot, inspect the intent row, resolve on-chain first
        log::error!("poisoned swap intent blocks reconciliation: {e}");
        // operator: verify the intent's tx hashes on-chain, then mark resolved/recoverable
    }
    return Err(e);
}

Prevention

When it happens

Trigger: An execution_intent row with purpose=swap and NULL client_order_id exists while connect() runs reconciliation; rows written by an older writer version before client_order_id capture was mandatory; someone manually inserted or edited intent rows; a torn write left the row half-populated.

Common situations: Upgrading a deployment that carries old intent rows forward; DBAs trimming 'unused' columns; restoring a Postgres backup across schema versions.

Understand the failure class

Background: "Missing required field" and "field is required" errors: why libraries reject payloads that omit mandatory fields — this error's family across 20 libraries.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@2114cf6f76 (2026-08-21). Data as JSON: /api/errors/e83c817952f015ff. Report an issue: GitHub.