StableOps
概念

自有地址

瞭解如何向 StableOps 匯入並管理自有的 EVM、TRON 或 Solana 收款地址,在資金和私鑰始終由商戶控制的前提下完成地址分配、支付監聽、確認追蹤與 Webhook 投遞,並正確處理地址狀態、複用策略和網路差異。

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 鏈路

下一步

這篇文件怎麼樣?

最後更新

本頁內容