面向区块链用户的 AI API 平台,核心是弄清楚什么适合上链、什么必须留在链下。
Credits 与 Points 的区别
两者经常被混淆,但定位完全不同:
| 项目 | Credits | Points |
|---|---|---|
| 是否可消费 | 是(API 扣费) | 否 |
| 是否能提现 | 是 | 否 |
| 是否等价美元 | 基本是 | 不是 |
| 是否实时扣减 | 是 | 一般不扣 |
| 后续用途 | 付费 API 使用 | Token 空投、VIP 等级、DAO 治理 |
Credits 是”钱”,用来实时扣 API 费用。充值 100 USDT → 获得 100 credits。
Points 是”生态贡献值”,记录用户长期行为,为后续 Token 发行、空投、质押做准备。首版只记录,不承诺兑换比例。
Points 推荐规则(防刷)
充值不给 points(容易刷)
实际 API 消费给 points:1 USD 消费 = 1 point
邀请用户消费后返 points:被邀请人消费 100 USD → 邀请人 +10 points
提现时扣减对应 points,避免”充值即拿 points 然后提现”的空刷。
链上 vs 链下的边界
AI API 平台天然偏中心化,因为每秒有大量 streaming、token 级扣费、fallback 重试。
| 内容 | 位置 | 原因 |
|---|---|---|
| 充值记录 | 链上 | 不可篡改,用户可自证 |
| 钱包资金 | 链上 | 透明储备 |
| Credits 余额 | 链下 | 高频修改,链上 Gas 不可接受 |
| API token 计费 | 链下 | streaming 无法逐 token 上链 |
| Fallback/retry 逻辑 | 链下 | 毫秒级响应要求 |
链上充值的意义不是”绝对无法改余额”,而是让作恶留下公开证据:用户可以用 tx hash 证明确实充值了,平台无法赖账。
Vault 合约设计
首版只需要一个极简充值金库,不需要 upgradeable proxy、自动提现或 token 合约。
核心功能
- 接收 BNB(
depositBNB) - 接收 ERC20(USDT/USDC,
depositToken) - 每笔充值绑定
orderId,供后端对账 - 管理员提现(用 Safe 多签作为 owner)
- Pausable + ReentrancyGuard
orderId 的必要性
不用 orderId 的话,用户直接 transfer 到合约,后端无法知道这笔钱对应哪个平台账户的哪个充值订单,难以补单和退款。
Solidity 实现
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";
contract VibeVault is Ownable, ReentrancyGuard, Pausable {
using SafeERC20 for IERC20;
mapping(address => bool) public allowedTokens;
event DepositBNB(address indexed user, uint256 amount, bytes32 indexed orderId);
event DepositToken(address indexed user, address indexed token, uint256 amount, bytes32 indexed orderId);
event WithdrawToken(address indexed token, address indexed to, uint256 amount);
event WithdrawBNB(address indexed to, uint256 amount);
constructor(address initialOwner) Ownable(initialOwner) {}
function setAllowedToken(address token, bool allowed) external onlyOwner {
allowedTokens[token] = allowed;
}
function pause() external onlyOwner { _pause(); }
function unpause() external onlyOwner { _unpause(); }
function depositBNB(bytes32 orderId) external payable whenNotPaused {
require(msg.value > 0, "invalid amount");
emit DepositBNB(msg.sender, msg.value, orderId);
}
function depositToken(
address token, uint256 amount, bytes32 orderId
) external whenNotPaused nonReentrant {
require(allowedTokens[token], "token not allowed");
require(amount > 0, "invalid amount");
IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
emit DepositToken(msg.sender, token, amount, orderId);
}
function withdrawToken(address token, address to, uint256 amount) external onlyOwner nonReentrant {
require(to != address(0), "invalid to");
IERC20(token).safeTransfer(to, amount);
emit WithdrawToken(token, to, amount);
}
function withdrawBNB(address payable to, uint256 amount) external onlyOwner nonReentrant {
require(to != address(0), "invalid to");
(bool success,) = to.call{value: amount}("");
require(success, "transfer failed");
emit WithdrawBNB(to, amount);
}
}
部署时 initialOwner 传入 Safe 多签地址,不要传 EOA 钱包,否则单私钥泄露即丢失全部资金。
链下 Credits 账本设计
Credits 必须在链下用数据库管理,但要做到可审计:
users: wallet_address, credits, frozen_credits, points
api_keys: user_id, key_hash, status
requests: user_id, model_tier, input_tokens, output_tokens, actual_cost, status
balance_logs: user_id, type(deposit/consume/refund/withdraw), amount, balance_before, balance_after, request_id
预扣费防并发刷余额
streaming 请求可能持续数分钟,必须先冻结余额:
请求开始 → 冻结 1 USD(frozen_credits += 1)
请求结束 → 按实际 token 数结算
实际费用 0.23 USD → 真正扣 0.23,解冻 0.77
上游失败 → 全部解冻,不扣费
Credits 存储用整数
内部:1 credit = 1,000,000 units(类似 USDC 的 6 位精度)
不要用 float,浮点误差会导致账单对不上
BSC 充值确认数建议
| 资产 | 建议确认数 |
|---|---|
| BNB | 6~12 |
| USDT | 12 |
| USDC | 12 |
后端监听 DepositBNB 和 DepositToken 事件,达到确认数后入账 credits。
降低管理员作恶风险
- Safe 多签(2/3):单私钥泄露无法提币
- 热钱包只放小额:大额资金存 Safe
- 每 6 小时上链余额 Merkle Root:用户可验证平台没有偷改余额
- balance_logs 不可删除:所有扣费操作可完整审计
