Skip to content

Executing a route

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.

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.

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.

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.

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');
}
}

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.

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 request
const 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();

Leave out the fee transactions: they’re sent after this, and only if the swap went through. Then:

  1. If charge_fee is true, the swap went through: show amount_in and amount_out, and send the fee transactions, if there are any. Otherwise, show error.
  2. 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 whose token_output differs from token_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.

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.

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.

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 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.