StableOps
概念

BYO Addresses

导入和管理自有钱包地址。

BYO(Bring Your Own)地址 允许你把自己的链上地址导入 StableOps 收款。资金始终在 你的私钥下,同时享受 StableOps 的支付监听、确认追踪与 Webhook 投递。

总览

StableOps 不提供收款地址,也不代你收款。资金直接到你自己的钱包地址。你只需导入已有的地址:

  • 导入已有地址(来自钱包或托管方案)
  • 完全掌控私钥(StableOps 永远看不到)
  • 使用硬件钱包或多签
  • 对接 Fireblocks、Coinbase Custody、BitGo 等托管商
  • 满足要求自托管的合规口径

工作流

┌─────────────────┐         ┌─────────────┐         ┌─────────────┐
│   Your Wallet   │────────▶│  StableOps  │────────▶│  Your App   │
│  (Private Keys) │         │  (Monitor)  │         │ (Webhooks)  │
└─────────────────┘         └─────────────┘         └─────────────┘
     你掌控资金                   仅做监听                收到事件
  1. 你自己生成地址(任意钱包或托管方案)
  2. 导入到 StableOps(API 或 dashboard)
  3. StableOps 监听链上入账
  4. 你收到 Webhook(detected / confirmed / finalized)
  5. 资金始终归你,提取时用自己的私钥

地址分配模式

StableOps 支持两种分配模式:

单地址模式(默认)

每个支付单分配一个独立地址,只用一次。

特点

  • 一单对应一地址
  • 分配后该地址锁定
  • 订单完成或过期后回收
  • 入账归属一目了然

适用场景

  • 电商结账
  • 发票收款
  • 一次性购买

示例

// 导入单地址池
await stableops.addresses.import({
  chain: 'base',
  addresses: [
    '0x1234567890123456789012345678901234567890',
    '0x2345678901234567890123456789012345678901',
    '0x3456789012345678901234567890123456789012',
  ],
  mode: 'single', // 默认值
})

// 每笔订单都会拿到不同地址
const order1 = await stableops.paymentOrders.create({
  merchantOrderId: 'order_1',
  amount: '10.00',
  acceptedAssets: [{ chain: 'base', asset: 'USDC' }],
  expiresAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
})
// order1.paymentInstructions[0].address = '0x1234...'

const order2 = await stableops.paymentOrders.create({
  merchantOrderId: 'order_2',
  amount: '20.00',
  acceptedAssets: [{ chain: 'base', asset: 'USDC' }],
  expiresAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
})
// order2.paymentInstructions[0].address = '0x2345...'(另一个地址)

共享模式

多个订单可以共享同一个地址,靠精确金额区分。

特点

  • 多个订单共用一个地址
  • 必须金额精确匹配
  • 不允许多付或少付
  • 用完地址仍然可用

适用场景

  • 固定价订阅
  • API 信用点(固定档位)
  • 周期性付款
  • 地址数受限的场景

示例

// 导入共享地址
await stableops.addresses.import({
  chain: 'base',
  addresses: ['0x1234567890123456789012345678901234567890'],
  mode: 'shared',
})

// 多个订单共用同一个地址
const order1 = await stableops.paymentOrders.create({
  merchantOrderId: 'sub_user1_jan',
  amount: '10.01', // 精确金额
  acceptedAssets: [{ chain: 'base', asset: 'USDC' }],
  expiresAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
})
// order1.paymentInstructions[0].address = '0x1234...'

const order2 = await stableops.paymentOrders.create({
  merchantOrderId: 'sub_user2_jan',
  amount: '10.02', // 不同金额
  acceptedAssets: [{ chain: 'base', asset: 'USDC' }],
  expiresAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
})
// order2.paymentInstructions[0].address = '0x1234...'(同一个地址!)

// 用户转账 10.01 USDC 匹配 order1,转账 10.02 USDC 匹配 order2

⚠️ 共享模式限制

  • 用户必须发送 精确金额(不能四舍五入)
  • 需要在 UX 上做用户教育

金额相同的情况

同一共享地址上,同一金额在任一时刻只能属于一个在途订单。StableOps 在分配时会自动保证这一点,但 exactauto 两种模式处理方式不同:

  • amount_mode: 'exact'(默认):先尝试单地址、再尝试共享地址,各组内按导入时间(createdAt ASC)逐个尝试,遇到同金额在途冲突就跳过,找下一个地址。若所有地址都有冲突,创建会失败。请导入更多地址或换一个金额。
  • amount_mode: 'auto':分两阶段分配。第一阶段不改基准金额,按最久未用优先在地址间轮询——只要有任一地址上基准金额空闲,订单就直接用基准金额,不做微调。只有当基准金额在所有地址上都有在途冲突时,才进入第二阶段,以 token 最小单位(如 USDC 的 0.000001)逐级微调(每个地址最多 1000 步),直到找到唯一金额。
  • 终态后复用:前一个订单到达终态(finalized/reverted/expired/canceled)后,其金额被释放,可在同一地址上复用。

自动金额微调(amount_mode: 'auto'

不想自己构造唯一金额时,创建订单时传 amount_mode: 'auto'。基准金额在所有地址上都冲突时,StableOps 才会把金额按 token 最小单位(如 USDC 的 0.000001)向上微调,直到在共享该地址的活跃订单中唯一,所以你每次传相同的基准金额即可,完全不必担心金额撞车。

const order = await stableops.paymentOrders.create({
  merchantOrderId: 'sub_user_jan',
  amount: '10.00', // 基准金额:无需自己保证唯一
  amountMode: 'auto',
  acceptedAssets: [{ chain: 'base', asset: 'USDC' }],
  expiresAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
})

// order.amount          → 基准金额在某个地址上空闲时就是 '10.00',
//                          全部冲突时才是 '10.000001' 这样的微调值
// order.requestedAmount → '10.00'  你传入的基准金额,用于对账

用户按返回的 amount 精确支付(6 位小数精度)。默认 amount_mode: 'exact' 行为不变,仍由你自己保证金额唯一。

导入地址

通过 API

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

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

// 导入地址
const result = await stableops.addresses.import({
  chain: 'base',
  addresses: [
    '0x1234567890123456789012345678901234567890',
    '0x2345678901234567890123456789012345678901',
    '0x3456789012345678901234567890123456789012',
  ],
  mode: 'single', // 或 'shared'
})

console.log(`导入了 ${result.imported} 个地址`)
// 已导入过的地址会被静默跳过,不计入 imported

通过控制台

  1. 进入控制台 → 地址
  2. 导入地址
  3. 选择链
  4. 选模式(Single 或 Shared)
  5. 粘贴地址,一行一个
  6. 导入

批量导入

地址数量很多时,分批导入:

// 从文件读取
const addresses = fs
  .readFileSync('addresses.txt', 'utf-8')
  .split('\n')
  .filter((addr) => addr.trim())

// 每批 100 个
const BATCH_SIZE = 100
for (let i = 0; i < addresses.length; i += BATCH_SIZE) {
  const batch = addresses.slice(i, i + BATCH_SIZE)

  await stableops.addresses.import({
    chain: 'base',
    addresses: batch,
    mode: 'single',
  })

  console.log(`完成第 ${i / BATCH_SIZE + 1} 批`)
}

地址池管理

池子健康度

StableOps 会盯着可用地址数量,低于阈值发告警:

// 查询池子状态
const pools = await stableops.addresses.getPools()

console.log('地址池:')
pools.forEach((pool) => {
  console.log(`${pool.chain}:`)
  console.log(`  可用: ${pool.available}`)
  console.log(`  已占用: ${pool.allocated}`)
  console.log(`  总数: ${pool.total}`)

  if (pool.available < pool.threshold) {
    console.warn(`⚠️  可用地址太少,尽快补充!`)
  }
})

说明:共享地址的 status 始终为 AVAILABLE,因此对共享链而言 available 恒等于 total。在途分配数需通过订单状态间接推算,而不是看 allocated 字段。

低水位告警(仅单地址池)

注意:低水位告警仅对纯 SINGLE 地址池的链生效。只要某链存在至少一个 SHARED 地址(共享地址可复用,无需囤积),该链跳过余量告警。

可用地址低于阈值时 StableOps 会发 address.pool.low Webhook:

{
  "type": "address.pool.low",
  "data": {
    "chain": "base",
    "environment": "live",
    "available": 2,
    "threshold": 3
  }
}

地址池按链统计(不区分资产)。默认阈值为 3,自部署时可通过环境变量 ADDRESS_POOL_LOW_THRESHOLD 调整。

处理低水位告警

app.post('/webhooks/stableops', async (req, res) => {
  const event = req.body

  if (event.type === 'address.pool.low') {
    const { chain, available } = event.data

    // 通知运维
    await sendAlert(`${chain} 可用地址只剩 ${available} 个`)

    // 自动补池(基于确定性钱包)
    await generateAndImportAddresses(chain, 50)
  }

  res.sendStatus(200)
})

地址生命周期

单地址模式(SINGLE)

AVAILABLE → ALLOCATED → AVAILABLE (订单进入终态后归还)

共享模式(SHARED)

AVAILABLE (始终保持,永不翻转为 ALLOCATED)

SHARED 地址的 status 永远为 AVAILABLE,不随分配变化。分配记录写入 AddressAllocation 表,状态机在订单级别追踪。

状态

  • AVAILABLE:可分配
  • ALLOCATED:当前绑定某个活跃订单(仅 SINGLE 地址使用)
  • RESERVED:手动保留,不进入分配池(例如留作热钱包,但暂时不希望系统派单)
  • DISABLED:不参与分配,仅作审计可见(例如已弃用但需要保留记录)

SINGLE 地址在其订单进入终态(finalized / reverted / expired / canceled)后会变回 AVAILABLE

生成地址

HD(分层确定性)钱包

用 HD 钱包确定性地批量生成:

import { ethers } from 'ethers'

// 从助记词派生
const mnemonic = process.env.WALLET_MNEMONIC
const hdNode = ethers.HDNodeWallet.fromPhrase(mnemonic)

const addresses: string[] = []
for (let i = 0; i < 100; i++) {
  const path = `m/44'/60'/0'/0/${i}` // 以太坊 BIP-44 路径
  const wallet = hdNode.derivePath(path)
  addresses.push(wallet.address)
}

// 导入 StableOps
await stableops.addresses.import({
  chain: 'base',
  addresses,
  mode: 'single',
})

硬件钱包

用 Ledger / Trezor 等硬件钱包派生:

import TransportNodeHid from '@ledgerhq/hw-transport-node-hid'
import Eth from '@ledgerhq/hw-app-eth'

const transport = await TransportNodeHid.create()
const eth = new Eth(transport)

const addresses: string[] = []
for (let i = 0; i < 100; i++) {
  const path = `44'/60'/0'/0/${i}`
  const { address } = await eth.getAddress(path)
  addresses.push(address)
}

await stableops.addresses.import({
  chain: 'base',
  addresses,
  mode: 'single',
})

托管商对接

Fireblocks

import { FireblocksSDK } from 'fireblocks-sdk'

const fireblocks = new FireblocksSDK(privateKey, apiKey)

const addresses: string[] = []
for (let i = 0; i < 100; i++) {
  const result = await fireblocks.createVaultAccount({
    name: `StableOps-${i}`,
    hiddenOnUI: false,
  })

  const address = await fireblocks.generateNewAddress({
    vaultAccountId: result.id,
    assetId: 'USDC_BASE',
  })

  addresses.push(address.address)
}

await stableops.addresses.import({
  chain: 'base',
  addresses,
  mode: 'single',
})

Coinbase Custody

import { CoinbaseClient } from '@coinbase/coinbase-custody-sdk'

const coinbase = new CoinbaseClient({
  apiKey: process.env.COINBASE_API_KEY,
  apiSecret: process.env.COINBASE_API_SECRET,
})

const addresses: string[] = []
for (let i = 0; i < 100; i++) {
  const address = await coinbase.createAddress({
    currency: 'USDC',
    network: 'base',
  })

  addresses.push(address.address)
}

await stableops.addresses.import({
  chain: 'base',
  addresses,
  mode: 'single',
})

安全最佳实践

1. 永远不要分享私钥

// ✅ 正确:只导入地址
await stableops.addresses.import({
  addresses: ['0x1234...'],
})

// ❌ 错误:永远不要把私钥发出去
// StableOps 也绝不会向你索取私钥

2. 高价值地址使用硬件钱包

接收大额支付的地址:

  • 用硬件钱包派生(Ledger、Trezor)
  • 私钥离线保存
  • 多签提升安全

3. 区分热钱包与冷钱包

// 热钱包:小额、频繁
await stableops.addresses.import({
  chain: 'base',
  addresses: hotWalletAddresses,
  mode: 'single',
})

// 冷钱包:大额、低频
// 只在需要时手动导入

4. 定期轮换地址

为隐私轮换地址:

// 每月生成新地址
const rotateAddresses = async () => {
  const newAddresses = await generateAddresses(100)

  await stableops.addresses.import({
    chain: 'base',
    addresses: newAddresses,
    mode: 'single',
  })

  // 旧地址不再分配后归档
}

5. 监控异常提币

// 监听链上出账
const monitorWithdrawals = async (address: string) => {
  const provider = new ethers.JsonRpcProvider(rpcUrl)

  provider.on({ address }, (log) => {
    if (!isAuthorizedWithdrawal(log)) {
      sendSecurityAlert(`检测到来自 ${address} 的未授权提币`)
    }
  })
}

资金提取

StableOps 只做监听,提币由你自己用私钥发起。

手动提取

import { ethers } from 'ethers'

const wallet = new ethers.Wallet(privateKey, provider)

const usdcContract = new ethers.Contract(
  USDC_ADDRESS,
  ['function transfer(address to, uint256 amount) returns (bool)'],
  wallet,
)

const tx = await usdcContract.transfer(
  destinationAddress,
  ethers.parseUnits('100.00', 6), // USDC 是 6 位小数
)

await tx.wait()
console.log(`已向 ${destinationAddress} 提取 100 USDC`)

自动归集

把资金定期归集到 Treasury:

const sweepAddress = async (address: string) => {
  const wallet = new ethers.Wallet(privateKey, provider)
  const usdcContract = new ethers.Contract(USDC_ADDRESS, USDC_ABI, wallet)

  const balance = await usdcContract.balanceOf(address)

  if (balance > 0) {
    const tx = await usdcContract.transfer(TREASURY_ADDRESS, balance)
    await tx.wait()

    console.log(`从 ${address} 归集 ${ethers.formatUnits(balance, 6)} USDC`)
  }
}

// 每天归集一次
cron.schedule('0 0 * * *', async () => {
  const addresses = await getActiveAddresses()
  for (const address of addresses) {
    await sweepAddress(address)
  }
})

地址校验

导入时 StableOps 会做这些校验:

校验规则

  • 格式:必须符合对应链的地址格式(EVM 为 0x + 40 位十六进制;TRON 为 base58 原样保存)
  • 归一化:EVM 地址统一转小写入库,不做 EIP-55 checksum 校验
  • 去重:重复导入同一地址会被静默跳过
  • 链匹配:地址必须与目标链兼容

示例

// ✅ 合法的 EVM 地址(大小写混合亦可,入库统一转小写)
'0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed'

// ✅ 合法的 EVM 地址(小写)
'0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed'

// ✅ 合法的 TRON 地址
'TRX9ZfPBvgS8Z8HnFvCfKvFXqvQzMvXkXJ'

// ❌ 不合法:格式错误
'not-an-address'

// ❌ 不合法:链不匹配
// 例如把以太坊地址导入到 TRON

合规与监管

自托管诉求

某些司法辖区要求企业自托管客户资金:

  • MiCA(欧盟):加密服务提供商必须托管客户资产
  • NYDFS(纽约):BitLicense 要求托管安排
  • MAS(新加坡):支付服务提供商必须保护资金

BYO 地址让你既满足合规,又能使用 StableOps 监听。

审计追溯

StableOps 留存完整审计日志,前往 控制台 → 审计日志 查看地址导入、分配、状态变更等操作记录。

最佳实践

1. 池子保有量

// 经验值:每日订单量的 2 倍
const dailyOrders = 100
const recommendedPoolSize = dailyOrders * 2

const pools = await stableops.addresses.getPools()
const pool = pools.find((p) => p.chain === 'base')

if (pool && pool.available < recommendedPoolSize) {
  console.warn(`池子太小!再补 ${recommendedPoolSize - pool.available} 个地址`)
}

2. 配置低水位告警

const ALERT_THRESHOLD = 20 // 可用低于 20 时升级告警

app.post('/webhooks/stableops', async (req, res) => {
  const event = req.body

  if (event.type === 'address.pool.low') {
    if (event.data.available < ALERT_THRESHOLD) {
      await sendPagerDutyAlert('严重:地址池即将耗尽')
    }
  }

  res.sendStatus(200)
})

3. 共享模式确保金额唯一

提示:不想自己挑金额?创建订单时设 amount_mode: 'auto',StableOps 会自动保证唯一(见上文「共享模式」)。

exact 模式下,同一共享地址上同一金额在任一时刻只能有一个在途订单。因此只要每个订单金额不同,就能正常区分:

// ✅ 正确:每个订单金额不同,可正常区分
await stableops.addresses.import({
  chain: 'base',
  addresses: ['0x1234...'],
  mode: 'shared',
})

const order1 = await stableops.paymentOrders.create({
  merchantOrderId: `sub_${userId}_jan`,
  amount: '10.00',
  acceptedAssets: [{ chain: 'base', asset: 'USDC' }],
  expiresAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
})

const order2 = await stableops.paymentOrders.create({
  merchantOrderId: `sub_${userId}_feb`,
  amount: '15.00',
  acceptedAssets: [{ chain: 'base', asset: 'USDC' }],
  expiresAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
})

// ❌ 错误:同一共享地址上同时存在两个 10.00 的在途订单,系统无法区分
// const orderBad = await stableops.paymentOrders.create({
//   amount: '10.00', // order1 尚未终态,此金额已被占用
//   ...
// })

4. 记录地址派生信息

// 保存派生信息以便后续恢复
await db.addresses.create({
  address: '0x1234...',
  derivationPath: "m/44'/60'/0'/0/0",
  walletType: 'ledger',
  importedAt: new Date(),
  chain: 'base',
  asset: 'USDC',
})

5. 上生产前先在沙盒测试

// 先在沙盒跑通
const stableops = new StableOps({
  apiKey: process.env.STABLEOPS_SANDBOX_API_KEY,
})

// 导入测试地址
await stableops.addresses.import({
  chain: 'base-sepolia', // 测试网
  addresses: testAddresses,
  mode: 'single',
})

// 创建测试订单
const order = await stableops.paymentOrders.create({
  merchantOrderId: 'test_order_1',
  amount: '0.01',
  acceptedAssets: [{ chain: 'base-sepolia', asset: 'USDC' }],
  expiresAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
})

// 发一笔测试支付,验证 Webhook 链路

下一步

这篇文档怎么样?

最后更新

本页内容