How to add token swaps to your app on NEAR
Any NEAR app can let its users swap tokens without writing a contract or running a backend. The Intear DEX Aggregator finds the best route across Rhea, Rhea DCL and Plach pools on Intear DEX, and returns it as transactions for the user's wallet to sign.
This guide builds a swap form step by step. The examples on this page send real transactions from your browser to the same endpoints your app would use.
What you'll use
| Service | What it does | Where |
|---|---|---|
| DEX Aggregator | Quotes, and the transactions that execute them | https://router.intear.tech |
| Intear API | Token list, prices, icons and balances | https://api.intea.rs |
| NEAR Connect | Connecting the user's wallet | NPM: @hot-labs/near-connect |
You can start by calling the APIs straight from the browser, with no API keys (rate limited), and no server is needed from you unless you prefer to self-host the aggregator. Everything runs on NEAR mainnet only, testnet is not supported.
1. Name tokens and amounts
The aggregator calls native NEAR near, and any other token nep141: followed by its contract, like nep141:usdt.tether-token.near. The Intear API uses just the contract ID, and has native NEAR's price and icon under wNEAR, wrap.near.
Amounts are whole numbers in the token's smallest unit, sent as strings: 1 USDt is 1000000 because USDt has 6 decimals, and 1 NEAR is 1 followed by 24 zeros. Get each token's decimals, icon and price from the Intear API, and use BigInt for every amount you send, since floating-point numbers lose precision long before 24 decimals.
Live example
- Aggregator token ID
- Decimals
- Reputation
- Amount to send
const params = new URLSearchParams({
q: 'usdc',
fields: 'symbol,decimals,icon_hash,price_usd,reputation',
limit: 6,
});
const response = await fetch(`https://api.intea.rs/tokens/search?${params}`);
const { columns, rows } = await response.json();
// columns: ["account_id", "symbol", "decimals", "icon_hash", "price_usd", "reputation"], then a row per token
// icons: https://api.intea.rs/icon/<icon_hash>.webp
function toRaw(amount, decimals) {
const [whole, fraction = ''] = amount.split('.');
return BigInt(whole + fraction.padEnd(decimals, '0').slice(0, decimals));
}
toRaw('1.5', 6); // 1500000n
Anyone can create a token with a familiar symbol: search for USDC above and you get several tokens called USDC. Mark the tokens whose reputation is Reputable (green check above) or NotFake (blue check), so users can tell them apart. The other values are Unknown and Spam.
For a token picker that keeps up with typing, send tokens.search over the API's WebSocket with no debounce for best speed. The search always ranks real verified tokens (like USDC) first, sorts by volume, liquidity, and other factors, so the list is ready to present as-is.
2. Get a quote
GET /route takes the two tokens, the amount and the slippage, and returns a quote from each DEX that found a route. They come best first, ranked by what the user gets after gas and storage costs, so you can just always take the first one.
Live example
const params = new URLSearchParams({
token_in: 'near',
token_out: 'nep141:usdt.tether-token.near',
amount_in: '1000000000000000000000000', // 1 NEAR
slippage_type: 'Auto',
min_slippage: '0.001',
max_slippage: '0.05',
trader_account_id: 'alice.near',
});
const response = await fetch(`https://router.intear.tech/route?${params}`);
if (!response.ok) throw new Error(await response.text());
const [best] = await response.json(); // undefined when no DEX found a route
What to show from a quote:
| Field | Show it as |
|---|---|
estimated_amount | What the user gets / what the user pays if exact-output |
worst_case_amount | Minimum received / maximum paid. If the price moves past the slippage before the swap executes, it fails and the input is refunded |
estimated_gas_usage | Gas fee, in yoctoNEAR |
estimated_max_gas_cost + storage_deposits | The most NEAR the swap can cost on top of the amount. Check the user has it before enabling the Swap button |
Pass trader_account_id if a wallet is connected: without it, the route can't include the storage deposits the user is missing, and the swap can fail. Before wallet is connected, you can quote without it to show rates. Auto slippage picks a limit between min_slippage and max_slippage depending on how volatile the tokens are, so established tokens get a tighter one than fresh memecoins.
No route is an empty array, not an error. Errors are 4xx responses with a plain-text message for you, the developer, rather than for users.
3. Keep the quote live
Pools can change every block, so a quote gets old fast. Instead of polling, open a WebSocket to wss://router.intear.tech/route/subscribe and send the same query string with an id. The server sends new quotes every time the result changes, and each message you send replaces the request before it.
Send a new request on every change of the form, with no debounce, and ignore updates whose id isn't the latest you sent: they belong to a request that's already replaced. Clear the quote on screen as soon as the form changes, so nobody signs a quote for an amount they've already edited. Open the socket below and type in the amount to see it happen.
Live example
class RouteSocket {
#socket = null; #requestId = 0; #query = null; #delay = 1500;
constructor(onRoutes, onError) { this.onRoutes = onRoutes; this.onError = onError; this.#open(); }
follow(query) { this.#query = query; this.#send(); } // a query string, or null to stop
#open() {
const socket = new WebSocket('wss://router.intear.tech/route/subscribe');
this.#socket = socket;
socket.onopen = () => { this.#delay = 1500; if (this.#query !== null) this.#send(); };
socket.onmessage = (event) => {
const update = JSON.parse(event.data);
if (update.id !== this.#requestId) return; // an update of a replaced request
if ('error' in update) this.onError(update.error); else this.onRoutes(update.routes);
};
// Reconnect on every close
socket.onclose = () => {
setTimeout(() => this.#open(), this.#delay);
this.#delay = Math.min(Math.floor(this.#delay * 1.8), 30000);
};
}
#send() {
this.#requestId += 1;
if (this.#socket.readyState !== WebSocket.OPEN) return;
this.#socket.send(JSON.stringify({ id: this.#requestId, query: this.#query ?? undefined }));
}
}
4. See what the user signs
A quote's execution_instructions are the transactions that execute it, in order: wrapping NEAR, storage deposits, registrations and the swap itself. Send them one after another, each once the one before has executed. Their arguments are base64-encoded JSON. Here are the transactions of the quote you picked in step 2:
Live example
NearTransactionis the only kind of instruction, andFunctionCallthe only kind of action, for now. If you see anything else, show an error instead of skipping it.- Transactions with
is_fee_transaction: truepay your app's fee (step 7). Hold them back until you know the swap went through.
5. Sign with the user's wallet
NEAR Connect shows a wallet picker and gives you the connected wallet: Intear Wallet, HOT, Meteor, NEAR Mobile and others.
import { NearConnector } from 'https://cdn.jsdelivr.net/npm/@hot-labs/near-connect@0.11.4/+esm';
const connector = new NearConnector({ network: 'mainnet' });
connector.on('wallet:signIn', (event) => useWallet(event.wallet, event.accounts[0].accountId));
connector.on('wallet:signOut', () => useWallet(null, null));
connectButton.onclick = () => connector.connect();
async function send(wallet, instructions) {
const transactions = instructions.map(({ NearTransaction: tx }) => ({
receiverId: tx.receiver_id,
actions: tx.actions.map(({ FunctionCall: call }) => ({
type: 'FunctionCall',
params: {
methodName: call.method_name,
args: JSON.parse(atob(call.args)), // the aggregator sends base64, near-connect accepts an object
gas: String(call.gas),
deposit: call.deposit,
},
})),
}));
const outcomes = await wallet.signAndSendTransactions({ transactions });
return outcomes.map((outcome) => outcome.transaction_outcome.id);
}
- When the promise rejects, the user most likely closed the wallet's popup: show it as cancelled rather than as an error.
- The wallet resolves even when the swap inside a transaction fails, so check the outcome before saying it's done.
- The connected account is part of the query, so quote again when it changes.
Building a wallet, a bot or a Telegram app that signs with its own keys? Pass signing_public_key with your route requests, and each quote comes with the nonce and block hash to sign with, so you can send transactions without asking an RPC for them first. See Executing a route.
6. Check the outcome
Ask /route/outcome what the transactions did: how much the user spent and received, and why the swap failed if it did, in words written for users. Call it right after sending: it waits for the transactions to execute. With wait_until=swap, it answers as soon as the swap executes, before the output reaches the user's wallet.
const params = new URLSearchParams({
token_in: 'near',
token_out: 'nep141:usdt.tether-token.near',
trader_account_id: 'alice.near',
tx_hashes: hashes.join(','), // without the fee transactions
fee_amount: quote.fee_amount,
wait_until: 'swap',
});
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();
{
"charge_fee": true,
"amount_in": "1000000000000000000000000",
"amount_out": "4880007",
"balance_changes": [],
"next_steps": [],
"error": null,
"settled": false
}
charge_fee: truemeans the swap went through: showamount_out, what the user actually got, rather than the estimate. Otherwise showerror, like "The price moved more than your slippage allows while the transaction was on its way. Try again, or increase the slippage."- If
charge_feeis true and the quote has fee transactions, send them now. - If
settledis false, ask again withwait_until=executedand send that answer'snext_steps. They move tokens that ended up somewhere else to where the user wanted them, like the wNEAR left over from a failed swap from NEAR. Usually there are none.
7. Charge your own frontend fee
Add fees to your route requests: a JSON object of up to 4 accounts and the fee each one charges, as a fraction of the swap, 5% at most together. The fee is charged once, on the input. Each account gets 80% of its fee and the aggregator keeps 20%. Quotes already have it taken out, so you show them as they are, and there's nothing to set up first.
Live example
URL-encoded:
Where the fee lands depends on the route:
- Plach pays it inside the swap, into your account's Intear DEX balance of the input token. See what you've collected and withdraw it on the Developers page.
- Rhea and Rhea DCL don't have a native fee system, so their routes add fee transactions that send it to your wallet to the end, with NEAR transferred as wNEAR. Send them only when the outcome says
charge_fee: true.
Pass referrer_id with your app's account too, to see the volume you bring on the Developers page, can be useful for tracking. If you have a referral system and split the fee with other accounts, on the same page you will see breakdown by who received the fees (your app's referrers).
8. Put it together
Here's everything above as one swap form: live quotes over the socket, balances that follow the account over the Intear API socket, and NEAR Connect for signing. The code is in examples.js.
This swaps real tokens on NEAR mainnet.
Live on mainnet
Before you ship
- Show the estimate, the minimum received, the route's DEX, your fee, and the most the swap can cost in NEAR.
- Check balances before enabling the Swap button: the input, plus NEAR for gas and storage. Keep about 0.05 NEAR back when the user presses Max on NEAR.
- After the swap, show the outcome's
amount_out, and link the transactions on an explorer like NearBlocks. - Follow balances with
balances.subscribeinstead of fetching them, so they update a block or two after the swap.
Next steps
- Read the DEX Aggregator quickstart, or try every parameter in the playground.
- Add charts, recent trades and token lists from the Intear API.
- Building with an AI agent? Give it the intear-swap-frontend skill: everything in this post, plus signing with your own keys for wallets, burner wallets and bots. Save it as
intear-swap-frontend/SKILL.mdin the agent's skills folder, like~/.claude/skills/for Claude Code. The docs also have a skill for each product. - Questions? Ask in Intear's Telegram chat.