不用绑卡,边聊边付:一个 Web3 版的支付实验

大家好,我是 Sonny,一个 Web3 新手。

在一个月前,我对区块链的理解还停留在“比特币很贵”“以太坊能发币”这种模糊印象,至于 UTXO、PoW、智能合约、状态通道……这些词听起来就像天书。但越是不懂,越觉得有趣。尤其是当我听说“未来的互联网服务可以边用边付,不用注册、不用绑卡、没有中间商”,我就忍不住想:这到底怎么做到的?

为了真正搞明白这些概念,我决定动手试试。正好看到 Nervos CKB 的文档对新手挺友好,又支持灵活的脚本和原生时间锁,于是就选它作为实验平台,从零开始实现了一套简易的 Spillman 支付通道(Spillman Payment Channel),并在大模型 AI 对话场景中得到了应用——**用户在与 AI 流式对话的过程中,每收到一段回复时(chunk),就自动完成一次链下微支付 ** 。

这个过程当然磕磕绊绊——搞不清 Cell 模型、被交易签名搞到头大、甚至一度以为时间锁失效了……但每解决一个问题,就离“理解 Web3 支付”更近一步。现在,我把这个小项目开源了 Github,也想和你分享这段从“完全不懂”到“勉强跑通”的探索旅程。

不过,在详细介绍这个项目之前,我想先聊聊:什么是 Spillman Channel?它到底解决了什么问题?

什么是 Spillman Channel?

Spillman Channel是一种单向的链下支付通道协议,最早由比特币社区提出。它的核心思想很简单:

买方先在区块链上锁定一笔资金,然后在链下多次向卖方付款,最后只需卖家一次上链操作完成最终结算。

整个过程大致分为三个阶段:

  1. 创建通道

买方首先和卖方协作,构建一个带时间锁的退款交易——这笔交易的作用是:如果通道长时间未结算,买方可以在未来某个时间点(比如 7 天后)独自把全部资金拿回来。

卖方先对这笔退款交易签名(表示同意这个“保险机制”),买方保存好这个已签名的交易作为“安全凭证”。

然后,买方才会真正向一个双方共管的多签地址(通常由一个 Cell 表示)转入保证金(比如 100 CKB)。

这样一来,资金虽然被“锁住”了,但买方始终握有“兜底退出”的能力,不怕卖方后续失联。

  1. 链下支付

在通道建立后,买方可以不断签发新的“支付承诺”,比如第一次说“我总共付你 5 CKB”,第二次说“我现在总共付你 5 + 3 = 8 CKB”(相当于在上次基础上再加 3)。每一份新承诺代表的是累计应付款总额,而不是单次增量。而且,新承诺会自动“覆盖”旧的——也就是说,卖方只需要保留最新的一份,因为只有它才代表当前双方认可的最终分配状态。

  1. 结算或退款:
  • 如果一切顺利,卖方拿着最新的支付承诺上链,拿走对应金额,剩余资金退还给买方。

  • 如果卖方消失,买方也可以等一段时间后,用预先签好的“退款交易”把全部资金拿回来(通过时间锁保障)。

这种方式把高频小额支付从链上转移到链下,极大降低了手续费和延迟,特别适合像 AI 回复、视频流、API 调用这类“按量计费”的场景。

多签合约

第一次读到 Spillman Channel 的设计时,我其实有点懵——“多签地址”“时间锁”“预签名交易”……这些词听起来很高大上,但具体怎么落地?尤其是作为 Web3 新手,连 UTXO 模型都还没完全吃透,更别说写智能合约了。

但我决定从最基础的一步开始:先实现一个支持“双方签名 + 时间锁”的脚本(Lock Script)。

我先是翻了 Nervos 官方的 system-scripts,看到里面有个现成的 secp256k1_blake160_multisig_all 多签合约。但转念一想:既然要真正理解,不如自己写一个!正好看到 CKB 的虚拟机(ckb-vm)支持用 JavaScript 写合约,我心里一亮——我可是前端出身啊!用 JS 写逻辑,简直如鱼得水。

这个合约只需要支持两件事:

  1. 验证两个参与方(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

  1. 支持时间锁:

本来以为要自己在合约里处理时间逻辑,结果发现 CKB 在交易层原生支持 since 字段! 只要在交易的 input 上设置 since = 7天后的时间戳,这笔钱就无法在设置的时间内被交易。 这意味着:时间锁根本不需要写进合约,协议层已经帮你搞定了。

写完这个合约后,我突然发现:那些曾经让我望而却步的词——加签、验签、公钥哈希、UTXO 模型、交易结构、Cell 状态……竟然不再那么陌生了。

虽然还谈不上精通,但至少我知道它们在哪儿起作用、怎么组合、为什么这样设计。

Spillman Channel 与大模型支付场景的结合

搞定了底层合约,下一步就是:找个真正需要“小额高频支付”的场景,把它用起来。

我一开始想过视频按秒付费、API 按调用次数计费,但总觉得不够那么“实时”——直到我想到大模型聊天。

现在的 AI 聊天,尤其是流式响应(streaming),本质上就是一段一段地吐出内容:第一段可能是“好的,我来帮你分析…”,第二段是具体观点,第三段是总结……每一段都消耗算力,这不正是天然的小额高频场景吗?

于是我想:能不能在用户每收到一个文本块(chunk)的同时,自动完成一次微支付?

不需要等整轮对话结束也不依赖中心化账户体系——用 Spillman 通道,边聊边付,用多少付多少。

更重要的是,这种模式把“价值交换”变得极其细粒度:

  • 如果 AI 回复卡住了,你只付了前两段的钱;

  • 如果回答特别长,你按实际接收的内容付费;

  • 甚至未来可以动态定价——简单问题便宜,复杂推理贵一点。
    token_111

如图,在 AI 回复的同时,Hello!How can i assist you today? 被大模型提供方分成了不同的 Chunk。

Chunk1: How

Chunk2: can

Chunk3: …

链下也在每一个 Chunk 到来时根据 Token 消耗量进行支付。

用户系统:以公钥为身份,构建去中心化的登录体验

在传统 Web2 应用中,用户体系通常依赖手机号或邮箱注册,背后是一套中心化的数据库管理账号信息。但当我们进入区块链世界时,身份本身就应该由用户掌控。

我思考:既然支付是基于签名的,那为什么不能直接用用户的公钥作为唯一标识?

于是,我设计了一个极简的“去中心化用户系统”——不依赖注册、不存储密码,只认公钥。

具体流程如下:

  1. 用户在登录时输入自己的 64 位十六进制私钥(比如 0x...);

  2. 前端使用该私钥生成对应的公钥,并计算出 blake160 哈希值(即 CKB 中常用的地址格式);

  3. 私钥仅保存在本地 localStorage,绝不会上传到服务器;

  4. 公钥哈希被发送至服务端,作为本次会话的“用户 ID”;

  5. 服务端根据这个公钥建立用户档案,记录其支付通道状态、历史交易等信息。

将私钥存入 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;

  • 从而让卖家提前对“退款路径”进行签名,而不影响买家的资金自由;

具体流程是怎样的?

  1. 前端先构建 funding 交易(但不上链)
  • 输入:用户自己的 UTXO

  • 输出:一个由买家和卖家公钥共同控制的多签 Cell

  • 金额 = 用户选择的充值额度

  • 计算出这笔交易的哈希值 fundingTxHash

  1. 前端基于 fundingTxHash 构建退款交易
  • Input:指向 fundingTxHash 的第 0 个输出(即未来的多签 Cell)

  • Output:将全部资金返回给买家自己的地址

  • 设置 since 字段为通道过期时间(例如 7 天后)

  • 在 Witness 中预留服务端签名的位置

  1. 前端将这笔“未签名的退款交易”发送给服务端
  • 服务端验证:

    • 退款金额是否等于产品选择的金额一致?

    • 时间锁是否和产品选择中的一致?

    • 多签脚本是否匹配当前协议?

  • 验证通过后,服务端用自己的私钥对交易签名,并将签名返回(如下图)

  1. 前端收到签名后,组装成完整的退款交易,并可以选择本地保存
  • 这笔交易现在可以随时在通道过期后广播,无需服务端再次参与

  • 即使服务端宕机或失联,用户也能拿回资金

  1. 最后,用户点击 PayNow 才广播 funding 交易上链,同时向服务端发送上链成功,服务端确认信息无误后,通道正式激活

链下支付:每收到一个 Chuck,就更新一次资金分配

token_111
一旦支付通道创建成功,真正的“边聊边付”就开始了。
假设用户创建通道时充值了 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 :sweat_smile:

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

No need to bind a card—pay as you chat: a Web3 payment experiment

Hello everyone, I’m Sonny, a Web3 beginner.

A month ago, my understanding of blockchain was still stuck at vague impressions like “Bitcoin is expensive” and “Ethereum can issue tokens.” As for UTXO, PoW, smart contracts, state channels… these terms sounded like gibberish. But the less I understood, the more interesting it became. Especially when I heard that “future internet services can be paid for as you use them, without registration, without binding cards, and without middlemen,” I couldn’t help but wonder: how is this actually achieved?

To truly understand these concepts, I decided to get my hands dirty. I happened to see that Nervos CKB’s documentation is quite beginner-friendly, and it supports flexible scripting and native timelocks, so I chose it as my experimental platform. Starting from scratch, I implemented a simplified Spillman Payment Channel and applied it in a large language model AI dialogue scenario—as users engage in streaming conversations with AI, each time they receive a response chunk, an off-chain micropayment is automatically completed.

This process was, of course, full of bumps and bruises—struggling to understand the Cell model, getting headaches over transaction signatures, and even thinking at one point that the timelock had failed… But every problem solved brought me one step closer to “understanding Web3 payments.” Now, I’ve open-sourced this small project on GitHub, and I want to share this exploration journey from “completely clueless” to “barely working” with you.

However, before diving into the details of this project, I’d like to first discuss: what is a Spillman Channel? What problem does it actually solve?

What is a Spillman Channel?

Spillman Channel is a unidirectional off-chain payment channel protocol originally proposed by the Bitcoin community. Its core idea is quite simple:

The buyer first locks up a sum of funds on the blockchain, then makes multiple payments to the seller off-chain, and finally, only the seller needs one on-chain operation to complete the final settlement.

The entire process is roughly divided into three phases:

  1. Creating the Channel

The buyer first collaborates with the seller to construct a refund transaction with a timelock—the purpose of this transaction is: if the channel remains unsettled for a long time, the buyer can retrieve all the funds alone at a future point in time (for example, after 7 days).

The seller first signs this refund transaction (indicating agreement with this “insurance mechanism”), and the buyer saves this signed transaction as a “security voucher.”

Only then does the buyer actually transfer the deposit (for example, 100 CKB) to a jointly managed multi-signature address (usually represented by a Cell).

This way, although the funds are “locked,” the buyer always holds the ability to “exit safely,” without fear of the seller disappearing later.

  1. Off-chain Payments

After the channel is established, the buyer can continuously issue new “payment commitments.” For example, the first time saying “I’ll pay you 5 CKB in total,” the second time saying “I’ll now pay you 5 + 3 = 8 CKB in total” (equivalent to adding 3 on top of the previous amount). Each new commitment represents the cumulative total amount payable, not a single incremental amount. Moreover, new commitments automatically “override” old ones—meaning the seller only needs to keep the latest one, because only it represents the current final allocation state recognized by both parties.

  1. Settlement or Refund:

    • If everything goes smoothly, the seller takes the latest payment commitment on-chain, takes the corresponding amount, and the remaining funds are refunded to the buyer.

    • If the seller disappears, the buyer can also wait for a period of time and then use the pre-signed “refund transaction” to retrieve all the funds (protected by the timelock).

This approach moves high-frequency, small-value payments from on-chain to off-chain, greatly reducing fees and latency. It’s particularly suitable for “pay-per-use” scenarios like AI responses, video streaming, and API calls.

Multi-signature Contract

When I first read about the Spillman Channel design, I was actually a bit confused—“multi-signature address,” “timelock,” “pre-signed transaction”… these terms sounded impressive, but how do you actually implement them? Especially as a Web3 beginner, I hadn’t even fully grasped the UTXO model, let alone writing smart contracts.

But I decided to start with the most basic step: first implementing a script (Lock Script) that supports “two-party signatures + timelock.”

I first looked through Nervos’s official system-scripts and saw that there was a ready-made secp256k1_blake160_multisig_all multi-signature contract. But then I thought: since I want to truly understand, why not write one myself! I happened to see that CKB’s virtual machine (ckb-vm) supports writing contracts in JavaScript, and my eyes lit up—I’m a frontend developer by background! Writing logic in JS is like a fish in water.

This contract only needs to support two things:

  1. Verifying signatures from both participants (A and B):

The contract uses the public key hash in script.args and the signature in witnessData for verification.

const firstSigResult = verifySignature(sign1, scriptArgs[0]);
const secondSigResult = verifySignature(sign2, scriptArgs[1]);
if (firstSigResult && secondSigResult) {
  // Verification passed!
}

Although the underlying layer uses secp256k1_blake160_sighash_all (single-signature script), by continuously verifying two signatures in JS, I simulated a 2-of-2 multi-signature effect—both bypassing complex multi-signature scripts and achieving the same security goals GitHub.

  1. Supporting Timelocks:

I originally thought I would need to handle time logic in the contract myself, but I discovered that CKB natively supports the since field at the transaction layer! As long as you set since = timestamp 7 days later on the transaction’s input, these funds cannot be transacted within the set time. This means: timelocks don’t need to be written into the contract at all; the protocol layer has already handled it for you.

After writing this contract, I suddenly realized: those terms that once intimidated me—signing, verification, public key hash, UTXO model, transaction structure, Cell state… they’re no longer so unfamiliar.

Although I can’t claim to be proficient, at least I know where they come into play, how they combine, and why they’re designed this way.

Combining Spillman Channel with Large Language Model Payment Scenarios

With the underlying contract sorted out, the next step was: find a scenario that truly needs “small-value, high-frequency payments” and put it to use.

I initially considered video pay-per-second, API pay-per-call, but they didn’t feel “real-time” enough—until I thought of large language model chat.

Today’s AI chat, especially streaming responses, is essentially content being delivered piece by piece: the first piece might be “Okay, let me help you analyze…”, the second piece is specific viewpoints, the third piece is a summary… Each piece consumes computing power. Isn’t this a natural small-value, high-frequency scenario?

So I thought: could we automatically complete a micropayment each time the user receives a text chunk?

No need to wait for the entire conversation to end, and no reliance on centralized account systems—using the Spillman channel, pay as you chat, use what you pay for.

More importantly, this model makes “value exchange” extremely granular:

  • If the AI response gets stuck, you only paid for the first two chunks;

  • If the answer is particularly long, you pay for the actual content received;

  • Even dynamic pricing could be possible in the future—simple questions are cheap, complex reasoning is more expensive.

token_111

As shown in the figure, while the AI responds, “Hello! How can I assist you today?” is divided into different chunks by the LLM provider.

Chunk1: How

Chunk2: can

Chunk3: …

Off-chain payments are also made based on token consumption with each incoming chunk.

User System: Using Public Key as Identity, Building a Decentralized Login Experience

In traditional Web2 applications, the user system typically relies on phone number or email registration, backed by a centralized database managing account information. But when we enter the blockchain world, identity itself should be controlled by the user.

I thought: since payments are signature-based, why not directly use the user’s public key as the unique identifier?

So, I designed a minimalist “decentralized user system”—no registration dependency, no password storage, only recognizing public keys.

The specific process is as follows:

  1. When logging in, the user enters their 64-digit hexadecimal private key (for example, 0x...);

  2. The frontend uses this private key to generate the corresponding public key and calculates the blake160 hash value (the address format commonly used in CKB);

  3. The private key is only stored locally in localStorage and never uploaded to the server;

  4. The public key hash is sent to the server as the “user ID” for this session;

  5. The server establishes a user profile based on this public key, recording their payment channel status, transaction history, and other information.

Storing private keys in localStorage is extremely dangerous in production environments! Because any XSS (cross-site scripting) attack could steal it, leading to loss of funds. This project only does this to simplify the demonstration—avoiding the complexity of wallet integration (like MetaMask, JoyID, etc.) and keeping the logic focused on the payment channel itself. In real products, secure wallet plugins or hardware signing should be used, and private keys should never be persistently stored in the frontend.

As shown in the figure, the login interface is extremely simple: just enter the private key to complete identity verification.

Although this design is simple, it has already achieved:

  • Users truly own identity control;

  • The system doesn’t need to maintain password tables;

  • Payment behavior is naturally bound to identity, facilitating on-chain verification.

More importantly, it makes the entire demo work out of the box without registration—as long as you have a CKB private key, you can immediately start “paying as you chat.”

Creating a Payment Channel: Using UTXO Characteristics to Achieve a “Sign First, Then On-chain” Security Mechanism

Next, we enter the core环节—creating a Spillman payment channel. In the interface, you can select the recharge amount (for example, 100,000 CKB), channel expiration time (30s to 7 days), and the system will automatically convert it to the available token amount (for example, 1 CKB = 0.01 Token).

After clicking the “Create Payment Channel” button, something seemingly simple but critically important happens behind the scenes:

The frontend constructs an “unpublished” funding transaction and sends its structure to the server, where the seller (service provider) signs in advance for future refunds.

This step is precisely the cornerstone of the entire payment channel’s security.

Why does the seller need to “sign first”?

In traditional thinking, if a buyer wants to send money to an address, they must first initiate a transaction and publish it on-chain. But this brings a serious problem: once the buyer’s money is sent to the multi-signature address, it’s permanently locked; because the seller can refuse to sign the refund transaction forever, preventing the buyer from retrieving the funds; and the buyer cannot verify whether the seller is truly willing to cooperate in closing the channel.

Is there a way for the buyer to promise payment first without actually locking the coins? The answer is: yes, and it’s precisely the capability that CKB’s UTXO model gives us.

Using UTXO Transaction Hash

In CKB, each transaction’s tx hash is determined by its inputs, outputs, witness, and other fields together. The most crucial point is: as long as the inputs remain unchanged, even if the transaction in the input hasn’t been published on-chain, its hash is deterministic.

This means:

  • We can generate a complete transaction structure without publishing it on-chain;

  • And use this transaction’s hash as the input for the subsequent refund transaction;

  • Thus allowing the seller to sign the “refund path” in advance without affecting the buyer’s fund freedom;

What is the specific process?

  1. The frontend first constructs the funding transaction (but doesn’t publish it)

    • Input: The user’s own UTXO

    • Output: A multi-signature Cell jointly controlled by the buyer’s and seller’s public keys

    • Amount = The recharge amount selected by the user

    • Calculate the hash value of this transaction fundingTxHash

  2. The frontend constructs the refund transaction based on fundingTxHash

    • Input: Points to the 0th output of fundingTxHash (i.e., the future multi-signature Cell)

    • Output: Returns all funds to the buyer’s own address

    • Sets the since field to the channel expiration time (for example, 7 days later)

    • Reserves a position for the server signature in the Witness

  3. The frontend sends this “unsigned refund transaction” to the server

    • Server verification:

      • Is the refund amount consistent with the amount selected for the product?

      • Is the timelock consistent with the product selection?

      • Does the multi-signature script match the current protocol?

    • After verification passes, the server signs the transaction with its own private key and returns the signature (as shown below)

  1. After receiving the signature, the frontend assembles it into a complete refund transaction and can choose to save it locally

    • This transaction can now be broadcast at any time after the channel expires, without requiring server participation again

    • Even if the server goes down or loses contact, the user can still retrieve their funds

  2. Finally, the user clicks PayNow to broadcast the funding transaction on-chain, and simultaneously sends the on-chain success to the server. After the server confirms the information is correct, the channel is officially activated.

Off-chain Payments: Updating Fund Allocation with Every Chunk Received

token_111

Once the payment channel is successfully created, the real “pay as you chat” begins.

Assuming the user recharged 1000 CKB when creating the channel and agreed that 1 CKB = 1 Token.

  • Initial state: There are 1000 CKB in the multi-signature address.

Why design it this way?

Because Spillman is a unidirectional payment channel, funds can only flow from buyer to seller.

So each “payment” is essentially the buyer saying: “I now agree to give you X CKB, and the rest goes to me. This is the latest allocation, please keep it.”

As long as the seller holds the latest commitment, they can settle on-chain at any time in the future and take their due portion. The buyer retains the ability to “refund according to the latest state if service is interrupted” (protected by the timelock).

  • First response arrives, consuming 2 Tokens: The frontend generates the first payment commitment: “Funds in the channel should be allocated as: buyer 998 CKB, seller 2 CKB” The user signs this commitment with their private key and sends it to the server. The server verifies and saves it.

  • Second response arrives, consuming another 3 Tokens (5 cumulative): The frontend generates the second payment commitment: “Funds in the channel should be allocated as: buyer 995 CKB, seller 5 CKB” Note: This isn’t “pay another 3,” but establishing a new commitment with the updated cumulative total.

  • Each subsequent chunk follows the same pattern, continuously signing new allocation states.

Whenever the user sends a question to the AI, the AI returns the answer in a streaming manner, chunk by chunk.

And our system automatically triggers an off-chain payment with each arriving chunk.

But please note: the “payment” here isn’t actually initiating an on-chain transaction, but rather signing a new fund allocation scheme off-chain—the “payment commitment” in the Spillman protocol.

On the interface, all of this is transparent

When users chat, the input side displays in real-time:

  • Tokens consumed

  • Tokens remaining

  • Recent payment records

When Auto Pay is enabled, whenever the AI returns a chunk, the frontend automatically generates an off-chain payment commitment for that chunk, signs it with the user’s private key, and immediately sends it to the seller.

Behind each record is a signed off-chain payment voucher—lightweight, instant, no need to wait for block confirmation, and no fees.

When Auto Pay is disabled, chunks won’t trigger automatic payment after returning. But the next time the user tries to initiate a new conversation, the frontend and server will jointly check: whether payment has been completed for the latest chunk.

If payment hasn’t been made, the conversation will be blocked until the user manually completes that payment.

Settlement Moment: Getting the Money Back Before Time Runs Out

According to the Spillman protocol design, once the channel expires (when the since time is reached), the buyer can unilaterally initiate a refund transaction to retrieve all funds, which is obviously unfair to the seller. So, we need a mechanism: automatically complete settlement before the channel is about to expire.

I designed a background scheduled task: runs once per minute, scanning all payment channels that have had off-chain payments. If a channel’s remaining validity is less than 15 minutes, immediately trigger the settlement process.

You can view the status and logs of this task by visiting http://localhost:3000/admin/tasks:

The settlement process is to publish the latest off-chain transaction of this payment channel on-chain. During the previous off-chain payment process, the server saves the transaction structure and the buyer’s signature for this transaction, so when publishing on-chain, only the seller’s signature needs to be added for successful publication.

At the same time, I’ve added a manual settlement feature to the user interface—users can terminate the channel at any time and retrieve the remaining deposit. This isn’t actually part of the Spillman Channel protocol, but from a user perspective, being able to retrieve your money anytime, anywhere is reassuring and delightful. In Web2, this experience hardly exists; but in Web3, it can be easily achieved.

Retrieving Deposit: Haven’t Used It? Get It Back Directly!

When a user has never had any off-chain payments with the seller (i.e., hasn’t used the service) and the payment channel has expired, the buyer should be able to retrieve the full deposit.

To make this scenario easier to experience in reality, I added a scheduled task check-expired-channels: the server checks all channels every 10 minutes. If it finds a channel that has expired and has never had off-chain payment records, it marks the status as expired.

This way, users can directly see the “Withdraw” button on the frontend interface, with one-click operation, without manually constructing transactions or handling complex signature processes.

It should be noted that this scheduled task and frontend interaction are not part of the Spillman Channel protocol itself; they’re purely for making it more convenient and intuitive for everyone to experience the “refund if unused” feature.

Web2 vs Web3

I previously worked in the Web2 domain on international payments for a while, integrating with dozens of countries’ local payment methods: credit cards, bank transfers, e-wallets, buy-now-pay-later… diverse and with different rules.

But there was one common pain point that couldn’t be avoided: fragmented user experience and high trust costs.

For example, many users are reluctant to “save credit cards” for future payments due to security concerns. As a result, they have to re-enter card numbers, CVV, SMS verification codes every time—the process is cumbersome, and payment success rates naturally drop.

And as a payment service provider, we also have to deal with PCI compliance, risk control interception, chargeback disputes… the entire payment chain is neither transparent nor efficient.

Someone might ask: “Isn’t your demo just Web2 recharge mode with a different shell? Don’t platforms now support recharging first and then deducting fees by token?”

Actually, there’s a big difference.

In Web2, when users recharge, the money goes into the platform’s ledger, and the platform has the final say: whether it can be refunded, how to refund, how long it’s valid—all determined by them. Once the platform runs away or freezes the account, the user’s money is gone.

But in the Web3 solution:

  • The user’s money is always in the multi-signature address controlled by the user themselves; the platform cannot access it;

  • Every consumption requires explicit user signature; there’s no “quietly deducting money in the background”;

  • Refund rules are written in the on-chain contract and automatically take effect upon expiration, without needing customer service approval;

  • Frontend + backend code is fully open source, logic is transparent, and cheating is impossible.

After completing this demo, I suddenly felt: so payments can be this “clean.”

Of course, this doesn’t mean Web3 payments are already perfect—for ordinary users, private key management, gas fees, transaction confirmation times… are still considerable barriers.

But the direction feels worth exploring: returning control to users, writing rules into code, and building trust on transparency.

Interestingly, this demo itself is actually more like a hybrid of Web2 and Web3:

  • Chat interface, scheduled tasks, user session management… all familiar Web2 engineering;

  • While fund settlement, payment channels, signature verification, timelocks… run entirely on CKB’s Web3 foundation.

It doesn’t deny Web2’s development efficiency and interaction experience, nor does it deify Web3’s decentralization ideal, but tries to find a practical balance between the two:

Using Web2 experience to carry Web3 value.

Perhaps payments in the distant future won’t be “Web2 vs Web3,” but “Web2 + Web3.”

Centralized services provide convenience; decentralized protocols protect rights.

And this is exactly the possibility I want to explore through this small project.

Final Words

This demo is certainly not perfect, and even somewhat “toy-like”—after all, this is my first hands-on project entering Web3.

Much of the code was actually AI-assisted (yes, I used AI as a “programming partner” while learning), and I encountered many pitfalls and made many revisions along the way.

But it was through this project that I truly understood those concepts that once sounded distant:

Encryption and signatures, verification processes, UTXO model, CKB’s Cell structure, timelocks, off-chain state channels, even writing on-chain contracts in JavaScript…

They’re no longer terms in documentation, but real logic that I’ve debugged, verified, and even “been tortured by” with my own hands.

If you’re also interested in AI, payments, or blockchain, welcome to check out the code on GitHub:

  • You can submit an issue to discuss together

  • Or directly clone it and run it

  • Maybe you can even help me find a few bugs :sweat_smile:

After all, all great systems start from an “imperfect demo.”

15 Likes

Hi Sonny, thanks for sharing this intriguing demo with your exploration and thoughts!

如果有计划在现有基础上对这个demo进行迭代的话,欢迎来申请 Spark 星火计划!

2 Likes

大神,能做comfyui的插件吗,它是开源的,接入fiber的话,comfyui出图片和视频按单个付款。图片和视频的需求更大。

我还不太了解 Fiber,目前还处在一个学习的阶段,不过我看 Fiber 跟 Lightning Network 的概念有点类似,后续如果有机会了解的话,跟大家也会分享一下。

牛啊姐妹,我十年前就知道这个,但到现在也没啥进步,你一个月后就搓出来项目了。

下次还有这种机会的话请私信我 :laughing:

1 Like

请问怎么保证过期商家一定会签名,并把钱退回给用户。这个方案中没有太看懂。我觉得在AI场景中,比边聊边付精准的,流式炒币,丢1块钱,马上开始赚钱,亏了,继续投币 :grinning_face:

@president_tin

可以参考这个流程,买家会把退款交易结构发送给卖家,卖家在确认交易无误后签名,此时买家拥有了已签名的退款交易,在买家拿到签名后才会真正向多签地址打钱,这也是整个支付通道的核心。

不好意思,技术小白。我理解是前面先做了一个付款凭证,且只有双方确认,这笔钱才能收到。这里得意思是说,如果这笔钱付出去了,但是存在退款,这个是从一个服务端来验证得,这个服务端是怎么保证商家会愿意签名退款。或者这么说,是不是只要我签名付款了,发生最终要退款得环节,还是需要双方协商来处理。这里只能保证的是,我承诺付的一定会付过去,如果对方超时了,卖家也会收到钱。

@president_tin 哈哈,没关系,能把一件事给不了解技术的同学讲明白也代表我是真正理解了这个项目,下面我尽量用通俗的语言描述下大致的流程:

第一步:开通道(建立信任)

你想用 AI 服务,先跟商家说:“我准备交 100 块押金,最多用 7 天。”

商家说:“行,但得按规则来。”

于是你写一张 “退款保证书”

“7 天后,如果我没消费,我能拿回全部押金。”

你让商家在这张保证书上签字。

只有等他签完字,你才真的把 100 块放进共管账户(就像一个两人共管的保险箱)。

——这时候,你的钱才真正“锁进去”。

第二步:边用边更新“欠条”

你开始聊天,AI 回了第一段话,算 2 块钱。

你就写一张 “欠条”

“目前我用了 2 块,剩下 98 块归我,2 块归商家。”

你在这张欠条上签字,发给商家。

商家收到后,就保存这张最新的欠条

接着 AI 又回一段,花了 3 块(累计 5 块),你再写一张新欠条:

“现在我用了 5 块,95 块归我,5 块归商家。”

第三步:怎么拿钱?

  • 商家结算:商家随时可以用你签的最新欠条,去共管账户里拿走他应得的钱;这里需要说明的是,商家有强烈的动机在过期前去结算,否则过期后,买家可以自己取出全部保证金,因为之前的退款保证书已经有商家的签名了,买家只需要签他自己的名字就可以取出。

  • 如果买家 7 天内没有消费:7 天一到,你直接凭那张“退款保证书”把剩下的钱全拿回来;

  • 如果商家跑路:到期后你照样能拿回全部押金,因为之前已经有退款协议了。

所以,整个过程买家始终掌握主动权

  • 钱没保障不打;

  • 用多少付多少

“我理解是前面先做了一个付款凭证,且只有双方确认,这笔钱才能收到。这里得意思是说,如果这笔钱付出去了,但是存在退款,这个是从一个服务端来验证得”

这里的不是先做了付款凭证,而是先有一个退款凭证,

“这个服务端是怎么保证商家会愿意签名退款”

这里的服务端其实就是商家,商家这里没有不愿意的动机,因为这笔钱只有在用户没消费并且过期后才能取出。如果用户消费了,那么商家可以在通道过期前上链来获得商家应有的那部分(从多签地址里出钱)。

“是不是只要我签名付款了,发生最终要退款得环节,还是需要双方协商来处理。这里只能保证的是,我承诺付的一定会付过去,如果对方超时了,卖家也会收到钱。”

退款环节的流向是从保险箱(多签地址)到买家,是需要双方的签名,但是之前卖家已经签过名了,所以在退款时只需要再加上买家一个人的签名就可以生效了。

1 Like