StableOps

订阅

使用 StableOps 创建稳定币订阅套餐、为商户用户开通和管理订阅、自动生成账单,并让终端用户通过托管收银台或自有钱包流程付款;再根据已验证的订阅事件更新服务状态。完整覆盖套餐变更、取消、恢复、账单支付和付款状态查询,帮助业务可靠维护订阅生命周期。

StableOps 订阅把商户侧管理和终端用户支付分开:你的后端用密钥管理套餐、订阅、账单设置和 Portal 会话;终端用户拿 Portal 令牌查看自己的账单,并通过托管收银台或你的自有钱包支付流程完成付款。

生命周期

  1. 创建一个或多个套餐,指定周期和金额,套餐金额以美元计价。
  2. 为你的 merchantUserId 创建订阅。
  3. 读取首期未支付账单,并为该用户创建 Portal 会话。
  4. 在 Portal 上下文为账单创建收银台会话,跳转到收银台支付,或拿到支付指令,交给你自己的钱包支付。
  5. 以 Webhook 和账单状态为准完成业务逻辑。successUrl 只是浏览器返回路径。

结算币种

套餐和账单不绑定某一种稳定币。账单发起支付时,StableOps 会按账单金额生成一张收款单,发起支付请求传入 acceptedAssets,StableOps 会按这些链和资产组合分配收款地址。付款人在收银台里选择一条链和一种币,或你的钱包从返回的支付指令里选择一项;实际打出的那种币就成为账单的结算币种。在此之前,账单的 assetnull,表示还没有已结算币种。

acceptedAssets 需要与你已导入的地址池可收款范围一致。如果请求的链/资产没有可用地址,创建收款单会像单独创建 payment order 一样失败。

支付方式选择

托管收银台是最快的集成方式:调用 portal.invoices.checkoutSession(invoice.id, { acceptedAssets, ... }),把用户重定向到返回的 checkoutUrl

如果你已经有自己的钱包支付流程,可以改为调用 portal.invoices.pay(invoice.id, { acceptedAssets })。响应里包含 paymentOrder.paymentInstructions,字段形状和单独创建 payment order 一致。你选择其中一条指令,让用户钱包在对应链和资产上把账单金额转到该地址,然后以 StableOps webhook 和账单状态作为最终结算依据。

结算模型

账单收银台会话包住的是账单自己的收款单。收款单元数据里有 invoice_id,账单记录也回写了 paymentOrderId。收款单到达 FINALIZED 后,StableOps 通过这对字段结算账单,把付款人实际打出的稳定币回填到账单的 asset,再把订阅状态推进到 active 或完成套餐升级。

角色

  • 后端或沙箱工具用密钥创建套餐、订阅和 Portal 会话。
  • 终端用户浏览器用 Portal 令牌读取自己的订阅和账单。
  • 控制台用于管理套餐、订阅、账单以及商户级订阅设置。

在线测试

下面的面板会在浏览器里使用你的沙箱 API 密钥,准备演示套餐、创建演示订阅、创建 Portal 会话,并打开账单对应的托管收银台。

开启时会在创建账单收银台会话前导入一个确定性 burner 地址,适合 org 还没有任何收款地址的场景。若只想使用自己管理的地址,请关闭。

订阅组件源码托管在 GitHub:github.com/StableOps/stableops-playground

SDK 示例

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

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

const plan = await stableops.merchantSubscriptions.plans.create(
  {
    code: 'starter',
    name: 'Starter',
    groupKey: 'starter',
    // 金额以美元计价;不指定币种,终端用户在收银台里选择 USDC 或 USDT。
    amount: '9.00',
    interval: 'month',
    intervalCount: 1,
  },
  { idempotencyKey: 'plan_starter' },
)

const created = await stableops.merchantSubscriptions.subscriptions.create(
  {
    planId: plan.id,
    merchantUserId: 'user_123',
  },
  { idempotencyKey: 'sub_user_123' },
)

const portalSession = await stableops.merchantSubscriptions.portalSessions.create({
  merchantUserId: 'user_123',
})

const portal = stableops.portal(portalSession.portalToken)
const invoice = created.invoice!
const acceptedAssets = [{ chain: 'base-sepolia', asset: 'USDC' }] as const
const checkout = await portal.invoices.checkoutSession(invoice.id, {
  acceptedAssets,
  // 可选。省略时默认 exact;auto 会在共享地址金额冲突时自动微调应付金额。
  amountMode: 'auto',
  successUrl: 'https://merchant.example/success',
  cancelUrl: 'https://merchant.example/cancel',
})

return Response.redirect(checkout.checkoutUrl, 303)

也可以使用你自己的钱包支付:

const payment = await portal.invoices.pay(invoice.id, {
  acceptedAssets,
  // 可选。省略时默认 exact;auto 时请始终使用返回的 paymentOrder.amount 发起转账。
  amountMode: 'auto',
})
const instruction = payment.paymentOrder.paymentInstructions[0]

// 交给你的钱包层:发送 payment.paymentOrder.amount 数量的
// instruction.asset 到 instruction.chain 上的 instruction.address。
await payWithYourWallet({
  chain: instruction.chain,
  asset: instruction.asset,
  amount: payment.paymentOrder.amount,
  to: instruction.address,
})

这篇文档怎么样?

最后更新

本页内容