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,
})

這篇文件怎麼樣?

最後更新

本頁內容