Tutorial: Build a DEX Trading Bot
Build a plugin that quotes and swaps tokens on STON.fi and DeDust DEXes.
Goal
By the end of this tutorial, you will have a plugin that:
- Compares token swap quotes across STON.fi and DeDust
- Executes swaps on the best-price DEX
- Shows the wallet's current jetton portfolio with USD values
Prerequisites
- Teleton Agent with a funded TON wallet (you need TON for gas fees and swaps)
- Familiarity with jetton addresses on TON (e.g., USDT, STON, SCALE)
@teleton-agent/sdkinstalled as a dev dependency
Warning: DEX swaps are irreversible blockchain transactions. Start with small amounts for testing. This tutorial uses real tokens on mainnet.
Step 1: Manifest
Define the plugin manifest. No secrets are needed since the SDK's DEX methods use the agent's configured TON wallet and TonAPI key internally.
import type { PluginSDK, SimpleToolDef, PluginManifest } from "@teleton-agent/sdk";
export const manifest: PluginManifest = {
name: "dex-trader",
version: "1.0.0",
description: "DEX trading bot — compare quotes and swap tokens on STON.fi and DeDust",
};Step 2: Tool — dex_quote
This tool fetches quotes from both STON.fi and DeDust, compares them, and recommends the best one. The sdk.ton.dex.quote() method handles all the comparison logic.
{
name: "dex_quote",
description: "Compare swap quotes from STON.fi and DeDust for a token pair",
parameters: {
type: "object",
properties: {
from: {
type: "string",
description: 'Source asset: "ton" or jetton master address',
},
to: {
type: "string",
description: 'Destination asset: "ton" or jetton master address',
},
amount: {
type: "number",
description: "Amount to swap in human-readable units (e.g. 10 for 10 TON)",
},
slippage: {
type: "number",
description: "Slippage tolerance (0.01 = 1%, default 0.01, range 0.001-0.5)",
},
},
required: ["from", "to", "amount"],
},
async execute(params) {
const quote = await sdk.ton.dex.quote({
fromAsset: params.from,
toAsset: params.to,
amount: params.amount,
slippage: params.slippage,
});
const result: Record<string, unknown> = {
recommended: quote.recommended,
savings: quote.savings,
};
if (quote.stonfi) {
result.stonfi = {
expectedOutput: quote.stonfi.expectedOutput,
minOutput: quote.stonfi.minOutput,
rate: quote.stonfi.rate,
priceImpact: quote.stonfi.priceImpact,
fee: quote.stonfi.fee,
};
} else {
result.stonfi = "No liquidity on STON.fi for this pair";
}
if (quote.dedust) {
result.dedust = {
expectedOutput: quote.dedust.expectedOutput,
minOutput: quote.dedust.minOutput,
rate: quote.dedust.rate,
priceImpact: quote.dedust.priceImpact,
fee: quote.dedust.fee,
poolType: quote.dedust.poolType,
};
} else {
result.dedust = "No liquidity on DeDust for this pair";
}
return result;
},
}Step 3: Tool — dex_swap
This tool executes the actual swap. It can auto-select the best DEX or be forced to use a specific one. The swap is an irreversible blockchain transaction.
{
name: "dex_swap",
description: "Execute a token swap on the best DEX (or a specific one). WARNING: Irreversible!",
parameters: {
type: "object",
properties: {
from: {
type: "string",
description: 'Source asset: "ton" or jetton master address',
},
to: {
type: "string",
description: 'Destination asset: "ton" or jetton master address',
},
amount: {
type: "number",
description: "Amount to swap in human-readable units",
},
slippage: {
type: "number",
description: "Slippage tolerance (default 0.01 = 1%)",
},
dex: {
type: "string",
description: 'Force a specific DEX: "stonfi" or "dedust" (omit for auto)',
enum: ["stonfi", "dedust"],
},
},
required: ["from", "to", "amount"],
},
async execute(params) {
// First get a quote to show the user what they're getting
const quote = await sdk.ton.dex.quote({
fromAsset: params.from,
toAsset: params.to,
amount: params.amount,
slippage: params.slippage,
});
const targetDex = params.dex || quote.recommended;
const targetQuote = targetDex === "stonfi" ? quote.stonfi : quote.dedust;
if (!targetQuote) {
return {
error: `No liquidity on ${targetDex} for this pair.`,
suggestion: quote.recommended !== targetDex
? `Try ${quote.recommended} instead.`
: "This pair may not have a pool on either DEX.",
};
}
// Execute the swap
const result = await sdk.ton.dex.swap({
fromAsset: params.from,
toAsset: params.to,
amount: params.amount,
slippage: params.slippage,
dex: params.dex as "stonfi" | "dedust" | undefined,
});
return {
success: true,
dex: result.dex,
amountIn: result.amountIn,
expectedOutput: result.expectedOutput,
minOutput: result.minOutput,
slippage: result.slippage,
message: `Swap submitted on ${result.dex}: ${result.amountIn} -> expected ${result.expectedOutput} (min ${result.minOutput})`,
};
},
}Step 4: Tool — dex_portfolio
A portfolio overview tool that lists all jetton holdings with current prices in USD.
{
name: "dex_portfolio",
description: "Show current jetton balances with USD values",
parameters: { type: "object", properties: {} },
async execute() {
const address = sdk.ton.getAddress();
if (!address) {
return { error: "Bot wallet not initialized." };
}
const [tonBalance, tonPrice, jettons] = await Promise.all([
sdk.ton.getBalance(),
sdk.ton.getPrice(),
sdk.ton.getJettonBalances(),
]);
const tonUsd = tonBalance && tonPrice
? parseFloat(tonBalance.balance) * tonPrice.usd
: 0;
// Fetch prices for each jetton in parallel
const jettonDetails = await Promise.all(
jettons.map(async (j) => {
const price = await sdk.ton.getJettonPrice(j.jettonAddress);
const balanceNum = parseFloat(j.balance);
const usdValue = price ? balanceNum * price.usd : null;
return {
symbol: j.symbol,
name: j.name,
balance: j.balance,
jettonAddress: j.jettonAddress,
priceUsd: price ? `$${price.usd.toFixed(6)}` : "unknown",
valueUsd: usdValue !== null ? `$${usdValue.toFixed(2)}` : "unknown",
};
})
);
const totalUsd = tonUsd + jettonDetails.reduce((sum, j) => {
const val = j.valueUsd.startsWith("$")
? parseFloat(j.valueUsd.slice(1))
: 0;
return sum + val;
}, 0);
return {
wallet: address,
ton: {
balance: tonBalance?.balance ?? "unknown",
priceUsd: tonPrice ? `$${tonPrice.usd.toFixed(2)}` : "unknown",
valueUsd: `$${tonUsd.toFixed(2)}`,
},
jettons: jettonDetails,
totalValueUsd: `$${totalUsd.toFixed(2)}`,
};
},
}Step 5: Complete Plugin Code
Here is the entire plugin file, ready to copy into your plugins directory:
import type { PluginSDK, SimpleToolDef, PluginManifest } from "@teleton-agent/sdk";
export const manifest: PluginManifest = {
name: "dex-trader",
version: "1.0.0",
description: "DEX trading bot — compare quotes and swap tokens on STON.fi and DeDust",
};
// Well-known jetton addresses for convenience
const KNOWN_JETTONS: Record<string, string> = {
usdt: "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs",
ston: "EQA2kCVNwVsil2EM2mB0SkXytxCqQjS4mttjDpnXmwG9T6bO",
scale: "EQBlqsm144Dq6SjbPI4jjZvlSHrzpwJKnL7MhRuRg1hIl-eH",
};
function resolveAsset(input: string): string {
const lower = input.toLowerCase();
return KNOWN_JETTONS[lower] || input;
}
export const tools = (sdk: PluginSDK): SimpleToolDef[] => [
{
name: "dex_quote",
description: "Compare swap quotes from STON.fi and DeDust for a token pair",
parameters: {
type: "object",
properties: {
from: {
type: "string",
description: 'Source asset: "ton", symbol (usdt/ston/scale), or jetton address',
},
to: {
type: "string",
description: 'Destination asset: "ton", symbol, or jetton address',
},
amount: {
type: "number",
description: "Amount to swap in human-readable units",
},
slippage: {
type: "number",
description: "Slippage tolerance (0.01 = 1%, default 0.01)",
},
},
required: ["from", "to", "amount"],
},
async execute(params) {
const quote = await sdk.ton.dex.quote({
fromAsset: resolveAsset(params.from),
toAsset: resolveAsset(params.to),
amount: params.amount,
slippage: params.slippage,
});
const result: Record<string, unknown> = {
recommended: quote.recommended,
savings: quote.savings,
};
if (quote.stonfi) {
result.stonfi = {
expectedOutput: quote.stonfi.expectedOutput,
minOutput: quote.stonfi.minOutput,
rate: quote.stonfi.rate,
priceImpact: quote.stonfi.priceImpact,
fee: quote.stonfi.fee,
};
} else {
result.stonfi = "No liquidity on STON.fi";
}
if (quote.dedust) {
result.dedust = {
expectedOutput: quote.dedust.expectedOutput,
minOutput: quote.dedust.minOutput,
rate: quote.dedust.rate,
priceImpact: quote.dedust.priceImpact,
fee: quote.dedust.fee,
poolType: quote.dedust.poolType,
};
} else {
result.dedust = "No liquidity on DeDust";
}
return result;
},
},
{
name: "dex_swap",
description: "Execute a token swap on the best DEX. WARNING: This is an irreversible blockchain transaction!",
parameters: {
type: "object",
properties: {
from: {
type: "string",
description: 'Source asset: "ton", symbol, or jetton address',
},
to: {
type: "string",
description: 'Destination asset: "ton", symbol, or jetton address',
},
amount: {
type: "number",
description: "Amount to swap in human-readable units",
},
slippage: {
type: "number",
description: "Slippage tolerance (default 0.01 = 1%)",
},
dex: {
type: "string",
description: 'Force a specific DEX: "stonfi" or "dedust" (omit for auto)',
enum: ["stonfi", "dedust"],
},
},
required: ["from", "to", "amount"],
},
async execute(params) {
const fromAsset = resolveAsset(params.from);
const toAsset = resolveAsset(params.to);
// Pre-check: get a quote first
const quote = await sdk.ton.dex.quote({
fromAsset,
toAsset,
amount: params.amount,
slippage: params.slippage,
});
const targetDex = params.dex || quote.recommended;
const targetQuote = targetDex === "stonfi" ? quote.stonfi : quote.dedust;
if (!targetQuote) {
return {
error: `No liquidity on ${targetDex} for this pair.`,
suggestion: quote.recommended !== targetDex
? `Try ${quote.recommended} instead.`
: "This pair may not have a pool on either DEX.",
};
}
// Execute the swap
const result = await sdk.ton.dex.swap({
fromAsset,
toAsset,
amount: params.amount,
slippage: params.slippage,
dex: params.dex as "stonfi" | "dedust" | undefined,
});
return {
success: true,
dex: result.dex,
amountIn: result.amountIn,
expectedOutput: result.expectedOutput,
minOutput: result.minOutput,
slippage: result.slippage,
message: `Swap submitted on ${result.dex}: ${result.amountIn} -> expected ${result.expectedOutput} (min ${result.minOutput})`,
};
},
},
{
name: "dex_portfolio",
description: "Show current jetton balances with USD values and total portfolio value",
parameters: { type: "object", properties: {} },
async execute() {
const address = sdk.ton.getAddress();
if (!address) {
return { error: "Bot wallet not initialized." };
}
const [tonBalance, tonPrice, jettons] = await Promise.all([
sdk.ton.getBalance(),
sdk.ton.getPrice(),
sdk.ton.getJettonBalances(),
]);
const tonUsd = tonBalance && tonPrice
? parseFloat(tonBalance.balance) * tonPrice.usd
: 0;
const jettonDetails = await Promise.all(
jettons.map(async (j) => {
const price = await sdk.ton.getJettonPrice(j.jettonAddress);
const balanceNum = parseFloat(j.balance);
const usdValue = price ? balanceNum * price.usd : null;
return {
symbol: j.symbol,
name: j.name,
balance: j.balance,
jettonAddress: j.jettonAddress,
priceUsd: price ? `$${price.usd.toFixed(6)}` : "unknown",
valueUsd: usdValue !== null ? `$${usdValue.toFixed(2)}` : "unknown",
};
})
);
const totalUsd = tonUsd + jettonDetails.reduce((sum, j) => {
const val = j.valueUsd.startsWith("$")
? parseFloat(j.valueUsd.slice(1))
: 0;
return sum + val;
}, 0);
return {
wallet: address,
ton: {
balance: tonBalance?.balance ?? "unknown",
priceUsd: tonPrice ? `$${tonPrice.usd.toFixed(2)}` : "unknown",
valueUsd: `$${tonUsd.toFixed(2)}`,
},
jettons: jettonDetails,
totalValueUsd: `$${totalUsd.toFixed(2)}`,
};
},
},
];Risk Warnings
- Irreversible: DEX swaps are on-chain transactions. Once submitted, they cannot be reversed or cancelled.
- Slippage: The default 1% slippage may not be enough for volatile tokens or low-liquidity pools. Increase slippage for small-cap tokens, but be aware of MEV/sandwich attacks.
- Price impact: Large swaps relative to pool size will move the price significantly. Check the
priceImpactfield in quotes before executing. - Gas fees: Each swap consumes TON for gas. Keep a reserve of at least 0.5 TON for transaction fees.
- Pool availability: Not all token pairs have pools on both DEXes. The quote tool will return
nullfor a DEX that lacks liquidity.
Common Pitfalls
| Pitfall | Solution |
|---|---|
| Using token symbols instead of addresses | The SDK expects "ton" or jetton master contract addresses. Map symbols to addresses in your plugin (as shown with KNOWN_JETTONS). |
| Slippage out of range | Slippage must be between 0.001 (0.1%) and 0.5 (50%). Values outside this range throw OPERATION_FAILED. |
| Swap fails with "insufficient balance" | Check that the wallet has enough of the source token plus TON for gas fees (~0.1-0.3 TON per swap). |
| Quote succeeds but swap fails | Quotes are snapshots. Pool state can change between quote and swap. Use a slightly higher slippage or retry. |
| Float precision in amounts | Pass amounts as simple numbers (e.g. 10, 1.5). The SDK uses string-based decimal conversion internally. |