加密货币支付网关是一个服务层,它将链上转账转化为商家可识别的支付事件:它创建支付意图,监控链上是否向预期地址发送了预期金额,应用确认策略,并报告结算结果。本文解释了该流程背后的架构,然后通过一个可运行的集成模式,演示支付意图、幂等的 Webhook 处理以及对账任务。它将网关的文档化行为与您必须自行制定和衡量的确认及监控决策区分开来。目标是构建一个在重试、链重组、支付不足和延迟支付情况下都正确的支付流程——而不仅仅是在顺利路径演示中能工作。
加密货币支付网关的实际作用
加密货币支付网关位于客户钱包和您的订单系统之间。它不通过卡网络转移资金;它观察区块链状态,并将转账转化为您的后端可以处理的支付事件。核心职责在各提供商之间是一致的:创建一个具有固定金额和目的地的支付意图,监控相关链上的匹配转账,应用确认阈值,并通过 Webhook 或轮询 API 通知商家。
重要的架构要点是,网关是链状态的预言机,而不是银行。您的订单系统仍然必须决定什么算作已支付、如何处理支付不足或超额支付,以及如何将网关记录与您自己的账本进行对账。将网关的 Webhook 视为需要验证的声明,而不是权威的余额更新。
- 支付意图:金额、资产、目标地址、过期时间和商家参考号。
- 检测:按地址、资产、金额和时间窗口匹配交易。
- 确认策略:在标记为已支付之前需要多少个区块或何种最终性规则。
- 结算:转换为法币或稳定币、托管模式以及支付时间表。
- 报告:交易 ID、费用以及用于对账的可导出对账单。
假设与前提条件
本指南假设您有一个可以接收 HTTPS Webhook 的后端服务、一个具有唯一约束的数据库,以及一种运行计划任务的方式。您需要网关 API 凭证,并且对于自托管或非托管设置,需要访问您所接受链的可靠 RPC 端点。如果您仍在选择端点提供商,RPC Assistant 提供了具体的提供商和网络建议;本文专注于集成方法。
在编写代码之前,您还应该了解您的结算模型。托管网关结算到商家余额并按计划支付;非托管网关直接将资金发送到您控制的钱包。结算模型会改变您对账的真实来源,因此请尽早决定。有关支持链监控的节点基础设施的背景知识,请参阅区块链节点托管指南。
支付生命周期作为状态机
将每笔支付建模为显式状态机,而不是布尔值 paid 标志。一组可行的状态是 created、awaiting_payment、detected、confirmed、underpaid、overpaid、expired 和 refunded。状态转换应由经过验证的事件驱动,并记录时间戳,以便日后审计。
网关自身的状态值在命名上会有所不同,因此在边界处将它们映射到您的状态。将原始网关负载与您的规范化状态一起保存。当出现争议或对账不匹配时,原始负载就是您需要的证据。
const PAYMENT_STATES = [
'created',
'awaiting_payment',
'detected',
'confirmed',
'underpaid',
'overpaid',
'expired',
'refunded'
];
// Allowed transitions; reject anything not listed.
const TRANSITIONS = {
created: ['awaiting_payment', 'expired'],
awaiting_payment: ['detected', 'expired'],
detected: ['confirmed', 'underpaid', 'overpaid', 'expired'],
confirmed: ['refunded'],
underpaid: ['refunded', 'confirmed'],
overpaid: ['refunded'],
expired: ['detected'],
refunded: []
};
function canTransition(from, to) {
return (TRANSITIONS[from] || []).includes(to);
}通过服务器端调用创建支付意图
仅在服务器上创建支付意图,绝不要在客户端代码中创建,因为请求携带您的 API 密钥,并且金额不能由用户控制。发送唯一的商家参考号,以便重试是幂等的。下面的示例使用占位字段名;请根据您的网关文档化 API 进行调整。
在向客户端返回结账响应之前,始终持久化网关的支付 ID 和目标地址。如果您的进程在网关创建意图之后、但在您存储它之前崩溃,您将有一个难以对账的孤立支付。
curl -X POST "$GATEWAY_API_URL/v1/payments" \
-H "Authorization: Bearer $GATEWAY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042-attempt-1" \
-d '{
"amount": "49.99",
"currency": "USD",
"asset": "USDC",
"network": "ethereum",
"merchant_reference": "order-1042",
"callback_url": "https://shop.example.com/webhooks/crypto",
"expires_in": 900
}'确认策略:您必须衡量的决策
确认阈值是加密货币网关集成中最重要的参数。太低,链重组或替换费用交易可能会逆转您已经履行的支付;太高,客户会不必要地等待。没有通用的数字,任何没有上下文引用的数字都应视为起始假设,而不是事实。
按链和按订单价值定义您的策略。高价值订单可能需要更多确认或最终性感知规则;低价值数字商品可能接受较少的确认。记录策略,然后根据真实数据进行衡量:记录检测时的区块高度、确认时的区块高度以及观察到的任何重组事件。下表是一个模板,供您填入自己的测量值,而不是一组推荐值。
- 记录每条链从广播到首次检测的时间。
- 记录从检测到您的确认阈值的时间。
- 记录观察到的任何重组深度以及是否影响了支付。
- 跟踪在检测前过期的支付比例。
- 根据观察到的重组和延迟数据每季度审查策略。
在信任 Webhook 之前验证它们
Webhook 端点是公开的,因此任何人都可以向它们发送 POST 请求。使用网关提供的共享密钥验证签名,使用恒定时间函数进行比较,并拒绝过时的时间戳以限制重放。然后从您自己的数据库中按 ID 加载支付,并在应用事件之前确认该事件是合法的状态转换。
使处理程序幂等:网关会重试 Webhook,同一事件可能会到达多次。对网关事件 ID 的唯一约束是最简单的防御措施。
import crypto from 'node:crypto';
function verifySignature(rawBody, signatureHeader, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
const a = Buffer.from(expected, 'utf8');
const b = Buffer.from(signatureHeader || '', 'utf8');
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}
// Express-style handler (raw body required for HMAC).
app.post('/webhooks/crypto', express.raw({ type: '*/*' }), async (req, res) => {
const sig = req.get('X-Gateway-Signature');
if (!verifySignature(req.body, sig, process.env.GATEWAY_WEBHOOK_SECRET)) {
return res.status(401).send('bad signature');
}
const event = JSON.parse(req.body.toString('utf8'));
await db.query(
'INSERT INTO gateway_events (event_id, payload) VALUES ($1, $2) ON CONFLICT DO NOTHING',
[event.id, event]
);
await applyPaymentEvent(event); // checks state machine + idempotency
res.status(200).send('ok');
});使用 RPC 端点直接监控链状态
托管网关通常为您处理链监控,但非托管设置和对账任务需要它们自己的链视图。一个最小的监控器轮询最新区块并扫描发送到您地址的转账。在生产环境中使用专用的 RPC 端点而不是公共端点,因为速率限制和不一致的头部高度会导致检测遗漏或重复。
下面的脚本是一个诊断工具,而不是生产索引器:它打印当前头部和最近区块号的样本,以便您确认端点正在推进且一致。如果您怀疑有延迟,请比较两个端点。有关端点选择,请参阅 RPC 定价和计划 和多链 RPC 端点指南。
import requests, time
RPC_URL = "RPC_URL" # e.g. your provider's HTTPS endpoint
def rpc(method, params):
r = requests.post(RPC_URL, json={
"jsonrpc": "2.0", "id": 1, "method": method, "params": params
}, timeout=10)
r.raise_for_status()
body = r.json()
if "error" in body:
raise RuntimeError(body["error"])
return body["result"]
head = int(rpc("eth_blockNumber", []), 16)
print("head:", head)
for offset in range(5):
block = rpc("eth_getBlockByNumber", [hex(head - offset), False])
print(block["number"], block["hash"][:18], len(block["transactions"]), "txs")
time.sleep(0.2)预期验证结果
连接流程后,在测试网或网关的沙盒上运行测试支付。您应该观察到:意图以稳定的 ID 创建,Webhook 以有效签名送达并被接受,支付从 awaiting_payment 过渡到 detected,然后到 confirmed,并且即使您手动重放 Webhook,您的订单也恰好标记为履行一次。
对于 RPC 诊断,头部应在运行之间增加,并且区块哈希在几个确认深度内应保持稳定。如果头部停滞或向后跳跃,您的端点不健康,或者您正在访问状态不一致的负载均衡器。将这些观察结果记录在简短的运行手册中,以便值班工程师可以重复它们。
排查常见的集成故障
大多数生产事故属于几类:签名不匹配、重复履行、支付遗漏和金额不匹配。按此顺序处理它们,因为每个都可能掩盖下一个。
- 签名不匹配:确认您哈希的是原始正文,而不是解析后的 JSON,并且密钥与环境匹配。
- 重复履行:检查事件 ID 的唯一约束以及状态转换是否拒绝重复。
- 支付遗漏:验证地址和资产与意图匹配,并且您的监控器没有落后于链头。
- 支付不足:将收到的金额与创建时锁定的汇率下的意图金额进行比较;决定是接受、请求差额还是退款。
- 过期后的延迟支付:在宽限期内保持意图开放,并手动对账,而不是自动履行。
- Webhook 从未到达:轮询网关的支付状态作为后备,并在轮询和 Webhook 不一致时发出警报。
对账与局限性
对账是加密货币网关与卡处理器差异最大的地方。构建一个每日任务,拉取该期间的网关交易列表,按商家参考号将其与您的订单连接,并标记任何状态与网关不一致的订单。存储网关的费用和转换率,以便您的账本与结算匹配。
诚实地对待局限性。确认阈值减少但不能消除重组风险。从意图创建到结算之间的汇率波动可能会产生小的不匹配。托管网关引入交易对手风险,而非托管网关将密钥管理和链监控推给您。这些都不是避免加密货币支付的理由,但每个都需要明确的负责人和文档化的后备方案。
生产就绪集成的后续步骤
通过添加三件事,从可工作的演示转向生产系统:由您自己的测量支持的按链确认策略、具有轮询后备的幂等 Webhook 管道,以及按计划运行的对账任务。然后对 Webhook 端点进行负载测试,并在暂存环境中模拟重组。
如果您运营自己的监控或结算基础设施,请查看专用节点选项和我们支持的网络,以使端点容量与您的支付量匹配。对于给定链使用哪个端点或提供商的具体问题,请询问 RPC Assistant,而不是从列表中猜测。最后,在启动前记录您的退款和争议流程——这是出问题时客户最先注意到的部分。