Showing Posts From

Solidity

Web3 AI API 平台设计:Credits、Points 与链上 Vault 合约

面向区块链用户的 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 + ReentrancyGuardorderId 的必要性 不用 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~12USDT 12USDC 12后端监听 DepositBNB 和 DepositToken 事件,达到确认数后入账 credits。 降低管理员作恶风险Safe 多签(2/3):单私钥泄露无法提币 热钱包只放小额:大额资金存 Safe 每 6 小时上链余额 Merkle Root:用户可验证平台没有偷改余额 balance_logs 不可删除:所有扣费操作可完整审计