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
- 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
- Re-request a fresh quote and check both signed legs before submitting
- 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, "e).await {
Err(e) if e.to_string().contains("not a positive output") => {
let quote = requote(&pool, &plan).await?;
execute_swap(&plan, "e).await
}
other => other,
} Prevention
- Derive zero_for_one only from the pool's token0/token1 ordering
- Simulate quotes with a known small swap in tests and assert the output leg sign
- Discard quotes older than a few blocks and re-quote before submission
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
- No deployed bytecode at {description} address {address}
- Router allowance {allowance} is below the swap amount {} for
- Input token {} balance {balance} is below the swap amount {}
- Order quantity must be positive
- Slippage {slippage_bps} bps must be below {BPS_DENOMINATOR}
AI-assisted analysis of nautechsystems/nautilus_trader@2114cf6f76 (2026-08-21).
Data as JSON: /api/errors/1cffea0dd520a76d.
Report an issue: GitHub.