# Executing a route

> How to turn a quote's execution instructions into signed NEAR transactions.

This page is part of the DEX Aggregator docs. The aggregator is at `https://router.intear.tech`, with live quotes at `wss://router.intear.tech/route/subscribe`, on NEAR mainnet only. https://docs.intear.tech/dex-aggregator/llms-full.txt has all of its pages in one file, and https://docs.intear.tech/llms.txt lists every page of the Intear docs.

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

Right now there's only one kind of instruction, `NearTransaction`:

```json
{
  "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](https://docs.intear.tech/dex-aggregator/guides/referral-fees.md) after the swap, see below |

The transaction is signed by the trader, the account you passed as `trader_account_id`.

:::caution
New instruction kinds may be added in the future. If you see a key other than `NearTransaction`, use another route or stop and show an error instead of skipping it and executing only `NearTransaction` types.
:::

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

Routes with [app fees](https://docs.intear.tech/dex-aggregator/guides/referral-fees.md) 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](#checking-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

**JavaScript**

In the browser, with [NEAR Connect](https://github.com/hot-dao/near-selector):

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

The whole file, tested in CI: https://docs.intear.tech/snippets/js/src/quickstart.js

On a server, with [near-api-js](https://github.com/near/near-api-js):

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

The whole file, tested in CI: https://docs.intear.tech/snippets/js/src/near-api-js.js

**Rust**

With [near-kit](https://docs.rs/near-kit), using the `Quote` types from the [Quickstart](https://docs.intear.tech/dex-aggregator/quickstart.md):

```rust
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");
    }
}
```

The whole file, tested in CI: https://docs.intear.tech/snippets/rust/src/lib.rs

`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

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`](https://docs.intear.tech/dex-aggregator/reference/quote.md#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

After sending the transactions, pass their hashes to [`/route/outcome`](https://docs.intear.tech/dex-aggregator/reference/outcome.md) 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.

**JavaScript**

```js
// 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();
```

The whole file, tested in CI: https://docs.intear.tech/snippets/js/src/outcome.js

**Rust**

```rust
#[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 transactions
let 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?;
```

The whole file, tested in CI: https://docs.intear.tech/snippets/rust/src/lib.rs

Leave out the [fee transactions](#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`](https://docs.intear.tech/dex-aggregator/guides/tokens.md#when-token_output-differs) 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.

### Showing the swap sooner

Pass `wait_until=swap` to [get the answer as soon as the swap executed](https://docs.intear.tech/dex-aggregator/reference/outcome.md#answering-at-the-swap), 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

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

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](https://docs.intear.tech/dex-aggregator/guides/live-quotes.md) to always have a fresh one.

## 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](#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`](#checking-the-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.
