DEV Community

Shoukat Aziz
Shoukat Aziz

Posted on

Build a Telegram Bot to Manage SMM Panel Orders with Python

If you resell social media services, you probably check balances and order statuses many times a day. Opening a dashboard on your phone every time gets old fast. A small Telegram bot can do it in one message: /balance, /status 23501 or /order 1 https://t.me/yourchannel 1000.

In this tutorial we will build that bot in about 100 lines of Python. It talks to any SMM panel that uses the standard API v2 format (most panels do). I will use the SafeSMM API (safesmm.net) in the examples, because that is the panel I work on, but you only need to change one URL to use another panel.

What the bot will do

  • /balance shows your panel balance
  • /services telegram searches the service list by keyword
  • /order service_id link quantity places an order
  • /status order_id shows status, start count and remains
  • It answers only you. Anyone else gets "this bot is private"

Before you start

You need:

  1. Python 3.10 or newer
  2. A Telegram bot token. Open Telegram, message @BotFather, send /newbot, pick a name, and copy the token it gives you.
  3. Your Telegram user ID. Message @userinfobot and it will reply with your numeric ID.
  4. An API key from your SMM panel. On most panels it is on the account or API page.

Install the two libraries:

pip install python-telegram-bot==21.6 requests
Enter fullscreen mode Exit fullscreen mode

How the panel API works (30-second version)

Every request is a POST to one URL with your key and an action:

Action Extra fields Reply example
balance none {"balance": "12.50", "currency": "USD"}
services none list of services with service, name, rate, min, max
add service, link, quantity {"order": 23501}
status order {"status": "Partial", "start_count": "3572", "remains": "157", "charge": "0.27"}

If something is wrong, the panel returns an error field, for example {"error": "Invalid API key."}. Our code turns that into a readable message in the chat.

The full bot

Save this as smm_bot.py:

import asyncio
import os

import requests
from telegram import Update
from telegram.ext import ApplicationBuilder, CommandHandler, ContextTypes

API_URL = "https://safesmm.net/api/v2"   # any panel with a standard API v2 works
API_KEY = os.environ["SMM_API_KEY"]
BOT_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]
OWNER_ID = int(os.environ["OWNER_ID"])    # your own Telegram user ID


def smm(action, **params):
    """Send one request to the panel API and return the JSON reply."""
    r = requests.post(API_URL, data={"key": API_KEY, "action": action, **params}, timeout=30)
    r.raise_for_status()
    data = r.json()
    if isinstance(data, dict) and "error" in data:
        raise RuntimeError(data["error"])
    return data


async def call(action, **params):
    # requests is blocking, so run it in a thread and keep the bot responsive
    return await asyncio.to_thread(smm, action, **params)


def owner_only(handler):
    async def wrapper(update: Update, context: ContextTypes.DEFAULT_TYPE):
        if update.effective_user is None or update.effective_user.id != OWNER_ID:
            await update.message.reply_text("Sorry, this bot is private.")
            return
        try:
            await handler(update, context)
        except Exception as e:
            await update.message.reply_text(f"Error: {e}")
    return wrapper


@owner_only
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    await update.message.reply_text(
        "Commands:\n"
        "/balance - show panel balance\n"
        "/services keyword - search services\n"
        "/order service_id link quantity - place an order\n"
        "/status order_id - check an order"
    )


@owner_only
async def balance(update: Update, context: ContextTypes.DEFAULT_TYPE):
    data = await call("balance")
    await update.message.reply_text(f"Balance: {data['balance']} {data.get('currency', '')}")


@owner_only
async def services(update: Update, context: ContextTypes.DEFAULT_TYPE):
    keyword = " ".join(context.args).lower()
    if not keyword:
        await update.message.reply_text("Usage: /services telegram members")
        return
    items = await call("services")
    found = [s for s in items if keyword in s["name"].lower() or keyword in s.get("category", "").lower()]
    if not found:
        await update.message.reply_text("No services found.")
        return
    lines = [f"{s['service']} | {s['name']} | {s['rate']}/1k | min {s['min']} max {s['max']}" for s in found[:10]]
    if len(found) > 10:
        lines.append(f"...and {len(found) - 10} more. Try a longer keyword.")
    await update.message.reply_text("\n".join(lines))


@owner_only
async def order(update: Update, context: ContextTypes.DEFAULT_TYPE):
    if len(context.args) != 3 or not context.args[0].isdigit() or not context.args[2].isdigit():
        await update.message.reply_text("Usage: /order 123 https://t.me/yourchannel 1000")
        return
    service_id, link, quantity = context.args
    data = await call("add", service=service_id, link=link, quantity=quantity)
    await update.message.reply_text(f"Order placed. Order ID: {data['order']}")


@owner_only
async def status(update: Update, context: ContextTypes.DEFAULT_TYPE):
    if len(context.args) != 1 or not context.args[0].isdigit():
        await update.message.reply_text("Usage: /status 23501")
        return
    data = await call("status", order=context.args[0])
    await update.message.reply_text(
        f"Status: {data.get('status')}\n"
        f"Start count: {data.get('start_count')}\n"
        f"Remains: {data.get('remains')}\n"
        f"Charge: {data.get('charge')} {data.get('currency', '')}"
    )


def main():
    app = ApplicationBuilder().token(BOT_TOKEN).build()
    app.add_handler(CommandHandler(["start", "help"], start))
    app.add_handler(CommandHandler("balance", balance))
    app.add_handler(CommandHandler("services", services))
    app.add_handler(CommandHandler("order", order))
    app.add_handler(CommandHandler("status", status))
    app.run_polling()


if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

A few things worth pointing out:

  • owner_only wraps every command. It checks the Telegram user ID, so even if someone finds your bot, they cannot see your balance or spend it. It also catches errors and sends them to you instead of crashing.
  • asyncio.to_thread runs the blocking requests call in a thread, so the bot stays responsive while it waits for the API.
  • Input checks in /order and /status stop typos (like a link in the quantity field) before they reach the API.
  • The /services search shows at most 10 results, because Telegram messages have a length limit and long lists are hard to read on a phone.

Run it

Never paste keys into the code. Set them as environment variables instead.

Linux or macOS:

export SMM_API_KEY="your_panel_api_key"
export TELEGRAM_BOT_TOKEN="123456:ABC..."
export OWNER_ID="123456789"
python smm_bot.py
Enter fullscreen mode Exit fullscreen mode

Windows (PowerShell):

$env:SMM_API_KEY="your_panel_api_key"
$env:TELEGRAM_BOT_TOKEN="123456:ABC..."
$env:OWNER_ID="123456789"
python smm_bot.py
Enter fullscreen mode Exit fullscreen mode

Now open your bot in Telegram and send /start. Try /balance first. It is the safest test because it does not spend anything.

Keep it running

run_polling() keeps the bot alive as long as the script runs. For a bot that works 24 hours a day, run it on a small VPS with systemd, pm2, or simply nohup python smm_bot.py &. Polling needs no domain, SSL or webhook setup, which makes it the easiest option for a personal bot.

Security checklist

  • Keep your API key and bot token out of GitHub. Add a .env file to .gitignore if you use one.
  • Consider a separate panel account for the bot with only the balance you need. If the key ever leaks, the damage is limited.
  • If a key leaks, generate a new one in the panel right away. The old one stops working.
  • Do not remove the owner_only check. A public bot with your API key can spend your balance.

Ideas to extend it

  • Order alerts: every 10 minutes, check open orders with status and send a message when one becomes Completed or Partial.
  • Refills: add a /refill order_id command using the refill action.
  • Multiple orders: many panels accept orders=1,2,3 in a single status call, which saves requests.
  • Low balance warning: check balance once an hour and alert yourself below a limit.

Wrapping up

With one Python file you can manage your panel from Telegram: check balance, search services, place orders and track them. The same smm() helper works for any panel that follows the API v2 format, so you can reuse it in scripts, dashboards or your own store.

If you want more examples in PHP and Python, the code from my earlier post is on GitHub: safesmm-api-examples. Full API reference: safesmm.net/api/docs.

Prefer a ready-made client? There is also a small dependency-free Python package on PyPI: pip install safesmm (pypi.org/project/safesmm).

Disclosure: I work on SafeSMM (safesmm.net), the panel used in these examples.

Top comments (0)