大家好,我是 Sonny,一个 Web3 新手。
在一个月前,我对区块链的理解还停留在“比特币很贵”“以太坊能发币”这种模糊印象,至于 UTXO、PoW、智能合约、状态通道……这些词听起来就像天书。但越是不懂,越觉得有趣。尤其是当我听说“未来的互联网服务可以边用边付,不用注册、不用绑卡、没有中间商”,我就忍不住想:这到底怎么做到的?
为了真正搞明白这些概念,我决定动手试试。正好看到 Nervos CKB 的文档对新手挺友好,又支持灵活的脚本和原生时间锁,于是就选它作为实验平台,从零开始实现了一套简易的 Spillman 支付通道(Spillman Payment Channel),并在大模型 AI 对话场景中得到了应用——**用户在与 AI 流式对话的过程中,每收到一段回复时(chunk),就自动完成一次链下微支付 ** 。
这个过程当然磕磕绊绊——搞不清 Cell 模型、被交易签名搞到头大、甚至一度以为时间锁失效了……但每解决一个问题,就离“理解 Web3 支付”更近一步。现在,我把这个小项目开源了 Github,也想和你分享这段从“完全不懂”到“勉强跑通”的探索旅程。
不过,在详细介绍这个项目之前,我想先聊聊:什么是 Spillman Channel?它到底解决了什么问题?
什么是 Spillman Channel?
Spillman Channel是一种单向的链下支付通道协议,最早由比特币社区提出。它的核心思想很简单:
买方先在区块链上锁定一笔资金,然后在链下多次向卖方付款,最后只需卖家一次上链操作完成最终结算。
整个过程大致分为三个阶段:
- 创建通道
买方首先和卖方协作,构建一个带时间锁的退款交易——这笔交易的作用是:如果通道长时间未结算,买方可以在未来某个时间点(比如 7 天后)独自把全部资金拿回来。
卖方先对这笔退款交易签名(表示同意这个“保险机制”),买方保存好这个已签名的交易作为“安全凭证”。
然后,买方才会真正向一个双方共管的多签地址(通常由一个 Cell 表示)转入保证金(比如 100 CKB)。
这样一来,资金虽然被“锁住”了,但买方始终握有“兜底退出”的能力,不怕卖方后续失联。
- 链下支付
在通道建立后,买方可以不断签发新的“支付承诺”,比如第一次说“我总共付你 5 CKB”,第二次说“我现在总共付你 5 + 3 = 8 CKB”(相当于在上次基础上再加 3)。每一份新承诺代表的是累计应付款总额,而不是单次增量。而且,新承诺会自动“覆盖”旧的——也就是说,卖方只需要保留最新的一份,因为只有它才代表当前双方认可的最终分配状态。
- 结算或退款:
-
如果一切顺利,卖方拿着最新的支付承诺上链,拿走对应金额,剩余资金退还给买方。
-
如果卖方消失,买方也可以等一段时间后,用预先签好的“退款交易”把全部资金拿回来(通过时间锁保障)。
这种方式把高频小额支付从链上转移到链下,极大降低了手续费和延迟,特别适合像 AI 回复、视频流、API 调用这类“按量计费”的场景。
多签合约
第一次读到 Spillman Channel 的设计时,我其实有点懵——“多签地址”“时间锁”“预签名交易”……这些词听起来很高大上,但具体怎么落地?尤其是作为 Web3 新手,连 UTXO 模型都还没完全吃透,更别说写智能合约了。
但我决定从最基础的一步开始:先实现一个支持“双方签名 + 时间锁”的脚本(Lock Script)。
我先是翻了 Nervos 官方的 system-scripts,看到里面有个现成的 secp256k1_blake160_multisig_all 多签合约。但转念一想:既然要真正理解,不如自己写一个!正好看到 CKB 的虚拟机(ckb-vm)支持用 JavaScript 写合约,我心里一亮——我可是前端出身啊!用 JS 写逻辑,简直如鱼得水。
这个合约只需要支持两件事:
- 验证两个参与方(A 和 B)的签名:
合约会用 script.args 中对应的公钥哈希和 witnessData 中的签名来验证。
const firstSigResult = verifySignature(sign1, scriptArgs[0]);
const secondSigResult = verifySignature(sign2, scriptArgs[1]);
if (firstSigResult && secondSigResult) {
// 验证通过!
}
虽然底层用的是 secp256k1_blake160_sighash_all(单签脚本),但通过在 JS 里连续验证两个签名,可以模拟出了 2-of-2 多签的效果——既绕过了复杂的多签脚本,又达到了同样的安全目标 Github。
- 支持时间锁:
本来以为要自己在合约里处理时间逻辑,结果发现 CKB 在交易层原生支持 since 字段! 只要在交易的 input 上设置 since = 7天后的时间戳,这笔钱就无法在设置的时间内被交易。 这意味着:时间锁根本不需要写进合约,协议层已经帮你搞定了。
写完这个合约后,我突然发现:那些曾经让我望而却步的词——加签、验签、公钥哈希、UTXO 模型、交易结构、Cell 状态……竟然不再那么陌生了。
虽然还谈不上精通,但至少我知道它们在哪儿起作用、怎么组合、为什么这样设计。
Spillman Channel 与大模型支付场景的结合
搞定了底层合约,下一步就是:找个真正需要“小额高频支付”的场景,把它用起来。
我一开始想过视频按秒付费、API 按调用次数计费,但总觉得不够那么“实时”——直到我想到大模型聊天。
现在的 AI 聊天,尤其是流式响应(streaming),本质上就是一段一段地吐出内容:第一段可能是“好的,我来帮你分析…”,第二段是具体观点,第三段是总结……每一段都消耗算力,这不正是天然的小额高频场景吗?
于是我想:能不能在用户每收到一个文本块(chunk)的同时,自动完成一次微支付?
不需要等整轮对话结束也不依赖中心化账户体系——用 Spillman 通道,边聊边付,用多少付多少。
更重要的是,这种模式把“价值交换”变得极其细粒度:
-
如果 AI 回复卡住了,你只付了前两段的钱;
-
如果回答特别长,你按实际接收的内容付费;
-
甚至未来可以动态定价——简单问题便宜,复杂推理贵一点。

如图,在 AI 回复的同时,Hello!How can i assist you today? 被大模型提供方分成了不同的 Chunk。
Chunk1: How
Chunk2: can
Chunk3: …
链下也在每一个 Chunk 到来时根据 Token 消耗量进行支付。
用户系统:以公钥为身份,构建去中心化的登录体验
在传统 Web2 应用中,用户体系通常依赖手机号或邮箱注册,背后是一套中心化的数据库管理账号信息。但当我们进入区块链世界时,身份本身就应该由用户掌控。
我思考:既然支付是基于签名的,那为什么不能直接用用户的公钥作为唯一标识?
于是,我设计了一个极简的“去中心化用户系统”——不依赖注册、不存储密码,只认公钥。
具体流程如下:
-
用户在登录时输入自己的 64 位十六进制私钥(比如
0x...); -
前端使用该私钥生成对应的公钥,并计算出
blake160哈希值(即 CKB 中常用的地址格式); -
私钥仅保存在本地
localStorage,绝不会上传到服务器; -
公钥哈希被发送至服务端,作为本次会话的“用户 ID”;
-
服务端根据这个公钥建立用户档案,记录其支付通道状态、历史交易等信息。
将私钥存入
localStorage在生产环境中是非常危险的做法! 因为任何 XSS(跨站脚本)攻击都可能窃取它,导致资金被盗。 本项目这么做纯粹是为了简化演示——省去钱包集成(如 MetaMask、JoyID 等)的复杂度,让逻辑更聚焦于支付通道本身。 真实产品中,应使用安全的钱包插件或硬件签名,绝不应在前端持久化存储私钥。
如图所示,登录界面极其简洁:只需输入私钥,即可完成身份验证。
这种设计虽然简单,但已经实现了:
-
用户真正拥有身份控制权;
-
系统无需维护密码表;
-
支付行为与身份天然绑定,便于链上验证。
更重要的是,它让整个 Demo 无需注册、开箱即用——只要你有 CKB 私钥,就能立刻开始“边聊边付”。
创建支付通道:利用 UTXO 特性实现“先签名、后上链”的安全机制
接下来,我们进入核心环节——创建 Spillman 支付通道。在界面中,可以选择充值金额(如 100,000 CKB)、通道过期时间(30s 到 7 天),系统会自动换算成可用 Token 数量(例如 1 CKB = 0.01 Token)。
点击「Create Payment Channel」按钮后,背后发生了一件看似简单却极其关键的事情:
前端构建一笔“未上链”的 funding 交易,并将其结构发送给服务端,由卖家(服务方)提前签名,用于未来退款。
这一步,正是整个支付通道安全性的基石。
为什么卖家需要“先签名”?
在传统思维中,如果买家要向某个地址打款,就必须先发起一笔交易并上链。但这就带来一个严重问题:买家的钱一旦打入多签地址,就被永久锁定;因为卖家可以永远不签名退款交易,导致买家无法取回资金;而且买家无法验证卖家是否真的愿意配合关闭通道。
那有没有一种方式,能让买家先承诺付款,但不真正锁币?答案是:有,而且正是 CKB 的 UTXO 模型赋予我们的能力。
利用 UTXO 的交易 Hash
在 CKB 中,每笔交易的 tx hash 是由其输入(Inputs)、输出(Outputs)、Witness 等字段共同决定的。 最关键的一点是:只要 Inputs 不变,哪怕 input 中的交易没上链,它的 Hash 也是确定的。
这意味着:
-
我们可以在不上链的前提下,生成一个完整的交易结构;
-
并将这个交易的 Hash 作为后续退款交易的 Input;
-
从而让卖家提前对“退款路径”进行签名,而不影响买家的资金自由;
具体流程是怎样的?
- 前端先构建 funding 交易(但不上链)
-
输入:用户自己的 UTXO
-
输出:一个由买家和卖家公钥共同控制的多签 Cell
-
金额 = 用户选择的充值额度
-
计算出这笔交易的哈希值
fundingTxHash
- 前端基于
fundingTxHash构建退款交易
-
Input:指向
fundingTxHash的第 0 个输出(即未来的多签 Cell) -
Output:将全部资金返回给买家自己的地址
-
设置
since字段为通道过期时间(例如 7 天后) -
在 Witness 中预留服务端签名的位置
- 前端将这笔“未签名的退款交易”发送给服务端
-
服务端验证:
-
退款金额是否等于产品选择的金额一致?
-
时间锁是否和产品选择中的一致?
-
多签脚本是否匹配当前协议?
-
-
验证通过后,服务端用自己的私钥对交易签名,并将签名返回(如下图)
- 前端收到签名后,组装成完整的退款交易,并可以选择本地保存
-
这笔交易现在可以随时在通道过期后广播,无需服务端再次参与
-
即使服务端宕机或失联,用户也能拿回资金
- 最后,用户点击 PayNow 才广播 funding 交易上链,同时向服务端发送上链成功,服务端确认信息无误后,通道正式激活
链下支付:每收到一个 Chuck,就更新一次资金分配

一旦支付通道创建成功,真正的“边聊边付”就开始了。
假设用户创建通道时充值了 1000 CKB,并约定 1 CKB = 1 Token。
- 初始状态:多签地址中有 1000 CKB。
为什么这样设计?
因为 Spillman 是单向支付通道,资金只能从买家流向卖家。
所以每次“支付”,本质上是买家在说:“我现在同意把 X CKB 给你,剩下的归我。这是最新的分配,请收好。”
只要卖家手握最新承诺,未来任何时候都可以拿着它上链结算,拿走应得的部分。而买家则保留了“如果服务中断,还能按最新状态退款”的能力(通过时间锁保障)。
-
第一段回复到来,消耗 2 Token: 前端生成第一份支付承诺:“通道内资金应分配为:买家 998 CKB,卖家 2 CKB” 用户用私钥签署这份承诺,并发送给服务端。服务端验证后保存。
-
第二段回复到来,又消耗 3 Token(累计 5): 前端生成第二份支付承诺:通道内资金应分配为:买家 995 CKB,卖家 5 CKB” 注意:这不是“再付 3”,而是重新再建立一条新的承诺,更新为最新累计总额。
-
后续每一段都如此,不断签署新的分配状态。
每当用户向 AI 发送一个问题,AI 会以流式(streaming)方式逐块(chunk)返回回答。
而我们的系统会在每个 chunk 到达时,自动触发一次链下支付。
但请注意:这里的“支付”并不是真的发起一笔链上交易,而是在链下签署一份新的资金分配方案——也就是 Spillman 协议中的“支付承诺”。
在界面上,这一切是透明的
用户聊天时, 输入侧会实时显示:
-
已消耗 Token 数
-
剩余 Token 数
-
最近几笔支付记录
当 Auto Pay 开启时,每当 AI 返回一个 chunk,前端会自动为该 chunk 生成一笔链下支付承诺,由用户私钥签名后立即发送给卖家。
每一条记录背后,都是一份经过签名的链下支付凭证——轻量、即时,无需等待区块确认,也没有手续费。
当 Auto Pay 关闭时,chunk 返回后不会自动支付。但当下次用户尝试发起新对话时,前端和服务端会共同检查:是否已为最新的 chunk 完成支付。
如果尚未支付,对话将被阻断,直到用户手动完成该笔支付为止。
结算时刻:在时间耗尽前,把钱拿回来
根据 Spillman 协议的设计,一旦通道过期(since 时间到达),买家就可以单方面发起退款交易,将全部资金取回,这对卖家显然不公平。所以,我们需要一个机制:在通道即将过期前,自动完成结算。
我设计了一个后台定时任务:每分钟运行一次,扫描所有有过链下支付的支付通道,如果某个通道的剩余有效期小于 15 分钟,就立即触发结算流程。
你可以通过访问 http://localhost:3000/admin/tasks 查看该任务的状态和日志:
结算的过程就是把这个支付通道最新的那笔链下交易上链,在之前的链下支付过程中,服务端会保存交易结构和买家对这笔交易的签名,所以在上链时只要加上卖家的签名就可以成功上链。
同时,我在用户界面也增加了手动结算的功能——用户可以随时终止通道,拿回剩余的保证金。这其实不属于 Spillman Channel 协议的一部分,但我觉得,从用户角度出发,能随时随地取回自己的钱,是一件很安心、也很开心的事。在 Web2 中,这种体验几乎不存在;而在 Web3 里,它却可以轻松实现。
取回保证金:没用过?直接退回来!
当用户从未与卖家发生过任何链下支付(即没有使用过服务),且支付通道已过期时,买家理应能取回全部保证金。
为了让这个场景更容易被真实体验到,我加了一个定时任务 check-expired-channels:服务端每 10 分钟检查一次所有通道,如果发现某个通道已过期且从未有过链下支付记录,就会将其状态标记为 expired。
这样一来,用户在前端界面上就能直接看到“提现”按钮,一键操作,无需手动构造交易或处理复杂的签名流程。
需要说明的是,这个定时任务和前端交互并不属于 Spillman Channel 协议本身,纯粹是我为了让大家更方便、更直观地体验“未使用即退款”这个功能。
Web2 vs Web3
我之前在 Web2 领域做过一段时间的国际支付,对接过几十个国家的本地支付方式:信用卡、网银转账、电子钱包、先买后付……五花八门,规则各异。
但有一个共同痛点始终绕不开:用户体验割裂,信任成本高。
比如,很多用户因为安全顾虑,不愿意“保存信用卡”用于下次支付。结果每次都要重新输入卡号、CVV、短信验证码——流程繁琐,支付成功率自然就低。
而作为支付服务商,我们还得处理 PCI 合规、风控拦截、拒付纠纷……整个支付链条既不透明,也不高效。
有人可能会问:“你这个 Demo 不就是 Web2 充值模式换了个壳吗?现在平台不也支持先充钱再按 token 扣费?”
其实还是有很大差异的。
在 Web2 中,用户充值的钱进了平台的账本,平台说了算:能不能退、怎么退、有效期多久,全由它定。一旦平台跑路或冻结账户,用户的钱就没了。
而在 Web3 方案里:
-
用户的钱始终在用户自己控制的多签地址里,平台无法动用;
-
每一笔消费都需要用户明确签名,没有“后台悄悄扣款”;
-
退款规则写在链上合约里,到期自动生效,不需要客服审批;
-
前端 + 后端代码全部开源,逻辑透明,无法作弊。
做完这个 Demo 之后,我突然觉得:原来支付可以这么“干净”。
当然,这并不是说 Web3 支付已经完美——对普通用户来说,私钥管理、Gas 费、交易确认时间……仍是不小的门槛。
但它的方向让我觉得值得探索:把控制权还给用户,把规则写进代码,把信任建立在透明之上。
有意思的是,这个 Demo 本身其实更像是 Web2 与 Web3 的混合体:
-
聊天界面、定时任务、用户会话管理……全是 Web2 的熟悉工程;
-
而资金结算、支付通道、签名验证、时间锁……则完全跑在 CKB 的 Web3 基础上。
它没有否定 Web2 的开发效率和交互体验,也没有神化 Web3 的去中心化理想,而是尝试在两者之间找到一个实用的平衡点:
用 Web2 的体验,承载 Web3 的价值。
也许在很久很久之后的支付,不是 “Web2 vs Web3”,而是 “Web2 + Web3”
中心化服务提供便利,去中心化协议保障权利。
而这,正是我想通过这个小项目探索的可能。
写在最后
这个 Demo 肯定不完美,甚至有点“玩具”性质——毕竟这是我入门 Web3 的第一个实战项目。
很多代码其实是靠 AI 辅助生成的(是的,我一边学一边用 AI 当“编程搭档”),过程中踩了不少坑,也改了很多遍。
但正是通过这个项目,我真正理解了那些曾经听起来很遥远的概念:
加密与签名、验签流程、UTXO 模型、CKB 的 Cell 结构、时间锁、链下状态通道、甚至用 JavaScript 写链上合约……
它们不再是文档里的术语,而是我亲手调试过、验证过、甚至“被它折磨过”的真实逻辑。
如果你也对 AI、支付、或者区块链 感兴趣,欢迎来 GitHub 看看代码:
-
可以提个 issue 一起讨论
-
也可以直接 clone 下来跑一跑
-
说不定你还能帮我发现几个 bug

毕竟,所有伟大的系统,都是从一个“不完美的 Demo”开始的。










