平台积分体系设计:Credits(余额)与 Points(积分)的区别与应用
Credits vs Points 是两套独立系统特性 Credits Points本质 消费余额(货币) 生态贡献积分可消费 是(API 调用扣费) 否可提现 是(换回 USDC/法币) 否是否等价法币 基本等价 不等价数量变化 实时增减 只增(或少量减)未来用途 功能消费 Token 空投、VIP、治理Credits:平台货币 用户充值后获得 Credits,Credits 直接用于消费: 充值 100 USDT = 获得 100 Credits 调用 API 消费 2 Credits → 余额 98 Credits 提现 50 Credits → 取回约 50 USDTCredits 必须与真实价值挂钩,用户信任建立在 1 Credit ≈ 1 USD 的基础上。 Points:生态积分 Points 不直接等于钱,是平台的"长期价值记录": 充值 100 USDT → +100 Points API 消费 20 USD → +20 Points 邀请好友充值 → +邀请奖励 Points 每日活跃 → +少量 PointsPoints 积累后可以用于:VIP 等级解锁 折扣和返佣 Token 空投快照 DAO 治理权重 锁仓质押防止套利:提现扣回 Points 如果充值得 Points、提现不扣,用户会利用套利: 1. 充值 1000 USDT → 得 1000 Credits + 1000 Points 2. 立刻提现 1000 Credits 3. 净得:0 Credits(本金拿回)+ 1000 Points(免费)解决方案:提现时扣回相应 Points: 提现 1000 Credits → 余额 -1000 Credits,同时 Points -1000或者更稳健:Points 只按实际消费累计,充值/提现不影响 Points: 充值 → Credits+,Points 不变 消费 1 USD → Points +1 提现 → Credits-,Points 不变数据库设计 CREATE TABLE user_balance ( user_id BIGINT PRIMARY KEY, credits DECIMAL(18, 6) DEFAULT 0, -- 可消费余额 points BIGINT DEFAULT 0 -- 累计积分 );-- 消费记录 CREATE TABLE consumption_log ( id BIGINT AUTO_INCREMENT, user_id BIGINT, credits_used DECIMAL(18, 6), points_earned INT, action VARCHAR(100), created_at DATETIME );Points 用整数(无小数),Credits 用高精度小数(避免浮点误差)。 Credits 扣费保证原子性 Credits 扣费和 Points 增加应在同一事务中完成: @Transactional public void consumeCredits(Long userId, BigDecimal amount, String action) { // 检查余额 UserBalance balance = balanceMapper.selectForUpdate(userId); if (balance.getCredits().compareTo(amount) < 0) { throw new InsufficientCreditsException(); } // 扣 Credits balanceMapper.deductCredits(userId, amount); // 加 Points(按消费比例) long points = amount.longValue(); balanceMapper.addPoints(userId, points); // 记录消费日志 consumptionLogMapper.insert(userId, amount, points, action); }SELECT FOR UPDATE 防止并发扣款导致余额超扣。 首版设计建议Credits 优先实现,这是核心盈利结构 Points 首版只记录,不设兑换规则(避免承诺后反悔) 后期发 Token 时,用 Points 快照作为分配依据 积分比例(1 Credits 消费 = 多少 Points)后期可调,但不能追溯修改历史记录
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 不可删除:所有扣费操作可完整审计
油猴脚本阻止页面脚本加载:hook appendChild 与 createElement
执行时机:document-start 油猴脚本的执行时机由 @run-at 控制,document-start 是最早的时机,在浏览器开始解析 HTML 前执行: // ==UserScript== // @name Block Scripts // @match *://*/* // @run-at document-start // ==/UserScript==此时 document.body 还不存在,但 DOM API 已可用,可以提前 hook 原型方法。 方案1:hook appendChild 拦截动态 script // ==UserScript== // @name Block Dynamic Scripts // @match *://example.com/* // @run-at document-start // ==/UserScript==(function () { const rawAppend = Element.prototype.appendChild; Element.prototype.appendChild = function (el) { if (el.tagName === 'SCRIPT') { const src = el.src || '(inline)'; console.log('[blocked]', src); // 返回 el 但不真正追加,阻止加载 return el; } return rawAppend.call(this, el); }; })();rawAppend 保存原始方法,避免无限递归。只拦截 SCRIPT 标签,其他元素正常追加。 方案2:hook createElement,在 src 赋值时拦截 部分页面通过 createElement('script') 创建后再设置 src: const rawCreate = document.createElement.bind(document);document.createElement = function (tag) { const el = rawCreate(tag); if (tag === 'script') { Object.defineProperty(el, 'src', { configurable: true, set(v) { if (v.includes('ads.example.com')) { console.log('[blocked src]', v); return; } el.setAttribute('src', v); }, get() { return el.getAttribute('src') || ''; } }); } return el; };Object.defineProperty 劫持 src 的 setter,在 src 被赋值时决定是否放行。 方案3:MutationObserver 监控已插入的 script 上面两种方案针对动态插入。对于 HTML 中静态的 <script> 标签,可以用 MutationObserver 在插入前禁用: const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.tagName === 'SCRIPT' && node.src.includes('tracker.js')) { node.type = 'text/blocked'; // 改掉 type,浏览器不执行 console.log('[neutralized]', node.src); } } } });observer.observe(document.documentElement, { childList: true, subtree: true });将 type 改为非 JavaScript MIME 类型,浏览器不会执行该脚本但 DOM 中仍有该节点。 三种方案对比方案 拦截时机 适用场景hook appendChild 追加时 动态 document.body.appendChild(script)hook createElement src src 赋值时 el.src = '...' 模式MutationObserver 节点插入 DOM 后 任意方式插入,包括 innerHTML实际页面可能混合使用多种方式,建议组合 hook。 注意事项document-start 时 document.body 为 null,不要访问 DOM 元素 hook 原型方法影响所有调用,测试时先在控制台验证 浏览器扩展的 Content Script 比油猴更底层,如需更强的拦截能力可考虑开发扩展
Go 后端接入 Web3 钱包登录:nonce + personal_sign 验签
Go 后端接 Web3 钱包登录,标准流程只有四步:后端下发 nonce 前端钱包用 personal_sign 签 "Login\nNonce: xxx" 后端 recover 出地址,比对是不是同一个 通过后签发 JWT / session省心的做法是前端签名 + 后端验签,别在后端搞私钥。 生成 nonce 一次性、5 分钟过期、写 Redis: package authimport ( "crypto/rand" "encoding/hex" )func GenerateNonce() string { b := make([]byte, 16) rand.Read(b) return hex.EncodeToString(b) }Redis key:wallet:nonce:0xabc... → <nonce>,TTL 5 分钟。 前端签名 const message = `Login\nNonce: ${nonce}`; const signature = await ethereum.request({ method: "personal_sign", params: [message, address], });要点:用 personal_sign,不要自己算 hash 消息用明文发给 MetaMask,它会自动加 EIP-191 前缀Go 侧验签 package authimport ( "fmt" "strings" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" )func VerifySignature(address, message, sigHex string) bool { sigHex = strings.TrimPrefix(sigHex, "0x") sig := common.FromHex(sigHex) if len(sig) != 65 { return false } // 关键点 1:v 值处理(MetaMask 返回 27/28,crypto 库要 0/1) if sig[64] != 27 && sig[64] != 28 { return false } sig[64] -= 27 // 关键点 2:EIP-191 前缀 prefixed := fmt.Sprintf( "\x19Ethereum Signed Message:\n%d%s", len(message), message, ) hash := crypto.Keccak256Hash([]byte(prefixed)) pubKey, err := crypto.SigToPub(hash.Bytes(), sig) if err != nil { return false } recovered := crypto.PubkeyToAddress(*pubKey) return strings.EqualFold(recovered.Hex(), address) }登录 handler type LoginReq struct { Address string `json:"address"` Signature string `json:"signature"` }func Login(c *gin.Context) { var req LoginReq if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, gin.H{"error": "bad request"}) return } key := "wallet:nonce:" + strings.ToLower(req.Address) nonce, err := redisClient.Get(ctx, key).Result() if err != nil { c.JSON(401, gin.H{"error": "nonce expired"}) return } message := "Login\nNonce: " + nonce if !VerifySignature(req.Address, message, req.Signature) { c.JSON(401, gin.H{"error": "invalid signature"}) return } // 防重放:立刻删掉 nonce redisClient.Del(ctx, key) token, _ := GenerateJWT(req.Address) c.JSON(200, gin.H{"token": token}) }三个必踩的坑 1. 少了 EIP-191 前缀 MetaMask 的 personal_sign 会自动加: "\x19Ethereum Signed Message:\n" + len(msg) + msg后端验签必须手动加回这个前缀再算 keccak。少了直接验不过。 2. v 值 27/28 vs 0/1 MetaMask 返回的签名最后一个字节(v)是 27 或 28(EIP-155 前的格式)。go-ethereum 的 crypto.SigToPub 要求是 0 或 1。必须减 27。 3. 忘了防重放 nonce 验完不删,攻击者抓包重放就能永远登进来。验签成功后立刻 redis.Del。 建议直接上 SIWE Sign-In with Ethereum(EIP-4361)是现在的标准,消息格式包括域名、URI、Chain ID、Issued At 等,钱包会更友好地显示: example.com wants you to sign in with your Ethereum account: 0xabc...123URI: https://example.com Version: 1 Chain ID: 1 Nonce: 8a2c... Issued At: 2026-05-10T16:31:04ZGo 侧现成库: go get github.com/spruceid/siwe-goimport siwe "github.com/spruceid/siwe-go"msg, err := siwe.ParseMessage(rawMessage) if err != nil { ... }if _, err := msg.Verify(signature, &domain, &nonce, nil); err != nil { // 验签失败 }SIWE 帮你处理消息构造、时效、chain id、防重放,比手写靠谱。 前端推荐栈wagmi + viem:现在 EVM dApp 的事实标准 直接 useSignMessage,签名一行搞定 不用自己处理 window.ethereum一句话总结 Web3 登录 = nonce + personal_sign + Go recover 比地址。三个坑:加 EIP-191 前缀、v 值减 27、nonce 用完立刻删。新项目直接上 SIWE,别自己拼消息格式。
Context7 是什么:给 AI 编程助手补最新文档的动态知识库
用 AI 编程助手(Cursor、Claude Code、OpenCode、Cline 等)时,经常遇到一个尴尬:模型知识截止到某个日期,生成的代码用的是旧版 API——Next.js 14 的 middleware 写法在 15 里改了、OpenAI SDK v4 和 v5 API 完全不同、Playwright 每个小版本都在换东西。 Context7 就是解决这个问题的——给模型动态注入最新的官方文档。 它做什么 流程大致: 用户提问:"Next.js 15 middleware 怎么写" ↓ AI 判断需要文档 ↓ Context7 拉取 Next.js 最新文档 ↓ 把相关片段塞进 prompt ↓ 模型基于真实文档生成代码不用它的差别:无 Context7:模型可能给出 Next.js 13/14 的老写法,甚至凭空造 API 有 Context7:读到 Next.js 15 最新文档,给现在正确的写法用户侧的触发方式通常是在 prompt 里加 use context7,或者在 MCP 层自动挂上。 覆盖范围 Context7 主要针对开源库和框架:前端:React、Next.js、Vue、Svelte、TanStack Router / Query 后端:Node.js、FastAPI、NestJS、Rails、Spring 语言 SDK:OpenAI、Anthropic、Google GenAI 工具链:Docker、K8s、Terraform、Cloudflare Workers 数据库 ORM:Prisma、Drizzle、SQLAlchemy AI Agent 相关:LangChain、LangGraph、Vercel AI SDK不覆盖:企业内部私有文档、闭源产品文档(那要走 RAG)。 和 RAG 的区别 看着都是"检索 + 生成",实际定位不同:维度 Context7 通用 RAG目标数据源 公开的开源库官方文档 任意语料(企业文档、书、票据)更新频率 跟随上游 release 自动同步 需要自己维护索引检索粒度 按库版本和 API 检索 通用 embedding 相似度主要面向 AI 编程助手 各种问答场景部署方式 云服务 + MCP 客户端 自建向量库 + 应用集成企业内私有代码库还是要自己搭 RAG。Context7 专注公共库这个细分场景,做得比通用 RAG 更精准。 MCP 是怎么接的 Context7 走 MCP(Model Context Protocol)——Anthropic 提出的、给 LLM 用的工具协议。客户端配置例: { "mcpServers": { "context7": { "command": "npx", "args": ["-y", "@upstash/context7-mcp"] } } }放到 Claude Desktop / Cursor / OpenCode 的 MCP 配置里,模型就能调用两个工具:resolve-library-id — 把"Next.js" 之类的名字解析成内部 ID get-library-docs — 按 ID 拿具体文档MCP 客户端会把工具 schema 注入到模型的系统提示里,模型自主决定什么时候调用。 用 OpenCode / Claude Code 集成 ~/.claude.json 里加: { "mcpServers": { "context7": { "command": "npx", "args": ["-y", "@upstash/context7-mcp"] } } }重启客户端,让 Claude Code 认到工具,之后你可以:直接问:"帮我按 Next.js 15 官方文档写一个 middleware" 或明确指令:"use context7 查一下 Cloudflare Workers 最新的 D1 API"模型会自己调用工具、拿到文档片段、然后写代码。 局限只能查 Context7 收录的库,长尾库覆盖不到 文档质量取决于上游 README / docs 站的质量 有请求配额(免费额度足够个人用) 对版本切换很敏感——比如你项目锁死用 Next.js 14,模型可能拉了 15 的文档给你混淆一句话总结 Context7 = AI 编程助手的动态文档知识库。核心用途是修复"模型不知道最新 API"这个通病。走 MCP 一键接入,Claude Code / Cursor / OpenCode 都能用。
