DEV Community

Moon Soon
Moon Soon

Posted on • Originally published at swapapi.dev

How to Add Token Swaps to a Telegram Bot

Telegram has over 1 billion monthly active users, and crypto trading bots are one of the fastest-growing categories on the platform. Bots like Banana Gun and Trojan have processed over $40 billion in cumulative swap volume, proving that users want to trade tokens without leaving their chat window. Adding token swaps to your own Telegram bot is simpler than you think: a single GET request to swapapi.dev returns ready-to-execute calldata, no API key required.

This guide walks you through building a Telegram bot in TypeScript that fetches swap quotes, displays them to users, and submits on-chain transactions — all from a /swap command.

What You'll Need

Before you start, make sure you have:

  • Node.js 18+ installed
  • A Telegram account to create a bot via BotFather
  • ethers.js v6 for on-chain transaction signing
  • grammY — a TypeScript-first Telegram bot framework
  • A wallet private key for signing (use a test wallet with small amounts)
  • Basic TypeScript and Ethereum knowledge

Here is a quick comparison of the two most popular TypeScript Telegram bot frameworks to justify the choice:

Feature grammY Telegraf
TypeScript support First-class, built in TS Typed but JS-first
Runtime Node.js, Deno, Bun Node.js only
Middleware Composer-based, type-safe Similar composer pattern
Plugin ecosystem 30+ official plugins Mature, large community
Documentation Excellent, interactive Good but less structured
Weekly npm downloads ~95,000 ~130,000

Both are solid choices. This guide uses grammY because its TypeScript types are more precise and it runs on Bun natively — a good fit since swapapi.dev itself runs on Bun + Hono.

Step 1: Create Your Telegram Bot with BotFather

Open Telegram, search for @BotFather, and send /newbot. Follow the prompts to name your bot and get an API token.

# BotFather will give you a token like this:
# 7123456789:AAH1234abcd5678efgh-XYZTOKEN
Enter fullscreen mode Exit fullscreen mode

Store this token in a .env file:

BOT_TOKEN=7123456789:AAH1234abcd5678efgh-XYZTOKEN
PRIVATE_KEY=0xYourWalletPrivateKeyHere
Enter fullscreen mode Exit fullscreen mode

Telegram has 500 million daily active users as of 2026, and bots are a first-class feature of the platform. BotFather is the official tool for registering bots — each bot gets a unique token that authenticates API calls to the Telegram Bot API.

:::warning
Never commit your .env file or share your private key. Use a dedicated hot wallet with minimal funds for bot operations.
:::

Step 2: Scaffold the Project

Initialize a Node.js project and install dependencies:

mkdir swap-telegram-bot && cd swap-telegram-bot
npm init -y
npm install grammy ethers dotenv
npm install -D typescript @types/node
npx tsc --init
Enter fullscreen mode Exit fullscreen mode

Create the entry point at src/bot.ts:

import { Bot } from "grammy";
import { config } from "dotenv";

config();

const bot = new Bot(process.env.BOT_TOKEN!);

bot.command("start", (ctx) =>
  ctx.reply(
    "Welcome! Use /swap to swap tokens.\n\n" +
    "Example: /swap 0.1 ETH USDC base"
  )
);

bot.start();
console.log("Bot is running...");
Enter fullscreen mode Exit fullscreen mode

Run it with npx ts-node src/bot.ts or bun src/bot.ts and send /start to your bot in Telegram. You should get a welcome message. With roughly 2.5 million new users joining Telegram daily, your bot has a growing audience from day one.

Step 3: Build the Swap Command Handler

The /swap command parses user input into the parameters swapapi.dev needs: chain, token pair, and amount. The API supports 46 EVM chains, so we define a lookup map for common chain names.

import { Context } from "grammy";

const CHAINS: Record<string, number> = {
  ethereum: 1,
  base: 8453,
  arbitrum: 42161,
  polygon: 137,
  optimism: 10,
  bsc: 56,
  avalanche: 43114,
};

const TOKENS: Record<string, Record<string, { address: string; decimals: number }>> = {
  "1": {
    ETH: { address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", decimals: 18 },
    WETH: { address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", decimals: 18 },
    USDC: { address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", decimals: 6 },
    USDT: { address: "0xdAC17F958D2ee523a2206206994597C13D831ec7", decimals: 6 },
  },
  "8453": {
    ETH: { address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", decimals: 18 },
    WETH: { address: "0x4200000000000000000000000000000000000006", decimals: 18 },
    USDC: { address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", decimals: 6 },
  },
  "42161": {
    ETH: { address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", decimals: 18 },
    WETH: { address: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", decimals: 18 },
    USDC: { address: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", decimals: 6 },
  },
};

function parseSwapCommand(text: string) {
  // Expected: /swap 0.1 ETH USDC base
  const parts = text.trim().split(/\s+/);
  if (parts.length < 5) return null;

  const amount = parseFloat(parts[1]);
  const tokenInSymbol = parts[2].toUpperCase();
  const tokenOutSymbol = parts[3].toUpperCase();
  const chainName = parts[4].toLowerCase();

  const chainId = CHAINS[chainName];
  if (!chainId || isNaN(amount)) return null;

  const chainTokens = TOKENS[String(chainId)];
  if (!chainTokens) return null;

  const tokenIn = chainTokens[tokenInSymbol];
  const tokenOut = chainTokens[tokenOutSymbol];
  if (!tokenIn || !tokenOut) return null;

  const rawAmount = BigInt(Math.floor(amount * 10 ** tokenIn.decimals)).toString();

  return { chainId, tokenIn, tokenOut, rawAmount, tokenInSymbol, tokenOutSymbol, amount };
}
Enter fullscreen mode Exit fullscreen mode

This parser handles the most common tokens on Ethereum, Base, and Arbitrum. You can expand the TOKENS map to cover all 46 supported chains — see the full chain list for every token address.

Step 4: Fetch a Swap Quote from swapapi.dev

The core integration is a single GET request. No API key, no OAuth, no account setup. The swapapi.dev API returns executable swap calldata in one call.

interface SwapResponse {
  success: boolean;
  data: {
    status: "Successful" | "Partial" | "NoRoute";
    tokenFrom: { symbol: string; decimals: number };
    tokenTo: { symbol: string; decimals: number };
    expectedAmountOut: string;
    minAmountOut: string;
    priceImpact: number;
    amountIn: string;
    tx: {
      from: string;
      to: string;
      data: string;
      value: string;
      gasPrice: number;
    };
    rpcUrls: string[];
  };
  timestamp: string;
}

async function getSwapQuote(
  chainId: number,
  tokenIn: string,
  tokenOut: string,
  amount: string,
  sender: string
): Promise<SwapResponse> {
  const url = new URL(`https://api.swapapi.dev/v1/swap/${chainId}`);
  url.searchParams.set("tokenIn", tokenIn);
  url.searchParams.set("tokenOut", tokenOut);
  url.searchParams.set("amount", amount);
  url.searchParams.set("sender", sender);
  url.searchParams.set("maxSlippage", "0.005");

  const res = await fetch(url.toString(), {
    signal: AbortSignal.timeout(15_000), // 15s timeout recommended
  });
  return res.json();
}
Enter fullscreen mode Exit fullscreen mode

DEX aggregators routed over $613 billion in spot volume in a single month in late 2025, according to DeFiLlama. The swapapi.dev API taps into the same deep liquidity pools across all 46 chains.

Try it yourself

Test the API right now with curl — no signup needed:

curl "https://api.swapapi.dev/v1/swap/8453?tokenIn=0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE&tokenOut=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&amount=100000000000000000&sender=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
Enter fullscreen mode Exit fullscreen mode

That fetches a quote for swapping 0.1 ETH to USDC on Base. The response includes the tx object you can submit directly on-chain.

Step 5: Display the Quote to the User

Present the quote in a clean Telegram message. Users need to see the output amount, price impact, and minimum received before confirming.

function formatQuote(
  quote: SwapResponse["data"],
  tokenInSymbol: string,
  tokenOutSymbol: string,
  inputAmount: number
): string {
  const outDecimals = quote.tokenTo.decimals;
  const expectedOut = Number(quote.expectedAmountOut) / 10 ** outDecimals;
  const minOut = Number(quote.minAmountOut) / 10 ** outDecimals;
  const impact = (quote.priceImpact * 100).toFixed(3);

  return [
    `Swap Quote`,
    `────────────────`,
    `${inputAmount} ${tokenInSymbol} -> ${expectedOut.toFixed(4)} ${tokenOutSymbol}`,
    ``,
    `Min received: ${minOut.toFixed(4)} ${tokenOutSymbol}`,
    `Price impact: ${impact}%`,
    `Slippage: 0.5%`,
    ``,
    `Reply /confirm to execute this swap.`,
  ].join("\n");
}
Enter fullscreen mode Exit fullscreen mode

Telegram bots process an average of $61.7 million in daily DEX trading volume according to CoinGecko research. A clear quote display is essential for user trust — always show the minimum received amount so users know their worst-case outcome.

Step 6: Sign and Submit the Transaction

Once the user confirms, use ethers.js to sign and broadcast the transaction. The API response includes a complete tx object — you just need to estimate gas and send it.

import { ethers } from "ethers";

async function executeSwap(
  quote: SwapResponse["data"],
  privateKey: string
): Promise<string> {
  // Use the RPC URLs provided by the API
  const provider = new ethers.JsonRpcProvider(quote.rpcUrls[0]);
  const wallet = new ethers.Wallet(privateKey, provider);

  // Safety check: reject high price impact
  if (quote.priceImpact < -0.05) {
    throw new Error("Price impact exceeds 5% — swap rejected for safety");
  }

  // Estimate gas with 20% buffer
  const gasEstimate = await provider.estimateGas({
    from: quote.tx.from,
    to: quote.tx.to,
    data: quote.tx.data,
    value: quote.tx.value,
  });
  const gasLimit = (gasEstimate * 120n) / 100n;

  // Submit the transaction
  const tx = await wallet.sendTransaction({
    to: quote.tx.to,
    data: quote.tx.data,
    value: quote.tx.value,
    gasLimit,
  });

  return tx.hash;
}
Enter fullscreen mode Exit fullscreen mode

According to the ethers.js documentation, the library automatically handles EIP-1559 gas pricing on supported chains — you do not need to manually set maxFeePerGas. The swapapi.dev response includes a suggested gasPrice for legacy chains, but ethers.js will pick the right gas model.

:::tip
Always call estimateGas before submitting. If it fails, the swap would revert on-chain. Do not submit without a successful gas estimate.
:::

Step 7: Wire It All Together

Connect the command handler, quote display, and execution into the grammY bot:

import { Bot, session } from "grammy";
import { config } from "dotenv";
import { ethers } from "ethers";

config();

const bot = new Bot(process.env.BOT_TOKEN!);
const WALLET_ADDRESS = new ethers.Wallet(process.env.PRIVATE_KEY!).address;

// Store pending quotes per user
const pendingQuotes = new Map<number, {
  quote: SwapResponse;
  tokenInSymbol: string;
  tokenOutSymbol: string;
  inputAmount: number;
}>();

bot.command("swap", async (ctx) => {
  const parsed = parseSwapCommand(ctx.message?.text ?? "");
  if (!parsed) {
    return ctx.reply(
      "Usage: /swap <amount> <tokenIn> <tokenOut> <chain>\n" +
      "Example: /swap 0.1 ETH USDC base"
    );
  }

  await ctx.reply("Fetching swap quote...");

  try {
    const quote = await getSwapQuote(
      parsed.chainId,
      parsed.tokenIn.address,
      parsed.tokenOut.address,
      parsed.rawAmount,
      WALLET_ADDRESS
    );

    if (!quote.success) {
      return ctx.reply(`Error: ${quote.error?.message ?? "Unknown error"}`);
    }

    if (quote.data.status === "NoRoute") {
      return ctx.reply("No swap route found. Try a different pair or amount.");
    }

    if (quote.data.status === "Partial") {
      await ctx.reply("Only a partial fill is available for this amount.");
    }

    pendingQuotes.set(ctx.from!.id, {
      quote,
      tokenInSymbol: parsed.tokenInSymbol,
      tokenOutSymbol: parsed.tokenOutSymbol,
      inputAmount: parsed.amount,
    });

    const msg = formatQuote(
      quote.data,
      parsed.tokenInSymbol,
      parsed.tokenOutSymbol,
      parsed.amount
    );
    return ctx.reply(msg);
  } catch (err) {
    return ctx.reply("Failed to fetch quote. Try again in a few seconds.");
  }
});

bot.command("confirm", async (ctx) => {
  const pending = pendingQuotes.get(ctx.from!.id);
  if (!pending) {
    return ctx.reply("No pending swap. Use /swap first.");
  }

  await ctx.reply("Submitting transaction...");

  try {
    const txHash = await executeSwap(
      pending.quote.data,
      process.env.PRIVATE_KEY!
    );
    pendingQuotes.delete(ctx.from!.id);
    return ctx.reply(`Swap submitted!\nTx: ${txHash}`);
  } catch (err: any) {
    return ctx.reply(`Swap failed: ${err.message}`);
  }
});

bot.command("start", (ctx) =>
  ctx.reply(
    "Token Swap Bot\n\n" +
    "Commands:\n" +
    "/swap 0.1 ETH USDC base — Get a swap quote\n" +
    "/confirm — Execute the pending swap\n\n" +
    "Supported chains: ethereum, base, arbitrum, polygon, optimism, bsc, avalanche"
  )
);

bot.start();
Enter fullscreen mode Exit fullscreen mode

This is a working single-file bot. For production, you should split the code into modules, add rate limiting (the swapapi.dev API allows ~30 requests per minute per IP), and persist quotes in a database instead of an in-memory Map.

Step 8: Handle Errors and Edge Cases

A production bot needs to handle several edge cases that the API surface exposes:

async function safeSwap(ctx: Context, quote: SwapResponse) {
  // 1. Check status
  if (quote.data.status === "NoRoute") {
    return ctx.reply("No liquidity for this pair. Try a different route.");
  }

  // 2. Check for partial fills
  if (quote.data.status === "Partial") {
    const filled = Number(quote.data.amountIn);
    await ctx.reply(
      `Only a partial fill is available. ` +
      `The swap will use ${filled} of your requested amount.`
    );
  }

  // 3. Price impact guard
  if (quote.data.priceImpact < -0.03) {
    return ctx.reply(
      `Price impact is ${(quote.data.priceImpact * 100).toFixed(2)}%. ` +
      `This is high — consider a smaller amount or a different pair.`
    );
  }

  // 4. Simulate with eth_call before sending
  const provider = new ethers.JsonRpcProvider(quote.data.rpcUrls[0]);
  try {
    await provider.call({
      from: quote.data.tx.from,
      to: quote.data.tx.to,
      data: quote.data.tx.data,
      value: quote.data.tx.value,
    });
  } catch {
    return ctx.reply(
      "Simulation failed — this swap would revert on-chain. " +
      "Check your balance and token approvals."
    );
  }

  // 5. Quote freshness — swapapi calldata expires in ~30 seconds
  // Always fetch a fresh quote right before executing
}
Enter fullscreen mode Exit fullscreen mode

Key points for robust error handling:

  • Quote expiry: Swap calldata includes a deadline. Always fetch a fresh quote immediately before submitting. If more than 30 seconds pass, re-fetch.
  • ERC-20 approvals: If tokenIn is not the native gas token, the user must approve the router contract (tx.to) to spend their tokens before the swap can execute.
  • Rate limits: The API returns a RATE_LIMITED error (HTTP 429) if you exceed ~30 requests per minute. Implement exponential backoff.
  • RPC fallbacks: The response includes up to 5 rpcUrls. If the first RPC fails, cycle through the list.

Frequently Asked Questions

Do I need an API key to use swapapi.dev?

No. The swapapi.dev API requires no API key, no authentication, and no account. You make a GET request and receive swap calldata immediately. This makes it ideal for Telegram bots where you want to minimize setup complexity. The API supports 46 EVM chains including Ethereum, Base, Arbitrum, Polygon, BSC, and Optimism.

How do I handle ERC-20 token approvals in my bot?

If the input token is an ERC-20 (not native ETH/BNB/POL), the sender wallet must approve the router contract address — found in data.tx.to of the API response — to spend at least the swap amount. Call the token contract's approve(spender, amount) function before executing the swap. Note that USDT on Ethereum requires setting the allowance to zero before setting a new non-zero value.

Can I use this with Telegram Mini Apps instead of a bot?

Yes. The swapapi.dev API is a standard REST endpoint, so it works from any client — Telegram bots, Mini Apps, web frontends, or backend services. For Mini Apps, you would call the API from your web app's JavaScript and use a wallet SDK like WalletConnect or Telegram's TON Connect for signing. The bot approach in this guide is simpler because it uses a server-side wallet.

What chains does swapapi.dev support?

The API supports 46 EVM-compatible chains. The most popular are Ethereum (1), Base (8453), Arbitrum (42161), Polygon (137), BSC (56), Optimism (10), and Avalanche (43114). See the full list of supported chains for all chain IDs and token addresses.

How do I avoid getting rate limited?

The API allows approximately 30 requests per minute per IP address. For a Telegram bot serving multiple users, implement a request queue with exponential backoff on 429 responses. If you need higher throughput, consider caching quotes for a few seconds — though remember that calldata expires after roughly 30 seconds.

Get Started

Adding token swaps to a Telegram bot takes one API call and zero configuration. The swapapi.dev API is free, requires no API key, and covers 46 EVM chains with a single GET endpoint.

Here is everything you need:

Test a swap quote right now:

curl "https://api.swapapi.dev/v1/swap/1?tokenIn=0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE&tokenOut=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48&amount=1000000000000000000&sender=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
Enter fullscreen mode Exit fullscreen mode

That returns a complete swap quote for 1 ETH to USDC on Ethereum — including the transaction object ready to sign and broadcast. No signup, no key, no waiting.

Top comments (0)