返回博客
Engineering
2026-07-117 分钟阅读作者:StableOps

如何在不托管资金的情况下接受 USDC:生产级架构

使用支付订单、链上监听、最终性和可靠 Webhook,将 USDC 收入商户控制的钱包。

USDC
非托管支付
支付架构

接受 USDC 付款并不要求把收款资金交给支付服务商控制。商户提供并控制收款钱包,客户把 USDC 直接转入这些地址。

但非托管只解决资金控制权,不会消除支付运营层。一套非托管 USDC 支付接入仍需要明确的业务订单、精确的链和资产付款指令、转账匹配、确认与最终性跟踪,以及能够安全触发履约的已验证事件。StableOps 负责订单和事件层;你的应用仍是客户业务订单的权威,商户仍控制收款钱包。

生产架构应把资金和支付状态分开

钱包只能回答“代币到了哪里”,不能回答“这笔转账支付了哪个购物车、是否按时、现在能否发货”。这些职责需要明确分层:

控制流:商户后端 -> StableOps API -> Payment Order -> Checkout 或自定义 UI
资金流:付款钱包 -> 区块链转账 -> 商户控制的收款地址
事件流:区块链 -> StableOps 扫描器 -> 签名 Webhook -> 商户后端
                                                       |
                                                       v
                                                  业务订单 / 履约

商户后端创建并拥有业务订单。StableOps 创建 Payment Order,从商户的收款地址池分配候选地址,并返回与链对应的付款指令。Hosted Checkout 或你的 UI 向客户展示这些指令。客户转账后,StableOps 观察并归一化链上事件,推进确认状态,再向服务端发送签名 Webhook。

在这个模型中,资金不经过 StableOps。StableOps 也不会替商户决定是否发货、开通权益或写入账本;你的应用在验证支付事件后,依据持久化的业务状态作出决定。

开始前,应按照 Quickstart 导入足够的收款地址并注册 Webhook 端点。地址容量属于生产依赖:如果某个链和资产没有可用的候选地址,系统就无法发出完整付款指令。

先创建业务订单,再创建支付订单

先持久化购物车、发票或订阅账单。将其不可变 ID 用作 merchantOrderId,并把已经保存的金额和过期时间作为 Payment Order 输入。这样,重试就有稳定身份,刷新浏览器也不会生成条款不同的第二次收款尝试。

import { StableOps } from '@stableops/api-sdk'

const stableops = new StableOps({
  apiKey: process.env.STABLEOPS_API_KEY!,
})

export async function createUsdcPayment(order: {
  id: string
  amount: string
  paymentExpiresAt: string
}) {
  return stableops.paymentOrders.create(
    {
      merchantOrderId: order.id,
      amount: order.amount,
      acceptedAssets: [{ chain: 'base', asset: 'USDC' }],
      expiresAt: order.paymentExpiresAt,
    },
    { idempotencyKey: `payment-order:${order.id}` },
  )
}

每次重试都必须复用相同的 merchantOrderId、金额、可接受资产、过期时间和幂等键。不要在每次请求时重新计算过期时间或生成随机键。应把 order.amount 与选中的 paymentInstructions 条目组合起来,展示准确的金额、链、资产和地址。如果以后启用 amountMode: 'auto',这个区别尤其重要,因为返回的 order.amount 可能不同于请求时的基准金额。这些值是付款指令,不是已经付款的证据。

Payment Orders 指南解释了地址分配和生命周期。如果希望由 StableOps 承载钱包交互和进度 UI,可以在相同订单模型上创建 Hosted Checkout Session。需要框架级示例时,可阅读如何在 Next.js 中接受 USDC 支付

匹配完整付款指令,而不是钱包余额

生产级匹配需要同时使用订单的链、资产、收款地址和预期金额。只检查钱包余额是否增加,会丢失转账与业务订单之间的关系。当多个客户支付相同金额、USDC 存在于多条支持链,或者转账在订单过期后才到达时,余额模型会产生歧义。

应把展示给客户的信息看成一个不可拆分的元组:

(支付订单, 链, 资产, 地址, 金额, 过期时间)

客户必须在指定链上发送指定资产。客服和运营策略还应分别覆盖错误网络、错误资产、少付、多付和迟到转账。不要把无法匹配的转账静默解释成某个恰好还未关闭的订单付款。

使用最小支付状态机

链上观察是一个过程,而不是一个布尔型 paid 字段。每个状态允许的应用行为不同。

事件含义安全的应用行为不安全的假设
payment.detected已观察到匹配转账展示“已收到付款,确认中”并记录交易哈希转账已经不可逆
payment.confirmed已达到配置的确认级别更新进度,或执行明确可撤销的动作所有链上风险都已消失
payment.finalized已达到最终履约状态幂等履约并写入最终收款记录无需验证事件即可处理
payment.reverted先前观察到的付款已被回滚停止待执行任务,并运行预先定义的补偿或复核低概率路径可以忽略

确认策略取决于具体链和产品风险。确认状态模型详细解释了确认、最终性与链重组之间的关系。

Checkout success URL 只是浏览器返回路径。钱包显示“已提交”和交易哈希可以用于展示进度或调查,但都不能证明付款已经最终完成。不可逆履约必须等待已验签、已去重的 payment.finalized Webhook。

把 Webhook 边界做成持久化流程

先读取未经修改的原始 request body,验证签名后再解析 JSON。在同一个数据库事务中,把 X-Event-Id 插入带唯一约束的表,并同时更新持久化业务状态,或者写入 outbox/履约任务;只有事务提交成功后才能返回 2xx。此时唯一键冲突才表示事件及其持久化后续动作都已提交,端点可以返回成功而不重复入队。

事件去重与履约幂等解决的是不同故障。事件 ID 阻止重试或重放把同一事件应用两次;按内部订单 ID 设计的幂等履约,则阻止两条合法代码路径重复发货或重复授予权益。

外部履约通常不能放进这个数据库事务。应由 worker 领取已经提交的 outbox 任务,按内部订单 ID 幂等执行副作用,再记录完成状态;对账 worker 负责恢复进程崩溃后仍处于可重试状态的任务。Webhook 处理器应足够短,以便可靠返回 2xx;验签、重试、重放和投递审计细节见 Webhook 指南

保留可对账的关联记录

每次收款尝试都应保存以下关系:

  • 内部业务订单 ID;
  • StableOps Payment Order ID;
  • 选定的链、资产、地址、金额和过期时间;
  • 每个 X-Event-Id 及其事件类型;
  • 链上交易哈希。

这些记录既能让 worker 在故障后恢复,也能让客服不依赖钱包余额回答付款发生了什么。定时对账任务应查找已经最终完成但履约未完成的付款、超过预期时间仍待处理的订单,以及已经记录事件但下游任务失败的情况。

生产检查清单

  • 明确产品接受的链与 USDC 组合;只有“USDC”并不等于已经选择网络。
  • 保持足够的合格收款地址;对单次使用地址池,应在可用容量耗尽前告警;对共享地址池,则应监控金额冲突与精确金额匹配。
  • 调用 StableOps 前持久化订单金额、过期时间、可接受资产和幂等键。
  • 为过期、迟到、错误链、错误资产、少付和多付制定处理策略。
  • 只有在风险合理时才把可撤销动作映射到 confirmed;不可逆履约留给 finalized
  • 从原始 body 验证 Webhook,把密钥保存在服务端,并规划密钥轮换。
  • X-Event-Id 建立唯一约束,按业务订单 ID 保证履约幂等。
  • 测试重复投递、重放、进程崩溃和 payment.reverted 处理。
  • 定时对账业务订单、Payment Order、事件和交易哈希。

FAQ

接受 USDC 付款需要托管客户或商户的钱包吗?

不需要。付款人从自己控制的钱包发起转账,商户提供收款地址。StableOps 管理支付订单、地址分配、链上观察和事件,但不持有商户收到的资金,也不接触客户私钥。

可以用一个钱包地址接收所有 USDC 付款吗?

它当然可以接收转账,但会增加可靠匹配和客服处理的难度。独立分配的地址能更清楚地关联订单与转账。如果地址策略使用共享地址,匹配模型仍必须用链、资产、金额和订单关系消除歧义。

USDC 付款什么时候可以安全履约?

不可逆履约应在服务端验签并去重 payment.finalized Webhook 后执行。payment.detectedpayment.confirmed 可用于展示进度或执行谨慎的可撤销动作,但不能代替最终付款凭据。

从一个完整付款闭环开始

按照 Quickstart 在 Sandbox 创建一个业务订单,用稳定幂等键创建对应 Payment Order,完成一次测试 USDC 转账,再把一个经过验证的 payment.finalized 处理器连接到幂等履约任务。确认这个闭环能承受重复投递和重放后,再扩展到更多链、资产与结账界面。

相关文章

对比托管网关、原始钱包转账和非托管支付基础设施,选择可靠的稳定币收款方案。

2026-07-09 · 7 分钟阅读

解释确认数、reorg、finalized 状态与商户履约的关系。

2026-07-10 · 6 分钟阅读

在 Next.js 中创建 Hosted USDC Checkout Session,安全跳转,并且只根据已验证的 Webhook 履约。