Swapping
https://superhero.com/defi/swap trades AEX-9 tokens against liquidity pools through the
AedexV2Router contract. You pick two tokens, the app finds a route, quotes it against the
router, and submits one of six swap entrypoints depending on whether native AE is on either side.
This is not the bonding curve. Buying a community token on its own page mints new supply against a fixed formula; swapping here moves tokens between pool reserves. The two prices for the same token are unrelated; see the comparison table.
The form
| Field | Behaviour |
|---|---|
| From / To | Token pickers. Native AE is selectable and handled specially. |
| Amount | Type into either side. Typing into From quotes exact-in; typing into To quotes exact-out. |
| Route | Shown once a path is found, including any intermediate hop. |
| Price impact | Computed from pool reserves, when the indexer supplied them. |
| Settings | Slippage tolerance and transaction deadline. |
Quotes are debounced 300 ms after you stop typing, and every quote carries a sequence number so a slow response from an earlier keystroke cannot overwrite a newer one. Below the form sit a candlestick chart for the selected pair and a Recent Activity list of your own swaps, wraps and unwraps.
Slippage and deadline
Open Settings on the swap card.
| Setting | Default | Presets | Allowed range | Stored as |
|---|---|---|---|---|
| Slippage tolerance | 5% | 0.1, 0.5, 1.0 | 0 to 50 | dex:slippage in local storage |
| Transaction deadline | 30 minutes | 10, 20, 30 | 1 to 60 | dex:deadline in local storage |
The 5% default applies to every swap until you change it. Set it deliberately: on a thin pool it is the difference between a bad fill and a rejected transaction.
- Slippage sets the protective bound, not the price. For an exact-in swap the app computes
amountOutMinby subtracting your tolerance from the quoted output. For exact-out it queriesget_amounts_in, adds the tolerance, and passes the result asamountInMax. If the pool has moved past that bound by the time the transaction executes, the router reverts and you keep your tokens. - Deadline becomes an absolute timestamp:
Date.now()plus your minutes, in milliseconds since epoch. Every router entrypoint opens withrequire(deadline >= Chain.timestamp, "AedexV2Router: EXPIRED"), and Sophia'sChain.timestampis unix time in milliseconds.
How a route is found
- AE to WAE, or WAE to AE, short-circuits. No route is computed; it becomes a wrap or unwrap at 1:1 with zero price impact. See Wrapped AE.
- Ask the DEX indexer.
GET /api/dex/swap-routes/<from_token>/<to_token>returns candidate routes, each a list of pairs with their reserves. Native AE is substituted with the WAE address before the lookup, and the two addresses are sorted before being sent. - Prefer synchronized routes. A pair is
synchronizedwhen both of its reserves are greater than zero. Routes where every pair is synchronized are used if any exist; otherwise all candidates are considered. A route whose derived path ends at the requested output token wins. - Fall back to the chain. If the indexer is unreachable or returns nothing, the app asks the
factory for a direct pair via
get_pair. If there is none, and neither token is WAE, it tries a two-hop path through WAE. - No path, no quote. If all of that fails, the quote errors with "No route found".
Routes are at most two pairs long: direct, or through a single intermediate token. The on-chain fallback has the same ceiling: direct, or one hop through WAE. If two tokens are only reachable through three pools, the swap form reports no route; make the intermediate swap yourself.
How the quote is produced
Two calculations run, for different purposes.
- Reserve math gives the spot ratio across the route and powers the price-impact and
liquidity checks. The ratio is the product of
reserveOut / reserveInacross each hop, shifted by the decimal difference between the two tokens. - The router gives the number displayed:
get_amounts_out(amountIn, path)for exact-in,get_amounts_in(amountOut, path)for exact-out. When the router answers, its figure replaces the reserve-derived estimate.
Price impact is computed from reserves alone, by simulating the trade hop by hop under the constant-product invariant:
For each hop, with reserves (rIn, rOut) and input amount a:
k = rIn × rOut
rIn' = rIn + a
rOut' = k / rIn'
received = rOut − rOut' (feeds the next hop as its input)
marketPrice = 1 / (product of rOut/rIn across the route)
executionPrice = amountIn / receivedOut
priceImpact = (executionPrice − marketPrice) × −100 / executionPriceThe simulation uses the raw constant product without the swap fee, so the figure is a pure depth measure, not a full cost estimate. It is only available when the indexer supplied reserves; if the route came from the on-chain fallback, no price impact is shown.
Liquidity ceiling. The app also checks your input against the first hop's input-side reserve. If you are trying to spend more than that reserve holds, it flags the swap as exceeding available liquidity and reports the maximum available.
Fees
The swap fee is 0.3% of the input amount. It is a constant in the contracts, the same for every pair, and it goes to the pool's liquidity providers, not to Superhero.
The router quotes with it:
private function apply_fee(amount: int): int = amount*997 // the fee is 0.3%
function get_amount_out(amount_in, reserve_in, reserve_out) =
let amount_in_with_fee = apply_fee(amount_in)
let numerator = amount_in_with_fee*reserve_out
let denominator = reserve_in*1000 + amount_in_with_fee
numerator / denominator997 of every 1000 input units buy output; the remaining 3 stay in the pool. get_amount_in
inverts the same ratio and adds one aetto of rounding headroom.
The pair enforces it inside swap, independently of the router's arithmetic:
let balance0_adjusted = balance0*1000 - amount0_in*3
let balance1_adjusted = balance1*1000 - amount1_in*3
require(
balance0_adjusted*balance1_adjusted >= reserve0*reserve1*1000000,
"AedexV2Pair: INSUFFICIENT_BALANCE"
)The post-trade product of reserves, with 0.3% of each input deducted, must be at least the
pre-trade product; a swap that tried to pay less fee aborts with
AedexV2Pair: INSUFFICIENT_BALANCE. There is no per-pair fee setting: the factory's
create_pair takes only min_liquidity and debug_time.
A sixth of the fee can be diverted to a protocol-fee collector if the factory's fee_to_setter
switches it on. That switch is off by default and is described under
Where the 0.3% goes, because the fee is minted as LP tokens
rather than deducted from your trade.
Because the fee is inside the invariant, it is already reflected in the router's
get_amounts_out answer and is not a separate line item. On top of it you pay the ordinary
æternity transaction fee, plus a second transaction fee if an allowance approval is needed.
Approvals
Native AE needs no allowance. Any AEX-9 input does.
Before an exact-in swap with a token input, the app reads your current allowance for the router
and raises it if it is short. The router's ct_ address is converted to its ak_ form for the
allowance record, because AEX-9 allowances are keyed by account address. Then:
- No allowance record at all →
create_allowance(router, needed) - A record that is too small →
change_allowance(router, needed − current)
That is a separate transaction you sign; the swap card shows a two-step indicator ("Approve
token", then "Execute swap"). For an exact-out swap the approval is deferred until after
get_amounts_in has run, so the allowance covers the slippage-padded maximum input rather than
the nominal amount.
The six swap entrypoints
Which router entrypoint is called depends only on whether native AE is the input, the output, or neither, and on whether you fixed the input or the output amount.
| Input | Output | Exact | Entrypoint |
|---|---|---|---|
| Token | Token | in | swap_exact_tokens_for_tokens(amount_in, amount_out_min, path, to, deadline, callback_opt) |
| Token | Token | out | swap_tokens_for_exact_tokens(amount_out, amount_in_max, path, to, deadline, callback_opt) |
| AE | Token | in | swap_exact_ae_for_tokens(amount_out_min, path, to, deadline, callback_opt), with the AE attached as call value |
| AE | Token | out | swap_ae_for_exact_tokens(amount_out, path, to, deadline, callback_opt), with the padded maximum attached as call value |
| Token | AE | in | swap_exact_tokens_for_ae(amount_in, amount_out_min, path, to, deadline, callback_opt) |
| Token | AE | out | swap_tokens_for_exact_ae(amount_out, amount_in_max, path, to, deadline, callback_opt) |
path is a list of token contracts, first the input and last the output; to is your own
address. An AE-to-AE route is rejected as invalid.
callback_opt is option(IAedexV2Callback): an optional contract the pair calls back into, via
aedex_v2_call(sender, amount0_out, amount1_out), after it has optimistically transferred the
output but before it checks the invariant. That is the flash-swap hook: a contract can receive
the output first and supply the input during the callback. Superhero always passes None.
The AE-side entrypoints do the wrapping for you. swap_exact_ae_for_tokens and
swap_ae_for_exact_tokens require the first element of path to be the WAE address and call
wae.deposit_to(first_pair, value = ...); swap_exact_tokens_for_ae and
swap_tokens_for_exact_ae require the last element to be WAE, swap into the router itself, then
wae.withdraw(amount) and Chain.spend(to, amount). Both exact-out AE-input forms refund the
unspent AE to you in the same transaction.
Router and factory addresses for both networks are in the deployment table.
What can go wrong
| Symptom | Cause |
|---|---|
| "No route found" | No pool connects the two tokens, directly or through WAE |
| Quote shows but the swap reverts | The pool moved past your slippage bound, or the deadline passed |
| A large price impact warning | The pool is thin relative to your trade size; split it or pick another route |
| "Exceeds available liquidity" | Your input is larger than the first hop's input reserve |
| Two wallet prompts for one swap | The first is the AEX-9 allowance approval |
| Insufficient balance error | Checked client-side before submission, against the input token's balance |
When the revert comes from the chain, the abort string names the check that failed:
| Abort string | What it means |
|---|---|
AedexV2Router: EXPIRED | Your deadline passed before the transaction was mined |
AedexV2Router: INSUFFICIENT_OUTPUT_AMOUNT | An exact-in swap would have returned less than amount_out_min |
AedexV2Router: EXCESSIVE_INPUT_AMOUNT | An exact-out swap would have cost more than amount_in_max |
AedexV2Library: NO_PAIR_FOUND | A hop in the path has no pair contract |
AedexV2Library: INSUFFICIENT_LIQUIDITY | A hop's reserves are zero: the pool is empty or one-sided |
AedexV2Router: INVALID_PATH_FIRST / INVALID_PATH_LAST | An AE entrypoint was called with a path that does not start or end at WAE |
AedexV2Pair: INSUFFICIENT_BALANCE | The pair's own invariant check failed: the input never arrived, or the fee was short |
The first three are the ordinary ones and mean the market moved. The last is a contract-level safety net you should not normally reach through the app.
Swap from a script
The agent skill's superhero-token-swap.mjs trades on the bonding curve, not the DEX. Its buy,
sell and price commands call the token sale contract. To swap on the AMM programmatically,
initialise the router with the AedexV2Router ACI from dex-contracts-v2 and call the
entrypoints in the table above. The router also exposes the pricing library as read-only
entrypoints, so you can quote without holding anything:
| Entrypoint | Use |
|---|---|
get_amounts_out(amount_in, path) | Exact-in quote across the whole route |
get_amounts_in(amount_out, path) | Exact-out quote across the whole route |
get_amount_out(amount_in, reserve_in, reserve_out) | One hop, from reserves you already have |
get_amount_in(amount_out, reserve_in, reserve_out) | One hop, inverted |
quote(amount_a, reserve_a, reserve_b) | The fee-free ratio: for liquidity math, not for swaps |
quote is amount_a * reserve_b / reserve_a with no fee applied, because it prices the second
side of a liquidity deposit. Do not use it to predict a swap.
Related
- Liquidity Pools: the other side of every swap, and where the 0.3% goes.
- Wrapped AE: the 1:1 shortcut the swap form takes for AE and WAE.
- Exploring the DEX: finding a pair and checking its depth before you trade.
- Bonding curve fees and slippage: the equivalent mechanics on the curve side.
- DEX API: the
/api/dex/*endpoints, including swap routes. - dex-contracts-v2 (opens in a new tab): the router, factory and pair contracts, with their ACIs.