一行 API 调用,支持 7 条链的 USDT 收款 ------ 不需要任何 Web3 知识。
做 SaaS 的时候,你可能遇到过这些问题:
- 海外客户没法用支付宝/微信
- Stripe 不支持你的目标市场(东南亚、中东、非洲)
- 信用卡手续费太高(2.9% + $0.30)
- 跨境结算要等 3-7 天
USDT(稳定币)可以解决这些问题。本文教你用 IronixPay 在 Next.js 项目中集成 USDT 支付 ------ 集成模式和 Stripe 几乎一样:服务端创建 Session → 重定向用户 → 收到 Webhook 确认。
我们要搭建的:
- 一个 Next.js API 路由,创建 Checkout Session
- 一个商品页面,带"Pay with USDT"按钮
- 一个 Webhook 处理器,验签 + 处理支付确认
- 成功页和取消页
前置条件: Node.js 18+,一个免费的 IronixPay 账号
💡 完整源码:github.com/IronixPay/i...
1. 项目初始化
bash
npx create-next-app@latest my-store --typescript --app
cd my-store
创建 .env.local 文件:
env
# 在 https://app.ironixpay.com → Dashboard → API Keys 获取
IRONIXPAY_SECRET_KEY=sk_test_your_key_here
IRONIXPAY_API_URL=https://sandbox.ironixpay.com
NEXT_PUBLIC_APP_URL=http://localhost:3000
不需要安装额外的包,只用原生 fetch 就够了。
2. API 封装
创建 src/lib/ironixpay.ts ------ 一个轻量的 API 封装:
typescript
// src/lib/ironixpay.ts
export type Network =
| "TRON" | "BSC" | "ETHEREUM"
| "POLYGON" | "ARBITRUM" | "OPTIMISM" | "BASE";
export interface CreateSessionParams {
amount: number; // USDT 微单位 (1 USDT = 1,000,000)
currency: "USDT";
network: Network;
success_url: string;
cancel_url: string;
client_reference_id?: string;
}
export interface CheckoutSession {
id: string;
url: string;
status: string;
amount_expected: string;
pay_address: string;
expires_at: string;
}
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: CreateSessionParams
): Promise<CheckoutSession> {
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 API error (${res.status}): ${error.message || res.statusText}`
);
}
return res.json();
}
关键概念
- 微单位 :所有金额使用最小单位。
10.50 USDT = 10_500_000。这样可以避免浮点数精度问题 ------ Stripe 用 cents 也是同样的思路。 - 仅服务端调用 :Secret Key(
sk_test_...)只在服务端使用,永远不暴露给客户端。 - 7 条链:TRON、BSC、Ethereum、Polygon、Arbitrum、Optimism、Base。用户可以选择自己偏好的链。
3. Checkout API 路由
创建 src/app/api/checkout/route.ts:
typescript
// src/app/api/checkout/route.ts
import { NextResponse } from "next/server";
import { createCheckoutSession } from "@/lib/ironixpay";
import type { Network } from "@/lib/ironixpay";
export async function POST(request: Request) {
try {
const { amount, network = "TRON" } = await request.json() as {
amount: number;
network?: Network;
};
// 校验:最少 1 USDT
if (!amount || amount < 1_000_000) {
return NextResponse.json(
{ error: "Amount must be at least 1 USDT" },
{ status: 400 }
);
}
const appUrl = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
const session = await createCheckoutSession({
amount,
currency: "USDT",
network,
success_url: `${appUrl}/success`,
cancel_url: `${appUrl}/cancel`,
client_reference_id: `order_${Date.now()}`,
});
return NextResponse.json({ id: session.id, url: session.url });
} catch (error) {
console.error("Checkout error:", error);
return NextResponse.json(
{ error: error instanceof Error ? error.message : "Internal error" },
{ status: 500 }
);
}
}
这等同于 Stripe 的 stripe.checkout.sessions.create()。返回值中的 url 就是要重定向用户去的支付页面。
4. 商品页面
创建 src/app/page.tsx:
tsx
"use client";
import { useState } from "react";
export default function Home() {
const [loading, setLoading] = useState(false);
const handleCheckout = async () => {
setLoading(true);
try {
const res = await fetch("/api/checkout", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
amount: 9_990_000, // 9.99 USDT(注意用整数,不要写 9.99 * 1_000_000 会有精度问题)
network: "TRON",
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
window.location.href = data.url; // 重定向到 IronixPay 支付页
} catch (err) {
alert(err instanceof Error ? err.message : "支付失败");
setLoading(false);
}
};
return (
<main style={{ maxWidth: 480, margin: "80px auto", textAlign: "center" }}>
<h1>My SaaS Product</h1>
<p>Pro Plan --- $9.99/月</p>
<button onClick={handleCheckout} disabled={loading}>
{loading ? "跳转中..." : "使用 USDT 支付 $9.99"}
</button>
</main>
);
}
用户点击"支付"后的流程:
- 调用我们的 API 路由(服务端,密钥安全)
- 拿到托管支付页的 URL
- 重定向 ------ IronixPay 展示二维码和收款地址
- 用户用任意 TRON 钱包(TronLink、Trust Wallet 等)支付
5. Webhook 处理
这是最关键的部分。用户支付后,IronixPay 会发送 Webhook 通知你链上支付已确认。
创建 src/app/api/webhooks/ironixpay/route.ts:
typescript
// src/app/api/webhooks/ironixpay/route.ts
import { NextResponse } from "next/server";
export async function POST(request: Request) {
const body = await request.text();
const signature = request.headers.get("x-signature") || "";
const timestamp = request.headers.get("x-timestamp") || "";
const secret = process.env.IRONIXPAY_WEBHOOK_SECRET || "";
// 第一步:验证 HMAC-SHA256 签名
if (secret) {
const isValid = await verifySignature(body, signature, timestamp, secret);
if (!isValid) {
return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
}
}
// 第二步:解析并处理事件
const event = JSON.parse(body);
switch (event.event_type) {
case "session.completed":
// ✅ 链上支付已确认 ------ 可以发货了!
const { id, amount_received, client_reference_id } = event.data;
console.log(`支付 ${id} 已确认:${amount_received} 微单位`);
// TODO: 更新数据库、激活订阅、发送邮件
break;
case "session.expired":
// ⏰ Session 超时 ------ 没有收到付款
console.log(`Session ${event.data.id} 已过期`);
break;
}
return NextResponse.json({ received: true });
}
// HMAC-SHA256 验签
async function verifySignature(
payload: string, signature: string, timestamp: string, secret: string
): Promise<boolean> {
// 拒绝超过 5 分钟的请求(防重放攻击)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp)) > 300) return false;
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
"raw", encoder.encode(secret),
{ name: "HMAC", hash: "SHA-256" }, false, ["sign"]
);
// IronixPay 签名格式:"{timestamp}.{payload}"
const message = `${timestamp}.${payload}`;
const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(message));
const computed = Array.from(new Uint8Array(sig))
.map(b => b.toString(16).padStart(2, "0")).join("");
return computed === signature;
}
为什么要验签?
不验签的话,任何人都可以伪造 "payment confirmed" 事件发到你的 Webhook URL。HMAC-SHA256 签名确保请求来自 IronixPay。
IronixPay 使用 HMAC(secret, timestamp + "." + body) 的格式 ------ 绑定时间戳可以防止重放攻击(攻击者截获一个 Webhook 后重复发送)。
6. 成功和取消页面
tsx
// src/app/success/page.tsx
export default function SuccessPage() {
return (
<main style={{ maxWidth: 480, margin: "80px auto", textAlign: "center" }}>
<h1>✅ 支付成功!</h1>
<p>你的 USDT 支付已经在链上确认。</p>
<a href="/">返回商店</a>
</main>
);
}
tsx
// src/app/cancel/page.tsx
export default function CancelPage() {
return (
<main style={{ maxWidth: 480, margin: "80px auto", textAlign: "center" }}>
<h1>⏰ 支付已取消</h1>
<p>Session 已过期,没有产生任何扣款。</p>
<a href="/">重试</a>
</main>
);
}
7. 跑起来
bash
npm run dev
# 打开 http://localhost:3000
# 点击 "Pay" → 会跳转到 IronixPay 支付页面
Sandbox 模式下,支付页面显示 TRON 测试网地址。你可以从 Nile 水龙头获取测试 TRX/USDT 来模拟支付。
完整流程
bash
你的应用 IronixPay 区块链
│ │ │
│── POST /api/checkout ────▶│ │
│ │── 创建 Session ──────────▶│
│◀── { url } ───────────────│ │
│ │ │
│── 重定向用户 ─────────────▶│ │
│ │── 展示二维码/地址 ────────▶│
│ │ │
│ │◀── USDT 转账 ─────────────│
│ │── 链上验证 ───────────────▶│
│ │ │
│◀── Webhook: completed ────│ │
│── 发货/激活订阅 │ │
│── 重定向到 /success │ │
上线生产
只需要改三个环境变量:
env
IRONIXPAY_SECRET_KEY=sk_live_your_production_key
IRONIXPAY_API_URL=https://api.ironixpay.com
IRONIXPAY_WEBHOOK_SECRET=whsec_your_webhook_secret
代码完全不用动。
为什么选 USDT?
| 信用卡 (Stripe) | USDT (IronixPay) | |
|---|---|---|
| 手续费 | 2.9% + $0.30 | 1% |
| 到账时间 | 2-7 天 | 实时 |
| 拒付风险 | 有(退单欺诈) | 不可能 |
| 全球覆盖 | 受国家限制 | 有钱包就能付 |
| 商户 KYC | 需要 | 不需要 |
<math xmlns="http://www.w3.org/1998/Math/MathML"> 100 的订单: S t r i p e 收 100 的订单:Stripe 收 </math>100的订单:Stripe收3.20,IronixPay 收 $1.00。规模大了之后,这个差距非常明显。
相关资源
- 📖 IronixPay 文档
- 🧑💻 完整源码 (GitHub)
- 💬 有问题?Telegram
IronixPay 支持 TRON、BSC、Ethereum、Polygon、Arbitrum、Optimism、Base 上的 USDT 收款 ------ 一套 API 搞定。免费开始 →