Trickle: Budget-Capped Streaming Micropayments for Fiber Network

Hey everyone, sharing Trickle, my submission for the hackathon, and looking for feedback from people building on Fiber.

Authorship Sealed on Scryve. You can seal your work too, proving you wrote it and preserving it permanently on-chain.

1. The Problem

Fiber settles in milliseconds, but there’s still no clean way to bill per unit on it. Two things get in the way:

  • You can’t open an HTLC hold for every token, every API call, or every second of usage. The hold locks up liquidity and eventually expires, and a stuck HTLC is a force-close waiting to happen.
  • You can’t put a confirmation prompt in front of a human for every fraction of a cent, so anything that wants true per-unit billing needs an authorization step that only happens once, not per tick.

This shows up whenever someone wants to charge:

  • Per token for AI inference
  • Per call for an API
  • Per megabyte for bandwidth or a CDN
  • Subscriptions and other pay-as-you-go billing
  • Agent-to-agent payments where an unattended process needs to spend under a ceiling

Hold-invoice gateways solve escrow for usage you haven’t spent yet. They don’t solve settling usage that already happened, under a budget you can reuse across thousands of ticks. That’s the gap Trickle fills.

2. How Trickle Works

A grant is a signed, off-chain object with no cell and no on-chain footprint: {payee, asset, maxTotal, maxRate, sessionId, nonce, expiry}.

The flow:

  1. User registers a passkey (or an M-of-N quorum is set up for delegated spending)
  2. User picks a budget and signs the grant digest once
  3. The grant is handed to an AllowanceSession, which calls stream()
  4. Each tick is a keysend send_payment carrying custom_records
  5. A local cap check refuses anything over maxTotal or maxRate before it ever reaches the node
  6. Keysend on fnn v0.8.1 is spontaneous (random payment_hash, preimage reveals at the far end), so a tick clears and closes on the spot, nothing stays open
  7. A SessionMeter on the receiving side watches the channel balance and releases service against value that has actually landed
  8. In a recorded run a tick round-tripped in about 150 to 200ms
  9. Hit the cap and the stream freezes automatically
  10. User taps once to top up and the stream picks up exactly where it left off

3. Current Functionality

  • Streaming path live against two real fnn v0.8.1 testnet nodes, not a mock
  • Cap enforcement (maxTotal, maxRate) and settled-value metering, both tested
  • In a recorded run, direct-channel ticks were free; a routed 3-node path measured at 0.1% proportional fee, no base fee, no per-tick floor
  • Grant signing and verification for a 2-of-3 quorum, with tampering and expiry both covered by tests
  • Passkey signing running for real through Windows Hello in the browser demo, plus a separate unit test against a simulated authenticator
  • An escrow sibling for the hold-invoice side: new_invoice → payment → Received → capture (passkey or quorum, bound to the invoice by a token, SHA256(payment_hash || amount || nonce), so a signature can’t be replayed onto a bigger or different capture) or cancel_invoice
  • Optional on-chain custody: a 2-of-3 account bootstrapped and swept with 300 CKB on Pudge, a 1-of-3 withdraw rejected on-chain, a valid 2-of-3 withdraw pushed through (details and tx hashes in the README’s Stage B section)
  • Packaged as four libraries and two adapters, 22 tests passing across the workspace, build and CI green
  • Runnable UI: browser stream at /, streamer dashboard at /studio, escrow at /escrow, plus a terminal run and a device-free smoke test

4. Architecture

  • fnn v0.8.1 over JSON-RPC: send_payment (keysend, target_pubkey, custom_records TLV map), get_payment, new_invoice, settle_invoice, cancel_invoice
  • Two adapters: one wraps a @fiber-pay/sdk-shaped client structurally, one bridges fnn-pay invoice webhooks, so Trickle layers under SDKs teams are already using instead of replacing them
  • @ckb-ccc/core (1.12.5) for building and sending Pudge transactions
  • On-chain custody reuses the ckb-smart-account M-of-N / WebAuthn lock (deployed v5), referenced by code hash, nothing new to deploy
  • @noble/curves for P-256 WebAuthn signatures, @noble/hashes for sha256 and blake2b (CKB’s default hash)
  • pnpm workspace, TypeScript throughout, tsc build, tsx runtime, Node’s built-in test runner (node:test); README asks for Node 20 or newer, CI runs Node 22
  • TRICKLE_DEMO=1 swaps in an in-memory Fiber mock for a no-node run; BUYER_RPC / MERCHANT_RPC point the web demo at real nodes

5. Initial Use Cases

  • Per-token billing for AI inference
  • Per-call metering for APIs
  • Per-megabyte billing for bandwidth or CDN delivery
  • Subscriptions and other recurring pay-as-you-go billing
  • Agent-to-agent payments, where a signed grant lets an unattended process spend under a ceiling someone else set
  • Dropping in under existing Fiber payment SDKs (fnn-pay, @fiber-pay/sdk) purely for the per-unit billing layer

We think the delegation angle (a treasury or a human signs a ceiling once, an agent spends under it) is where this gets more interesting than a single-user metering tool.

6. Early Validation Request

A few things we’d love feedback on from people closer to Fiber internals:

  1. Is there a roadmap for the receiver side to see incoming keysend payments before they settle? Right now metering more than one payer means one channel per payer, and that’s the limitation we’d most like to see close.
  2. Where does PTLC sit on the Fiber roadmap? Ticks aren’t currently atomic with delivery, we’re trusting the counterparty for one small tick at a time, and PTLC would close that gap.
  3. Is custom_records the right place to carry session/tick metadata, or is there an emerging convention we should align with instead?
  4. Any guidance on multi-hop reliability (LSP peering, graph sync, hop hints)? We’ve measured the fee model on a routed path but haven’t stress-tested routing failures.
  5. Does packaging this as a client-side library (versus a hosted service) match how other teams want to consume metering infrastructure, or is a hosted gateway what people actually want?
  6. For UDT-funded streams, is there anything Fiber-specific we should account for beyond wiring udt_type_script through the tick and RPC layer (which is already there)?

7. How to Test

  1. pnpm install
  2. pnpm -r --filter "./packages/*" build
  3. pnpm test
  4. For a no-node run, set TRICKLE_DEMO=1 and start the browser demo
  5. For a real run, point BUYER_RPC and MERCHANT_RPC at two fnn v0.8.1 testnet nodes
  6. Open / for the streaming demo, /studio for the streamer dashboard, /escrow for the hold-invoice flow
  7. Passkey signing needs Chrome or Edge; WebAuthn treats localhost as secure so no HTTPS is required locally
  8. The on-chain custody path is optional and needs a funded Pudge testnet key (CKB_DEPLOYER_KEY_FILE); it reuses an already-deployed lock, so there’s nothing to deploy yourself
  9. Everything above is testnet only, no real funds are at risk

8. Project Links

6 Likes

Really interesting direction, one question I have: would each stream essentially be a higher-level abstraction over repeated channel updates, or are you envisioning some new accounting primitive that allows the stream state to accumulate off-chain and only settle against the channel periodically?

Also, small note: the GitHub repo link seems to be returning a 404 at the moment:
https://github.com/LusoCryptoLabs/trickle

3 Likes

Good question, appreciate you digging into it. It’s the first one, not a new primitive: each stream really is repeated channel updates under a thin allowance wrapper. The “grant” is a local spend cap the payer signs once (budget + rate + expiry) so nothing needs approving per-tick, but every tick is a real settled keysend on the channel, no batching, nothing accumulates off-chain waiting to be reconciled later.

We went back and forth on doing it the other way (hold value per unit, settle in batches) but that brings back the thing we were trying to kill: an HTLC held per unit means expiry/force-close risk on every single unit of usage. So we leaned into “settle every tick, never hold” instead, more channel updates, but zero exposure per tick.

Also worth noting the provider doesn’t trust the sender’s bookkeeping at all, it meters off its own channel balance delta and releases service against value that’s actually landed. So even the accounting is real-time, not something either side has to trust the other on.

We do have a separate hold-and-capture escrow piece in the repo for the “accumulate then settle” case, but that’s really for the opposite use case (deposits, pay-on-delivery), not streaming itself.

The repo is now public, thanks for the heads up.

2 Likes