Build the swap with STON.fi, hand the transaction to TON Connect, and let the connected wallet sign and broadcast it.
A STON.fi swap is not sent by giving your dApp access to a user's private key. Your application prepares the contract message required for the swap, converts it into a TON Connect transaction request, and asks the connected TON wallet to approve it. The wallet remains responsible for signing and broadcasting the transaction.
For a modern STON.fi integration, the recommended mainnet pattern is to simulate the swap first, use the router metadata returned by STON.fi, build the corresponding transaction parameters with @ston-fi/sdk, and pass those parameters to sendTransaction() from TON Connect.
The full path looks like this:
- Connect the TON wallet.
- Simulate the swap.
- Use the simulation's router information.
- Ask the STON.fi SDK to build the contract message.
- Convert that message into TON Connect format.
- Ask the wallet to sign and broadcast it.
- Verify the resulting transaction on-chain.
The separation between steps 4 and 5 is particularly useful. STON.fi knows how the swap contract should be called. TON Connect knows how to request authorization from the user's wallet.
What are you actually sending?
When you call one of the STON.fi SDK transaction-building methods, the SDK produces transaction parameters describing an internal message. The common result contains three important fields:
{
to: Address;
value: bigint;
body?: Cell | null;
}
to is the destination contract address. value is the amount of TON that must accompany the message. body is the serialized contract payload describing the operation that STON.fi should execute.
Your dApp does not sign this message.
Instead, it transforms those parameters into something TON Connect can send to the wallet:
{
address: swapParams.to.toString(),
amount: swapParams.value.toString(),
payload: swapParams.body?.toBoc().toString("base64"),
}
That distinction is worth remembering:
STON.fi SDK builds the swap message. TON Connect requests the wallet signature. The wallet broadcasts the transaction.
TON itself is message-driven. Smart contracts receive messages, process their payloads, and may generate transactions that change contract state. The payload created by the STON.fi SDK is therefore not arbitrary metadata. It is the instruction that the relevant contract will interpret when the message arrives.
Set up TON Connect first
For a React dApp, TON documentation recommends @tonconnect/ui-react. The package provides TonConnectUIProvider, TonConnectButton, useTonConnectUI, and wallet-state hooks that you can use without handling private keys yourself.
Install the packages needed for the basic STON.fi flow:
npm install @ston-fi/sdk @ston-fi/api @tonconnect/ui-react @ton/ton
You also need a public TON Connect manifest. For example:
{
"url": "https://swap.example.com",
"name": "My STON.fi Swap App",
"iconUrl": "https://swap.example.com/icon-180.png"
}
In production, TON requires the manifest to be publicly reachable, and current documentation says it should be served through HTTPS. The wallet uses this metadata to identify the dApp during connection.
Wrap your React app with the provider:
import { TonConnectUIProvider } from "@tonconnect/ui-react";
export function Root() {
return (
<TonConnectUIProvider
manifestUrl="https://swap.example.com/tonconnect-manifest.json"
>
<App />
</TonConnectUIProvider>
);
}
Then expose the wallet connection button:
import { TonConnectButton } from "@tonconnect/ui-react";
export function Header() {
return <TonConnectButton />;
}
Inside your swap component you can read the connected address and access the transaction API:
import {
useTonAddress,
useTonConnectUI,
} from "@tonconnect/ui-react";
const userAddress = useTonAddress();
const [tonConnectUI] = useTonConnectUI();
If userAddress is empty, there is no connected account available to sign the swap.
Simulate before building the transaction
For STON.fi v2 mainnet swaps, hardcoding a router is not the preferred production pattern. STON.fi documentation recommends obtaining routing information from the API and using the router returned by the simulation. This lets the integration follow the router selected for the actual swap instead of assuming one specific contract configuration.
A simplified simulation looks like this:
import { StonApiClient } from "@ston-fi/api";
const stonApi = new StonApiClient();
const simulationResult = await stonApi.simulateSwap({
offerAddress: fromAsset.contractAddress,
askAddress: toAsset.contractAddress,
offerUnits,
slippageTolerance: "0.01",
});
The amount supplied as offerUnits must already be expressed in the asset's smallest units. Do not assume that every TON ecosystem asset uses the same number of decimals. The STON.fi quickstart explicitly uses asset metadata when converting user-facing amounts into blockchain units.
A simulation is useful for more than displaying an estimated output. It gives the transaction-building stage values such as the offer amount, minimum acceptable output, token addresses, and router information associated with that swap.
That creates an important safety boundary in your UI:
- changing the input asset should invalidate the old simulation
- changing the output asset should invalidate it
- changing the amount should invalidate it
- changing the slippage setting should trigger a new simulation
- the transaction should be built from the current simulation, not from stale UI state
You want the transaction the wallet sees to correspond to the swap the user just reviewed.
Build the STON.fi swap message
Once you have a valid simulation and connected wallet, create the router from the router metadata supplied by STON.fi.
A compact version of the current API-driven pattern looks like this:
import { dexFactory, Client } from "@ston-fi/sdk";
const tonClient = new Client({
endpoint: "https://toncenter.com/api/v2/jsonRPC",
});
const routerInfo = simulationResult.router;
const dexContracts = dexFactory(routerInfo);
const router = tonClient.open(
dexContracts.Router.create(routerInfo.address)
);
const proxyTon = dexContracts.pTON.create(
routerInfo.ptonMasterAddress
);
STON.fi's current v2 documentation specifically recommends feeding the router returned by the simulation into dexFactory() for mainnet-facing integrations rather than hardcoding the router contract.
Next, prepare the fields shared by the swap variants:
const sharedTxParams = {
userWalletAddress: userAddress,
offerAmount: simulationResult.offerUnits,
minAskAmount: simulationResult.minAskUnits,
};
The exact builder depends on what you are swapping.
TON to jetton
const swapParams =
await router.getSwapTonToJettonTxParams({
...sharedTxParams,
proxyTon,
askJettonAddress: simulationResult.askAddress,
});
Jetton to TON
const swapParams =
await router.getSwapJettonToTonTxParams({
...sharedTxParams,
proxyTon,
offerJettonAddress: simulationResult.offerAddress,
});
Jetton to jetton
const swapParams =
await router.getSwapJettonToJettonTxParams({
...sharedTxParams,
offerJettonAddress: simulationResult.offerAddress,
askJettonAddress: simulationResult.askAddress,
});
The important result is the same in each case: you receive the destination, attached TON value, and payload needed for the contract call. STON.fi documents separate transaction builders for TON-to-jetton, jetton-to-TON, and jetton-to-jetton swaps.
Send the transaction through the TON wallet
Now the STON.fi-specific construction work is finished.
The remaining step is to hand the message to the wallet through TON Connect:
const result = await tonConnectUI.sendTransaction({
validUntil: Math.floor(Date.now() / 1000) + 300,
network: "-239",
messages: [
{
address: swapParams.to.toString(),
amount: swapParams.value.toString(),
payload: swapParams.body?.toBoc().toString("base64"),
},
],
});
This is the point where the wallet UI should appear and ask the user to review and approve the transaction.
There are several details here that are easy to overlook.
address comes directly from the STON.fi transaction parameters. Do not replace it with the address of the asset, pool, or some router address stored elsewhere in your UI.
amount is the TON value attached to the contract message. Pass the SDK-generated value rather than manually reconstructing the gas requirement.
payload is the contract body serialized as a Bag of Cells and encoded in base64 so TON Connect can include it in the outgoing message.
validUntil is a Unix timestamp in seconds. Current TON Connect documentation defines it in seconds and shows the pattern Math.floor(Date.now() / 1000) + 600. Some older examples in the ecosystem use Date.now() directly, which produces milliseconds, so this is a field worth checking carefully when adapting existing code.
network should be set explicitly. TON Connect currently identifies TON mainnet as -239 and testnet as -3. If the requested network conflicts with the wallet's connected network, the wallet can reject the request.
STON.fi's transaction-sending guide uses the same underlying pattern: obtain to, value, and body from the SDK, then pass them to the wallet implementation.
Put the full handler together
Here is the essential flow in one React handler:
import { StonApiClient } from "@ston-fi/api";
import { Client, dexFactory } from "@ston-fi/sdk";
import {
useTonAddress,
useTonConnectUI,
} from "@tonconnect/ui-react";
const stonApi = new StonApiClient();
export function SwapButton({
fromAsset,
toAsset,
simulationResult,
}) {
const userAddress = useTonAddress();
const [tonConnectUI] = useTonConnectUI();
const handleSwap = async () => {
if (!userAddress) {
throw new Error("Connect a wallet first");
}
if (!simulationResult) {
throw new Error("Simulate the swap first");
}
const tonClient = new Client({
endpoint: "https://toncenter.com/api/v2/jsonRPC",
});
const routerInfo = simulationResult.router;
const dexContracts = dexFactory(routerInfo);
const router = tonClient.open(
dexContracts.Router.create(routerInfo.address)
);
const proxyTon = dexContracts.pTON.create(
routerInfo.ptonMasterAddress
);
const sharedTxParams = {
userWalletAddress: userAddress,
offerAmount: simulationResult.offerUnits,
minAskAmount: simulationResult.minAskUnits,
};
let swapParams;
if (fromAsset.kind === "Ton") {
swapParams =
await router.getSwapTonToJettonTxParams({
...sharedTxParams,
proxyTon,
askJettonAddress: simulationResult.askAddress,
});
} else if (toAsset.kind === "Ton") {
swapParams =
await router.getSwapJettonToTonTxParams({
...sharedTxParams,
proxyTon,
offerJettonAddress:
simulationResult.offerAddress,
});
} else {
swapParams =
await router.getSwapJettonToJettonTxParams({
...sharedTxParams,
offerJettonAddress:
simulationResult.offerAddress,
askJettonAddress:
simulationResult.askAddress,
});
}
const result = await tonConnectUI.sendTransaction({
validUntil:
Math.floor(Date.now() / 1000) + 300,
network: "-239",
messages: [
{
address: swapParams.to.toString(),
amount: swapParams.value.toString(),
payload:
swapParams.body
?.toBoc()
.toString("base64"),
},
],
});
console.log("Wallet response:", result);
};
return (
<button onClick={handleSwap}>
Swap
</button>
);
}
This code intentionally keeps quote generation separate from transaction sending. In a real application, you should also disable the button while a request is in progress, handle rejected wallet requests separately from network failures, refresh stale simulations, and display useful status feedback.
TON Connect documents USER_REJECTS_ERROR separately from malformed requests and unsupported methods, so a user pressing "Cancel" should not be presented as a protocol failure.
What happens after the wallet approves?
sendTransaction() asks the connected wallet to sign and broadcast the outgoing message. Current TON Connect documentation says the response can contain the base64 BoC of the broadcast external message, which can then be used as part of an on-chain lookup or tracking flow.
A resolved wallet promise should therefore not be treated as "the swap is definitely complete."
There are several different states your interface may need to distinguish:
- the wallet request was opened
- the user approved the request
- the wallet broadcast the external message
- the relevant contract received and processed the internal message
- the swap completed successfully on-chain
- the resulting token balances became visible in your application
TON transactions are produced as contracts process messages, so wallet authorization and smart contract execution are related but not identical events.
For a production interface, update the UI only when you have enough evidence for the state you are displaying. "Transaction submitted" is more accurate immediately after wallet submission than "Swap completed."
Common mistakes when sending STON.fi swaps
Most integration failures around this final step come from a small number of mismatches.
Building from stale simulation data. If the amount or asset changes after simulation, create a new simulation before building the transaction.
Hardcoding a production router. Current STON.fi v2 guidance favors the router metadata returned by its mainnet API.
Recreating the payload manually. Let the STON.fi SDK produce the contract body unless you have a specific low-level reason to construct the message yourself.
Using the wrong time unit for validUntil. TON Connect currently expects Unix seconds, not JavaScript milliseconds.
Assuming wallet approval equals swap completion. Submission still has to result in successful on-chain contract execution.
Treating cancellation as an execution error. A user can simply reject the wallet prompt, and TON Connect exposes a specific rejection error for that case.
Testing copied mainnet addresses or amounts blindly. TON's current documentation explicitly warns that mainnet transfers are irreversible and recommends replacing sample values and testing appropriately before using real funds.
Practical takeaway: keep the integration boundary simple. Use the STON.fi API to simulate the current swap, let dexFactory() and the relevant router helper produce the exact transaction parameters, then pass those parameters almost unchanged into TON Connect. Your dApp should coordinate the process, not sign on behalf of the user and not guess the contract payload.
Frequently Asked Questions
Does my dApp need the user's TON wallet seed phrase to send a STON.fi swap?
No. A normal client-side TON Connect integration should not obtain the user's seed phrase or private key. Your application constructs the requested transaction, while the connected wallet handles approval, signing, and broadcasting. This separation is one of the main reasons to integrate through TON Connect rather than importing wallet credentials into the application.
What does the STON.fi SDK actually provide to TON Connect?
The SDK provides the information required for the contract message, including its destination, attached TON value, and payload cell. Your application serializes the payload to a base64 BoC and places these values inside the messages array passed to sendTransaction().
Why should I simulate the swap before sending it?
Simulation gives you the swap-specific values needed to review and build the transaction, including the minimum output and router information used by the current API-driven v2 workflow. It also gives your interface a natural point to show the expected result before asking the wallet for authorization.
Should validUntil use milliseconds or seconds?
Use Unix seconds. Current TON Connect documentation defines validUntil as Unix seconds and demonstrates it with Math.floor(Date.now() / 1000) + .... This is important because JavaScript's Date.now() alone returns milliseconds.
What does network: "-239" mean?
It identifies TON mainnet in the current TON Connect transaction request format. Testnet is identified as -3. Setting the network explicitly lets the wallet reject a transaction request if the connected account is on an incompatible network.
Can the same wallet flow handle TON-to-jetton and jetton-to-jetton swaps?
Yes, but the STON.fi SDK method used to construct the transaction differs by swap type. The final TON Connect step still follows the same basic model: take the resulting destination, attached value, and payload, then request wallet approval through sendTransaction().
Does a successful sendTransaction() response prove that the STON.fi swap finished?
Not by itself. It proves that the wallet-side transaction request reached the submission stage represented by TON Connect's response. Contract processing happens on-chain afterward, so a production dApp should track or verify the resulting operation before presenting the swap as completed.
What should I verify immediately before sending a STON.fi swap from a TON wallet?
Verify that the wallet is still connected, the simulation still matches the selected assets and amount, the minimum output reflects the intended slippage setting, the router comes from the current simulation, and the transaction parameters were generated by the matching STON.fi SDK helper. Only then transform those parameters into the TON Connect request shown to the wallet for approval.
Sources and Further Reading
- STON.fi Swap Guide (React) - Official end-to-end React example covering wallet connection, swap simulation, router selection, transaction construction, and TonConnect execution
- STON.fi Swap v2 documentation - Official v2 swap documentation describing the API-driven mainnet workflow and dynamic router construction
- STON.fi Transaction Sending - Official explanation of the
to,value, andbodytransaction parameters returned by SDK transaction builders - STON.fi Via TonConnect - Official STON.fi example for converting SDK transaction parameters into a TonConnect transaction request
- TON Connect Get Started - Official TON documentation for manifests, React integration, wallet connection, providers, and the TON Connect SDK choices
- TON Connect Send a Transaction - Official current specification for
sendTransaction,validUntil, network identifiers, raw messages, responses, and wallet errors - TON Messages and Transactions Overview - Official TON explanation of how messages are processed and how contract state changes become transactions
- STON.fi API Client Source - Official STON.fi API implementation showing the API client and router-related methods used by developer integrations







Top comments (0)