Last updated: March 16, 2026

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:

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:

Step 5 - Privacy Considerations

When implementing PayJoin, consider these privacy-critical aspects:

  1. UTXO Selection - Both parties should avoid creating obvious change outputs. The receiver’s contribution should roughly match the payment amount to minimize detectable change.

  2. Fee Management - PayJoin transactions are larger than standard payments, requiring higher fees. Budget for approximately 2-3x the normal transaction fee.

  3. Timing - Avoid initiating or completing PayJoin transactions at predictable intervals. Random delays between request and completion improve unlinkability.

  4. Network Isolation - Run your PayJoin receiver over Tor to prevent IP address correlation between the sender and receiver.

  5. 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:

Production Deployment Checklist

Before deploying PayJoin in production:

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