DEV Community

Cover image for How to Build a Basic STON.fi Swap Interface

How to Build a Basic STON.fi Swap Interface

A practical React guide to wallet connection, token selection, swap simulation, transaction building, and on-chain execution on TON.

A basic STON.fi swap interface needs to do much more than place two token selectors next to a Swap button. A useful implementation has to connect a TON wallet, load supported assets, convert human-readable amounts into blockchain units, simulate the trade, preserve the resulting slippage protection, build the correct STON.fi transaction, and finally ask the wallet to sign it.

The cleanest architecture separates those responsibilities. STON.fi provides a REST API for asset data and swap simulation, its TypeScript SDK builds the contract transaction, and TON Connect handles wallet interaction. Your frontend coordinates the three without ever taking custody of the user's private keys.

What the interface actually needs to do

What the interface actually needs to do in STON.fi

For a minimal React application, think of the swap as five connected states:

  1. Wallet state: Is a TON wallet connected, and what is its address?
  2. Asset state: Which token is being sold and which token is being received?
  3. Amount state: How many blockchain units does the entered amount represent?
  4. Simulation state: What route, minimum output, and Router should the transaction use?
  5. Transaction state: Has the wallet received, rejected, or submitted the transaction?

That division matters because a swap quote is not permanent. If the user changes the source token, destination token, or amount, the previous simulation should immediately become invalid.

A minimal interface therefore needs only a few visible controls:

  • Connect Wallet
  • From token selector
  • To token selector
  • Amount input
  • Simulated output
  • Minimum received
  • Swap button
  • Loading, rejection, and transaction status messages

You can add charts, token balances, price impact displays, routing information, or transaction history later. They are useful improvements, but they are not required to understand the core STON.fi flow.

Set up the React project

React project for STON.fi swap

STON.fi's official React quickstart uses its API client, DEX SDK, and TON Connect together. The essential dependencies are:

npm install @ston-fi/sdk @ston-fi/api @tonconnect/ui-react @ton/ton
Enter fullscreen mode Exit fullscreen mode

The roles are intentionally different.

@ston-fi/api gives the frontend access to STON.fi data and simulation methods. @ston-fi/sdk converts the selected swap into contract-ready transaction parameters. @tonconnect/ui-react connects the application to the user's wallet and submits the transaction for approval.

A simple component tree might look like this:

App
├── TonConnectButton
└── SwapForm
    ├── FromAssetSelect
    ├── AmountInput
    ├── ToAssetSelect
    ├── QuotePanel
    └── SwapButton
Enter fullscreen mode Exit fullscreen mode

You do not need to split the first prototype into that many files. One App.jsx component is perfectly reasonable while learning the flow. The important part is keeping the logical stages separate in your state.

Connect a TON wallet first

Connect a TON wallet and load assets on STON.fi

TON Connect is the standard wallet connection protocol for TON applications. The dApp receives the connected account and can request signatures or transactions, but the wallet retains control of the keys.

Wrap the React application with TonConnectUIProvider:

import { TonConnectUIProvider } from "@tonconnect/ui-react";

<TonConnectUIProvider
  manifestUrl={`${window.location.origin}/tonconnect-manifest.json`}
>
  <App />
</TonConnectUIProvider>
Enter fullscreen mode Exit fullscreen mode

Then place a connect button somewhere visible:

import {
  TonConnectButton,
  useTonAddress,
  useTonConnectUI,
} from "@tonconnect/ui-react";

const walletAddress = useTonAddress();
const [tonConnectUI] = useTonConnectUI();
Enter fullscreen mode Exit fullscreen mode

The manifest is not an optional branding detail. Wallets use it to identify your application. Host tonconnect-manifest.json from your own domain and provide your real application name, URL, and icon.

A basic file looks like this:

{
  "url": "https://example.com",
  "name": "My STON.fi Swap",
  "iconUrl": "https://example.com/icon.png"
}
Enter fullscreen mode Exit fullscreen mode

For production, avoid copying another project's manifest or permanently pointing to a demo manifest. The wallet approval screen should identify the application the user is actually interacting with.

Load assets without assuming every token has nine decimals

The next job is populating the two asset selectors.

STON.fi exposes asset information through its REST API, including an asset query endpoint. The official API client wraps those calls, so you can initialize it directly:

import { StonApiClient } from "@ston-fi/api";

const api = new StonApiClient();
Enter fullscreen mode Exit fullscreen mode

You can then fetch assets when the application loads and keep the result in React state.

One implementation detail deserves more attention than it usually gets: do not assume every asset uses nine decimal places.

A wallet might display 1.25 TOKEN, while the blockchain and STON.fi API work with integer base units. The conversion depends on that token's metadata.

For example, with nine decimals:

1.25 TOKEN = 1,250,000,000 units
Enter fullscreen mode Exit fullscreen mode

With six decimals:

1.25 TOKEN = 1,250,000 units
Enter fullscreen mode Exit fullscreen mode

STON.fi's own swap quickstart reads the decimal precision dynamically from asset metadata for exactly this reason.

For production code, avoid converting large token values with floating-point arithmetic such as Number(amount) * 10 ** decimals. A string-to-BigInt parser is safer:

function toUnits(value, decimals) {
  const [whole = "0", fraction = ""] = value.trim().split(".");
  const padded = (fraction + "0".repeat(decimals)).slice(0, decimals);

  return (
    BigInt(whole) * 10n ** BigInt(decimals) +
    BigInt(padded || "0")
  ).toString();
}
Enter fullscreen mode Exit fullscreen mode

You should also reject negative amounts, malformed decimals, excessive precision, and zero before calling the API.

Simulate before allowing the user to sign

How to simulate swap on STON.fi

The swap button should not construct a trade from an old displayed price or a hardcoded pool.

STON.fi provides simulateSwap, which lets the application calculate the swap before execution. Its REST API exposes a dedicated swap simulation endpoint for expected output, fees, and transaction-related data.

A simplified request looks like this:

const simulation = await api.simulateSwap({
  offerAddress: fromAsset.contractAddress,
  askAddress: toAsset.contractAddress,
  offerUnits,
  slippageTolerance: "0.01",
});
Enter fullscreen mode Exit fullscreen mode

Here, 0.01 represents a 1 percent slippage tolerance.

Simulation is not merely a cosmetic price preview. It connects the UI state to the transaction you will eventually build. In the current DEX v2 integration pattern, the result also supplies routing information that should be reused during transaction construction.

Your quote panel should show enough information for a meaningful decision. At minimum, display:

  • amount being sold
  • expected or quoted output
  • minimum acceptable output
  • selected slippage tolerance
  • source and destination assets

Most importantly, clear the simulation whenever the user changes an input.

function resetQuote() {
  setSimulation(null);
}

function onAmountChange(value) {
  setAmount(value);
  resetQuote();
}
Enter fullscreen mode Exit fullscreen mode

Otherwise, a user can simulate 10 tokens, change the field to 100, and still see a Swap button associated with the stale 10-token result.

Let the STON.fi API choose the Router

Let the STON.fi API choose the Router

One of the most important implementation details in the current STON.fi DEX v2 documentation is that a production application should not hardcode a Router simply because an example contract address worked previously.

The recommended mainnet flow is:

  1. Simulate the swap.
  2. Read the Router metadata returned by the simulation.
  3. Pass that Router metadata into dexFactory().
  4. Build the transaction with the resulting Router contract.

STON.fi explicitly recommends this API-driven approach so integrations can follow Router changes without being tied to a manually embedded contract address. Its REST API currently serves mainnet data, while testnet integration requires a different, manually configured approach.

The central setup is compact:

import { dexFactory, Client } from "@ston-fi/sdk";

const tonClient = new Client({
  endpoint: "https://toncenter.com/api/v2/jsonRPC",
});

const routerInfo = simulation.router;
const contracts = dexFactory(routerInfo);

const router = tonClient.open(
  contracts.Router.create(routerInfo.address)
);

const proxyTon = contracts.pTON.create(
  routerInfo.ptonMasterAddress
);
Enter fullscreen mode Exit fullscreen mode

The simulation and transaction should remain one logical operation. Reuse fields such as offerUnits, minAskUnits, token addresses, and Router metadata from the simulation rather than independently reconstructing the swap from UI values.

That keeps the transaction you ask the wallet to sign aligned with the trade the interface just showed.

Build the right transaction for the asset pair

STON.fi exposes different Router helpers because TON and jettons do not enter the swap through exactly the same path.

Your interface therefore needs three branches:

const common = {
  userWalletAddress: walletAddress,
  offerAmount: simulation.offerUnits,
  minAskAmount: simulation.minAskUnits,
};

let txParams;

if (fromAsset.kind === "Ton") {
  txParams = await router.getSwapTonToJettonTxParams({
    ...common,
    proxyTon,
    askJettonAddress: simulation.askAddress,
  });
} else if (toAsset.kind === "Ton") {
  txParams = await router.getSwapJettonToTonTxParams({
    ...common,
    proxyTon,
    offerJettonAddress: simulation.offerAddress,
  });
} else {
  txParams = await router.getSwapJettonToJettonTxParams({
    ...common,
    offerJettonAddress: simulation.offerAddress,
    askJettonAddress: simulation.askAddress,
  });
}
Enter fullscreen mode Exit fullscreen mode

The Router helper returns the information the wallet needs, including the destination, attached TON value, and message body.

At the protocol level, the Router acts as the DEX entry point and directs token operations toward the appropriate pool. The pool contains the AMM state used for the swap. In DEX v2, the swap payload can also carry parameters such as minimum output, receiver, refund information, and an execution deadline.

For a basic interface, you do not need to manually construct those low-level cells. That is exactly the problem the official SDK is designed to solve.

Send the transaction through TON Connect

How to send, track and verify the swap on STON.fi

Once txParams exists, convert it into a TON Connect transaction request.

await tonConnectUI.sendTransaction({
  validUntil: Math.floor(Date.now() / 1000) + 300,
  network: "-239",
  messages: [
    {
      address: txParams.to.toString(),
      amount: txParams.value.toString(),
      payload: txParams.body?.toBoc().toString("base64"),
    },
  ],
});
Enter fullscreen mode Exit fullscreen mode

There is one small detail here that can prevent a surprisingly confusing bug: validUntil uses a Unix timestamp in seconds, not JavaScript milliseconds. Five minutes from now is therefore calculated with Math.floor(Date.now() / 1000) + 300. TON's current TON Connect documentation defines mainnet as network -239.

Calling sendTransaction() does not expose the wallet's private key to your React application. Instead, the wallet presents the transaction to the user for approval and then signs and broadcasts it if they accept.

Your UI should distinguish at least four outcomes:

  • waiting for wallet approval
  • rejected by the user
  • submitted to TON
  • execution confirmed or failed on-chain

Do not interpret the successful return of sendTransaction() as proof that every downstream contract action completed successfully. TON transactions can generate chains of asynchronous messages, so broadcast and final swap settlement are different stages.

STON.fi exposes swap status and transaction-related API endpoints that can help an application follow the operation after submission.

Following one swap from input to settlement

Consider a user opening your interface to exchange TON for a jetton.

They connect a wallet through TON Connect and choose TON in the first selector. Your application loads the destination asset metadata and converts the entered TON amount into blockchain units.

Next, simulateSwap() asks STON.fi what the trade currently looks like. Instead of merely taking a displayed number from that result, the application stores the entire simulation object.

The simulation gives the frontend the values that matter for execution, including the minimum acceptable output and Router metadata. dexFactory() uses that Router description to instantiate the corresponding contracts. Because TON is the source asset, the frontend calls the TON-to-jetton Router helper and receives the prepared transaction parameters.

Only then does the application open the wallet approval flow.

The user's decision therefore occurs after the following chain:

Choose assets
   |
Enter amount
   |
Convert to base units
   |
Simulate through STON.fi API
   |
Review minimum output
   |
Create Router from simulation metadata
   |
Build swap transaction with STON.fi SDK
   |
Request signature through TON Connect
   |
Track on-chain execution
Enter fullscreen mode Exit fullscreen mode

That is the core of a basic STON.fi swap interface. The UI is small because STON.fi's API and SDK handle much of the protocol-specific construction, but the frontend still has an important responsibility: it must preserve the relationship between what was simulated, what was displayed, and what was actually signed.

Common mistakes that make a basic swap unsafe or unreliable

A prototype can appear functional while still containing several subtle integration problems.

Hardcoding the Router. For current DEX v2 mainnet integrations, use Router metadata supplied by the STON.fi simulation and dexFactory() instead.

Using floating-point arithmetic for token amounts. Convert decimal strings into integer blockchain units using token-specific metadata.

Keeping an old quote after an input changes. Clear simulation state whenever the asset pair, amount, or relevant swap setting changes.

Setting minimum output to an arbitrary tiny value. minAskAmount is the user's execution protection. Reuse the simulation result rather than replacing it with a convenient placeholder.

Treating wallet submission as final settlement. A signed and broadcast TON transaction can still lead to later contract messages, failures, or refunds.

Sending transactions from an unidentified dApp. Serve a correct TON Connect manifest from your own application domain.

Ignoring user rejection. Declining a wallet request is a normal outcome, not an exceptional application failure. Give the user a clean way to retry.

A solid first version does not need professional trading features. It needs faithful state management and a transaction that corresponds to the quote on screen.

Practical takeaway: build the interface around the simulation object, not around the Swap button. Once a trade has been simulated, treat that result as the source for Router selection, offered units, minimum output, and transaction construction. If anything affecting the trade changes, discard the simulation and request a new one before enabling execution.

Frequently Asked Questions

Do I need a backend to build a basic STON.fi swap interface?

Not necessarily. A basic React implementation can call the STON.fi API, build the transaction with the SDK, and ask a connected wallet to sign through TON Connect directly from the frontend. A backend becomes useful for application-specific analytics, caching, access control, monitoring, or other services, but it should never require collecting the user's wallet private key.

Why should a swap be simulated before execution?

Simulation tells the application what the trade currently looks like before the wallet signs anything. It also produces values such as the minimum acceptable output and, in the current STON.fi DEX v2 workflow, Router metadata used to build the actual transaction. If inputs change after simulation, request a new simulation rather than executing the old one.

What is the difference between expected output and minimum received?

Expected output describes the result indicated by the current quote or simulation. Minimum received is the lower execution boundary created by the selected slippage tolerance. If execution would produce less than that minimum, the swap should not simply proceed at any price. Your interface should make that protection visible instead of hiding it behind the Swap button.

Can I assume every TON token uses nine decimals?

No. Token amounts displayed to people must be converted into integer blockchain units using that asset's own decimal metadata. Hardcoding nine decimals can make an interface submit a radically different amount for tokens with another precision. Read the metadata supplied for the selected asset and use integer-safe conversion logic.

Should I hardcode a STON.fi v2 Router address?

Not for the recommended production mainnet flow. Current STON.fi documentation advises developers to simulate the swap first, use the Router metadata contained in that result, and instantiate the corresponding contracts through dexFactory(). This reduces the chance that an integration becomes tied to an obsolete Router configuration.

Does TON Connect execute the STON.fi swap itself?

No. TON Connect is the wallet communication layer. Your application builds the STON.fi transaction, then TON Connect asks the user's wallet to approve, sign, and broadcast it. STON.fi smart contracts process the resulting on-chain messages. Keeping those roles separate makes the integration easier to reason about and prevents the frontend from handling private keys.

How can I verify that a STON.fi swap actually completed?

Do not rely only on the fact that the wallet accepted the transaction request. Treat submission and settlement as separate states. Track the resulting on-chain activity or use STON.fi transaction and swap-status facilities where appropriate. Update the interface only when you have evidence of the final result, including a possible failure or refund path.

Sources and Further Reading

  • STON.fi Swap Guide (React) - Official end-to-end example for building a React swap interface with the STON.fi API, SDK, and TON Connect
  • STON.fi DEX v2 Swap - Current API-driven production pattern, Router discovery, dexFactory(), and v2 transaction construction
  • STON.fi REST API Reference - Swap simulation, asset queries, Router information, transaction queries, and swap status endpoints
  • STON.fi REST API - Overview of the official HTTP interface used alongside the DEX contracts
  • STON.fi DEX Architecture - Explanation of the Router, Pool, and contract roles involved in swaps
  • STON.fi Router v2 Reference - Low-level swap payload fields, minimum output behavior, deadlines, routing, and refund mechanics
  • TON Connect Get Started - Official React setup for the provider, wallet connection, manifest, and connection state
  • TON Connect Send Transaction - Official transaction request format, validUntil, network selection, message format, response, and wallet errors

Top comments (1)

Collapse
 
ivan_cryptovazimazima profile image
Ivan “Crypto Vazima” Zimanov

Hi. If you find any errors in the text, please be sure to let us know in the comments so we can help others with this guide! Thank you so much for your support and assistance!