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
- Inspect the row: SELECT id, status, purpose, client_order_id FROM execution_intent WHERE id = <id from error>.
- 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.
- Ensure the writer deployment is on the current EXECUTION_SCHEMA_VERSION so new intents always persist the client order id.
- 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
- Never hand-edit execution_intent rows; use the client's status-transition APIs.
- Run the NULL-column pre-flight query above before rolling out schema or version upgrades.
- Keep writer and reader deployments on the same EXECUTION_SCHEMA_VERSION.
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
- Persisted swap intent has no instrument ID
- Persisted swap intent has no pool address
- Persisted swap intent has no input amount
- Execution transaction hash {transaction_hash} was not found
- Replacement hash {transaction_hash} conflicts with another i
AI-assisted analysis of nautechsystems/nautilus_trader@2114cf6f76 (2026-08-21).
Data as JSON: /api/errors/e83c817952f015ff.
Report an issue: GitHub.