CChainPay
Anmelden

Accept USDT payments in a Telegram bot

A Telegram bot can sell access, subscriptions or digital goods for USDT without a payment provider account. The bot needs two things: a way to show the payer an address, and a public URL that receives the confirmation. This guide uses Python with aiogram 3 and aiohttp.

The flow

  1. The user sends /pay.
  2. The bot creates a ChainPay deposit address with a unique externalReference and shows it.
  3. The user sends USDT.
  4. After confirmation ChainPay sweeps the funds to your wallet and calls the bot's webhook; the bot messages the user.

No registration and no API key are needed. Your own wallet is the forwardAddress.

The bot

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())

Run it behind HTTPS (a reverse proxy such as Caddy or nginx): ChainPay refuses callbackUrl values that point to localhost or a private IP, so for local testing use a tunnel with a public URL.

What to improve before production

Related