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
-
/balanceshows your panel balance -
/services telegramsearches the service list by keyword -
/order service_id link quantityplaces an order -
/status order_idshows status, start count and remains - It answers only you. Anyone else gets "this bot is private"
Before you start
You need:
- Python 3.10 or newer
-
A Telegram bot token. Open Telegram, message
@BotFather, send/newbot, pick a name, and copy the token it gives you. -
Your Telegram user ID. Message
@userinfobotand it will reply with your numeric ID. - 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
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()
A few things worth pointing out:
-
owner_onlywraps 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_threadruns the blockingrequestscall in a thread, so the bot stays responsive while it waits for the API. -
Input checks in
/orderand/statusstop typos (like a link in the quantity field) before they reach the API. - The
/servicessearch 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
Windows (PowerShell):
$env:SMM_API_KEY="your_panel_api_key"
$env:TELEGRAM_BOT_TOKEN="123456:ABC..."
$env:OWNER_ID="123456789"
python smm_bot.py
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
.envfile to.gitignoreif 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_onlycheck. A public bot with your API key can spend your balance.
Ideas to extend it
-
Order alerts: every 10 minutes, check open orders with
statusand send a message when one becomesCompletedorPartial. -
Refills: add a
/refill order_idcommand using therefillaction. -
Multiple orders: many panels accept
orders=1,2,3in a singlestatuscall, which saves requests. -
Low balance warning: check
balanceonce 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)