Build a production-minded swap flow with the STON.fi API, SDK v2, React, and TON Connect.
Integrating a STON.fi swap is not just a matter of calling one SDK function. A reliable application first requests a simulation from the STON.fi API, uses the returned routing information to select the correct smart contracts, builds the transaction through the TypeScript SDK, and finally asks the connected wallet to sign it through TON Connect.
The SDK never needs access to a seed phrase or private key. It prepares the blockchain message, while the wallet remains responsible for authorization and broadcasting. That separation lets you add non-custodial swaps to a wallet, Telegram Mini App, game, payment interface, or DeFi dashboard without taking custody of user funds.
The integration has four separate layers
A common source of confusion is treating the STON.fi SDK as a complete swapping service. It is more accurate to think of the integration as a pipeline with four components.
| Component | Main responsibility | Signs transactions? |
|---|---|---|
@ston-fi/api |
Loads assets, simulates swaps, and returns routing metadata | No |
@ston-fi/sdk |
Creates the correct Router, pTON, and transaction parameters | No |
| TON RPC client | Reads contract data and opens contract wrappers | No |
| TON Connect | Sends the prepared message to the connected wallet | The wallet does |
The STON.fi API should normally be the starting point. Its simulation response includes the expected amounts, minimum protected output, asset addresses, and the complete Router object. You pass that Router object to dexFactory(), which selects the correct contract implementation for the route.
This API-driven approach is safer than copying a Router address from an old tutorial. STON.fi supports different Router versions and contract types, so an address or class that works for one pool may not be suitable for another. The official v2 documentation recommends allowing the simulation response to dictate which Router should be used.
The complete flow looks like this:
- Connect a wallet with TON Connect.
- Select the offer and ask assets.
- Convert the entered amount into blockchain units.
- Simulate the swap through
@ston-fi/api. - Create the correct DEX contracts with
dexFactory(). - Build the appropriate swap transaction parameters.
- Send the generated message through TON Connect.
- Track the submitted transaction separately.
Set up the TypeScript project
The examples below use React and Vite, but the same SDK flow works with Next.js, Vue, Svelte, a Telegram Mini App, or a backend service that only prepares unsigned transaction messages.
Install the required packages:
npm install @ston-fi/sdk @ston-fi/api @tonconnect/ui-react @ton/ton
For a Vite application, you may also need a browser polyfill for Buffer, which TON libraries use when serializing a cell into a base64 Bag of Cells, or BoC:
npm install --save-dev vite-plugin-node-polyfills
A minimal Vite configuration can expose only the required buffer polyfill:
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { nodePolyfills } from "vite-plugin-node-polyfills";
export default defineConfig({
plugins: [
react(),
nodePolyfills({
include: ["buffer"],
globals: {
Buffer: true,
},
}),
],
});
The official STON.fi quickstart uses the SDK for transaction construction, the API client for asset discovery and simulation, and TON Connect UI for wallet communication.
Connect the wallet without handling private keys
Wrap the application with TonConnectUIProvider:
// src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { TonConnectUIProvider } from "@tonconnect/ui-react";
import App from "./App";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<TonConnectUIProvider
manifestUrl={`${window.location.origin}/tonconnect-manifest.json`}
>
<App />
</TonConnectUIProvider>
</React.StrictMode>
);
Create public/tonconnect-manifest.json:
{
"url": "https://your-app.example",
"name": "STON.fi Swap Demo",
"iconUrl": "https://your-app.example/icon.png"
}
Use your real production domain and a publicly accessible icon before deployment. The wallet reads this manifest to identify the application requesting a connection.
Inside the swap component, add the wallet button and hooks:
import {
TonConnectButton,
useTonAddress,
useTonConnectUI,
} from "@tonconnect/ui-react";
export function SwapPanel() {
const userAddress = useTonAddress();
const [tonConnectUI] = useTonConnectUI();
return (
<section>
<TonConnectButton />
<p>{userAddress || "Connect a wallet to continue"}</p>
</section>
);
}
TON Connect creates an encrypted connection between the application and wallet. Your application can read the connected address and request a transaction, but it does not receive the wallet's private key.
Treat token amounts as integer units
Smart contracts do not receive values such as 1.25 directly. They receive integers expressed in the asset's smallest units.
An asset with nine decimals represents 1.25 as 1250000000. An asset with six decimals represents the same displayed amount as 1250000.
Do not automatically apply toNano() to every jetton. That helper assumes nine decimals, while some TON assets use another precision. The SDK provides decimal-aware utilities, and the STON.fi API also supplies token metadata that your interface can use.
A small string-based parser avoids the rounding problems that can occur when a decimal amount is first converted to a JavaScript number:
type SwapAsset = {
contractAddress: string;
kind: "Ton" | "Jetton";
decimals: number;
symbol: string;
};
export function parseUnits(value: string, decimals: number): string {
const normalized = value.trim();
if (!/^\d+(\.\d+)?$/.test(normalized)) {
throw new Error("Enter a valid positive decimal amount");
}
const [whole, fraction = ""] = normalized.split(".");
if (fraction.length > decimals) {
throw new Error(
`This asset supports no more than ${decimals} decimal places`
);
}
const units = `${whole}${fraction.padEnd(decimals, "0")}`.replace(
/^0+(?=\d)/,
""
);
return units || "0";
}
function getApiAddress(asset: SwapAsset): string {
return asset.kind === "Ton" ? "ton" : asset.contractAddress;
}
The user-facing name of the native network coin may be displayed as Gram, but current contract and API identifiers can still contain names such as Ton, "ton", and pTON. Those values are part of the integration interface and should not be replaced with display labels.
Simulate the swap before building anything
Simulation is the quote stage. It tells the application which route is available and provides the values needed to create a protected transaction.
Initialize the API client and request a simulation:
import { StonApiClient } from "@ston-fi/api";
const stonApi = new StonApiClient();
export async function simulateStonSwap(params: {
fromAsset: SwapAsset;
toAsset: SwapAsset;
amount: string;
}) {
const { fromAsset, toAsset, amount } = params;
if (fromAsset.contractAddress === toAsset.contractAddress) {
throw new Error("Select two different assets");
}
const offerUnits = parseUnits(amount, fromAsset.decimals);
if (BigInt(offerUnits) <= 0n) {
throw new Error("The swap amount must be greater than zero");
}
return stonApi.simulateSwap({
offerAddress: getApiAddress(fromAsset),
askAddress: getApiAddress(toAsset),
offerUnits,
slippageTolerance: "0.01",
});
}
In this example, "0.01" represents a 1 percent slippage tolerance. It is an example rather than a universal recommendation. Your interface should explain the setting and allow the product to choose an appropriate default.
Several fields from the response are especially important:
-
offerUnitsis the input amount in blockchain units. -
minAskUnitsis the minimum protected amount that the transaction may accept. -
offerAddressandaskAddressidentify the assets used by the route. -
routercontains the version and contract information required by the SDK.
Do not calculate minAskUnits independently after receiving a simulation. Use the value returned for that exact quote. If the amount, assets, or slippage setting changes, discard the previous simulation and request a new one.
A production interface should display the expected output as well as the minimum protected output. These values answer different questions: expected output describes the estimate, while minimum output defines the transaction's on-chain protection.
STON.fi's current v2 workflow is mainnet-first. Its public REST API supplies mainnet routing data, so it should not be combined with a testnet RPC endpoint or testnet wallet when constructing a real swap.
Let dexFactory() select the Router
After simulation, avoid manually choosing DEX.v2_1, DEX.v2_2, a constant-product Router, or another contract class. Feed the returned Router information into dexFactory().
The resulting factory output contains contract classes compatible with that Router, including the appropriate pTON implementation when the native coin participates in the route.
import { Client, dexFactory } from "@ston-fi/sdk";
type SwapSimulation = Awaited<
ReturnType<StonApiClient["simulateSwap"]>
>;
type BuildSwapParams = {
simulation: SwapSimulation;
fromAsset: SwapAsset;
toAsset: SwapAsset;
userAddress: string;
rpcEndpoint: string;
};
export async function buildStonSwapMessage({
simulation,
fromAsset,
toAsset,
userAddress,
rpcEndpoint,
}: BuildSwapParams) {
const tonClient = new Client({
endpoint: rpcEndpoint,
});
const routerInfo = simulation.router;
const dex = dexFactory(routerInfo);
const router = tonClient.open(
dex.Router.create(routerInfo.address)
);
const commonParams = {
userWalletAddress: userAddress,
offerAmount: simulation.offerUnits,
minAskAmount: simulation.minAskUnits,
};
if (fromAsset.kind === "Ton") {
const proxyTon = dex.pTON.create(
routerInfo.ptonMasterAddress
);
return router.getSwapTonToJettonTxParams({
...commonParams,
proxyTon,
askJettonAddress: simulation.askAddress,
});
}
if (toAsset.kind === "Ton") {
const proxyTon = dex.pTON.create(
routerInfo.ptonMasterAddress
);
return router.getSwapJettonToTonTxParams({
...commonParams,
proxyTon,
offerJettonAddress: simulation.offerAddress,
});
}
return router.getSwapJettonToJettonTxParams({
...commonParams,
offerJettonAddress: simulation.offerAddress,
askJettonAddress: simulation.askAddress,
});
}
The three branches exist because the native coin is not a jetton. STON.fi uses a compatible pTON contract when the route begins or ends with the native asset. A jetton-to-jetton swap does not require that parameter.
dexFactory() also protects the integration from a broader compatibility problem. New Router and pool types do not always expose identical contract behavior. Selecting a generic or hardcoded class may compile while still producing a transaction that fails for a specific route.
Send the generated message through TON Connect
The SDK returns transaction parameters containing three values your wallet request needs:
-
to, the destination contract address -
value, the native amount attached for execution and gas -
body, the serialized contract payload
Convert the body to a base64 BoC and send it through TON Connect:
type SendSwapParams = {
simulation: SwapSimulation;
fromAsset: SwapAsset;
toAsset: SwapAsset;
userAddress: string;
rpcEndpoint: string;
tonConnectUI: {
sendTransaction: (request: {
validUntil: number;
network?: string;
messages: Array<{
address: string;
amount: string;
payload?: string;
}>;
}) => Promise<{ boc: string }>;
};
};
export async function sendStonSwap({
simulation,
fromAsset,
toAsset,
userAddress,
rpcEndpoint,
tonConnectUI,
}: SendSwapParams) {
const txParams = await buildStonSwapMessage({
simulation,
fromAsset,
toAsset,
userAddress,
rpcEndpoint,
});
const payload = txParams.body
?.toBoc()
.toString("base64");
if (!payload) {
throw new Error("The SDK did not create a transaction payload");
}
const result = await tonConnectUI.sendTransaction({
validUntil: Math.floor(Date.now() / 1000) + 300,
network: "-239",
messages: [
{
address: txParams.to.toString(),
amount: txParams.value.toString(),
payload,
},
],
});
return result.boc;
}
validUntil must be a Unix timestamp in seconds. Adding 300 gives the user five minutes to review the wallet request. Passing Date.now() + 300000 would produce milliseconds instead of seconds and should be avoided.
The network value "-239" identifies TON mainnet. Setting the network explicitly helps prevent a mainnet transaction from being submitted through a wallet connected to another network.
A resolved sendTransaction() call means the wallet accepted and broadcast the signed external message. It does not prove that every subsequent smart contract message completed successfully. Save the returned BoC or another correlation identifier and use a blockchain indexer, explorer service, or backend reconciliation process to determine the final result.
Production checks that prevent expensive mistakes
A demo can stop after opening the wallet. A production integration needs stronger validation, state management, and transaction tracking.
Invalidate stale simulations. Clear the simulation whenever the amount, token pair, wallet, or slippage setting changes. Consider requesting a fresh simulation immediately before opening the wallet.
Keep decimal handling asset-specific. Read decimals from verified asset metadata and use integer or string arithmetic. Do not convert large blockchain-unit values through JavaScript floating-point numbers.
Do not hardcode Router and pTON addresses. Use simulation.router, dexFactory(), and the Router's ptonMasterAddress.
Keep the network consistent. The STON.fi API route, RPC endpoint, connected wallet, and transaction request must all refer to mainnet for the production DEX flow.
Validate the wallet request. Before calling TON Connect, verify that the destination, attached amount, and payload came from the current SDK result rather than from editable form data.
Distinguish rejection from execution failure. A user can close the wallet prompt, a request can expire, an RPC call can fail, or the on-chain route can fail after broadcasting. These outcomes need different interface messages.
Protect infrastructure credentials. Do not expose a privileged RPC key in a public frontend bundle. Use a restricted browser-safe endpoint or proxy sensitive infrastructure through your backend.
Test the complete route matrix. Exercise Gram-to-jetton, jetton-to-Gram, and jetton-to-jetton swaps. Also test assets with different decimals, small balances, insufficient gas, stale simulations, wallet rejection, and invalid token pairs.
The most useful first production exercise is a minimal swap with a limited amount. Log the simulation response, selected Router type, generated destination, attached value, returned BoC, and final on-chain result. Once that lifecycle is reliable, connect it to the rest of your application rather than debugging the complete product and swap flow at the same time.
Frequently Asked Questions
Does the STON.fi TypeScript SDK perform the swap by itself?
No. The SDK creates contract wrappers and transaction parameters, but it does not sign on behalf of the wallet holder. Your application passes the generated destination, amount, and payload to TON Connect. The connected wallet displays the request, receives authorization from its owner, signs it, and broadcasts it to TON.
Why does the application need to simulate the swap first?
Simulation returns more than an output estimate. It supplies the minimum protected output, asset addresses, and Router metadata needed to build the correct transaction. Without a current simulation, the application may use an outdated price, an incompatible contract, or a minimum output that no longer reflects current pool conditions.
Can I hardcode the STON.fi Router address?
You technically can for a tightly controlled contract integration, but it is not the recommended production pattern. STON.fi routes can use different Router versions and types. Using simulation.router with dexFactory() lets the API select the contracts compatible with the current route and reduces maintenance when the protocol introduces new contract implementations.
Should I use toNano() for every swap amount?
No. toNano() assumes nine decimal places. That is correct for the native coin and many jettons, but not for every asset. Read the asset's decimals from metadata and convert the displayed value into integer units accordingly. Incorrect precision can make the submitted amount much larger or smaller than the amount shown in your interface.
How should slippage be handled in the interface?
Treat slippage as a visible product setting rather than a hidden constant. Explain that it affects minAskUnits, validate the permitted range, and request a new simulation whenever it changes. Extremely loose tolerance can expose the swap to an unexpectedly poor result, while extremely tight tolerance can cause execution to fail as pool conditions move.
Can I test the same API-driven swap flow on TON testnet?
Not directly with production routing data. The current STON.fi REST API workflow described in the v2 documentation serves mainnet routes. Combining its simulation result with a testnet RPC endpoint or testnet wallet creates a network mismatch. Separate unit tests and message-building tests from small, carefully reviewed mainnet integration tests.
Does a successful TON Connect response mean the swap finished?
No. The response confirms that the wallet created and broadcast the signed external message. TON processes smart contract messages asynchronously, so the application should still verify the resulting on-chain activity. Store the returned BoC or another tracking identifier and update the interface only after your monitoring layer determines the final outcome.
What should I verify before enabling STON.fi swaps for real users?
Verify token addresses and decimals, all three swap directions, Router selection, pTON compatibility, quote invalidation, mainnet configuration, wallet rejection, request expiration, insufficient gas, and final transaction tracking. Start with limited values and review the complete message lifecycle before allowing larger transactions or connecting swaps to automated product actions.
Sources and Further Reading
- STON.fi SDK overview - installation, SDK versions, and available DEX capabilities
- STON.fi Swap v2 guide - API-driven Router selection and the mainnet-first transaction flow
- STON.fi Swap Quickstart - complete React example using the API, SDK, and TON Connect
- STON.fi SDK GitHub repository - official TypeScript source code and Next.js demonstration application
- STON.fi SDK changelog - Router types,
dexFactory(), contract migrations, and decimal-aware utilities - STON.fi API GitHub repository - official TypeScript client for assets, pools, simulations, and Router information
- TON Connect transaction guide - current request format, Unix expiration time, network selection, and returned BoC
- TON Connect overview - wallet connection architecture and private-key separation
- STON.fi integration article - official overview of adding swaps to a React application






Top comments (1)
Thank you so much for all your support! If you find any errors in my text, please let me know in the comments, and I'll be sure to correct the main post-guide.