
How to Implement PayJoin in Your Bitcoin Wallet Using PayJoin Dev Kit
A technical guide to integrating PayJoin (P2EP) transactions in Bitcoin wallets using PayJoin Dev Kit, with practical implementation steps.
Every standard Bitcoin transaction leaks information. Chain analysis firms operate on a simple assumption: all inputs in a transaction belong to the same entity. PayJoin breaks that assumption by having both sender and receiver contribute inputs to a single transaction, making surveillance significantly harder.
PayJoin Dev Kit (PDK) provides the tools to implement this privacy-enhancing protocol in your wallet. Built in Rust with bindings for Python, Dart, Swift, Kotlin, JavaScript, and C#, PDK enables wallet integration in under 2,000 lines of code according to developer interviews from November 2025. Here's how to make it work.
Understanding the PayJoin Protocol
PayJoin, also known as Pay-to-EndPoint (P2EP), is a collaborative transaction protocol where both parties contribute inputs. This breaks the common-input-ownership heuristic that chain analysis relies on. If an observer sees five inputs going to two outputs, they can no longer assume all five inputs belong to the sender.
The protocol has evolved considerably. BIP78 (V1), standardized in 2021, required the receiver to run an HTTP endpoint, which created deployment friction and privacy concerns. BIP77 (V2), merged in July 2025, introduced async PayJoin using Oblivious HTTP (OHTTP). This means neither party needs to be online simultaneously, and receivers don't expose public server infrastructure.
PDK implements both versions, though V2 is where the action is for new integrations.
Setting Up Your Development Environment
Start by adding the PayJoin crate to your Rust project with the appropriate features:
```toml
[dependencies]
payjoin = { version = "0.x", features = ["receive", "v2", "io"] }
```
For non-Rust projects, PDK provides FFI bindings. The Python bindings work well for prototyping, while the Swift and Kotlin bindings target mobile wallet development.
You'll also need:
- A Bitcoin Core node with RPC access (for UTXO management and transaction signing)
- Access to a PayJoin Directory relay (like `https://pj.bobspacebkk.com`)
- OHTTP keys from the directory for encrypted communication
Implementing the Receiver Side
The receiver implementation handles incoming PayJoin requests and contributes UTXOs to the collaborative transaction. Here's the workflow:
1. Bootstrap OHTTP
Fetch OHTTP keys from the PayJoin Directory. This enables the encrypted relay communication that makes V2 async PayJoin possible:
```rust
let ohttp_keys = fetch_ohttp_keys(&directory_url).await?;
```
2. Initialize the Session
Connect to your Bitcoin Core RPC wallet and create a receiver session with your receiving address and the directory URL:
```rust
let session = Receiver::new(
receiver_address,
directory_url,
ohttp_keys,
)?;
```
3. Register and Generate the PayJoin URI
Register your session via an OHTTP POST to the directory. This returns a BIP21 URI that includes your PayJoin endpoint:
```rust
let pj_uri = session.register().await?;
// Share this URI with the sender
```
The URI looks like a standard BIP21 payment request but includes a `pj=` parameter pointing to your session.
4. Listen for Proposals
Set up a loop to poll the directory for sender proposals:
```rust
loop {
match session.poll_for_proposal().await? {
Some(proposal) => {
// Validate and process
}
None => {
// Wait and retry
tokio::time::sleep(Duration::from_secs(5)).await;
}
}
}
```
5. Validate the Proposal
This step is critical for security and privacy. Check that:
- The proposed inputs aren't already owned by your wallet (prevents probing attacks)
- The transaction is suitable for broadcast (proper fees, valid structure)
- The payment amount matches what you expect
```rust
let validated = proposal.validate(|input| {
// Return true if this input is NOT yours
!wallet.is_mine(&input)
})?;
```
6. Contribute Your Input
Select a UTXO from your wallet to add to the transaction. This is where the privacy magic happens:
```rust
let utxo = wallet.select_utxo_for_payjoin()?;
let with_input = validated.contribute_input(utxo)?;
```
Choose UTXOs strategically. Adding an input of similar value to the payment amount provides better privacy than adding a much larger or smaller one.
7. Finalize and Respond
Sign your input and send the modified PSBT back through the OHTTP relay:
```rust
let signed = wallet.sign_psbt(with_input.psbt())?;
session.respond(signed).await?;
```
Implementing the Sender Side
The sender implementation is somewhat simpler. You're creating an initial transaction proposal and waiting for the receiver to modify it.
1. Parse the PayJoin URI
Extract the PayJoin endpoint from the BIP21 URI:
```rust
let pj_uri = payjoin::Uri::parse(&uri_string)?;
let pj_endpoint = pj_uri.pj_endpoint()?;
```
2. Create the Original PSBT
Build a standard transaction as you normally would, paying to the receiver's address:
```rust
let original_psbt = wallet.create_psbt(
pj_uri.address(),
amount,
fee_rate,
)?;
```
3. Send the Proposal
Submit your PSBT to the receiver via the PayJoin Directory:
```rust
let sender = Sender::new(original_psbt, pj_endpoint, ohttp_keys)?;
let response = sender.send_proposal().await?;
```
4. Validate the Response
This is where you protect yourself from malicious receivers. Verify that:
- Your outputs are preserved (you're still paying the right amount)
- No unexpected outputs were added that drain your funds
- The fee increase is reasonable
```rust
let validated = response.validate(|psbt| {
// Verify your outputs are intact
wallet.verify_outputs_preserved(psbt)
})?;
```
5. Sign and Broadcast
Sign your inputs in the modified transaction and broadcast:
```rust
let signed = wallet.sign_psbt(validated.psbt())?;
wallet.broadcast(signed)?;
```
Important Implementation Considerations
UTXO Requirements
The receiver needs at least one UTXO to participate. If the receiver's wallet is empty, PayJoin isn't possible, and you should fall back to a standard transaction. PDK handles this gracefully.
Privacy Leak Prevention
PayJoin improves transaction privacy, but careless implementation can leak metadata elsewhere. Consider:
- Network-level privacy (Tor for RPC connections)
- Timing analysis (don't respond immediately every time)
- UTXO selection patterns (randomize when possible)
Fallback Behavior
Always implement fallback to standard transactions. If the receiver is offline, the directory is unreachable, or something else fails, the payment should still complete. Users shouldn't even notice when PayJoin fails; they just get a normal transaction.
Production Deployments to Reference
Bull Bitcoin and Cake Wallet shipped PDK integrations in 2025, using PayJoin as the default for on-chain payments. These implementations demonstrate that the protocol works at scale with real users. Developers at the MIT Bitcoin Expo Hackathon also demonstrated integrations with Liana wallet and Boltz exchange.
BTCPay Server offers merchant-side PayJoin through its store settings, requiring only a hot wallet with SegWit addresses and at least one UTXO. Studying their implementation can provide practical insights for your own.
Moving Forward
PayJoin adoption has been slow historically, but PDK significantly lowers the integration barrier. The async V2 protocol eliminates the biggest deployment friction from V1, and the cross-language bindings mean you're not locked into Rust.
The protocol requires no consensus changes. It's purely a transaction construction technique that's fully compatible with existing Bitcoin infrastructure. The main barrier now is wallet support on both sides of transactions.
If you're building a Bitcoin wallet in 2026, PayJoin support is worth the engineering investment. The privacy benefits compound as more wallets participate, and PDK makes the technical lift manageable. Start with the receiver implementation, since that's where most of the complexity lives, then add sender support to complete the integration.