Discord has over 200 million monthly active users and hosts some of the largest crypto communities on the internet. Crypto Discord servers grew 40% year-over-year in 2025, with DeFi-focused servers seeing the fastest growth. Meanwhile, over 70% of crypto spot trading is now automated, driven by bots and algorithmic systems. Discord bots handle over 4 billion commands per month across the platform, and the discord.js library alone has over 25 million weekly npm downloads, making it one of the most active open-source ecosystems in JavaScript.
The gap: most crypto Discord bots only show prices. They can tell you ETH is at $2,500 but can't do anything about it. This guide shows you how to build a Discord bot that fetches real-time swap quotes and returns executable transaction data -- all from a single slash command.
We'll use swapapi.dev, a free DEX aggregator API that requires no API key, no account, and no SDK. It supports 46 EVM chains and returns ready-to-execute transaction calldata from a single GET request.
What You'll Need
- Node.js 18+ (or Bun)
- discord.js v14 -- the standard library for building Discord bots
- A Discord bot token -- create one at the Discord Developer Portal
- A Discord server where you have permission to add bots
- swapapi.dev -- free, no API key required
Step 1: Set Up the Bot Project
Initialize a new project and install discord.js:
npm init -y
npm install discord.js
Create an index.ts file with the bot client:
import { Client, GatewayIntentBits, REST, Routes, SlashCommandBuilder } from "discord.js";
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
const TOKEN = process.env.DISCORD_BOT_TOKEN!;
const CLIENT_ID = process.env.DISCORD_CLIENT_ID!;
Set your bot token and client ID as environment variables. You get both from the Discord Developer Portal after creating your application.
Step 2: Register the /swap Slash Command
Discord slash commands need to be registered before users can invoke them. Define a /swap command that accepts the parameters needed for a token swap quote:
const swapCommand = new SlashCommandBuilder()
.setName("swap")
.setDescription("Get a token swap quote")
.addStringOption(opt => opt.setName("from").setDescription("Input token symbol").setRequired(true))
.addStringOption(opt => opt.setName("to").setDescription("Output token symbol").setRequired(true))
.addStringOption(opt => opt.setName("amount").setDescription("Amount to swap").setRequired(true))
.addIntegerOption(opt => opt.setName("chain").setDescription("Chain ID (default: 1)").setRequired(false));
Register it with Discord's API:
const rest = new REST({ version: "10" }).setToken(TOKEN);
await rest.put(Routes.applicationCommands(CLIENT_ID), { body: [swapCommand.toJSON()] });
This registers a global slash command. Users will see /swap with autocomplete for each parameter.
Step 3: Map Token Symbols to Addresses
The swap API requires contract addresses, not symbols. Create a lookup map for common tokens:
const TOKENS: Record<string, Record<string, { address: string; decimals: number }>> = {
"1": {
ETH: { address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", decimals: 18 },
USDC: { address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", decimals: 6 },
USDT: { address: "0xdAC17F958D2ee523a2206206994597C13D831ec7", decimals: 6 },
DAI: { address: "0x6B175474E89094C44Da98b954EedeAC495271d0F", decimals: 18 },
WETH: { address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", decimals: 18 },
},
"42161": {
ETH: { address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", decimals: 18 },
USDC: { address: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", decimals: 6 },
USDT: { address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", decimals: 6 },
},
"8453": {
ETH: { address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", decimals: 18 },
USDC: { address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", decimals: 6 },
},
};
This covers Ethereum (chain 1), Arbitrum (42161), and Base (8453). You can add more chains and tokens as needed. The native gas token on any chain always uses the address 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE with 18 decimals.
Step 4: Fetch the Swap Quote from swapapi.dev
Write a function that calls the swap API and returns the quote data:
async function getSwapQuote(chainId: string, tokenIn: string, tokenOut: string, amount: string, sender: string) {
const url = `https://api.swapapi.dev/v1/swap/${chainId}?tokenIn=${tokenIn}&tokenOut=${tokenOut}&amount=${amount}&sender=${sender}`;
const res = await fetch(url);
return res.json();
}
The API returns everything in a single call: the quoted output amount, price impact, slippage-protected minimum, and the full transaction object (to, data, value) ready to submit on-chain. No separate quote-then-swap flow. No SDK. Response time is typically 1-5 seconds.
Step 5: Handle the Slash Command Interaction
Wire up the interaction handler that processes /swap commands, fetches the quote, and displays the result:
client.on("interactionCreate", async (interaction) => {
if (!interaction.isChatInputCommand() || interaction.commandName !== "swap") return;
await interaction.deferReply();
const fromSymbol = interaction.options.getString("from", true).toUpperCase();
const toSymbol = interaction.options.getString("to", true).toUpperCase();
const amount = interaction.options.getString("amount", true);
const chainId = String(interaction.options.getInteger("chain") ?? 1);
const chainTokens = TOKENS[chainId];
if (!chainTokens || !chainTokens[fromSymbol] || !chainTokens[toSymbol]) {
await interaction.editReply("Unknown token or chain. Supported: ETH, USDC, USDT, DAI, WETH on chains 1, 42161, 8453.");
return;
}
const tokenIn = chainTokens[fromSymbol];
const tokenOut = chainTokens[toSymbol];
const rawAmount = BigInt(parseFloat(amount) * 10 ** tokenIn.decimals).toString();
const sender = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
const quote = await getSwapQuote(chainId, tokenIn.address, tokenOut.address, rawAmount, sender);
if (!quote.success) {
await interaction.editReply(`Error: ${quote.error.message}`);
return;
}
if (quote.data.status === "NoRoute") {
await interaction.editReply(`No swap route found for ${fromSymbol} to ${toSymbol} on chain ${chainId}.`);
return;
}
const expectedOut = Number(quote.data.expectedAmountOut) / 10 ** tokenOut.decimals;
const minOut = Number(quote.data.minAmountOut) / 10 ** tokenOut.decimals;
const impact = (quote.data.priceImpact * 100).toFixed(2);
const embed = {
title: `${fromSymbol} -> ${toSymbol} Swap Quote`,
fields: [
{ name: "You Send", value: `${amount} ${fromSymbol}`, inline: true },
{ name: "You Receive", value: `~${expectedOut.toFixed(4)} ${toSymbol}`, inline: true },
{ name: "Minimum Output", value: `${minOut.toFixed(4)} ${toSymbol}`, inline: true },
{ name: "Price Impact", value: `${impact}%`, inline: true },
{ name: "Chain", value: chainId, inline: true },
{ name: "Status", value: quote.data.status, inline: true },
],
color: quote.data.status === "Partial" ? 0xffaa00 : 0x00ff88,
footer: { text: "Powered by swapapi.dev" },
};
await interaction.editReply({ embeds: [embed] });
});
The handler uses deferReply() because the API call takes a few seconds. It converts the user-friendly amount ("1.5") into raw token units, fetches the quote, and formats the result as a Discord embed.
Notice the status check: the API can return Successful, Partial (only part of the amount can be filled), or NoRoute (no swap path exists). All three return HTTP 200, so you must check data.status to distinguish them.
Step 6: Add Swap Execution Support
To let users actually execute swaps, add a confirmation button that reveals the transaction data:
import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js";
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder().setCustomId("show_tx").setLabel("Show Transaction Data").setStyle(ButtonStyle.Primary)
);
await interaction.editReply({ embeds: [embed], components: [row] });
Handle the button click to display the raw transaction:
client.on("interactionCreate", async (interaction) => {
if (!interaction.isButton() || interaction.customId !== "show_tx") return;
const tx = quote.data.tx;
const txData = `**To:** \`${tx.to}\`\n**Value:** \`${tx.value}\`\n**Data:** \`${tx.data.slice(0, 66)}...\``;
await interaction.reply({ content: txData, ephemeral: true });
});
The transaction object from the API contains to (the DEX router contract), data (ABI-encoded swap calldata), and value (native token amount in wei). Users can paste these into their wallet to execute. For a production bot, you'd integrate a signing library like ethers.js or viem to submit transactions directly.
Step 7: Start the Bot
Add the login call and run it:
client.login(TOKEN);
DISCORD_BOT_TOKEN=your_token DISCORD_CLIENT_ID=your_id npx tsx index.ts
The bot is now live. Users type /swap from:ETH to:USDC amount:1 chain:1 and get a real-time quote embed with the current exchange rate, price impact, and minimum output after slippage.
Handling Edge Cases
Production bots need to handle three scenarios the API can return:
Partial fills. When liquidity is thin, the API returns status: "Partial" with a reduced amountIn and expectedAmountOut. The transaction object is still executable but only swaps the partial amount. Compare quote.data.amountIn to your requested amount to detect this. Display a warning to the user showing how much of their order can actually be filled.
Price impact warnings. The priceImpact field is a decimal where -0.01 means -1%. For swaps with impact worse than -5%, warn users before they execute. Large swaps on low-liquidity pairs can lose significant value to slippage.
Rate limits. The API allows approximately 30 requests per minute per IP. For busy Discord servers, implement a cooldown per user or cache recent quotes for identical token pairs. The API returns error code RATE_LIMITED (HTTP 429) when you hit the limit -- wait 5-10 seconds before retrying.
FAQ
Does swapapi.dev require an API key?
No. The API is free with no authentication, no API keys, and no account required. You can start making requests immediately.
Which chains are supported?
The API supports 46 EVM chains including Ethereum, Arbitrum, Base, Optimism, Polygon, BSC, Avalanche, and many more. Pass the chain ID as a path parameter.
Can the bot execute swaps directly?
The API returns complete transaction calldata. To execute on-chain, you need a signer (private key or wallet connection). The bot can display the transaction data for users to submit manually, or you can integrate ethers.js/viem to sign and broadcast from a server-side wallet.
How do I handle tokens not in my lookup map?
Users can pass raw contract addresses instead of symbols. The API accepts any valid ERC-20 token address. You can extend the bot to accept either symbols or addresses.
What about cross-chain swaps?
The API is single-chain only. Each request swaps tokens within one chain. For cross-chain workflows, you would need to bridge assets separately.
Is there a rate limit?
Approximately 30 requests per minute per IP. For most Discord bots this is more than enough. If you need higher throughput, implement request queuing or caching.
Get Started
The full bot code above is under 100 lines of TypeScript. You can have a working /swap command in your Discord server in under 15 minutes.
- swapapi.dev -- free token swap API, 46 chains, no API key
- API Documentation -- complete endpoint reference
- Discord.js Guide -- slash commands and interactions
Start with quote display, add execution once you're comfortable with the response format. The API handles all DEX routing and calldata encoding server-side, so your bot stays simple.
Top comments (0)