{"record":{"id":"cd94e470064f896c","repo":"nautechsystems/nautilus_trader","slug":"no-quote-spend-ceiling-for-buy-token-pair","errorCode":null,"errorMessage":"No quote spend ceiling for BUY token pair {} -> {}","messagePattern":"No quote spend ceiling for BUY token pair (.+?) -> (.+?)","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/adapters/blockchain/src/execution/client.rs","lineNumber":4094,"sourceCode":"    }\n}\n\nfn verified_swap_amounts(plan: &SwapPlan, quote: UniswapV3Quote) -> anyhow::Result<(U256, U256)> {\n    anyhow::ensure!(\n        !quote.amount.is_zero(),\n        \"Independent swap quote returned zero\"\n    );\n    let base_amount =\n        quantity_to_raw_amount(plan.order.quantity(), plan.pool.get_base_token().decimals)?;\n    let slippage_bps = plan.slippage_bps;\n    match plan.order.order_side() {\n        OrderSide::Sell => Ok((\n            base_amount,\n            derive_min_amount_out(quote.amount, slippage_bps)?,\n        )),\n        OrderSide::Buy => {\n            let ceiling = plan.quote_spend_ceiling.ok_or_else(|| {\n                anyhow::anyhow!(\n                    \"No quote spend ceiling for BUY token pair {} -> {}\",\n                    plan.token_in,\n                    plan.token_out\n                )\n            })?;\n            anyhow::ensure!(\n                quote.amount <= ceiling.max_amount,\n                \"BUY quote amount {} exceeds the configured quote-spend maximum {} for {} -> {}\",\n                quote.amount,\n                ceiling.max_amount,\n                plan.token_in,\n                plan.token_out\n            );\n            Ok((\n                quote.amount,\n                derive_min_amount_out(base_amount, slippage_bps)?,\n            ))\n        }","sourceCodeStart":4076,"sourceCodeEnd":4112,"githubUrl":"https://github.com/nautechsystems/nautilus_trader/blob/18893faf8b356be3320add8de2f861b0b647cf06/crates/adapters/blockchain/src/execution/client.rs#L4076-L4112","documentation":"In `verified_swap_amounts` (crates/adapters/blockchain/src/execution/client.rs:4094), BUY orders route tokens with the swap's input amount coming from the quote, so a hard ceiling on how much quote token may be spent is mandatory. This error means the SwapPlan for a BUY order has `quote_spend_ceiling == None`, so the client cannot bound the spend and aborts instead of executing an unbounded trade.","triggerScenarios":"Constructing a SwapPlan with order_side()==Buy but leaving quote_spend_ceiling unset, then running it through quote verification / verified_swap_amounts.","commonSituations":"An adapter or strategy builder that sets the ceiling only for SELL plans; a config loader that omits the max-spend field; refactored code paths that construct SwapPlan manually without the ceiling; an upgrade where quote_spend_ceiling was newly introduced and old callers were not migrated.","solutions":["Set quote_spend_ceiling (with max_amount in quote-token raw units) on the SwapPlan for every BUY order.","If building plans from config, ensure the quote-spend maximum is loaded and validated for buy-side orders before plan creation.","Only use OrderSide::Buy when the caller can supply a spend ceiling; otherwise convert the intent to a Sell-side formulation.","Reject BUY plans without a ceiling at plan construction with a clear error instead of at quote time."],"exampleFix":"// before\nlet plan = SwapPlan {\n    order,\n    token_in,\n    token_out,\n    quote_spend_ceiling: None,\n    ..plan_base\n};\n// after\nlet plan = SwapPlan {\n    order,\n    token_in,\n    token_out,\n    quote_spend_ceiling: Some(QuoteSpendCeiling { max_amount: max_quote_spend_raw }),\n    ..plan_base\n};","handlingStrategy":"validation","validationCode":"if plan.order.order_side() == OrderSide::Buy && plan.quote_spend_ceiling.is_none() {\n    return Err(\"BUY plan requires quote_spend_ceiling\");\n}","typeGuard":"fn has_buy_ceiling(plan: &SwapPlan) -> bool {\n    plan.order.order_side() != OrderSide::Buy || plan.quote_spend_ceiling.is_some()\n}","tryCatchPattern":"match client.execute_swap(plan).await {\n    Err(e) if e.to_string().contains(\"No quote spend ceiling\") => {\n        // rebuild the plan with a ceiling and retry once\n    }\n    other => other?,\n}","preventionTips":["Validate at SwapPlan construction that every BUY plan carries a spend ceiling.","Load the quote-spend maximum from config and fail fast at startup if absent.","Add a builder method that makes the ceiling mandatory for buy-side plans.","Cover both order sides in unit tests for plan construction."],"tags":["blockchain","configuration","buy-order","risk-limit"],"backgroundTag":"missing-required-config-field","analyzedSha":"18893faf8b356be3320add8de2f861b0b647cf06","analyzedAt":"2026-09-08T20:49:34.690Z","contentChangedAt":"2026-09-08T20:49:34.690Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}