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 都能用。

Context7:给 AI 编程助手注入实时文档的 MCP 工具

Context7 是一个给 AI 编程助手补充实时官方文档上下文的工具,核心解决的问题是:大模型的训练数据有截止日期,新框架 API 或频繁变动的库往往使用旧版写法,导致生成的代码无法运行。 工作流程 用户提问 ↓ AI 判断需要文档 ↓ Context7 拉取对应库的最新文档 ↓ 把文档片段注入 Prompt ↓ 模型基于真实文档生成代码例如询问"Next.js 15 middleware 怎么写",普通模型可能给出 Next.js 12 的旧 API,Context7 会先拉取最新 Next.js 文档再生成答案。 与 RAG 的区别维度 RAG Context7数据来源 用户自定义文档库 官方文档仓库更新频率 手动维护 跟随上游自动同步适合场景 内部知识库 公开库文档集成方式 自建 pipeline MCP 协议MCP 接入配置 Context7 提供 MCP server,支持 Cursor、Claude Code、OpenCode 等工具: { "mcpServers": { "context7": { "command": "npx", "args": ["-y", "@upstash/context7-mcp@latest"] } } }配置后在对话中使用 use context7 指令触发文档检索: Next.js 15 的 middleware 怎么配置重定向?use context7支持的文档范围 常见库均有覆盖:React / Next.js / Remix Node.js / Bun LangChain / LangGraph OpenAI SDK / Anthropic SDK Prisma / Drizzle / TypeORM Docker / Kubernetes 各大主流 npm 包在 AI Agent 架构中的位置 OpenCode / Claude Code ├── LLM ├── Browser ├── Terminal ├── Skills ├── Context7 / RAG ← 文档检索层 └── MCP ToolsContext7 填补了"文档检索层"——让 Agent 在生成代码时能查到当前正确的 API,而不是凭训练数据猜测。 替代方案工具 定位Context7 公开库官方文档Mintlify Scraper 自定义文档爬取Crawl4AI 通用网页抓取自建 pgvector RAG 私有文档库对于内部项目文档,更适合自建 RAG(pgvector + embedding);对于开源库,直接用 Context7 省去维护成本。