← BackPython + Telegram bot

100% Automatic Telegram Deposit Bot

User types /deposit, enters an amount, picks a wallet, sees the exact crypto amount converted live, pays. The IPN webhook auto-credits the balance the moment the deposit lands — no manual "Check status" button required.

User → /deposit → enter 100 USD
     → bot lists wallets WITH live quotes:
         USDT TRC20  →  92.4012 USDT
         USDT BEP20  →  92.4012 USDT
         BNB BSC     →  0.1531  BNB
         Binance Pay →  100 USD (exact)
     → user taps one → invoice created (30-min expiry, 2% tolerance)
     → user pays exactly that amount to the shown address
     → Binance sees the deposit → our server matches it
     → IPN webhook fires → your bot credits balance + DMs the user

What we'll build

1. Install + .env (3 secrets)

Three secrets in total. Generate them in your dashboard:

  • API_KEY + API_SECRETAPI Keys → "Create key" (the secret is shown ONCE).
  • WEBHOOK_SECRET — Settings → Webhooks → Add endpoint (the secret is shown ONCE).
  • TELEGRAM_BOT_TOKEN — talk to @BotFather in Telegram.
bash
# Python 3.10+
python -m venv .venv && source .venv/bin/activate
pip install requests flask python-dotenv "python-telegram-bot==21.*"
dotenv
# .env  — keep out of git
API_KEY="ci_live_xxxxxxxxxxxxxxxx"
API_SECRET="sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
WEBHOOK_SECRET="whsec_xxxxxxxxxxxxxxxxxxxx"   # shown ONCE when you add a webhook
TELEGRAM_BOT_TOKEN="123456:AA..."             # from @BotFather

2. Shared API client

Includes the new quote_wallet() helper — call it before creating an invoice to show the user the exact crypto amount per wallet.

python
# api_client.py — shared HTTP client
import os, requests
from dotenv import load_dotenv
load_dotenv()

API_BASE = "https://pay.itemodeverify.com"
KEY      = os.environ["API_KEY"]
SECRET   = os.environ["API_SECRET"]
AUTH     = {"Authorization": f"Bearer {KEY}:{SECRET}"}   # the colon is required

def api(method, path, **kw):
    r = requests.request(method, f"{API_BASE}{path}",
        headers={**AUTH, "Content-Type": "application/json", **kw.pop("headers", {})},
        timeout=15, **kw)
    if r.status_code >= 400:
        raise RuntimeError(f"{method} {path} -> {r.status_code} {r.text}")
    return r.json() if r.content else {}

# ---- Wallets ---------------------------------------------------------------
def list_wallets():
    """Active payment methods on this account."""
    return api("GET", "/api/public/v1/wallets")["data"]

def quote_wallet(wallet_id, amount, currency="USD"):
    """How much the user must send to THIS wallet for the given USD/other amount.
       Returns: { pay_amount, pay_currency, rate, ... }
       - BINANCE_PAY  -> pay_amount == amount in invoice currency (Binance handles FX)
       - Same-currency wallet -> pay_amount == amount
       - Cross-currency      -> live rate via Binance ticker (cached 30s)"""
    return api("GET", f"/api/public/v1/wallets/{wallet_id}/quote",
               params={"amount": amount, "currency": currency})

# ---- Invoices --------------------------------------------------------------
def create_invoice(amount, currency="USD", wallet_id=None,
                   expires_in_minutes=30, tolerance_pct=2, **extra):
    """Always bind a wallet_id so no hosted pay page is required.
       tolerance_pct=2 → 2% accepted for cross-currency / on-chain crypto.
       Binance-Pay same-asset payments are matched EXACTLY (server-side rule)."""
    body = {"amount": amount, "currency": currency,
            "expires_in_minutes": expires_in_minutes,
            "tolerance_pct": tolerance_pct, **extra}
    if wallet_id: body["wallet_id"] = wallet_id
    return api("POST", "/api/public/v1/invoices", json=body)

def get_invoice(invoice_id):
    return api("GET", f"/api/public/v1/invoices/{invoice_id}")

def cancel_invoice(invoice_id):
    return api("POST", f"/api/public/v1/invoices/{invoice_id}/cancel")

3. Telegram bot — amount → live quotes → invoice

Buttons render with the converted amount baked in, e.g. USDT TRC20 • 92.4012 USDT. When the user taps, an invoice is created bound to that wallet (no hosted pay page) and the bot shows the address, QR, and exact pay amount.

python
# bot.py — Telegram deposit bot (no hosted pay page)
# Flow:
#   /deposit  →  ask amount in USD
#             →  list active wallets + LIVE QUOTE per wallet
#                  e.g.  USDT TRC20  →  92.4012 USDT
#                        USDT BEP20  →  92.4012 USDT
#                        BNB BSC     →  0.1531  BNB
#                        Binance Pay →  100 USD (any asset)
#             →  user picks a method
#             →  create invoice bound to that wallet
#             →  show: address, QR, EXACT pay_amount, expiry
#             →  webhook auto-credits when the matching tx arrives
import html, logging, asyncio
from telegram import Update, InlineKeyboardButton as IKB, InlineKeyboardMarkup as IKM
from telegram.ext import (Application, CommandHandler, CallbackQueryHandler,
                          MessageHandler, ContextTypes, filters)
from api_client import (list_wallets, quote_wallet, create_invoice,
                        get_invoice, cancel_invoice)
import os

logging.basicConfig(level=logging.INFO)
TOKEN     = os.environ["TELEGRAM_BOT_TOKEN"]
QUOTE_CCY = "USD"     # change to "USDT" if you prefer

# chat_id -> {"amount": float, "quotes": {wallet_id: {...}}}
PENDING: dict[int, dict] = {}
# invoice_id -> chat_id (so the webhook server can DM the user)
INVOICE_CHAT: dict[str, int] = {}

# ---- /start, /deposit ------------------------------------------------------
async def cmd_start(update: Update, ctx):
    await update.message.reply_text(
        "👋 Welcome! Type /deposit to top up your balance.")

async def cmd_deposit(update: Update, ctx):
    chat_id = update.effective_chat.id
    PENDING[chat_id] = {"amount": None}
    await update.message.reply_text(
        f"💰 Enter the amount in {QUOTE_CCY} you want to deposit:")

# ---- Amount handler --------------------------------------------------------
async def on_amount(update: Update, ctx):
    chat_id = update.effective_chat.id
    if chat_id not in PENDING or PENDING[chat_id].get("amount") is not None:
        return
    try:
        amount = float(update.message.text.strip())
        assert amount > 0
    except Exception:
        return await update.message.reply_text("❌ Enter a positive number. Try again.")
    PENDING[chat_id]["amount"] = amount

    wallets = list_wallets()
    if not wallets:
        PENDING.pop(chat_id, None)
        return await update.message.reply_text("⚠️ No payment methods are active right now.")

    # Live FX quote per wallet (parallel-ish; sequential is fine for a few wallets)
    quotes = {}
    rows = []
    for w in wallets:
        try:
            q = quote_wallet(w["id"], amount, QUOTE_CCY)
        except Exception as e:
            logging.warning("quote failed for %s: %s", w["id"], e)
            continue
        quotes[w["id"]] = {"wallet": w, "quote": q}
        label = w.get("label") or f"{w['currency']} {w['network']}"
        pay   = f"{q['pay_amount']:.6f}".rstrip("0").rstrip(".")
        rows.append([IKB(f"{label}  •  {pay} {q['pay_currency']}",
                         callback_data=f"pick:{w['id']}")])
    if not rows:
        PENDING.pop(chat_id, None)
        return await update.message.reply_text("⚠️ Could not fetch live rates. Try again later.")

    PENDING[chat_id]["quotes"] = quotes
    await update.message.reply_text(
        f"Select a payment method for <b>{amount} {QUOTE_CCY}</b>:\n"
        f"<i>Binance-Pay = exact match. Other networks accept ±2% to absorb rate movement.</i>",
        parse_mode="HTML", reply_markup=IKM(rows))

# ---- User picks a wallet → create invoice ---------------------------------
async def on_pick(update: Update, ctx):
    q = update.callback_query; await q.answer()
    chat_id = q.message.chat.id
    wallet_id = q.data.split(":", 1)[1]
    state = PENDING.pop(chat_id, None)
    if not state or not state.get("amount"):
        return await q.message.reply_text("Session expired. Send /deposit again.")
    amount = state["amount"]

    # Create the invoice bound to the chosen wallet, 30-minute expiry, 2% tolerance
    inv = create_invoice(amount=amount, currency=QUOTE_CCY,
                         wallet_id=wallet_id,
                         expires_in_minutes=30,
                         tolerance_pct=2,
                         description=f"TG deposit • chat {chat_id}",
                         customer_email=None)
    INVOICE_CHAT[inv["id"]] = chat_id   # so the webhook can DM the user later

    w   = inv["wallet"] or {}
    pay = inv.get("pay_amount") or amount
    cur = inv.get("pay_currency") or QUOTE_CCY
    text = (
        f"🧾 <b>Invoice</b> <code>{inv['id']}</code>\n\n"
        f"💵 <b>Pay exactly:</b> <code>{pay} {cur}</code>\n"
        f"🌐 <b>Network:</b> {html.escape(str(w.get('network','-')))} ({html.escape(str(w.get('currency','-')))})\n"
        f"📍 <b>Address:</b>\n<code>{html.escape(str(w.get('address','-')))}</code>\n"
        f"⏱ <b>Expires:</b> {inv.get('expires_at','-')}\n\n"
        "⚠️ Send the <b>exact</b> amount.\n"
        "✅ Balance updates automatically as soon as the deposit lands."
    )
    kb = IKM([[IKB("📷 QR", url=w.get("qr_url","https://example.com")),
               IKB("🗑 Cancel", callback_data=f"cancel:{inv['id']}")]])
    await q.message.reply_text(text, parse_mode="HTML", reply_markup=kb,
                               disable_web_page_preview=True)

async def on_cancel(update: Update, ctx):
    q = update.callback_query; await q.answer()
    invoice_id = q.data.split(":", 1)[1]
    try: cancel_invoice(invoice_id)
    except Exception: pass
    INVOICE_CHAT.pop(invoice_id, None)
    await q.message.reply_text("❎ Invoice cancelled.")

def main():
    app = Application.builder().token(TOKEN).build()
    app.add_handler(CommandHandler("start",   cmd_start))
    app.add_handler(CommandHandler("deposit", cmd_deposit))
    app.add_handler(CallbackQueryHandler(on_pick,   pattern=r"^pick:"))
    app.add_handler(CallbackQueryHandler(on_cancel, pattern=r"^cancel:"))
    app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, on_amount))
    app.run_polling(allowed_updates=Update.ALL_TYPES)

if __name__ == "__main__":
    main()

4. IPN webhook server — auto-credit on payment

This is what makes the flow 100% automatic. The server fires invoice.paid the moment Binance reports a matching deposit; your endpoint credits the balance and DMs the user.

python
# ipn_server.py — Flask IPN endpoint (THIS is what makes it 100% automatic)
# Run on a public HTTPS URL, then register it as a webhook in your dashboard.
# The server signs each delivery with: X-Webhook-Signature: HMAC_SHA256(raw_body)
import os, hmac, hashlib, json, logging
from flask import Flask, request, abort
from api_client import get_invoice

logging.basicConfig(level=logging.INFO)
app = Flask(__name__)
WEBHOOK_SECRET = os.environ["WEBHOOK_SECRET"].encode()

# In-memory idempotency cache. In production use Redis or a DB unique index.
PROCESSED: set[str] = set()

def verify_signature(raw_body: bytes, signature_header: str) -> bool:
    expected = hmac.new(WEBHOOK_SECRET, raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature_header or "")

@app.post("/ipn")
def ipn():
    raw = request.get_data()                                 # MUST be the raw body
    sig = request.headers.get("X-Webhook-Signature", "")
    if not verify_signature(raw, sig):
        logging.warning("bad signature"); abort(401)

    event = json.loads(raw)
    # Shape: { "event": "invoice.paid", "data": { invoice_id, status, amount,
    #          currency, paid_amount, tx_hash, wallet_network, wallet_currency } }
    if event.get("event") != "invoice.paid":
        return "", 200

    d = event["data"]
    key = f"{d['invoice_id']}:{d['tx_hash']}"
    if key in PROCESSED:
        return "", 200                                       # idempotent — already credited
    PROCESSED.add(key)

    # OPTIONAL: re-fetch to verify server-side state (defence-in-depth)
    fresh = get_invoice(d["invoice_id"])
    if fresh["status"] != "paid":
        logging.error("paid event but server says %s", fresh["status"])
        return "", 200

    # 1) Credit the balance in YOUR database (do it inside a TX with the idempotency key)
    credit_user_balance(
        invoice_id = d["invoice_id"],
        amount     = d["amount"],         # invoice amount in invoice currency (USD)
        currency   = d["currency"],
        paid_amount= d["paid_amount"],    # what actually arrived (e.g. 92.4012 USDT)
        tx_hash    = d["tx_hash"],
    )

    # 2) DM the Telegram user (the bot stored chat_id when the invoice was created)
    try:
        from bot import INVOICE_CHAT
        chat_id = INVOICE_CHAT.pop(d["invoice_id"], None)
        if chat_id:
            import requests
            requests.post(
                f"https://api.telegram.org/bot{os.environ['TELEGRAM_BOT_TOKEN']}/sendMessage",
                json={"chat_id": chat_id, "parse_mode": "HTML",
                      "text": f"✅ <b>Payment received</b>\n"
                              f"Invoice <code>{d['invoice_id']}</code>\n"
                              f"Paid: {d['paid_amount']} {d.get('wallet_currency') or d['currency']}\n"
                              f"Your balance has been credited."},
                timeout=10)
    except Exception as e:
        logging.exception("telegram notify failed: %s", e)

    # IMPORTANT: respond 2xx fast. Anything else triggers the auto-retry queue
    # (6 attempts, exponential backoff up to 24h).
    return "", 200

def credit_user_balance(**kw):
    # TODO: your DB write. Make the (invoice_id, tx_hash) pair UNIQUE.
    logging.info("CREDIT %s", kw)

if __name__ == "__main__":
    # In production: gunicorn -w 2 -b 0.0.0.0:8080 ipn_server:app
    app.run(host="0.0.0.0", port=8080)
  • Always verify the HMAC signature against the raw request body — never against parsed JSON.
  • Make it idempotent. Use (invoice_id, tx_hash) as a UNIQUE key in your DB.
  • Respond 2xx fast. Non-2xx triggers the auto-retry queue (6 attempts, exponential backoff up to 24 h).

5. Register the webhook (one time)

bash
# 1) Run ipn_server.py behind HTTPS (Cloudflare Tunnel, Render, Fly.io, ngrok…)
#    Final URL example:  https://bot.your-domain.com/ipn
#
# 2) Register it ONCE in the dashboard:  Settings → Webhooks → Add endpoint
#    URL    : https://bot.your-domain.com/ipn
#    Events : invoice.paid  (also: invoice.expired, invoice.cancelled if you want)
#    Copy the WEBHOOK_SECRET shown ONCE → paste into .env
#
# 3) Verify it works — from the dashboard click "Send test event"
#    or from the CLI:
curl -X POST https://bot.your-domain.com/ipn \
     -H "Content-Type: application/json" \
     -H "X-Webhook-Signature: $(printf '%s' '{"event":"invoice.paid","data":{}}' \
                                | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" -r | cut -d' ' -f1)" \
     -d '{"event":"invoice.paid","data":{}}'
#    → expect HTTP 200

For local development behind a tunnel: ngrok http 8080 → use the public HTTPS URL as the endpoint.

6. Tolerance rules

text
# ✅ Tolerance rules (enforced server-side, you don't configure this in the bot)
#
#   Binance Pay  (binance → binance, same asset as invoice)   → 0%   (EXACT)
#   Binance Pay  (cross-asset, server converts via live FX)   → tolerance_pct (default 2%)
#   On-chain crypto deposit                                   → tolerance_pct (default 2%)
#
# So when a buyer pays you in USDT for a USDT invoice from their Binance to your
# Binance Pay account, the amount must match EXACTLY. For everything else the
# server allows up to 2% (configurable per invoice up to 10%) to absorb the
# tiny rate movement between quote time and settlement time.

7. Polling fallback (optional)

Not needed if your IPN endpoint is healthy — webhook deliveries already auto-retry. Include only if you want a belt-and-braces check from a worker.

python
# OPTIONAL polling fallback — only useful if your IPN server has downtime.
# The webhook delivery already has 6 auto-retries (exponential backoff up to 24h),
# so most setups DON'T need this. Keep it commented out unless you want it.
import time
from api_client import get_invoice

def wait_until_paid(invoice_id, every_s=30, max_minutes=35):
    for _ in range(max_minutes * 60 // every_s):
        inv = get_invoice(invoice_id)
        if inv["status"] in ("paid", "expired", "cancelled"):
            return inv
        time.sleep(every_s)
    return get_invoice(invoice_id)

8. End-to-end test

Run this once after editing .env — it exercises every endpoint the bot uses and cancels the invoice so nothing gets credited.

python
# admin_test.py — one-shot end-to-end check
# Run this from your laptop after editing .env. It exercises every API call
# the bot uses, then cancels the invoice so nothing gets credited.
from api_client import list_wallets, quote_wallet, create_invoice, get_invoice, cancel_invoice

def main():
    print("→ /wallets"); ws = list_wallets()
    assert ws, "No active wallets — enable at least one in the dashboard"
    w = ws[0]; print("   ok:", w["currency"], w["network"])

    print("→ /quote"); q = quote_wallet(w["id"], 100, "USD")
    print(f"   100 USD = {q['pay_amount']} {q['pay_currency']}  (rate {q['rate']})")

    print("→ POST /invoices"); inv = create_invoice(
        amount=100, currency="USD", wallet_id=w["id"],
        expires_in_minutes=30, tolerance_pct=2,
        description="end-to-end test")
    print(f"   created {inv['id']}  pay_amount={inv['pay_amount']} {inv['pay_currency']}")

    print("→ GET /invoices/{id}"); print("  ", get_invoice(inv["id"])["status"])
    print("→ cancel"); cancel_invoice(inv["id"]); print("   ok")
    print("\n✅ All API calls succeeded. Your bot is wired up correctly.")

if __name__ == "__main__":
    main()

Run it

bash
# terminal 1 — IPN server (public HTTPS)
gunicorn -w 2 -b 0.0.0.0:8080 ipn_server:app

# terminal 2 — Telegram bot
python bot.py

# now message your bot in Telegram:
#   /deposit   →   100   →   pick a wallet   →   pay   →   ✅ auto-credited