nautechsystems/nautilus_trader · error

Swap quote output amount {amount} is not a positive output

Error message

Swap quote output amount {amount} is not a positive output

What it means

exact_output_amount extracts the output leg from a SwapQuote. Quote amounts are signed pool deltas (amount0/amount1 as I256, negative = tokens paid out to the trader, positive = tokens taken in), so the trader's output leg MUST be a negative delta. The bail fires when the selected amount is zero or positive, i.e. the quote says that token is not actually being received.

Source

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

        if !(raw % divisor).is_zero() {
            anyhow::bail!(
                "Order quantity {quantity} is not exactly representable in {decimals} base token decimals"
            );
        }
        Ok(raw / divisor)
    }
}

/// Extracts the positive output amount from an exact-input swap quote.
fn exact_output_amount(quote: &SwapQuote, zero_for_one: bool) -> anyhow::Result<U256> {
    let amount = if zero_for_one {
        quote.amount1
    } else {
        quote.amount0
    };

    if !amount.is_negative() {
        anyhow::bail!("Swap quote output amount {amount} is not a positive output");
    }
    Ok(amount.unsigned_abs())
}

/// Derives the minimum output accepted for the swap: the quoted output reduced by
/// `slippage_bps`, in exact integer arithmetic. Rejects a zero minimum, which would leave
/// the swap without slippage protection.
fn derive_min_amount_out(quoted_amount_out: U256, slippage_bps: u32) -> anyhow::Result<U256> {
    if slippage_bps >= BPS_DENOMINATOR {
        anyhow::bail!("Slippage {slippage_bps} bps must be below {BPS_DENOMINATOR}");
    }
    let min_amount_out = quoted_amount_out
        .checked_mul(U256::from(BPS_DENOMINATOR - slippage_bps))
        .and_then(|scaled| scaled.checked_div(U256::from(BPS_DENOMINATOR)))
        .ok_or_else(|| anyhow::anyhow!("Minimum output derivation overflow"))?;
    if min_amount_out.is_zero() {
        anyhow::bail!(
            "Derived minimum output is zero for quoted output {quoted_amount_out} at {slippage_bps} bps slippage"

View on GitHub (pinned to 2114cf6f76)

Solutions

  1. Recompute zero_for_one from the pool's token0/token1 ordering (token_in == token0 means zero_for_one = true) rather than from insertion order of the plan
  2. Re-request a fresh quote and check both signed legs before submitting
  3. If quotes are built in your own code, populate the output leg as a negative pool delta (e.g. -out_amount as I256)

Example fix

// before: direction assumed from plan field order
let zero_for_one = plan.token_in < plan.token_out; // wrong ordering basis

// after: derive from the pool's token0 ordering
let (token0, _) = pool.token0_token1();
let zero_for_one = plan.token_in == token0;
Defensive patterns

Strategy: validation

Validate before calling

// Validate the quote's output leg before execution
let (token0, _token1) = pool.token0_token1();
let zero_for_one = plan.token_in == token0;
let out_leg = if zero_for_one { quote.amount1 } else { quote.amount0 };
anyhow::ensure!(out_leg.is_negative(), "quote has no output leg; direction mismatch");

Type guard

fn quote_has_output(quote: &SwapQuote, zero_for_one: bool) -> bool {
    let amount = if zero_for_one { quote.amount1 } else { quote.amount0 };
    amount.is_negative()
}

Try / catch

// Direction/quote bugs are deterministic: catch, re-derive zero_for_one from token0
// ordering, re-quote, and retry once; never resubmit the same stale quote.
match execute_swap(&plan, &quote).await {
    Err(e) if e.to_string().contains("not a positive output") => {
        let quote = requote(&pool, &plan).await?;
        execute_swap(&plan, &quote).await
    }
    other => other,
}

Prevention

When it happens

Trigger: zero_for_one is computed inconsistently with the pool's token0/token1 ordering, so the wrong leg (the input leg) is read as the output; a degenerate or zero-output quote (e.g. amount_in too large for current reserves producing a zero/negative-direction result); passing a stale quote whose direction flags no longer match the pool state.

Common situations: Sorting token addresses differently than the pool definition when deriving zero_for_one; reusing quote structs built against a different pool orientation; edge tests with synthetic quotes that fill amount0/amount1 as unsigned values.

Related errors


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