nautechsystems/nautilus_trader · error · anyhow::Error

Restored pool {instrument_id} has no fee

Error message

Restored pool {instrument_id} has no fee

What it means

restore_swap_plan (crates/adapters/blockchain/src/execution/client.rs:878) reads pool.fee as a U24 for the restored pool; when the cached Pool for the instrument has fee: None the reconciliation aborts. The fee is required to rebuild swap calldata and quote-token math for the restored instrument. A fee-less pool means the pool was registered or restored without its fee parameter - incomplete instrument metadata rather than an on-chain anomaly.

Source

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

            intent
                .pool_address
                .as_deref()
                .ok_or_else(|| anyhow::anyhow!("Persisted swap intent has no pool address"))?,
        )?;
        anyhow::ensure!(
            pool.address == pool_address,
            "Persisted pool {pool_address} does not match restored pool {}",
            pool.address
        );
        let amount_in = U256::from_str(
            intent
                .amount_in
                .as_deref()
                .ok_or_else(|| anyhow::anyhow!("Persisted swap intent has no input amount"))?,
        )?;
        let fee = U24::try_from(
            pool.fee
                .ok_or_else(|| anyhow::anyhow!("Restored pool {instrument_id} has no fee"))?,
        )?;
        let quote_token = pool.get_quote_token();
        let quote_currency = Currency::new_checked(
            &quote_token.symbol,
            quote_token.decimals,
            0,
            &quote_token.name,
            CurrencyType::Crypto,
        )?;
        let token_in = pool.get_base_token().address;
        let token_out = quote_token.address;

        Ok(SwapPlan {
            order,
            quote_currency,
            pool,
            instrument_id,
            pool_address,

View on GitHub (pinned to 2114cf6f76)

Solutions

  1. Check how the pool for the failing instrument got registered and ensure its fee tier (e.g. 3000 for 0.3%) is provided.
  2. Refresh the pool from an authoritative source so fee is populated, then reconnect.
  3. If the pool row in Postgres lacks the fee, update it through the ingestion source (not by hand-editing live rows if avoidable).
  4. Resolve or complete the active intent once pool metadata is correct.
Defensive patterns

Strategy: try-catch

Validate before calling

// Rust: verify instrument pools carry a fee tier before booting reconciliation
// (resolve_pool must produce pools with Some(fee), e.g. 500/3000/10000 for Uniswap V3)
for instrument in active_swap_instruments() {
    let pool = client.resolve_pool(&instrument)?;
    anyhow::ensure!(pool.fee.is_some(), "pool for {instrument} lacks a fee tier");
}

Type guard

fn is_pool_missing_fee(e: &anyhow::Error) -> bool {
    e.to_string().contains("has no fee")
}

Try / catch

if let Err(e) = client.connect().await {
    if is_pool_missing_fee(&e) {
        // instrument metadata incomplete: refresh pool registration with fee tiers
        log::error!("pool registered without fee tier; fix ingestion then reconnect: {e}");
    }
    return Err(e);
}

Prevention

When it happens

Trigger: The pool for the intent's instrument exists in the cache but was registered without a fee tier (custom registration path or data source that omits fee); a persisted pool row lacking the fee restored into memory; reconciling a swap intent against such a pool at connect().

Common situations: Custom pool ingestion pipelines that skip the fee field; pools added through config with only an address; database pool rows written before fee capture existed.

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/4b7b0b1676d761a3. Report an issue: GitHub.