Приём платежей в USDT в Telegram-боте
Telegram-бот может продавать доступ, подписки или цифровые товары за USDT без аккаунта в платёжном провайдере. Боту нужны две вещи: показать плательщику адрес и иметь публичный URL для подтверждения. Этот гайд использует Python, aiogram 3 и aiohttp.
Схема
- Пользователь отправляет
/pay. - Бот создаёт в ChainPay адрес для депозита с уникальным
externalReferenceи показывает его. - Пользователь отправляет USDT.
- После подтверждения ChainPay переводит средства на ваш кошелёк и вызывает вебхук бота; бот пишет пользователю.
Регистрация и API-ключ не нужны. Ваш кошелёк — это forwardAddress.
Бот
import asyncio
import hmac
import os
import uuid
import aiohttp
from aiohttp import web
from aiogram import Bot, Dispatcher
from aiogram.filters import Command
from aiogram.types import Message
API = "https://api.chainpay.site"
SECRET = os.environ["WEBHOOK_SECRET"]
WALLET = os.environ["FORWARD_ADDRESS"]
PUBLIC_URL = os.environ["PUBLIC_URL"] # e.g. https://bot.example.com
bot = Bot(os.environ["BOT_TOKEN"])
dp = Dispatcher()
orders: dict[str, int] = {} # externalReference -> Telegram user id (use a database in production)
seen: set[str] = set() # processed "txHash:status" pairs
@dp.message(Command("pay"))
async def pay(message: Message):
ref = f"tg-{message.from_user.id}-{uuid.uuid4().hex[:8]}"
orders[ref] = message.from_user.id
async with aiohttp.ClientSession() as session:
async with session.post(f"{API}/deposit-address", json={
"ticker": "trc20/usdt",
"callbackUrl": f"{PUBLIC_URL}/crypto-webhook?token={SECRET}",
"forwardAddress": WALLET,
"externalReference": ref,
}) as resp:
data = await resp.json()
await message.answer(
f"Send USDT (TRC20) to:\n<code>{data['address']}</code>", parse_mode="HTML"
)
async def crypto_webhook(request: web.Request):
if not hmac.compare_digest(request.query.get("token", ""), SECRET):
return web.Response(status=403)
event = await request.json()
key = f"{event['txHash']}:{event['status']}"
if event["status"] == "confirmed" and key not in seen:
seen.add(key)
user_id = orders.get(event["externalReference"])
if user_id:
await bot.send_message(
user_id, f"Received {event['amount']} {event['tokenSymbol']}. Thank you!"
)
return web.Response(text="ok")
async def main():
app = web.Application()
app.router.add_post("/crypto-webhook", crypto_webhook)
runner = web.AppRunner(app)
await runner.setup()
await web.TCPSite(runner, "0.0.0.0", 8080).start()
await dp.start_polling(bot)
asyncio.run(main())
Запускайте за HTTPS (обратный прокси, например Caddy или nginx): ChainPay отклоняет callbackUrl, указывающие на localhost или приватный IP, поэтому для локальных тестов нужен туннель с публичным адресом.
Что доработать перед продакшеном
- Храните
ordersиseenв базе. В примере они теряются при перезапуске. - Один адрес на заказ (как выше) — проще сверка; один на пользователя (
externalReference = tg-<user id>) — для пополнений и балансов. - Задайте минимальный платёж. В TRON фиксированная часть комиссии — около 4 USDT по текущим ценам, см. Pricing. В BSC используйте
bep20/usdt, там она ниже. - Если вебхук запаздывает, опрашивайте
GET /deposit-address/:id/status.