Provide Liquidity (v2)
Provide liquidity on STON.fi v2 - single-sided deposits with automatic vault management
If you are simulating a liquidity provision with the simulate liquidity provision endpoint and your LP account already holds some tokens (e.g., "lp_account_token_a_balance": "more than 0" or "lp_account_token_b_balance": "more than 0"), setting token_a_units or token_b_units in request might lead to an inaccurate min_lp_units in the simulation result. This could result in permanent loss of funds. We strongly encourage you to refund any old LP account tokens before proceeding.
The production-ready pattern is API-driven. Simulate the provision first, let the STON.fi API tell you which router to hit, and then build contract instances dynamically. This keeps your integration compatible with future router upgrades and avoids hardcoding contract addresses.
Mainnet-first workflow
STON.fi’s REST API (api.ston.fi) only serves mainnet data, so every production liquidity provision should follow this flow:
Simulate the provision to obtain routing metadata (router address, pool address, token amounts).
Fetch router metadata via
client.getRouter().Build contracts dynamically with
dexFactory().Generate the liquidity provision transaction parameters with the router helpers.
import { Client, dexFactory, toUnits } from "@ston-fi/sdk";
import { StonApiClient } from "@ston-fi/api";
const tonClient = new Client({
endpoint: "https://toncenter.com/api/v2/jsonRPC",
});
const apiClient = new StonApiClient();
const userWalletAddress = "<your wallet address>";
const tokenA = { address: "<asset A address or 'ton'>", decimals: 9 };
const tokenB = { address: "<asset B address>", decimals: 9 };
const amountA = "10"; // human-readable amount (e.g. "10")
const amountB = "5"; // human-readable amount for the second token
const slippageTolerance = "0.001";
// Discover existing pools for the pair. If none are found we are creating a new pool.
const [poolInfo] = await apiClient.getPoolsByAssetPair({
asset0Address: tokenA.address,
asset1Address: tokenB.address,
});
const provisionType = poolInfo ? "Balanced" : "Initial"; // or "Arbitrary" for manual ratios
const simulationInput: Parameters<StonApiClient["simulateLiquidityProvision"]>[0] = {
tokenA: tokenA.address,
tokenB: tokenB.address,
provisionType,
slippageTolerance,
walletAddress: userWalletAddress,
...(poolInfo ? { poolAddress: poolInfo.address } : {}),
};
if (provisionType === "Initial") {
simulationInput.tokenAUnits = toUnits(amountA, tokenA.decimals).toString();
simulationInput.tokenBUnits = toUnits(amountB, tokenB.decimals).toString();
} else if (provisionType === "Balanced") {
// Supply **one** side; the API calculates the matching amount for the other token.
simulationInput.tokenAUnits = toUnits(amountA, tokenA.decimals).toString();
} else {
// "Arbitrary" – provide any ratio. To go single-sided, set one of the amounts to "0".
simulationInput.tokenAUnits = toUnits(amountA, tokenA.decimals).toString();
simulationInput.tokenBUnits = toUnits(amountB, tokenB.decimals).toString();
}
const simulationResult = await apiClient.simulateLiquidityProvision(simulationInput);
// 2. Load router metadata based on simulation result
const routerMetadata = await apiClient.getRouter(simulationResult.routerAddress);
const dexContracts = dexFactory(routerMetadata);
// 3. Open the router contract
const router = tonClient.open(
dexContracts.Router.create(routerMetadata.address)
);
// Optional helper when TON is part of the provision
const proxyTon = dexContracts.pTON.create(routerMetadata.ptonMasterAddress);The simulationResult object contains provisionType, tokenA, tokenB, tokenAUnits, tokenBUnits, minLpUnits, and routerAddress. Reuse those values when building the actual provision transaction to ensure the signed payload matches the simulated path.
Interpreting simulation results
Initial – no pool exists yet. Provide positive amounts for both assets; the router will deploy the pool and mint the first LP tokens.
Balanced – an existing pool is topped up at the current ratio. Provide one token amount (the other is computed by the API). The result always contains two positive
token*Unitsvalues.Arbitrary – add liquidity to an existing pool using any ratio. Provide explicit amounts for both tokens. To perform a single-sided deposit, set one of the amounts to
"0"; the SDK will produce a single message that leans on the vault mechanics.
Generating transaction messages
Define the canonical TON address and map the simulation result to message “legs”. Filter out empty amounts so that single-sided Arbitrary provisions naturally collapse to one message.
const TON_ADDRESS = "EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c";
const legs = [
{
userWalletAddress,
minLpOut: simulationResult.minLpUnits,
sendTokenAddress: simulationResult.tokenA,
sendAmount: simulationResult.tokenAUnits,
otherTokenAddress: simulationResult.tokenB,
},
{
userWalletAddress,
minLpOut: simulationResult.minLpUnits,
sendTokenAddress: simulationResult.tokenB,
sendAmount: simulationResult.tokenBUnits,
otherTokenAddress: simulationResult.tokenA,
},
].filter(({ sendAmount }) => BigInt(sendAmount) > 0n);
const isSingleSide =
simulationResult.provisionType === "Arbitrary" && legs.length === 1;
const txParams = await Promise.all(
legs.map((leg) => {
if (leg.sendTokenAddress === TON_ADDRESS) {
const tonLeg = {
...leg,
proxyTon,
};
return isSingleSide
? router.getSingleSideProvideLiquidityTonTxParams(tonLeg)
: router.getProvideLiquidityTonTxParams(tonLeg);
}
const jettonLeg = {
...leg,
otherTokenAddress:
leg.otherTokenAddress === TON_ADDRESS
? proxyTon.address.toString()
: leg.otherTokenAddress,
};
return isSingleSide
? router.getSingleSideProvideLiquidityJettonTxParams(jettonLeg)
: router.getProvideLiquidityJettonTxParams(jettonLeg);
}),
);Send the resulting txParams array in a single transaction. See our transaction sending guide for wallet-specific examples.
Execute the transaction using your preferred wallet integration. TonConnect, Tonkeeper SDK, custodial signers, and other libraries all accept the txParams generated above.
Testnet provisions (manual setup)
If you really need to exercise liquidity provision on the TON testnet, you must fall back to hardcoded contracts because api.ston.fi serves mainnet only. Liquidity is scarce, so plan to source or mint the jettons yourself, create and fund the required pools, and only then attempt the provision.
To mirror the examples above on testnet, testers often use TesREED/TestBlue and run both single-sided and balanced deposits.
import { TonClient, toNano } from "@ton/ton";
import { DEX, pTON } from "@ston-fi/sdk";
const client = new TonClient({
endpoint: "https://testnet.toncenter.com/api/v2/jsonRPC",
});
const router = client.open(
DEX.v2_1.Router.CPI.create("kQALh-JBBIKK7gr0o4AVf9JZnEsFndqO0qTCyT-D-yBsWk0v") // CPI Router v2.1.0 (testnet)
);
const proxyTon = pTON.v2_1.create("kQACS30DNoUQ7NfApPvzh7eBmSZ9L4ygJ-lkNWtba8TQT-Px"); // pTON v2.1.0 (testnet)
const txParams = await router.getProvideLiquidityJettonTxParams({
userWalletAddress: "<your testnet wallet>",
sendTokenAddress: "kQDLvsZol3juZyOAVG8tWsJntOxeEZWEaWCbbSjYakQpuYN5", // TesREED jetton (testnet)
sendAmount: toNano("1"),
otherTokenAddress: "kQB_TOJSB7q3-Jm1O8s0jKFtqLElZDPjATs5uJGsujcjznq3", // TestBlue jetton (testnet)
minLpOut: "1",
queryId: 12345,
});This manual approach is strictly for testing. Switch back to the API-driven workflow for anything mainnet facing.
Last updated