Bitcoin PayJoin (BIP 78) lets the receiver contribute inputs to a transaction, making it impossible for blockchain observers to identify who sent the funds. Unlike normal payments where the sender’s inputs are obvious, PayJoin creates ambiguity that defeats standard on-chain analysis heuristics. This guide provides step-by-step technical implementation for both senders and receivers, including Python and Flask code examples, testing strategies, and production deployment checklists.
Prerequisites
Before implementing PayJoin, ensure you have:
- A Bitcoin full node (Bitcoin Core recommended)
- A wallet with multiple UTXOs
- Python 3.9+ or a Bitcoin library like
bitcoinliborbtcpy - Tor for receiver endpoints (recommended for privacy)
Step 2 - Implementing the Sender (Payee-Initiated)
The sender initiates the PayJoin by requesting a PayJoin URL from the receiver. Here’s a Python implementation using the btcpy library:
import urllib.parse
from btcpy.structs.address import Address
from btcpy.structs.transaction import TxBuilder, TxExecutor
from btcpy.structs.script import P2wpkhScript
class PayjoinSender:
def __init__(self, node_url="127.0.0.1:8332", wallet_name="payjoin_sender"):
self.node_url = node_url
self.wallet_name = wallet_name
def create_payjoin_proposal(self, receiver_url: str, send_amount: int):
"""
Create a PayJoin proposal for the receiver.
send_amount: in satoshis
"""
# Parse receiver's PayJoin endpoint
parsed = urllib.parse.urlparse(receiver_url)
# Get UTXOs from our wallet
utxos = self.getWalletUtxos()
# Calculate maximum we can contribute
total_available = sum(u['value'] for u in utxos)
change_amount = total_available - send_amount
# Create the unsigned transaction
tx = TxBuilder().add_inputs(utxos).add_output(
address=parsed.hostname, # Receiver's address
amount=send_amount
).add_output(
address=self.get_change_address(),
amount=change_amount
).build()
# Create the PayJoin URL with our PSBT
psbt = self.tx_to_psbt(tx)
return self.create_payjoin_url(receiver_url, psbt)
def getWalletUtxos(self):
# Call Bitcoin Core RPC
# Implementation depends on your setup
pass
def tx_to_psbt(self, tx):
# Convert transaction to Partially Signed Transaction
pass
Step 3 - Implementing the Receiver (Payee)
The receiver must set up an endpoint to receive PayJoin requests. Here’s a Flask-based implementation:
from flask import Flask, request, jsonify
from btcpy.structs.transaction import Psbt
from btcpy.structs.script import P2wpkhScript
import bitcoinrpc
app = Flask(__name__)
class PayjoinReceiver:
def __init__(self, rpc_connection):
self.rpc = rpc_connection
self.extended_private_key = "xprv..." # Your xpub
def handle_payjoin_request(self, psbt_base64: str):
"""
Process incoming PayJoin proposal from sender.
"""
# Decode the PSBT
psbt = Psbt.from_base64(psbt_base64)
# Verify the transaction is valid so far
if not self.validate_proposal(psbt):
return {"error": "Invalid proposal"}, 400
# Add our inputs to the transaction
psbt = self.add_receiver_inputs(psbt)
# Set up our output (the payment amount we expect to receive)
psbt = self.add_payment_output(psbt, expected_amount=100000)
# Sign our inputs
psbt = self.sign_psbt(psbt)
# Return the finalized PSBT
return {
"psbt": psbt.to_base64(),
"originalOutputs": psbt.tx.outputs
}
def add_receiver_inputs(self, psbt):
"""
Select our UTXOs to add to the transaction.
"""
# Get our available UTXOs
our_utxos = self.get_receiver_utxos()
# We want to contribute enough to make the amount ambiguous
# but not so much that we create unnecessary change
receiver_contribution = self.select_receiver_utxo(
psbt.tx.outputs[0].amount # Match sender's payment
)
for utxo in receiver_contribution:
psbt.add_input(utxo)
return psbt
def validate_proposal(self, psbt):
"""
Validate the sender's proposal:
- Check fee rate is reasonable
- Verify output amounts
- Ensure no unexpected inputs
"""
# Implementation details
return True
Step 4 - PayJoin URL Format
The PayJoin protocol uses a specific URL scheme defined in BIP 78:
https://receiver.example.com/pj?amount=0.01&label=Payment%20for%20Invoice%20123
Parameters include:
amount: Suggested payment amount in BTClabel: Optional label for the paymentexp: Expiration time (Unix timestamp)expire: Expiration duration in secondssignature: Signature proving the receiver controls the endpoint
Step 5 - Privacy Considerations
When implementing PayJoin, consider these privacy-critical aspects:
-
UTXO Selection - Both parties should avoid creating obvious change outputs. The receiver’s contribution should roughly match the payment amount to minimize detectable change.
-
Fee Management - PayJoin transactions are larger than standard payments, requiring higher fees. Budget for approximately 2-3x the normal transaction fee.
-
Timing - Avoid initiating or completing PayJoin transactions at predictable intervals. Random delays between request and completion improve unlinkability.
-
Network Isolation - Run your PayJoin receiver over Tor to prevent IP address correlation between the sender and receiver.
-
Replay Protection - Ensure each PayJoin session uses fresh UTXOs to prevent transaction replay.
Step 6 - Test Your Implementation
Test PayJoin on Bitcoin’s testnet before production use:
Start a testnet node
bitcoind -testnet -server -rpcuser=testuser -rpcpassword=testpass
Create test wallets
./bitcoin-cli -testnet createwallet "payjoin_sender"
./bitcoin-cli -testnet createwallet "payjoin_receiver"
Get testnet coins from a faucet
https://bitcoinfaucet.net/
Verify your implementation using the PayJoin Dev Toolkit:
from payjoin import Sender, Receiver, Environment
Test that your implementation follows BIP 78
def test_payjoin_compliance():
sender = Sender(Environment.TESTNET)
receiver = Receiver(Environment.TESTNET)
# Run the compliance test suite
result = sender.test_receiver_compatibility(receiver)
assert result.is_valid(), f"Compliance errors: {result.errors}"
Step 7 - Common Implementation Mistakes
Avoid these frequent errors when implementing PayJoin:
-
Fixed Contribution Amounts - Always vary the receiver’s contribution amount to prevent amount-based clustering.
-
Missing Expiration - Always implement expiration to prevent indefinite waiting and resource consumption.
-
Inadequate Fee Estimation - PayJoin transactions require more vbytes. Use a fee estimator that accounts for additional inputs.
-
No Input Value Matching - The receiver should attempt to match or exceed the sender’s payment amount to maximize privacy.
-
Ignoring RBF: Always enable Replace-By-Fee for PayJoin transactions to handle fee market volatility.
Production Deployment Checklist
Before deploying PayJoin in production:
- Implement Tor hidden service for receiver endpoint
- Set up monitoring for failed PayJoin attempts
- Configure reasonable expiration times (5-15 minutes recommended)
- Implement proper error handling and logging
- Test with multiple wallet implementations
- Verify chain analysis resistance with tools like OXT or Samurai Wallet
- Document API endpoints for integration
- Set up key rotation procedures
PayJoin represents one of the most effective practical improvements in Bitcoin transaction privacy. By carefully implementing both sender and receiver components, you can significantly reduce on-chain analysis effectiveness while maintaining full Bitcoin security guarantees.
Troubleshooting
Configuration changes not taking effect
Restart the relevant service or application after making changes. Some settings require a full system reboot. Verify the configuration file path is correct and the syntax is valid.
Permission denied errors
Run the command with sudo for system-level operations, or check that your user account has the necessary permissions. On macOS, you may need to grant terminal access in System Settings > Privacy & Security.
Connection or network-related failures
Check your internet connection and firewall settings. If using a VPN, try disconnecting temporarily to isolate the issue. Verify that the target server or service is accessible from your network.
Frequently Asked Questions
Who is this article written for?
This article is written for developers, technical professionals, and power users who want practical guidance. Whether you are evaluating options or implementing a solution, the information here focuses on real-world applicability rather than theoretical overviews.
How current is the information in this article?
We update articles regularly to reflect the latest changes. However, tools and platforms evolve quickly. Always verify specific feature availability and pricing directly on the official website before making purchasing decisions.
Are there free alternatives available?
Free alternatives exist for most tool categories, though they typically come with limitations on features, usage volume, or support. Open-source options can fill some gaps if you are willing to handle setup and maintenance yourself. Evaluate whether the time savings from a paid tool justify the cost for your situation.
Can I trust these tools with sensitive data?
Review each tool’s privacy policy, data handling practices, and security certifications before using it with sensitive data. Look for SOC 2 compliance, encryption in transit and at rest, and clear data retention policies. Enterprise tiers often include stronger privacy guarantees.
What is the learning curve like?
Most tools discussed here can be used productively within a few hours. Mastering advanced features takes 1-2 weeks of regular use. Focus on the 20% of features that cover 80% of your needs first, then explore advanced capabilities as specific needs arise.
Related Articles
- Bitcoin Dust Attack Explained How Small Transactions
- Bitcoin Inheritance Planning Using Multisig With Family
- Wasabi Wallet Coinjoin Setup Guide For Bitcoin Transaction
- Use Tor With Encrypted Email for Maximum Sender Anonymity
- How To Buy Bitcoin Without Kyc Verification Private Purchase
- AI Coding Assistant Session Data Lifecycle Built by theluckystrike. More at zovo.one