nautechsystems/nautilus_trader · error · anyhow::Error

Execution intent {intent_id} is not prepared for nonce {nonc

Error message

Execution intent {intent_id} is not prepared for nonce {nonce}

What it means

The guarded UPDATE in assign_execution_intent_nonce matched zero rows: the intent is not in status 'prepared', or it already owns a different nonce (the nonce IS NULL OR nonce = $2 predicate failed). This is the API failing closed against binding two different nonces to one intent, which would desynchronize the signer's on-chain nonce sequence.

Source

Thrown at crates/adapters/blockchain/src/cache/database.rs:3417

        nonce: u64,
    ) -> anyhow::Result<()> {
        let nonce_db = i64::try_from(nonce)
            .with_context(|| format!("Execution nonce {nonce} exceeds PostgreSQL BIGINT"))?;
        let result = sqlx::query(
            "
            UPDATE execution_intent
            SET nonce = $2, updated_at = NOW()
            WHERE id = $1
              AND status = 'prepared'
              AND (nonce IS NULL OR nonce = $2)
            ",
        )
        .bind(intent_id)
        .bind(nonce_db)
        .execute(&self.pool)
        .await
        .map_err(|e| anyhow::anyhow!("Failed to assign nonce {nonce} to execution intent: {e}"))?;
        anyhow::ensure!(
            result.rows_affected() == 1,
            "Execution intent {intent_id} is not prepared for nonce {nonce}"
        );
        Ok(())
    }

    /// Releases an intent when no broadcast attempt can have occurred.
    ///
    /// # Errors
    ///
    /// Returns an error if the intent advanced to broadcast or persistence fails.
    pub async fn mark_execution_intent_recoverable(&self, intent_id: i64) -> anyhow::Result<()> {
        let mut transaction = self.pool.begin().await.map_err(|e| {
            anyhow::anyhow!("Failed to start recoverable execution transition: {e}")
        })?;
        let current_status = sqlx::query_scalar::<_, String>(
            "SELECT status FROM execution_intent WHERE id = $1 FOR UPDATE",
        )

View on GitHub (pinned to 2114cf6f76)

Solutions

  1. Load the intent first and verify status is 'prepared' and nonce is NULL or equal before assigning
  2. If a different nonce is already stored, stop and reconcile against the node's transaction count - do not force the UPDATE
  3. If status advanced past 'prepared', resume from the persisted state instead of re-assigning
  4. Pass the identical nonce on retries so the assignment stays idempotent

Example fix

// before: assign whatever nonce was just fetched
let nonce = provider.transaction_count(wallet).await?;
db.assign_execution_intent_nonce(intent_id, nonce).await?;

// after: keep the persisted nonce authoritative
let nonce = match row.nonce {
    Some(stored) => stored as u64,
    None => provider.transaction_count(wallet).await?,
};
db.assign_execution_intent_nonce(row.id, nonce).await?;
Defensive patterns

Strategy: validation

Validate before calling

// Verify ownership preconditions before assigning
let row: Option<(String, Option<i64>)> = sqlx::query_as(
    "SELECT status, nonce FROM execution_intent WHERE id = $1",
)
.bind(intent_id)
.fetch_optional(&pool)
.await?;
match row {
    Some((status, nonce)) if status == "prepared" && nonce.map_or(true, |n| n == nonce_db) => {
        // safe to assign
    }
    _ => { /* reconcile instead of assigning */ }
}

Type guard

fn intent_accepts_nonce(row: &ExecutionIntentRow, nonce: u64) -> bool {
    row.status.as_str() == "prepared"
        && row.nonce.map_or(true, |stored| stored == nonce as i64)
}

Try / catch

match db.assign_execution_intent_nonce(intent_id, nonce).await {
    Err(e) if e.to_string().contains("is not prepared for nonce") => {
        // load the intent and reconcile: resume from its persisted nonce or halt for manual review
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling assign after the intent advanced to 'signed'; a crash-recovery flow re-derives a nonce from the node and assigns it to an intent that already stored one; passing the wrong intent_id.

Common situations: Restart between reserve and assign where nonce management computed a fresh value; concurrent executors targeting the same intent; drift between the node's transaction count and the persisted nonce.

Related errors


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