Telegram Bot 接入 USDT 支付完整教程

28 阅读3分钟

给你的 TG Bot 加上链上收款能力 —— 从 /buy 命令到自动确认,20 分钟搞定。


做 Telegram Bot 的开发者,大概率遇到过收款难题:

  • Stripe / PayPal 在 Bot 里体验很差,要跳外部浏览器
  • 很多目标用户在东南亚、中东、CIS,没有信用卡
  • 加密货币用户群体天然和 Telegram 高度重合
  • 你卖的是数字产品(VPN、会员、积分),不需要复杂的支付流

USDT 是最合适的选择 —— 用户熟悉、价格稳定(锚定美元)、链上结算不可逆。

本文教你用 IronixPay + grammY 框架搭一个带支付功能的 Telegram Bot。完整代码不到 200 行。

最终效果:

  • 用户发 /buy → 看到商品列表(Inline Button)
  • 选择商品 → Bot 创建支付链接
  • 点击「💳 Pay Now」→ 跳转 IronixPay 支付页
  • 用户用钱包转 USDT → 链上确认
  • Bot 自动发送「✅ 支付成功」消息

💡 完整源码:github.com/IronixPay/i…


1. 初始化项目

mkdir my-tg-bot && cd my-tg-bot
npm init -y
npm install grammy express dotenv
npm install -D typescript tsx @types/express @types/node

在 Telegram 找 @BotFather/newbot → 拿到 token。

创建 .env

BOT_TOKEN=123456:ABC-DEF...
IRONIXPAY_SECRET_KEY=sk_test_your_key_here
IRONIXPAY_API_URL=https://sandbox.ironixpay.com
IRONIXPAY_WEBHOOK_SECRET=whsec_your_secret_here
WEBHOOK_PORT=3000
PUBLIC_URL=https://your-domain.com

IronixPay 密钥在 app.ironixpay.com → API Keys 获取。


2. IronixPay API 封装

创建 src/ironixpay.ts

import crypto from "node:crypto";

const API_URL = process.env.IRONIXPAY_API_URL || "https://sandbox.ironixpay.com";
const SECRET_KEY = process.env.IRONIXPAY_SECRET_KEY || "";

export async function createCheckoutSession(params: {
    amount: number;      // 微单位:1 USDT = 1,000,000
    currency: "USDT";
    network: string;
    success_url: string;
    cancel_url: string;
    client_reference_id?: string;
}) {
    const res = await fetch(`${API_URL}/v1/checkout/sessions`, {
        method: "POST",
        headers: {
            Authorization: `Bearer ${SECRET_KEY}`,
            "Content-Type": "application/json",
        },
        body: JSON.stringify(params),
    });

    if (!res.ok) {
        const error = await res.json().catch(() => ({}));
        throw new Error(`IronixPay error (${res.status}): ${error.message || res.statusText}`);
    }
    return res.json();
}

export function verifyWebhookSignature(
    payload: string,
    signature: string,
    timestamp: string,
    secret: string
): boolean {
    // 拒绝超过 5 分钟的请求(防重放攻击)
    const now = Math.floor(Date.now() / 1000);
    if (Math.abs(now - parseInt(timestamp)) > 300) return false;

    // IronixPay 签名格式:"{timestamp}.{payload}"
    const message = `${timestamp}.${payload}`;
    const computed = crypto.createHmac("sha256", secret).update(message).digest("hex");

    if (computed.length !== signature.length) return false;

    return crypto.timingSafeEqual(
        Buffer.from(computed, "hex"),
        Buffer.from(signature, "hex")
    );
}

为什么用 timingSafeEqual

普通的 === 字符串比较在第一个不匹配的字符就会返回,攻击者可以通过测量响应时间逐字节猜出正确的签名。timingSafeEqual 保证无论匹配与否,比较时间都是恒定的。


3. Bot 主逻辑

创建 src/index.ts

import { Bot, InlineKeyboard } from "grammy";
import { createCheckoutSession } from "./ironixpay.js";
import { startWebhookServer } from "./webhook-server.js";

const bot = new Bot(process.env.BOT_TOKEN!);
const PUBLIC_URL = process.env.PUBLIC_URL || "http://localhost:3000";

// 商品列表(金额用微单位)
const PRODUCTS = [
    { id: "starter", name: "⚡ Starter", price: 9_990_000 },   // $9.99
    { id: "pro",     name: "🚀 Pro",     price: 29_990_000 },  // $29.99
];

// 内存订单表(生产环境请用数据库!)
const pendingOrders = new Map<string, { chatId: number; productId: string }>();

// /buy — 展示商品列表
bot.command("buy", async (ctx) => {
    const kb = new InlineKeyboard();
    for (const p of PRODUCTS) {
        kb.text(`${p.name} — $${(p.price / 1_000_000).toFixed(2)}`, `buy:${p.id}`).row();
    }
    await ctx.reply("🛒 选择商品:", { reply_markup: kb });
});

// 用户选择商品 → 创建支付
bot.callbackQuery(/^buy:(.+)$/, async (ctx) => {
    const product = PRODUCTS.find(p => p.id === ctx.match[1]);
    if (!product) return ctx.answerCallbackQuery({ text: "商品不存在" });

    await ctx.answerCallbackQuery({ text: "创建支付中..." });

    try {
        const orderId = `tg_${ctx.from.id}_${Date.now()}`;
        const session = await createCheckoutSession({
            amount: product.price,
            currency: "USDT",
            network: "TRON",
            success_url: `${PUBLIC_URL}/success`,
            cancel_url: `${PUBLIC_URL}/cancel`,
            client_reference_id: orderId,
        });

        // 保存订单,webhook 收到后用来匹配
        pendingOrders.set(orderId, { chatId: ctx.chat!.id, productId: product.id });

        const kb = new InlineKeyboard().url("💳 Pay Now", session.url);
        await ctx.editMessageText(
            `${product.name}\n💰 $${(product.price / 1_000_000).toFixed(2)} USDT\n\n点击下方按钮支付:`,
            { reply_markup: kb }
        );
    } catch (error) {
        console.error("支付创建失败:", error);
        await ctx.editMessageText("❌ 创建支付失败,请用 /buy 重试");
    }
});

// 启动 webhook 服务 + bot

startWebhookServer(bot, pendingOrders);
bot.start({ onStart: () => console.log("🤖 Bot 已启动!") });

用户视角的完整流程:

发 /buy → 看到商品按钮
点 "⚡ Starter — $9.99" → Bot 创建 IronixPay 支付 Session
点 "💳 Pay Now" → 浏览器打开支付页
用钱包转 USDT → IronixPay 链上验证
Bot 发消息 "✅ 支付已确认!" → 完成!

4. Webhook 服务

创建 src/webhook-server.ts

import express from "express";
import type { Bot } from "grammy";
import { verifyWebhookSignature } from "./ironixpay.js";

export function startWebhookServer(
    bot: Bot,
    pendingOrders: Map<string, { chatId: number; productId: string }>
) {
    const app = express();
    app.use("/webhooks", express.text({ type: "application/json" }));

    app.post("/webhooks/ironixpay", async (req, res) => {
        const body = req.body as string;
        const sig = (req.headers["x-signature"] as string) || "";
        const ts = (req.headers["x-timestamp"] as string) || "";
        const secret = process.env.IRONIXPAY_WEBHOOK_SECRET || "";

        // 验证 HMAC-SHA256 签名
        if (secret && !verifyWebhookSignature(body, sig, ts, secret)) {
            return res.status(401).json({ error: "签名无效" });
        }

        const event = JSON.parse(body);

        if (event.event_type === "session.completed") {
            const order = pendingOrders.get(event.data.client_reference_id);
            if (order) {
                const amount = (parseInt(event.data.amount_received) / 1_000_000).toFixed(2);
                await bot.api.sendMessage(order.chatId,
                    `✅ 支付已确认!\n💰 收到 $${amount} USDT\n\n感谢购买!🎉`
                );
                pendingOrders.delete(event.data.client_reference_id);
            }
        }

        res.json({ received: true });
    });

    const port = process.env.WEBHOOK_PORT || 3000;
    app.listen(port, () => console.log(`🌐 Webhook 服务已启动,端口 ${port}`));
}

为什么需要单独的 Express 服务?

Telegram Bot 用「长轮询」接收消息。但 IronixPay 需要通过 HTTP 主动推送支付通知给你。所以我们在 Bot 旁边跑一个小型 Express 服务来接收 webhook。


5. 跑起来

npx tsx --require dotenv/config src/index.ts

打开 Telegram → 给你的 Bot 发 /buy → 选商品 → 点"Pay Now"。

本地测试 webhook 可以用 curl 模拟:

curl -X POST http://localhost:3000/webhooks/ironixpay \
  -H "Content-Type: application/json" \
  -d '{"event_type":"session.completed","data":{"amount_received":"9990000","client_reference_id":"你的订单ID"}}'

如果要测真实 webhook,用 ngrok 暴露端口:

npx ngrok http 3000
# 把 ngrok 地址配到 IronixPay 后台的 Webhook URL

完整架构

Telegram 用户          你的服务器              IronixPay          区块链
     │                      │                       │                │
     │── /buy ─────────────▶│                       │                │
     │◀── 商品按钮 ─────────│                       │                │
     │── 点击 "Starter" ───▶│                       │                │
     │                      │── 创建 Session ──────▶│                │
     │                      │◀── { url } ──────────│                │
     │◀── "Pay Now" 按钮 ──│                       │                │
     │                      │                       │                │
     │── 点击 Pay Now ─────▶│──── 打开支付页 ──────▶│                │
     │                      │                       │◀── USDT 转账 ──│
     │                      │                       │── 链上验证 ────▶│
     │                      │◀── Webhook ──────────│                │
     │◀── "✅ 支付成功!" ──│                       │                │

上线生产

  1. 切换密钥:
IRONIXPAY_SECRET_KEY=sk_live_...
IRONIXPAY_API_URL=https://api.ironixpay.com
  1. 把内存 pendingOrders 换成 Redis 或 PostgreSQL
  2. 部署到服务器,用反向代理(Caddy/Nginx)暴露 Webhook 端口
  3. 在 IronixPay 后台配置 Webhook URL

生产模式自动支持 7 条链(TRON、BSC、Ethereum、Polygon、Arbitrum、Optimism、Base),用户可以选择自己偏好的链支付。


相关资源


IronixPay —— 一套 API 支持 7 条链的 USDT 收款,不需要任何 Web3 知识。免费开始 →