← back

Building Tzimtzum's Payment Layer: Spark, T-PRE, Tether's WDK, and the Road to RGB

What we learned wiring Bitcoin L2 payments into a sovereign publishing platform.


A few weeks ago I wrote about why we chose Spark Protocol for tzimtzum's payment layer. That article was about the decision framework. This one is about what happened when we actually built it.

Quick Context

Tzimtzum is a P2P publishing platform on Pear Runtime. No server, no cloud. Content replicates over Hyperswarm, identity derives from a BIP-39 seed, and everything is E2E encrypted via the Noise protocol. Authors publish, readers discover, and paywalled content is encrypted locally with decryption keys delivered peer-to-peer.

The payment layer needed to match: sovereign, P2P, censorship-resistant, capable of handling both sats and stablecoins.

Tether's WDK on Pear Runtime: The Integration Story

Spark won the evaluation because it solved the recipient problem. Lightning requires channel management. Spark doesn't. A creator receiving their first payment doesn't need to open channels, manage liquidity, or run a node.

For the integration, we used Tether's Wallet Development Kit (@tetherto/wdk-wallet-spark), a non-custodial toolkit that wraps the Spark SDK for programmatic use. WDK runs headless; it generates keys, signs transactions, and manages payments without user intervention. Built for applications, not for humans clicking around in a browser extension. For tzimtzum, where the wallet runs inside the Bare process alongside the content layer, this was the right choice.

The wallet embeds directly in the tzimtzum binary. WalletManagerSpark imports straight into the Bare process, handles seed generation, account creation, sats and USDT payments, all in-process.

What's currently implemented:

The paywall system turned out to be payment-agnostic by design. It doesn't care how the payment happens. It just needs a "payment verified" signal before releasing the content key. This was accidental at first but became important later.

The Friction: Making WDK Run on Bare

Getting WDK-Spark to run on Pear's Bare runtime was the hardest integration challenge in the project. WDK is well-designed for Node.js. But Bare is a different beast. It's a minimal JavaScript runtime that ships with Pear, and while it's compatible in many ways, the edges are sharp.

The patch script we ended up writing is 350 lines of targeted fixes. Here's what we hit:

with { imports } syntax. WDK and the underlying Spark SDK use with { imports: 'bare-node-runtime/imports' } in their ES module exports. Bare's module loader doesn't support this syntax yet. We strip it out and handle the import mapping ourselves.

Dynamic imports fail silently. Bare's module cache uses file:// URLs internally, but V8 reports the referrer as pear://dev/... for dynamic import() calls. The module loader can't find the referrer, so the import fails. The fix: convert dynamic imports to static imports in WDK's wallet manager source.

Node.js builtins everywhere. WDK's dependency tree pulls in ws, grpc-js, protobufjs, OpenTelemetry, axios, and dozens of other packages that assume Node.js builtins exist (events, util, http, crypto, stream, net, tls). Bare has its own equivalents (bare-events, bare-http1, etc.). We inject import maps into 25+ transitive dependencies to remap Node builtins to their Bare counterparts.

@noble/hashes crypto resolution. Bare's module resolver matches both "bare" and "node" export conditions. Without an explicit "bare" condition, @noble/hashes resolves to cryptoNode.js which imports node:crypto, and that doesn't exist in Bare. We patch the package.json exports to add a "bare" condition that resolves to the WebCrypto-compatible path.

Axios Node/browser split. Axios's Node.js adapter pulls in form-data, crypto, and other Node builtins. We redirect it to the browser adapter, which uses FormData and fetch, both available in Bare via globals.

OpenTelemetry platform detection. Five different OpenTelemetry packages have platform-specific code paths that default to Node.js. We redirect all of them to their browser implementations.

We submitted the core findings as issues upstream to the WDK team at Tether. The with { imports } syntax fix and the dynamic import conversion are things that should ideally be handled in the package itself.

The result: WDK-Spark runs natively in Bare, in-process, no sidecar. The patch script runs as a postinstall hook. Not elegant, but reliable. The wallet lives inside the same binary as the content layer. One runtime, one process, one identity.

T-PRE: Building Rust on Spark's Federation

The most technically ambitious piece we built lives outside tzimtzum's main repo. It's a fork of Spark's federation infrastructure.

Threshold Proxy Re-Encryption (T-PRE) is a cryptographic scheme where encrypted data can be re-encrypted for a different recipient without ever being decrypted. The "threshold" part means no single party can perform the re-encryption alone. You need a quorum of federation nodes to cooperate.

We implemented this in Rust, building on Spark's existing FROST (Flexible Round-Optimized Schnorr Threshold) signing infrastructure. The tpre.rs module is roughly 950 lines of Rust implementing Umbral cryptography with key splitting. On the Go side, we added gRPC service handlers for the Spark operators to coordinate the re-encryption.

Why this matters for tzimtzum: T-PRE enables a paywall model where the author encrypts content once, and the federation can re-encrypt it for any authorized reader without the author being online. The author doesn't need to be present for every key delivery. The federation handles it, but no single federation node can decrypt the content.

A different security model from the direct key delivery we currently use (where the author's Hyperswarm node delivers the decryption key to the reader after payment). T-PRE removes the requirement for the author to be online at the moment of purchase. For a publishing platform, that's significant.

The T-PRE integration is on a feature branch (feature/tpre-paywall). It works on regtest. The architectural pattern, extending Spark's existing operator federation with new cryptographic capabilities, proved that the federation infrastructure is more flexible than its original statechain-transfer purpose suggests.

What We Learned

Patch scripts are infrastructure. When your runtime is 95% compatible with a dependency tree, the last 5% is where all the work is. Our 350-line patch script is as critical as any feature code. It runs on every install and makes the difference between "WDK works in Bare" and "WDK crashes on import." Treat compatibility patches as first-class code.

The sidecar pattern is underrated. For WDK-Spark (pure JavaScript), we got it running natively in Bare with patches. For RGB (native Rust addons via SWIG), native addons produce .node binaries incompatible with Bare's .bare format. There, we use a sidecar: a separate Node.js process on localhost, spawned by Bare. The complexity is bounded and well-understood.

Payment-agnostic paywalls were the right abstraction. By avoiding coupling to a specific payment method, we accidentally built a system that can accept any future payment rail. Spark sats today. RGB assets tomorrow. Whatever comes after that. The paywall just needs a signal.

Rust and JavaScript can coexist. We write application-layer code in JavaScript on Pear Runtime. We write cryptography in Rust (T-PRE, and the native addons in WDK/rgb-lib are Rust under the hood). The boundary is the FFI/sidecar layer. These languages are good at different things. Use both.

Spark's federation is extensible. The FROST threshold signing infrastructure is general-purpose. We bolted T-PRE onto it. Someone else could bolt other threshold cryptographic operations. The architecture supports it.

What's Next

The immediate roadmap:

Identity backup and restore is built and stashed, ready to merge. One BIP-39 mnemonic restores everything: Hypercore keypair, Bitcoin wallet, Spark wallet, content signing keys. Lose your device, restore from 12 words.

Multi-device support via Autobase, so an author can publish from their laptop and their phone, with the Hypercore log staying consistent across devices.

RGB Protocol integration is the big one. RGB enables client-side validated smart contracts on Bitcoin. Assets (tokens, stablecoins, NFTs) live on Bitcoin UTXOs with all validation happening off-chain. Tether announced native USDT on RGB in August 2025. The protocol reached production status in mid-2025, with both v0.11.1 (RGB-Tools/Bitfinex) and v0.12 (LNP/BP Association) releases targeting mainnet.

For tzimtzum, RGB means:

We already have the sidecar architecture for RGB (WDK/rgb-lib runs in a Node.js process, same pattern as the Spark integration). What's missing is the consignment transport: how the proof data gets from sender to receiver. The ecosystem's current solution is a centralized HTTP proxy (rgb-proxy-server), a single relay that all RGB wallets post consignments to. The proxy operator can log IPs, observe timing, and be taken down. For a protocol built on censorship resistance, that's a structural weakness. We're working on a P2P alternative built on the same Hyperswarm infrastructure that already powers tzimtzum's content replication.

The payment-agnostic paywall design means RGB slots in as another payment signal alongside Spark. The content encryption, the key delivery, the Hyperswarm infrastructure, all already built. We just add another way to trigger the "payment verified" signal.

The Bigger Picture

When I wrote the Spark article, tzimtzum had one payment rail. Now it has a path to multiple rails, each serving different trade-offs:

The wallet picks the best available rail. The user sees one button. The infrastructure decides how.

That was always the goal: make payments invisible for creators. We're getting closer.


Adriano Sousa builds sovereign P2P infrastructure at reshimu labs. Code at github.com/adrianosousa.