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
Store this token in a .env file:
BOT_TOKEN=7123456789:AAH1234abcd5678efgh-XYZTOKEN
PRIVATE_KEY=0xYourWalletPrivateKeyHere
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
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...");
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 };
}
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();
}
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"
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");
}
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;
}
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();
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
}
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
tokenInis 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_LIMITEDerror (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:
- Getting started guide — first API call in under a minute
- API reference — full parameter and response documentation
- Supported chains — all 46 chains with token addresses
Test a swap quote right now:
curl "https://api.swapapi.dev/v1/swap/1?tokenIn=0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE&tokenOut=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48&amount=1000000000000000000&sender=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
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)