ESC
Start typing to search...

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/sdk installed 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.

dex-trader/index.ts — Manifest
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.

dex_quote tool
{
  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.

dex_swap tool
{
  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.

dex_portfolio tool
{
  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:

dex-trader/index.ts — Complete Plugin
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 priceImpact field 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 null for a DEX that lacks liquidity.

Common Pitfalls

PitfallSolution
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.