Telegram bots are the backbone of Solana trading culture. BullX, Trojan, Photon — the most popular trading interfaces on Solana are Telegram bots. But you do not need to build a full trading platform to benefit from a custom bot. A simple bot that monitors wallets, sends price alerts, or executes basic swaps can be built in a weekend.
This tutorial walks you through building a Solana Telegram bot from scratch. We will cover the bot framework (grammY), Solana blockchain interaction (@solana/web3.js), data from RPC providers, and deployment. By the end, you will have a working bot that you can extend with whatever functionality you need.
Looking for a specific build instead of the from-scratch foundations? We have three ready-made paths: a rug-check bot in 10 minutes (zero dependencies, clone and run), a webhook-driven deployer alert bot, and the data-layer guide for Telegram bot SaaS products if you're building for paying users.
Prerequisites
You need:
- Node.js 20+ installed
- Basic TypeScript/JavaScript knowledge
- A Telegram account
- A Solana RPC endpoint (we will set this up)
- Familiarity with how Solana transactions work
Step 1: Create Your Telegram Bot
Every Telegram bot starts with BotFather.
- Open Telegram and search for
@BotFather.
- Send
/newbot.
- Choose a name (display name) and username (must end in
bot).
- BotFather gives you an API token. Save it — this is your bot's authentication key.
Security note: Never commit your bot token to version control. Use environment variables.
Step 2: Project Setup
Initialize a new TypeScript project:
mkdir solana-tg-bot && cd solana-tg-bot
npm init -y
npm install grammy @solana/web3.js dotenv
npm install -D typescript @types/node tsx
npx tsc --init
Update tsconfig.json to use modern settings:
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
Create a .env file:
BOT_TOKEN=your_telegram_bot_token
SOLANA_RPC_URL=https://mainnet.helius-rpc.com/?api-key=YOUR_KEY
HELIUS_API_KEY=your_helius_api_key
Step 3: Choose Your RPC Provider
Your bot needs a Solana RPC endpoint to read blockchain data and submit transactions. The free public endpoint (https://api.mainnet-beta.solana.com) has aggressive rate limits and is unreliable for production use. You need a dedicated provider.
Recommended Providers
Helius is the top choice for Solana bot development. Beyond standard RPC, Helius provides:
- Enhanced APIs for parsed transaction history, token metadata, and NFT data — saving you from writing complex parsing logic.
- Webhooks for real-time event notifications (wallet activity, token transfers) without polling.
- DAS (Digital Asset Standard) API for comprehensive token and NFT data.
- Free tier: 100K requests/day. More than enough for development and small bots.
Shyft offers similar enhanced APIs with a focus on parsed data:
- Transaction parsing, token data, and wallet portfolios via REST APIs.
- GraphQL API for flexible data queries.
- Callback/webhook support for real-time monitoring.
QuickNode provides high-performance RPC with add-ons:
- Solana-specific add-ons for token balances, NFT data, and priority fee estimation.
- Streams (webhooks) for real-time blockchain event monitoring.
- Global edge network for low-latency responses.
For this tutorial, we will use Helius. Sign up at helius.dev and grab an API key. If you are cost-sensitive or building cross-chain, our GetBlock Solana RPC review of its free tier and dedicated stack breaks down a cheaper alternative with a genuinely usable free plan.
Step 4: Basic Bot Structure
Create src/bot.ts:
import "dotenv/config";
import { Bot, Context, session } from "grammy";
const bot = new Bot(process.env.BOT_TOKEN!);
// Session data for tracking user preferences
interface SessionData {
watchedWallets: string[];
priceAlerts: { token: string; targetPrice: number; direction: "above" | "below" }[];
}
bot.use(session({ initial: (): SessionData => ({
watchedWallets: [],
priceAlerts: [],
})}));
// Start command
bot.command("start", (ctx) => {
ctx.reply(
"Welcome to Solana Monitor Bot!\n\n" +
"Commands:\n" +
"/price <token> - Get current price\n" +
"/watch <wallet> - Track a wallet\n" +
"/alert <token> <price> <above|below> - Set price alert\n" +
"/balance <wallet> - Check SOL balance\n" +
"/portfolio <wallet> - View token holdings"
);
});
bot.start();
console.log("Bot is running...");
Run it: npx tsx src/bot.ts. Send /start to your bot on Telegram. If it replies, the foundation works.
Step 5: Solana Connection and Balance Checking
Create src/solana.ts:
import { Connection, PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js";
const connection = new Connection(process.env.SOLANA_RPC_URL!, "confirmed");
export async function getSOLBalance(address: string): Promise<number> {
const pubkey = new PublicKey(address);
const balance = await connection.getBalance(pubkey);
return balance / LAMPORTS_PER_SOL;
}
export async function getTokenBalances(address: string) {
// Using Helius enhanced API for parsed token balances
const response = await fetch(
`https://api.helius.xyz/v0/addresses/${address}/balances?api-key=${process.env.HELIUS_API_KEY}`
);
return response.json();
}
export function isValidSolanaAddress(address: string): boolean {
try {
new PublicKey(address);
return true;
} catch {
return false;
}
}
Add the balance command to bot.ts:
import { getSOLBalance, isValidSolanaAddress } from "./solana.js";
bot.command("balance", async (ctx) => {
const address = ctx.match?.trim();
if (!address || !isValidSolanaAddress(address)) {
return ctx.reply("Usage: /balance <solana_address>");
}
try {
const balance = await getSOLBalance(address);
ctx.reply(`Balance: ${balance.toFixed(4)} SOL`);
} catch (error) {
ctx.reply("Error fetching balance. Check the address and try again.");
}
});
Step 6: Token Price Lookup
Create src/prices.ts. We will use Birdeye's public API or Jupiter's price API:
interface TokenPrice {
symbol: string;
price: number;
change24h: number;
}
// Jupiter Price API (free, no key required)
export async function getTokenPrice(mintAddress: string): Promise<TokenPrice | null> {
try {
const response = await fetch(
`https://api.jup.ag/price/v2?ids=${mintAddress}`
);
const data = await response.json();
const priceData = data.data?.[mintAddress];
if (!priceData) return null;
return {
symbol: priceData.mintSymbol || "UNKNOWN",
price: parseFloat(priceData.price),
change24h: 0, // Jupiter price API v2 focuses on current price
};
} catch {
return null;
}
}
// Common token mint addresses
export const KNOWN_TOKENS: Record<string, string> = {
sol: "So11111111111111111111111111111111111111112",
usdc: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
jup: "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN",
jto: "jtojtomepa8beP8AuQc6eXt5FriJwfFMwQx2v2f9mCL",
bonk: "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263",
wif: "EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm",
pyth: "HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3",
};
export function resolveTokenMint(input: string): string | null {
const lower = input.toLowerCase();
if (KNOWN_TOKENS[lower]) return KNOWN_TOKENS[lower];
// Check if input is already a mint address
if (input.length >= 32 && input.length <= 44) return input;
return null;
}
Add the price command:
import { getTokenPrice, resolveTokenMint } from "./prices.js";
bot.command("price", async (ctx) => {
const input = ctx.match?.trim();
if (!input) return ctx.reply("Usage: /price <token_symbol_or_mint>");
const mint = resolveTokenMint(input);
if (!mint) return ctx.reply("Unknown token. Try a symbol (sol, jup) or paste a mint address.");
const price = await getTokenPrice(mint);
if (!price) return ctx.reply("Could not fetch price for this token.");
ctx.reply(
`${price.symbol}\n` +
`Price: $${price.price < 0.01 ? price.price.toExponential(2) : price.price.toFixed(4)}`
);
});
Step 7: Wallet Monitoring with Webhooks
This is where it gets powerful. Instead of polling for changes, we use Helius webhooks to get notified instantly when a watched wallet has activity.
Create src/webhooks.ts:
import express from "express";
const app = express();
app.use(express.json());
// Store chat IDs for wallet notifications
const walletSubscriptions = new Map<string, Set<number>>();
export function subscribeWallet(wallet: string, chatId: number) {
if (!walletSubscriptions.has(wallet)) {
walletSubscriptions.set(wallet, new Set());
// Register webhook with Helius
registerHeliusWebhook(wallet);
}
walletSubscriptions.get(wallet)!.add(chatId);
}
async function registerHeliusWebhook(wallet: string) {
await fetch(`https://api.helius.xyz/v0/webhooks?api-key=${process.env.HELIUS_API_KEY}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
webhookURL: `${process.env.WEBHOOK_URL}/helius-webhook`,
transactionTypes: ["TRANSFER", "SWAP"],
accountAddresses: [wallet],
webhookType: "enhanced",
}),
});
}
// Webhook endpoint that Helius calls
app.post("/helius-webhook", (req, res) => {
const transactions = req.body;
for (const tx of transactions) {
// Process and notify subscribers
const involvedAccounts = tx.accountData?.map((a: any) => a.account) || [];
for (const [wallet, subscribers] of walletSubscriptions) {
if (involvedAccounts.includes(wallet)) {
const message = formatTransaction(tx, wallet);
for (const chatId of subscribers) {
// Send notification via bot (needs bot reference)
globalThis.botInstance?.api.sendMessage(chatId, message);
}
}
}
}
res.sendStatus(200);
});
function formatTransaction(tx: any, wallet: string): string {
const type = tx.type || "UNKNOWN";
const sig = tx.signature?.slice(0, 8) || "???";
return (
`Wallet Activity Detected\n` +
`Type: ${type}\n` +
`Signature: ${sig}...\n` +
`Explorer: https://solscan.io/tx/${tx.signature}`
);
}
export function startWebhookServer(port = 3333) {
app.listen(port, () => console.log(`Webhook server on port ${port}`));
}
Install express: npm install express @types/express
Step 8: Price Alert System
Create src/alerts.ts:
import { getTokenPrice } from "./prices.js";
interface PriceAlert {
chatId: number;
token: string;
mint: string;
targetPrice: number;
direction: "above" | "below";
}
const activeAlerts: PriceAlert[] = [];
export function addAlert(alert: PriceAlert) {
activeAlerts.push(alert);
}
export function removeAlertsForChat(chatId: number) {
const indices = activeAlerts
.map((a, i) => a.chatId === chatId ? i : -1)
.filter(i => i !== -1)
.reverse();
for (const i of indices) activeAlerts.splice(i, 1);
}
export async function checkAlerts(
notify: (chatId: number, message: string) => void
) {
const uniqueMints = [...new Set(activeAlerts.map(a => a.mint))];
for (const mint of uniqueMints) {
const price = await getTokenPrice(mint);
if (!price) continue;
const triggered = activeAlerts.filter(a =>
a.mint === mint &&
((a.direction === "above" && price.price >= a.targetPrice) ||
(a.direction === "below" && price.price <= a.targetPrice))
);
for (const alert of triggered) {
notify(
alert.chatId,
`Price Alert Triggered!\n` +
`${alert.token.toUpperCase()} is now $${price.price.toFixed(6)}\n` +
`Target: ${alert.direction} $${alert.targetPrice}`
);
// Remove triggered alert
const idx = activeAlerts.indexOf(alert);
if (idx !== -1) activeAlerts.splice(idx, 1);
}
}
}