DEX swap preflight checks can pass and still leave you with a failed or different execution later. Between simulation and inclusion, pool state, balances, allowances and fees can change. Your application needs to treat the simulation as evidence about a moment, not a promise about the future.
We’ll use an illustrative EVM swap flow. Let’s check the proposal first, then put the protections that matter into the transaction itself and inspect what actually happened afterwards.
Record the quote and its context
Store chain ID, router and token addresses, input amount, quoted output, reference block, quote time and expiry. Use integer base units. Token symbols and decimal display strings aren’t sufficient identities or arithmetic inputs.
Record the route and the policy version used to create it. If the route changes after the user approves the proposal, decide whether that change is within the approval or needs a new confirmation.
Don’t let the model invent token addresses or a recipient. Resolve those from trusted application state and validate them under the operation’s policy.
Check balances and permissions before signing
Read the current input balance and the relevant allowance for the actual spender. Check native asset balance for the expected fee needs under your transaction policy.
An approval transaction may itself be pending. Don’t treat submitting it as evidence that the allowance is already available at the block used for simulation. Track its receipt and chain state before the dependent action.
Use coherent block references for related preflight reads where appropriate. Even then, later inclusion can see different state, which is why transaction-level bounds matter.
Put the limit in the transaction
An output floor expressed only in your frontend or agent prompt can’t protect an on-chain execution. Use the swap router’s actual supported parameters and semantics.
Uniswap’s single-swap guide discusses minimum output protection for the demonstrated router flow. The exact interface depends on the router and version you deploy. Uniswap swap reference.
Calculate integer bounds carefully and validate the configured slippage range. A mistaken unit conversion can remove the protection or make every transaction fail.
Build a preflight policy test
The example checks a quote, allowance and minimum output using integer arithmetic. It then changes the observed output and verifies that execution below the floor is rejected in the simulated contract rule.
import assert from 'node:assert/strict';
const quote={input:1000n,output:500n,expiresAt:100,slippageBps:100n};
function floor(q){
if(q.slippageBps<0n||q.slippageBps>10000n)throw new Error('Invalid basis points');
return q.output*(10000n-q.slippageBps)/10000n;
}
function preflight(q,{now,balance,allowance}){
if(now>=q.expiresAt)throw new Error('Expired quote');
if(balance<q.input||allowance<q.input)throw new Error('Insufficient balance or allowance');
return floor(q);
}
const minimum=preflight(quote,{now:50,balance:2000n,allowance:1000n});
assert.equal(minimum,495n);
const execute=actual=>{if(actual<minimum)throw new Error('Output below minimum');return actual;};
assert.throws(()=>execute(490n));assert.equal(execute(498n),498n);
assert.throws(()=>preflight(quote,{now:101,balance:2000n,allowance:1000n}));
assert.throws(()=>preflight(quote,{now:50,balance:2000n,allowance:0n}));
console.log('PASS: integer output bound, quote expiry and required allowance checks');
The fixture doesn’t execute a real router or assess a trading strategy. It tests the boundary conditions of the local policy and the distinction between a preflight estimate and a later result.
Understand what simulation establishes
A successful simulation shows that the call could succeed against the simulated state and assumptions. It doesn’t establish which transactions will execute before it or what state will exist at inclusion.
Keep simulation block, request fields and result for investigation. If the signed transaction differs from what was simulated, the comparison loses much of its value.
Use a short enough proposal lifetime for the product’s needs, but don’t promise inclusion before expiry. A transaction can remain pending, and an expired deadline can cause a revert rather than safely disappear from every queue.
Reconcile actual execution
After inclusion, inspect receipt status and the relevant token movements or verified balance changes for the supported assets. A transaction hash or successful broadcast response isn’t proof of the intended swap.
Account for token behaviour your application supports. Fee-on-transfer, rebasing or nonstandard contracts can make simple balance assumptions wrong. Restrict supported assets or implement their semantics deliberately.
Record actual input, output, fees and recipient under the operation. Don’t report the quoted output as the amount received merely because that was the last number the UI displayed.
Keep replacements inside approval
If a transaction is delayed, a fee replacement may be permitted under a cap. A changed output floor, route or recipient may be a different commercial decision. Record what the original approval allowed.
Never respond to an uncertain broadcast by creating an unrelated swap with a new nonce. Reconcile the original attempt and any replacements under one business operation.
For a confirmed revert, classify the cause before proposing another attempt. A fresh quote may be needed, and the user may no longer want the trade under the new conditions.
Test state changes between steps
Change allowance, balance and pool output after preflight. Delay inclusion until the quote or transaction deadline expires. Confirm that the local service and transaction bounds reject the unsupported cases.
Then test a successful receipt with an unexpected asset movement and a reverted receipt after successful broadcast. The customer-facing result should come from actual execution evidence.
Use a controlled local fork or development deployment for contract integration tests, with no assumption that the fixture here replaces them. Record the exact router and contract versions.
Start by storing the quote context and putting the output bound into the real transaction interface. Then reconcile what executed. That closes the gap between an optimistic preview and an honest account of the result.
Run the example and adapt it
Save the code as 30-swap-preflight-reconciliation.mjs and run it with Node.js 24 or later:
node 30-swap-preflight-reconciliation.mjs
The code above includes assertions you can run locally. It tests the local decision logic with controlled inputs; it doesn’t connect to a live provider or establish production performance. Keep that boundary when adapting it to your application.

