DEV Community

Cover image for SDK Integration Walkthrough: Building Your First STON.fi Swap Feature
Web3KD
Web3KD

Posted on

SDK Integration Walkthrough: Building Your First STON.fi Swap Feature

Adding a token swap to a dApp sounds simple until you actually sit down to do it: you need live pricing, a way to route through the best available liquidity, transaction building that matches the target chain's quirks, wallet signing, and a way to track whether the trade actually went through. On TON, STON.fi's SDKs handle most of that heavy lifting for you - specifically the Omniston layer, which aggregates quotes across on-chain pools and off-chain RFQ resolvers and hands you back a ready-to-sign transaction. This walkthrough builds a working swap feature from a blank React project to a working "Swap" button, using the official @ston-fi/omniston-sdk-react package, TonConnect for wallet signing, and the actual API surface exposed by the SDK as of its v1beta8 protocol version.

By the end, you'll have a component that lets a user pick two tokens, see a live quote update as market conditions shift, sign a transaction with their wallet, and watch the swap's status change from pending to confirmed - all without writing a single line of raw smart contract code.

What You'll Need Before Starting

  • Node.js 18+ and a package manager (npm, yarn, or pnpm)
  • A React project (Vite or Next.js both work fine - this walkthrough uses a generic React setup so it's easy to adapt)
  • A TON wallet for testing - Tonkeeper or MyTonWallet both support TonConnect
  • Basic familiarity with React hooks and async/await - no prior TON or blockchain development experience is required

STON.fi actually ships two related but distinct SDK families: the classic


@ston-fi/sdk,

which talks directly to STON.fi's own Router and Pool contracts, and


@ston-fi/omniston-sdk

(plus its React binding), which talks to Omniston - STON.fi's aggregation layer that shops a trade across multiple liquidity sources, not just STON.fi's own pools, and returns whichever route quotes best. For a new swap feature in 2026, Omniston is the more future-proof choice: it gets you STON.fi's own liquidity and other connected sources through one consistent API, so that's what this walkthrough uses.

Step 1: Install the Dependencies

Start by scaffolding a React project if you don't already have one, then add the Omniston React SDK alongside TonConnect for wallet interaction.


npm install @ston-fi/omniston-sdk-react
npm install @tonconnect/ui-react


@ston-fi/omniston-sdk-react

is a TypeScript-first package built on top of RxJS observables and TanStack Query, so you get loading states, retries, and stream handling out of the box instead of writing that plumbing yourself.


@tonconnect/ui-react

gives you a drop-in wallet connect button and a signer you'll use later to actually authorize the swap transaction.

Step 2: Wire Up Wallet Connection

Before a user can swap anything, they need a connected wallet. TonConnect handles the connection modal, deep-linking into wallet apps, and session persistence for you.


// src/providers/WalletProvider.tsx
import { TonConnectUIProvider } from "@tonconnect/ui-react";

const manifestUrl = "https://your-app-domain.com/tonconnect-manifest.json";
export function WalletProvider({ children }: React.PropsWithChildren) {
return (
<TonConnectUIProvider manifestUrl={manifestUrl}>
{children}
</TonConnectUIProvider>
);
}

The tonconnect-manifest.json file needs to be hosted at a publicly reachable URL and describes your app's name, icon, and domain to the wallet the user connects with - this is a TonConnect requirement, not a STON.fi-specific one, and most starter templates include a sample manifest you can adapt.
With the provider in place, dropping a connect button anywhere in your UI is a one-liner:

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

function Header() {
return (
<header>
<TonConnectButton />
</header>
);
}

Step 3: Set Up the Omniston Provider

Next, create an Omniston instance and wrap your app (or just the swap feature's subtree) in OmnistonProvider. This gives every component under it access to Omniston's hooks without prop-drilling a client instance around.

// src/providers/OmnistonProvider.tsx
import { Omniston, OmnistonProvider } from "@ston-fi/omniston-sdk-react";

const omniston = new Omniston({
apiUrl: "wss://omni-ws.ston.fi",
});
export function AppOmnistonProvider({ children }: React.PropsWithChildren) {
return <OmnistonProvider omniston={omniston}>{children}</OmnistonProvider>;
}

While you're building and testing, point at the sandbox endpoint instead so test trades don't touch real liquidity:

const omniston = new Omniston({
apiUrl: "wss://omni-ws-sandbox.ston.fi",
});

If your app already has a TanStack Query client set up elsewhere, pass it in so Omniston reuses it instead of spinning up a second one:

<OmnistonProvider omniston={omniston} queryClient={queryClient}>
{children}
</OmnistonProvider>

Nest this provider inside your TonConnectUIProvider from Step 2 - the order doesn't matter functionally, but keeping wallet context outermost tends to make the component tree easier to reason about as the app grows.

Step 4: Define What You're Swapping

Omniston identifies tokens with an AssetId structure rather than a raw contract address string, which keeps the API consistent across chains (TON, EVM chains, and whatever gets added later). For a TON-native jetton swap - say, USDT to STON - that looks like this:

// src/features/swap/assets.ts
import type { AssetId } from "@ston-fi/omniston-sdk-react";

export const USDT: AssetId = {
chain: {
$case: "ton",
value: {
kind: {
$case: "jetton",
value: "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs",
},
},
},
};
export const STON: AssetId = {
chain: {
$case: "ton",
value: {
kind: {
$case: "jetton",
value: "EQA2kCVNwVsil2EM2mB0SkXytxCqQjS4mttjDpnXmwG9T6bO",
},
},
},
};

In a real app you'd source this list from a token registry or search API rather than hardcoding it, but hardcoding two tokens is the fastest way to get a working prototype before you build out a token picker UI.

Step 5: Request a Live Quote

This is where Omniston's aggregation actually happens. useRfq() opens a subscription - not a one-shot HTTP request - because prices move and Omniston keeps pushing updated quotes as long as you're subscribed. You also need to decide up front which settlement methods you're willing to accept: swap for classic on-chain execution, order for signed-order/HTLC flows (useful for cross-chain trades), or both if you want Omniston to pick whichever is best.

// src/features/swap/useSwapQuote.ts
import {
useRfq,
type QuoteRequest,
type SettlementParams,
} from "@ston-fi/omniston-sdk-react";
import { USDT, STON } from "./assets";

const settlementParams: SettlementParams[] = [
{
params: {
$case: "swap",
value: {
maxPriceSlippagePips: 10_000, // 1% max slippage
flexibleIntegratorFee: true,
},
},
},
];
export function useSwapQuote(amountInBaseUnits: string) {
const quoteRequest: QuoteRequest = {
inputAsset: USDT,
outputAsset: STON,
amount: {
$case: "inputUnits",
value: amountInBaseUnits,
},
settlementParams,
};
return useRfq(quoteRequest);
}

And the component that consumes it:

// src/features/swap/QuotePreview.tsx
import { useSwapQuote } from "./useSwapQuote";

export function QuotePreview({ amount }: { amount: string }) {
const { data: event, error } = useSwapQuote(amount);
if (error) {
return <p className="error">Couldn't fetch a quote right now.</p>;
}
switch (event?.$case) {
case "ack":
return <p>Looking for the best rate…</p>;
case "quoteUpdated":
return <p>Quote ready - id: {event.value.quoteId}</p>;
case "noQuote":
return <p>No route available for this pair right now.</p>;
case "unsubscribed":
return <p>Quote stream closed.</p>;
default:
return <p>Waiting for quote…</p>;
}
}

A few things worth flagging here for anyone integrating this for the first time: the maxPriceSlippagePips value is in "pips" (hundredths of a basis point), so 10_000 means 1% - it's easy to be off by an order of magnitude here if you're used to plain percentage inputs, so double-check this value against your UI's slippage setting before shipping. Second, quoteUpdated can fire more than once for the same RFQ - that's the aggregator refreshing the price as the request stays open, not a bug - so store the latest quote in state rather than assuming the first one you see is final.

Step 6: Build the Swap Transaction

Once you have a quote and the user has confirmed they want to proceed, branch on quote.settlementData?.$case to figure out whether you're in a swap flow or an order flow, then call the matching builder. For a straightforward TON-to-TON swap, that's useTonBuildSwap().

// src/features/swap/useBuildSwapTx.ts
import {
useTonBuildSwap,
type ChainAddress,
type QuoteOfType,
} from "@ston-fi/omniston-sdk-react";

export function useBuildSwapTx(
quote: QuoteOfType<"swap">,
traderAddress: ChainAddress
) {
return useTonBuildSwap({
quoteId: quote.quoteId,
transferSrcAddress: traderAddress,
refundSrcAddress: traderAddress,
gasExcessAddress: traderAddress,
traderDstAddress: traderAddress,
});
}

Each of those four address fields serves a distinct purpose, and getting them wrong is one of the more common integration mistakes: transferSrcAddress is where the input tokens are pulled from, refundSrcAddress is where funds go if the swap fails partway, gasExcessAddress recovers any unused gas, and traderDstAddress is where the output tokens land. In the common case they're all the same connected wallet address, but keeping them as separate parameters means your app can support more advanced flows later (like swapping on behalf of a smart contract wallet) without changing the API shape.

Step 7: Sign and Send the Transaction with TonConnect

useTonBuildSwap() gives you back a set of unsigned messages - it doesn't sign or broadcast anything itself. That's intentional: signing is wallet territory, and Omniston stays wallet-agnostic. Here's how to hand those messages to TonConnect:

// src/features/swap/SwapButton.tsx
import { useTonConnectUI, useTonAddress } from "@tonconnect/ui-react";
import { useBuildSwapTx } from "./useBuildSwapTx";
import type { QuoteOfType } from "@ston-fi/omniston-sdk-react";

export function SwapButton({ quote }: { quote: QuoteOfType<"swap"> }) {
const [tonConnectUI] = useTonConnectUI();
const rawAddress = useTonAddress();
const traderAddress = {
chain: { $case: "ton" as const, value: rawAddress },
};
const { data: swapTx } = useBuildSwapTx(quote, traderAddress);
async function handleSwap() {
if (!swapTx) return;
await tonConnectUI.sendTransaction({
validUntil: Math.floor(Date.now() / 1000) + 300, // 5-minute validity window
messages: swapTx.messages.map((message) => ({
address: message.address,
amount: message.amount,
payload: message.payload,
})),
});
}
return (
<button onClick={handleSwap} disabled={!swapTx}>
Confirm Swap
</button>
);
}

The validUntil field matters more than it looks like it should: if you set it too far in the future, a stale transaction can sit in the wallet's queue and eventually execute against a quote that's no longer accurate, potentially at a much worse price than the user agreed to. A five-minute window is a reasonable default for most swap UIs - long enough that a slow wallet app doesn't time out, short enough that market movement doesn't invalidate the trade.

Step 8: Track the Swap's Status

Sending the transaction isn't the end of the story - the user still wants to know whether it actually went through. swapTrack() gives you a live status stream keyed off the quote and the outgoing transaction.

// src/features/swap/useSwapTracking.ts
import { useEffect, useState } from "react";
import { useOmniston, type ChainAddress, type Quote } from "@ston-fi/omniston-sdk-react";

export function useSwapTracking(
quote: Quote,
traderAddress: ChainAddress,
outgoingTxQuery: string
) {
const omniston = useOmniston();
const [status, setStatus] = useState<string>("pending");
useEffect(() => {
let unsubscribe = () => {};
void omniston
.swapTrack({
quoteId: quote.quoteId,
traderAddress,
outgoingTxQuery,
})
.then((stream) => {
const subscription = stream.subscribe({
next(event) {
switch (event?.$case) {
case "awaitingTransfer":
setStatus("Waiting for wallet transfer to confirm…");
break;
case "progress":
setStatus(event.value.status);
break;
case "unsubscribed":
setStatus("Tracking stream closed");
break;
}
},
});
unsubscribe = () => subscription.unsubscribe();
});
return () => unsubscribe();
}, [omniston, quote.quoteId, traderAddress, outgoingTxQuery]);
return status;
}

outgoingTxQuery can be a transaction hash, message hash, or the outgoing message body - whichever identifier your wallet integration surfaces most reliably after sendTransaction() resolves. TonConnect's sendTransaction() response includes a boc (bag of cells) you can decode to extract this, though the exact extraction step depends on which TonConnect version and helper library you're using, so it's worth checking the current TonConnect docs for the specific decode helper rather than assuming the API surface hasn't shifted.

Common Pitfalls Worth Knowing Before You Ship

Treating the RFQ stream as a single request. Because useRfq() is a subscription, holding a quote in state and never re-checking it before building the transaction can mean signing against a price that's since moved. Always build the transaction from the latest quote event, not whatever was first received.

Skipping the sandbox environment. It's tempting to point straight at production (wss://omni-ws.ston.fi) while developing, but the sandbox endpoint exists precisely so test trades don't compete for real liquidity or risk real funds during development - use it until your flow is stable.

Assuming every quote is a swap quote. Because Omniston can return either swap or order settlement data depending on route and settlement params, code that only handles useTonBuildSwap() will silently break the moment a quote comes back as an order (which happens more often for cross-chain routes). Always branch on quote.settlementData?.$case rather than assuming one shape.

Ignoring the SDK's pre-1.0 status. As of writing, @ston-fi/omniston-sdk is still under active development with a major version of zero, meaning breaking changes can land in minor releases under semantic versioning conventions. Pin your version explicitly in package.json rather than trusting a caret range, and check the changelog before bumping.

Going From Prototype to Production

A few things worth doing before this ships to real users, beyond what's covered above:

Hardcode fewer addresses. Replace the hardcoded USDT/STON asset IDs from Step 4 with a proper token search or registry integration so users can swap any supported pair, not just the two you picked for testing.

Surface slippage as a user-facing setting, rather than a fixed constant in code - traders on volatile pairs often want to widen or tighten maxPriceSlippagePips themselves.

Handle the noQuote case gracefully in your UI instead of leaving a blank state - it happens for illiquid pairs or when input size exceeds what any connected resolver can fill.

Add error boundaries around the wallet-signing step specifically, since users rejecting a transaction in their wallet app is a normal, expected outcome your UI needs to handle without looking broken.

Wrapping Up

What you've built here - a live quote stream, a build-and-sign flow through TonConnect, and status tracking - is the same core loop every swap feature on TON ultimately needs, whether it's a full DEX front end or a single "swap" button tucked into an unrelated dApp. The heavy lifting Omniston does for you (multi-source routing, quote refreshing, settlement-type branching) is exactly the part that's tedious and error-prone to hand-roll against raw contracts. From here, the natural next steps are wiring in a real token list, adding the order/HTLC flow for cross-chain swaps if your app needs them, and hardening the edge cases called out above before pointing the integration at production.

🔗 Sources & Further Reading

STON.fi Developer Docs - Introduction: https://docs.ston.fi/
Omniston SDK - Node.js Quickstart: https://docs.ston.fi/developer-section/omniston/sdk/nodejs
Omniston SDK - React Quickstart: https://docs.ston.fi/developer-section/omniston/sdk/react
Omniston Protocol Overview: https://docs.ston.fi/developer-section/omniston
@ston-fi/omniston-sdk on npm: https://www.npmjs.com/package/@ston-fi/omniston-sdk
@ston-fi/omniston-sdk-react on npm: https://www.npmjs.com/package/@ston-fi/omniston-sdk-react
@ston-fi/sdk (classic DEX SDK) on npm: https://www.npmjs.com/package/@ston-fi/sdk
STON.fi SDK - GitHub source: https://github.com/ston-fi/sdk
Omniston SDK - GitHub source and example React app: https://github.com/ston-fi/omniston-sdk/tree/main/examples/react-app
Live SDK demo app: https://sdk-demo-app.ston.fi
TonConnect UI React docs: https://docs.tonconnect.io/docs/quick-start/react
TON Core - @ton/ton package installation guide: https://www.npmjs.com/package/@ton/ton

This walkthrough is based on the STON.fi and Omniston SDK documentation as of mid-2026 (protocol version v1beta8). Both the SDK API surface and the underlying protocol are under active development - verify method signatures and package versions against the current docs at docs.ston.fi before shipping to production, and check the official demo app (sdk-demo-app.ston.fi) for a working reference implementation.

Top comments (1)

Collapse
 
ivan_cryptovazimazima profile image
Ivan “Crypto Vazima” Zimanov

Nice! Thanks for info!