Executing a route
This page is also Markdown, for agents: /dex-aggregator/guides/executing-routes.md, and /llms.txt lists every page.
Every quote has execution_instructions, an ordered list of steps that perform the swap. You don’t need to know anything about the DEX contracts: sign what you get, in order.
Instruction format
Section titled “Instruction format”Right now there’s only one kind of instruction, NearTransaction:
{ "NearTransaction": { "receiver_id": "wrap.near", "actions": [ { "FunctionCall": { "method_name": "near_deposit", "args": "e30=", "gas": 5000000000000, "deposit": "1000000000000000000000000" } } ], "is_fee_transaction": false }}| Field | Type | Meaning |
|---|---|---|
receiver_id |
string | The contract the transaction is sent to |
actions |
array | Actions of the transaction, in order |
is_fee_transaction |
boolean | Pays referral fees after the swap, see below |
The transaction is signed by the trader, the account you passed as trader_account_id.
Order matters
Section titled “Order matters”Send the transactions one at a time, in the given order, and wait for each to succeed before sending the next. Later steps depend on earlier ones: a storage deposit has to land before a token can be received, wNEAR has to be unwrapped before native NEAR can be swapped, and so on. If a transaction fails, stop.
Fee transactions
Section titled “Fee transactions”Routes with app fees on Rhea and Rhea DCL end with transactions that have is_fee_transaction: true, because Rhea’s own referral system is very limited and permissioned. The fee transactions send the fees in the input token, after a storage_deposit for each account that isn’t registered with the token yet, and always come last, after any conversion of the output.
Send them only if the swap went through. A swap that fails on chain still lets the transaction carrying it succeed, so once the transactions before them have executed, check the outcome with the quote’s fee_amount, and send the fee transactions, in order, if it says charge_fee: true. The swap is done at that point, so you can show it as complete without waiting for them.
Signing
Section titled “Signing”In the browser, with NEAR Connect:
import { NearConnector } from '@hot-labs/near-connect';
const connector = new NearConnector({ network: 'mainnet' });await connector.connect();const wallet = await connector.wallet();
const quote = quotes[0];if (!quote) throw new Error('No route found');for (const { NearTransaction: tx } of quote.execution_instructions) { // App fees: send these only once /route/outcome says charge_fee is true if (tx.is_fee_transaction) break; const { status } = await wallet.signAndSendTransaction({ receiverId: tx.receiver_id, actions: tx.actions.map(({ FunctionCall: call }) => ({ type: 'FunctionCall', params: { methodName: call.method_name, args: JSON.parse(atob(call.args)), // args are base64-encoded JSON gas: String(call.gas), deposit: call.deposit, }, })), }); if (typeof status === 'object' && status.Failure) { throw new Error(`Transaction failed: ${JSON.stringify(status.Failure)}`); } // ft_transfer_call returns the amount the DEX used: "0" if it refunded everything const transferCall = tx.actions.at(-1)?.FunctionCall.method_name === 'ft_transfer_call'; if (transferCall && typeof status === 'object' && status.SuccessValue === btoa('"0"')) { throw new Error('Swap refunded'); }}On a server, with near-api-js:
import { Account, actions } from 'near-api-js';
const account = new Account('alice.near', 'https://free.rpc.fastnear.com', 'ed25519:<private key>');
for (const { NearTransaction: tx } of quote.execution_instructions) { // App fees: send these only once /route/outcome says charge_fee is true if (tx.is_fee_transaction) break; // Throws if the transaction fails const { status } = await account.signAndSendTransaction({ receiverId: tx.receiver_id, actions: tx.actions.map(({ FunctionCall: call }) => actions.functionCall( call.method_name, Buffer.from(call.args, 'base64'), BigInt(call.gas), BigInt(call.deposit), ), ), }); // ft_transfer_call returns the amount the DEX used: "0" if it refunded everything const transferCall = tx.actions.at(-1)?.FunctionCall.method_name === 'ft_transfer_call'; if (transferCall && typeof status === 'object' && 'SuccessValue' in status && status.SuccessValue === btoa('"0"')) { throw new Error('Swap refunded'); }}With near-kit, using the Quote types from the Quickstart:
use base64::prelude::{BASE64_STANDARD, Engine as _};use near_kit::{Gas, Near, protocol::Action};
let near = Near::mainnet() .credentials(std::env::var("NEAR_PRIVATE_KEY")?, "alice.near")? .build();
let quote = quotes .into_iter() .next() .ok_or_else(|| anyhow::anyhow!("No route found"))?;for ExecutionInstruction::NearTransaction { receiver_id, actions, is_fee_transaction,} in quote.execution_instructions{ // App fees: send these only once /route/outcome says charge_fee is true if is_fee_transaction { break; } let transfer_call = actions.last().is_some_and( |InstructionAction::FunctionCall { method_name, .. }| method_name == "ft_transfer_call", ); let actions = actions .into_iter() .map( |InstructionAction::FunctionCall { method_name, args, gas, deposit, }| { Ok(Action::function_call( method_name, BASE64_STANDARD.decode(args)?, Gas::from_gas(gas), deposit, )) }, ) .collect::<anyhow::Result<Vec<_>>>()?; let outcome = near.transaction(receiver_id).add_actions(actions).await?; if outcome.is_failure() { anyhow::bail!("Transaction failed: {:?}", outcome.failure_message()); } // ft_transfer_call returns the amount the DEX used: "0" if it refunded everything if transfer_call && outcome.result()? == b"\"0\"" { anyhow::bail!("Swap refunded"); }}ExecutionInstruction and InstructionAction are single-variant enums, so a response with a new instruction or action kind fails to deserialize instead of being skipped.
Nonce and block hash
Section titled “Nonce and block hash”When you sign with the trader’s key yourself, pass it as signing_public_key in the route request too. Each quote then has signing: the key’s nonce and a recent block hash, so you can sign without asking an RPC for them first. The first transaction takes nonce + 1, and each one after it the nonce after the one before.
Checking the outcome
Section titled “Checking the outcome”After sending the transactions, pass their hashes to /route/outcome to find out what they did: what the trader spent and received, and why the swap failed if it did, in a message you can show them. It waits for the transactions to execute before responding, so you can call it right away. With NEAR Connect, a transaction’s hash is the transaction_outcome.id of what signAndSendTransaction returns.
// The token_in, token_out and trader_account_id of the route requestconst params = new URLSearchParams({ token_in: tokenIn, token_out: tokenOut, trader_account_id: trader, tx_hashes: txHashes.join(','), // the route's transactions before its fee transactions fee_amount: quote.fee_amount,});const response = await fetch(`https://router.intear.tech/route/outcome?${params}`);if (!response.ok) throw new Error(await response.text());const outcome = await response.json();#[derive(Debug, Deserialize)]struct SwapOutcome { charge_fee: bool, amount_in: String, amount_out: String, next_steps: Vec<ExecutionInstruction>, error: Option<String>,}
// The route's transactions before its fee transactionslet tx_hashes = tx_hashes.join(",");let url = reqwest::Url::parse_with_params( "https://router.intear.tech/route/outcome", [ // The token_in, token_out and trader_account_id of the route request ("token_in", token_in), ("token_out", token_out), ("trader_account_id", trader), ("tx_hashes", tx_hashes.as_str()), ("fee_amount", quote.fee_amount.as_str()), ],)?;let response = reqwest::get(url).await?;if !response.status().is_success() { anyhow::bail!(response.text().await?);}let outcome: SwapOutcome = response.json().await?;Leave out the fee transactions: they’re sent after this, and only if the swap went through. Then:
- If
charge_feeistrue, the swap went through: showamount_inandamount_out, and send the fee transactions, if there are any. Otherwise, showerror. - Also execute
next_steps, in order, the same way as the route’s instructions. They move tokens that ended up somewhere else to where the trader wanted them, like the output of a route whosetoken_outputdiffers fromtoken_out, or the wNEAR left over from a failed swap from NEAR. They’re usually empty, but this is important for a smooth user experience without having tokens stuck in the wrong place.
Showing the swap sooner
Section titled “Showing the swap sooner”Pass wait_until=swap to get the answer as soon as the swap executed, before the output reaches the trader, callbacks and gas refunds. Show it and send the fee transactions then, as above. Its next_steps are empty when it says settled: false, so ask again with wait_until=executed once the fee transactions are sent, and execute the next_steps of that answer. A swap that failed is only answered once everything executed, with settled: true and its next_steps.
Costs and balance
Section titled “Costs and balance”Each quote tells you what it costs in NEAR, on top of the swap amount:
| Field | Use it for |
|---|---|
estimated_gas_usage |
The gas fee to show to the user |
estimated_max_gas_cost |
The most gas can cost, if all attached gas is burnt. Make sure the wallet has at least this much |
storage_deposits |
NEAR spent on storage deposits. Usually only non-zero the first time the trader uses a token or DEX |
So before swapping, the trader needs at least storage_deposits + estimated_max_gas_cost NEAR, plus the input when swapping from NEAR: amount_in, or worst_case_amount.amount_in for exact output.
These costs are already taken into account when quotes are sorted, so a route with slightly more output but much higher costs ranks lower.
Keep quotes fresh
Section titled “Keep quotes fresh”A quote is based on the pool state at the moment it was found. Pools change every block, so don’t execute a quote that’s more than a few seconds old. Request a new one right before signing, or use live quotes to always have a fresh one.
If something fails
Section titled “If something fails”If the price moves past your slippage limit, the swap fails and the DEX refunds the input. When the swap is an ft_transfer_call (Rhea, Rhea DCL, IntearDex from a NEP-141 token), the transaction itself still succeeds: its return value is the amount the DEX used, "0" when everything was refunded. Check it before showing the swap as done, as the samples in Signing do. Steps that already succeeded stay done, so the trader may end up with an intermediate form of the token, like wNEAR from a near_deposit that ran before the failed swap.
/route/outcome takes care of both: it reports the swap as failed, with an error to show, and its next_steps unwrap the wNEAR back to NEAR.