TRON Energy Satin Alan Bir Telegram Botu Olusturma
Telegram is the de facto communication platform for the crypto community. If you manage TRON wallets, run a dApp, or simply want a convenient way to purchase energy without opening a browser, a Telegram bot is a practical tool. This tutorial walks through building a complete Telegram bot that checks energy prices, purchases energy through MERX, and sends notifications when orders fill.
By the end of this article, you will have a working bot with three commands -- /price, /buy, and /balance -- plus webhook integration for real-time order status updates.
Prerequisites
- Node.js 18 or later
- A Telegram bot token (from @BotFather)
- A MERX API key (from https://merx.exchange)
- Basic familiarity with TypeScript
Project Setup
mkdir tron-energy-bot
cd tron-energy-bot
npm init -y
npm install telegraf merx-sdk dotenv express
npm install -D typescript @types/node @types/express ts-node
Create the TypeScript configuration:
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"resolveJsonModule": true
},
"include": ["src/**/*"]
}
Set up your environment variables:
# .env
TELEGRAM_BOT_TOKEN=your_telegram_bot_token
MERX_API_KEY=your_merx_api_key
WEBHOOK_PORT=3001
WEBHOOK_URL=https://your-server.com/webhooks/merx
Bot Architecture
The bot has three layers:
- Telegram command handlers -- Parse user commands and respond
- MERX client -- Interact with the energy market
- Webhook server -- Receive asynchronous order notifications
User sends /price 65000
|
v
[Telegram Bot] -- parses command
|
v
[MERX Client] -- queries prices
|
v
[Telegram Bot] -- formats and sends response
|
v
User receives price table
User sends /buy 65000 1h TAddress
|
v
[Telegram Bot] -- validates input
|
v
[MERX Client] -- creates order
|
v
[Webhook Server] -- receives order.filled
|
v
[Telegram Bot] -- notifies user
Core Implementation
Entry Point
// src/index.ts
import { Telegraf, Context } from 'telegraf';
import { MerxClient } from 'merx-sdk';
import express from 'express';
import dotenv from 'dotenv';
dotenv.config();
const bot = new Telegraf(process.env.TELEGRAM_BOT_TOKEN!);
const merx = new MerxClient({ apiKey: process.env.MERX_API_KEY! });
// Store chat IDs for order notifications
const orderChatMap = new Map<string, number>();
// --- Command Handlers ---
bot.command('start', (ctx) => {
ctx.reply(
'TRON Energy Bot\n\n' +
'Commands:\n' +
'/price <amount> - Check energy prices\n' +
'/buy <amount> <duration> <address> - Buy energy\n' +
'/balance - Check your MERX balance\n\n' +
'Example:\n' +
'/price 65000\n' +
'/buy 65000 1h TYourAddress123'
);
});
bot.command('price', handlePrice);
bot.command('buy', handleBuy);
bot.command('balance', handleBalance);
// --- Webhook Server ---
const app = express();
app.use(express.json());
app.post('/webhooks/merx', handleMerxWebhook);
// --- Start ---
const PORT = parseInt(process.env.WEBHOOK_PORT || '3001');
app.listen(PORT, () => {
console.log(`Webhook server listening on port ${PORT}`);
});
bot.launch().then(() => {
console.log('Telegram bot started');
});
process.once('SIGINT', () => bot.stop('SIGINT'));
process.once('SIGTERM', () => bot.stop('SIGTERM'));
The /price Command
The /price command queries MERX for current energy prices across all providers:
// src/handlers/price.ts
async function handlePrice(ctx: Context): Promise<void> {
const args = (ctx.message as any).text.split(' ').slice(1);
if (args.length < 1) {
await ctx.reply(
'Usage: /price <energy_amount> [duration]\n' +
'Example: /price 65000\n' +
'Example: /price 65000 1h'
);
return;
}
const amount = parseInt(args[0]);
if (isNaN(amount) || amount < 10000) {
await ctx.reply('Energy amount must be a number >= 10,000');
return;
}
const duration = args[1] || '1h';
try {
await ctx.reply('Checking prices across 7 providers...');
const prices = await merx.getPrices({
energy_amount: amount,
duration: duration
});
let response = `Energy prices for ${amount.toLocaleString()} units (${duration}):\n\n`;
// Format provider prices as a table
for (const offer of prices.providers) {
const totalTrx = (offer.price_sun * amount) / 1e6;
const marker = offer.provider === prices.best.provider
? ' << BEST'
: '';
response +=
`${offer.provider}: ${offer.price_sun} SUN ` +
`(${totalTrx.toFixed(2)} TRX)${marker}\n`;
}
const bestTotal = (prices.best.price_sun * amount) / 1e6;
response += `\nBest price: ${prices.best.price_sun} SUN `;
response += `via ${prices.best.provider}\n`;
response += `Total cost: ${bestTotal.toFixed(2)} TRX`;
await ctx.reply(response);
} catch (error: any) {
await ctx.reply(`Error fetching prices: ${error.message}`);
}
}
The /buy Command
The /buy command places an energy order through MERX:
// src/handlers/buy.ts
async function handleBuy(ctx: Context): Promise<void> {
const args = (ctx.message as any).text.split(' ').slice(1);
if (args.length < 3) {
await ctx.reply(
'Usage: /buy <amount> <duration> <tron_address>\n' +
'Example: /buy 65000 1h TYourAddress123\n\n' +
'Durations: 5m, 10m, 30m, 1h, 3h, 6h, 12h, 1d, 3d, 14d'
);
return;
}
const amount = parseInt(args[0]);
const duration = args[1];
const targetAddress = args[2];
// Validate inputs
if (isNaN(amount) || amount < 10000) {
await ctx.reply('Energy amount must be a number >= 10,000');
return;
}
if (!isValidTronAddress(targetAddress)) {
await ctx.reply('Invalid TRON address. Must start with T.');
return;
}
const validDurations = [
'5m', '10m', '30m', '1h', '3h',
'6h', '12h', '1d', '3d', '14d'
];
if (!validDurations.includes(duration)) {
await ctx.reply(
`Invalid duration. Choose from: ${validDurations.join(', ')}`
);
return;
}
try {
// Get the best price first
const prices = await merx.getPrices({
energy_amount: amount,
duration: duration
});
const totalTrx =
(prices.best.price_sun * amount) / 1e6;
await ctx.reply(
`Placing order:\n` +
`Amount: ${amount.toLocaleString()} energy\n` +
`Duration: ${duration}\n` +
`Target: ${targetAddress}\n` +
`Price: ${prices.best.price_sun} SUN ` +
`(${totalTrx.toFixed(2)} TRX)\n` +
`Provider: ${prices.best.provider}\n\n` +
`Processing...`
);
const order = await merx.createOrder({
energy_amount: amount,
duration: duration,
target_address: targetAddress
});
// Store the chat ID for webhook notification
orderChatMap.set(order.id, ctx.chat!.id);
await ctx.reply(
`Order placed.\n` +
`Order ID: ${order.id}\n` +
`Status: ${order.status}\n\n` +
`You will be notified when the order fills.`
);
} catch (error: any) {
await ctx.reply(`Error placing order: ${error.message}`);
}
}
function isValidTronAddress(address: string): boolean {
return /^T[1-9A-HJ-NP-Za-km-z]{33}$/.test(address);
}
The /balance Command
// src/handlers/balance.ts
async function handleBalance(ctx: Context): Promise<void> {
try {
const balance = await merx.getBalance();
await ctx.reply(
`MERX Account Balance:\n\n` +
`Available: ${balance.available_trx} TRX\n` +
`Reserved (in orders): ${balance.reserved_trx} TRX\n` +
`Total: ${balance.total_trx} TRX`
);
} catch (error: any) {
await ctx.reply(`Error fetching balance: ${error.message}`);
}
}
Webhook Handler
The webhook handler receives order status notifications from MERX and forwards them to the user via Telegram:
// src/handlers/webhook.ts
import { Request, Response } from 'express';
async function handleMerxWebhook(
req: Request,
res: Response
): Promise<void> {
const event = req.body;
try {
switch (event.type) {
case 'order.filled': {
const chatId = orderChatMap.get(event.data.order_id);
if (chatId) {
await bot.telegram.sendMessage(
chatId,
`Order filled.\n\n` +
`Order ID: ${event.data.order_id}\n` +
`Energy: ${event.data.energy_amount.toLocaleString()}\n` +
`Provider: ${event.data.provider}\n` +
`Price: ${event.data.price_sun} SUN\n` +
`Target: ${event.data.target_address}\n\n` +
`Energy has been delegated to the target address.`
);
orderChatMap.delete(event.data.order_id);
}
break;
}
case 'order.failed': {
const chatId = orderChatMap.get(event.data.order_id);
if (chatId) {
await bot.telegram.sendMessage(
chatId,
`Order failed.\n\n` +
`Order ID: ${event.data.order_id}\n` +
`Reason: ${event.data.reason}\n\n` +
`Your balance has been refunded.`
);
orderChatMap.delete(event.data.order_id);
}
break;
}
}
} catch (error) {
console.error('Webhook processing error:', error);
}
res.status(200).json({ received: true });
}
Adding Price Alerts
Extend the bot with a /alert command that uses MERX standing orders to notify users when prices drop below a threshold:
bot.command('alert', async (ctx) => {
const args = (ctx.message as any).text.split(' ').slice(1);
if (args.length < 2) {
await ctx.reply(
'Usage: /alert <energy_amount> <max_price_sun>\n' +
'Example: /alert 65000 25\n\n' +
'You will be notified when energy price drops below the target.'
);
return;
}
const amount = parseInt(args[0]);
const maxPrice = parseInt(args[1]);
try {
const standing = await merx.createStandingOrder({
energy_amount: amount,
max_price_sun: maxPrice,
duration: '1h',
repeat: false
});
orderChatMap.set(standing.id, ctx.chat!.id);
await ctx.reply(
`Price alert set.\n\n` +
`Watching for: ${amount.toLocaleString()} energy ` +
`at or below ${maxPrice} SUN\n` +
`Alert ID: ${standing.id}\n\n` +
`You will be notified when this price is available.`
);
} catch (error: any) {
await ctx.reply(`Error setting alert: ${error.message}`);
}
});
Uretim Hususlari
Persistent Storage
The in-memory orderChatMap used above is fine for development but loses data on restart. For production, use Redis or a database:
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
async function storeOrderChat(
orderId: string,
chatId: number
): Promise<void> {
// Store with 24-hour TTL
await redis.set(
`order:${orderId}:chat`,
chatId.toString(),
'EX',
86400
);
}
async function getOrderChat(
orderId: string
): Promise<number | null> {
const chatId = await redis.get(`order:${orderId}:chat`);
return chatId ? parseInt(chatId) : null;
}
User Authentication
For multi-user bots, associate Telegram user IDs with MERX accounts:
// Store user API keys securely
async function setUserApiKey(
telegramId: number,
apiKey: string
): Promise<void> {
// Encrypt before storing
const encrypted = encrypt(apiKey);
await redis.set(
`user:${telegramId}:apikey`,
encrypted
);
}
async function getUserClient(
telegramId: number
): Promise<MerxClient | null> {
const encrypted = await redis.get(
`user:${telegramId}:apikey`
);
if (!encrypted) return null;
const apiKey = decrypt(encrypted);
return new MerxClient({ apiKey });
}
Rate Limiting
Prevent abuse by limiting command frequency:
const rateLimits = new Map<number, number>();
const RATE_LIMIT_MS = 5000; // 5 seconds between commands
function isRateLimited(userId: number): boolean {
const lastCommand = rateLimits.get(userId) || 0;
const now = Date.now();
if (now - lastCommand < RATE_LIMIT_MS) {
return true;
}
rateLimits.set(userId, now);
return false;
}
// Apply to all commands
bot.use(async (ctx, next) => {
const userId = ctx.from?.id;
if (userId && isRateLimited(userId)) {
await ctx.reply('Please wait a few seconds between commands.');
return;
}
return next();
});
Hata Yonetimi
Wrap all command handlers with consistent error handling:
function withErrorHandling(
handler: (ctx: Context) => Promise<void>
) {
return async (ctx: Context) => {
try {
await handler(ctx);
} catch (error: any) {
console.error('Command error:', error);
await ctx.reply(
`An error occurred: ${error.message}\n` +
`Please try again or contact support.`
);
}
};
}
bot.command('price', withErrorHandling(handlePrice));
bot.command('buy', withErrorHandling(handleBuy));
bot.command('balance', withErrorHandling(handleBalance));
Deployment
Running with PM2
npm run build
pm2 start dist/index.js --name "tron-energy-bot"
pm2 save
Docker
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY dist/ ./dist/
CMD ["node", "dist/index.js"]
docker build -t tron-energy-bot .
docker run -d \
--name tron-energy-bot \
--env-file .env \
-p 3001:3001 \
tron-energy-bot
Testing the Bot
- Start the bot:
npx ts-node src/index.ts - Open Telegram and find your bot
- Send
/startto see available commands - Send
/price 65000to check current energy prices - Send
/buy 65000 1h TYourAddressto place an order - Wait for the webhook notification confirming the order filled
Sonuc
A Telegram bot for TRON energy purchasing turns a web-based workflow into a conversational interface. With three core commands and webhook integration, users can check prices, buy energy, and receive fill notifications without leaving Telegram.
The implementation leverages the MERX SDK for all energy market interactions, which means the bot automatically gets best-price routing across seven providers, standing order support for price alerts, and reliable order execution with failover.
The complete source code in this article is production-ready with the addition of persistent storage and proper authentication. The total implementation is under 300 lines of TypeScript.
For API documentation, visit https://merx.exchange/docs. For the MCP server integration, see https://github.com/Hovsteder/merx-mcp.