Back to Blog
How to Add PayJoin Privacy to Your Bitcoin Wallet Using PayJoin Dev Kit
·6 min read

How to Add PayJoin Privacy to Your Bitcoin Wallet Using PayJoin Dev Kit

A step-by-step technical guide for wallet developers to implement PayJoin V2 transactions using PDK, breaking chain analysis heuristics.

Every Bitcoin transaction you make leaves a trail. Chain analysis firms exploit a simple assumption: all inputs in a transaction belong to the same person. This "common input ownership heuristic" has become the backbone of blockchain surveillance. PayJoin breaks it.

By combining inputs from both sender and receiver into a single transaction, PayJoin makes collaborative payments indistinguishable from ordinary ones. For wallet developers looking to give users meaningful privacy, implementing PayJoin V2 with the PayJoin Dev Kit is now straightforward enough to complete in about 30 minutes.

What PayJoin Actually Does

In a standard Bitcoin transaction, if you see three inputs, the reasonable assumption is that one entity controls all three. Chain analysis tools use this pattern to cluster addresses and trace funds across the network.

PayJoin (also called P2EP, or Pay-to-Endpoint) disrupts this pattern at its core. When a sender pays a receiver, both parties contribute inputs to the transaction. The result looks like any normal payment on-chain, but the ownership assumption no longer holds.

The privacy benefit is mutual. The sender's transaction history becomes harder to trace, and the receiver's UTXO set gets consolidated without revealing which inputs belong to whom. No third-party trust required.

Why PayJoin V2 Matters for Wallet Integration

The original PayJoin protocol (V1) had a significant limitation: both parties needed to be online simultaneously. For mobile wallets or intermittent connections, this was impractical.

PayJoin V2, specified in BIP77, solves this through asynchronous coordination. The protocol uses Oblivious HTTP (OHTTP) to relay encrypted messages through a directory server. The directory only sees an 8-kilobyte encrypted blob; it never learns transaction details or IP addresses.

This architectural change makes PayJoin viable for the kinds of wallets people actually use.

Prerequisites for Implementation

Before diving into code, you'll need:

  • Rust programming environment with Cargo
  • A running Bitcoin Core node (use Signet for testing)
  • OHTTP key configuration from a Payjoin Directory
  • The `payjoin` crate from PayJoin Dev Kit

The PayJoin Dev Kit is a full, standalone implementation maintained by the PayJoin Foundation, which received 501(c)(3) non-profit status in July 2025. The project has explicit goals to accelerate adoption through 2026, with funding support from OpenSats.

Step-by-Step Implementation for Receiving PayJoin V2

The receiving side is typically more complex, so let's walk through it.

Step 1: Fetch OHTTP Keys

First, your wallet needs to obtain OHTTP keys from the Payjoin Directory via an Oblivious HTTP Relay. This establishes the encrypted communication channel.

```rust

use payjoin::receive::v2::Receiver;

// Fetch OHTTP keys from directory via relay

let ohttp_keys = fetch_ohttp_keys_from_directory(&relay_url, &directory_url).await?;

```

The relay acts as a privacy layer between your wallet and the directory, ensuring the directory never learns your IP address.

Step 2: Initialize a Receiver Session

With OHTTP keys in hand, initialize a receiver session that will listen for incoming PayJoin proposals.

```rust

let receiver = Receiver::new(

address,

directory_url,

ohttp_keys,

expire_after,

)?;

```

The session handles the asynchronous coordination, meaning your wallet can go offline and return later to complete the transaction.

Step 3: Generate a PayJoin URI

PayJoin URIs extend standard BIP21 URIs with the endpoint information senders need. Generate one and present it to your user (typically as a QR code).

```rust

let pj_uri = receiver.pj_uri();

// Display as QR code or shareable string

```

Senders with PayJoin-compatible wallets will automatically recognize the URI and initiate the collaborative protocol.

Step 4: Validate Incoming Proposals

When a sender creates a proposal, your wallet receives it through the encrypted relay channel. The PayJoin Dev Kit provides check methods to validate proposals before proceeding.

```rust

let proposal = receiver.process_response(response)?;

// Validate the proposal meets your requirements

let checked = proposal

.check_broadcast_suitability(None, |tx| check_broadcast(&tx))?

.check_inputs_not_owned(|script| is_owned(script))?

.check_no_mixed_input_scripts()?

.check_no_inputs_seen_before(|outpoint| is_known(outpoint))?;

```

These checks prevent various attacks and ensure the proposal is legitimate.

Step 5: Contribute Your Inputs

This is where the privacy magic happens. Your wallet adds its own inputs to the transaction, breaking the common input ownership heuristic.

```rust

let provisional = checked.identify_receiver_outputs(|script| is_mine(script))?;

// Select UTXOs to contribute

let selected_utxo = select_utxo_for_payjoin();

let payjoin = provisional.contribute_witness_input(selected_utxo, outpoint)?;

```

The selection of which UTXOs to contribute is a wallet design decision. Contributing larger UTXOs provides more privacy benefit but requires more capital.

Step 6: Finalize and Sign

Complete the transaction by finalizing the proposal and signing your contributed inputs.

```rust

let payjoin_psbt = payjoin.finalize_proposal(

|psbt| wallet.sign(psbt),

Some(min_feerate),

)?;

// Send back through the relay channel

receiver.respond(payjoin_psbt)?;

```

The sender receives the modified PSBT, verifies it, adds their signature, and broadcasts the final transaction.

Implementation Considerations

A few practical notes from the protocol documentation and developer discussions:

Fee handling: The receiver typically doesn't increase the fee, but the protocol allows for fee contributions. Decide on a policy that makes sense for your wallet's use case.

UTXO selection: Random selection is simple but not optimal. Consider implementing strategies that maximize privacy benefit, such as preferring UTXOs of similar value to the payment amount.

Error handling: The async nature of V2 means you need robust handling for expired sessions, network failures, and malformed proposals. The PDK provides typed errors to help with this.

Testing: Use Bitcoin Signet rather than mainnet during development. The PayJoin Foundation maintains test infrastructure for developers.

The Adoption Challenge

PayJoin's effectiveness depends on network effects. A PayJoin transaction requires both sender and receiver to support the protocol. With limited wallet adoption, users can't always take advantage of the privacy benefits.

BTCPay Server has supported PayJoin for merchant payments, and the PayJoin Foundation's push through 2026 aims to expand wallet integration significantly. Dan Gould, lead developer of PayJoin Dev Kit, has stated the project's goal is to "accelerate Payjoin adoption in 2026."

The more wallets that implement PayJoin, the more useful it becomes for everyone. This creates a coordination problem, but also an opportunity: early implementers position their wallets as privacy-forward options in an increasingly surveillance-aware market.

Moving Forward

PayJoin represents a pragmatic approach to Bitcoin privacy. It doesn't require protocol changes, doesn't break Bitcoin's transparency guarantees for auditing, and provides mutual benefit to both transaction parties.

For wallet developers, the PayJoin Dev Kit removes most implementation complexity. The code is production-ready, the protocol is standardized in BIP77, and the supporting infrastructure (relays, directories) exists.

The technical barrier is low. The remaining challenge is coordination: getting enough wallets to implement PayJoin that users can actually use it. If you're building a Bitcoin wallet and privacy matters to your users, this is worth the 30 minutes to prototype.