Disappear Into the Crowd: Wasabi's Hidden Payment Superpower

Disappear Into the Crowd: Wasabi's Hidden Payment Superpower

The Best Bitcoin Privacy Feature You're Not Using

Wasabi Wallet has a feature called payincoinjoin that most users overlook. Instead of the typical privacy flow - coinjoin your coins, then send a payment, then coinjoin the change - you embed your payment directly inside the coinjoin transaction itself.

The result? Your payment becomes not only cheaper but also more private, it's one of dozens of identical-looking outputs. Chain analysts see a coinjoin with many participants. They can't tell which output is your payment and which outputs are change returning to other participants. No separate transaction to correlate. No timing analysis. No amount fingerprinting if you use standard denominations.

The Problem

Wasabi's RPC interface exposes this feature, but the raw commands are ugly:

curl -s -d '{"jsonrpc":"2.0","id":"1","method":"payincoinjoin","params":["tb1q...",50000]}' http://127.0.0.1:37128/MyWallet

And then you have to manually start coinjoin, babysit it, check if payments went through, and remember to stop it when done.

The Solution: Three Simple Scripts

I vibed three interactive scripts that make this workflow painless.

wpay.sh - Queue Payments

Queue multiple payments with smart denomination suggestions:

$ ./wpay.sh
Wallets:

  [1] savings
  [2] spending

Select wallet: 2

Loading wallet spending (this may take a moment)...
Wallet ready.

=== Add Payments ===

Address: tb1qxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Amount (sats): 75000

Standard denominations blend better in coinjoins.

Options:

  [L] Send less:  65536 sats  (-9464 sats, -12.62%)
  [M] Send more:  100000 sats  (+25000 sats, +33.33%)
  [E] Exact amount: 75000 sats  (non-standard)

Choice [L/M/E]: m

Queued: 100000 sats -> tb1qxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Payment ID: 65c21ec1-9865-4cd6-bd67-c2f058a45d24

Add another payment? [Y/n]: n

Done. Run wcj.sh to start coinjoin.

The script suggests rounding to standard denominations because they blend in with other coinjoin outputs. A payment of exactly 73,847 sats stands out. A payment of 65,536 sats looks like everyone else's change.

wcancel.sh - Cancel Payments

Made a mistake? Cancel payments interactively:

$ ./wcancel.sh
Wallets:

  [1] savings
  [2] spending

Select wallet: 2

Loading wallet spending (this may take a moment)...
Wallet ready.

=== Pending Payments ===

  [1] 100000 sats -> tb1qxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  [2] 50000 sats -> tb1qyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy

  [A] Cancel all
  [Q] Quit

Cancel which? 2
Cancelled: 50000 sats -> tb1qyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy

Select by number, cancel multiple with 1,3 or 1 3, or cancel all with A.

wcj.sh - Run Coinjoin

Start coinjoin and monitor until all payments complete:

$ ./wcj.sh
Wallets:

  [1] savings
  [2] spending

Select wallet: 2

Loading wallet spending (this may take a moment)...
Wallet ready.

=== Wallet: spending ===

Pending payments:
  100000 sats -> tb1qxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  50000 sats -> tb1qyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy

=== CoinJoin started ===

[14:32:15] Sent: 50000 sats -> tb1qyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
[14:45:02] Sent: 100000 sats -> tb1qxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

=== All payments done ===
CoinJoin stopped

The script:

  • Shows initial state
  • Starts coinjoin automatically
  • Detects when payments complete
  • Detects if you add new payments while running
  • Stops coinjoin when done
  • Handles Ctrl+C gracefully

Setup

  1. Enable RPC in Wasabi's Config.json:
"JsonRpcServerEnabled": true
  1. Make scripts executable:
chmod +x wpay.sh wcancel.sh wcj.sh
  1. Requirements: curl, jq

Typical Workflow

./wpay.sh      # Queue your payments
./wcj.sh       # Start coinjoin, wait for completion

That's it. Your payments disappear into the crowd.


Full Scripts

wpay.sh

#!/usr/bin/env bash

# Wasabi Pay in CoinJoin
# Interactive payment queuing with standard denomination suggestions

RPC=" http://127.0.0.1:37128"

# Check RPC connection
status=$(curl -s --connect-timeout 3 -d '{"jsonrpc":"2.0","id":"1","method":"getstatus"}' "$RPC" 2>/dev/null)
if [ -z "$status" ]; then
    echo "Error: Cannot connect to Wasabi RPC at $RPC"
    echo "Make sure Wasabi is running and RPC is enabled in Config.json"
    exit 1
fi

DENOMS=(5000 6561 8192 10000 13122 16384 19683 20000 32768 39366 50000 59049 65536 100000 118098 131072 177147 200000 262144 354294 500000 524288 531441 1000000 1048576 1062882 1594323 2000000 2097152 3188646 4194304 4782969 5000000 8388608 9565938 10000000 14348907 16777216 20000000 28697814 33554432 43046721 50000000 67108864 86093442 100000000 129140163 134217728 200000000 258280326 268435456 387420489 500000000 536870912 774840978 1000000000 1073741824 1162261467 2000000000 2147483648 2324522934 3486784401 4294967296 5000000000 6973568802 8589934592 10000000000 10460353203 17179869184 20000000000 20920706406 31381059609 34359738368 50000000000 62762119218 68719476736 94143178827 100000000000 137438953472)

# Get wallet list
wallets=$(curl -s -d '{"jsonrpc":"2.0","id":"1","method":"listwallets"}' "$RPC" | jq -r '.result')
wallet_count=$(echo "$wallets" | jq 'length')

if [ "$wallet_count" -eq 0 ]; then
    echo "No wallets found."
    exit 1
fi

# Select wallet
echo "Wallets:"
echo ""
for i in $(seq 0 $((wallet_count - 1))); do
    num=$((i + 1))
    name=$(echo "$wallets" | jq -r ".[$i].walletName")
    echo "  [$num] $name"
done
echo ""
read -p "Select wallet: " wallet_choice

idx=$((wallet_choice - 1))
if [ "$idx" -lt 0 ] || [ "$idx" -ge "$wallet_count" ]; then
    echo "Invalid selection."
    exit 1
fi

WALLET=$(echo "$wallets" | jq -r ".[$idx].walletName")

# Load wallet if not already loaded
echo ""
echo "Loading wallet $WALLET (this may take a moment)..."
load_result=$(curl -s -d '{"jsonrpc":"2.0","id":"1","method":"loadwallet","params":["'"$WALLET"'"]}' "$RPC")
load_error=$(echo "$load_result" | jq -r '.error.message // empty')

if [ -n "$load_error" ] && [[ "$load_error" != *"already"* ]]; then
    echo "Error loading wallet: $load_error"
    exit 1
fi
echo "Wallet ready."
echo ""
echo "=== Add Payments ==="

add_payment() {
    echo ""
    # Get address
    read -p "Address: " ADDRESS
    if [ -z "$ADDRESS" ]; then
        echo "Address required."
        return 1
    fi

    # Get amount
    read -p "Amount (sats): " AMOUNT
    if ! [[ "$AMOUNT" =~ ^[0-9]+$ ]]; then
        echo "Invalid amount."
        return 1
    fi

    # Find nearest denominations
    local lower=""
    local higher=""
    local exact=""

    for d in "${DENOMS[@]}"; do
        if [ "$d" -eq "$AMOUNT" ]; then
            exact="$d"
            break
        elif [ "$d" -lt "$AMOUNT" ]; then
            lower="$d"
        elif [ "$d" -gt "$AMOUNT" ] && [ -z "$higher" ]; then
            higher="$d"
            break
        fi
    done

    # Calculate differences
    local lower_diff lower_pct higher_diff higher_pct
    if [ -n "$lower" ]; then
        lower_diff=$((AMOUNT - lower))
        lower_pct=$(awk "BEGIN {printf \"%.2f\", ($lower_diff / $AMOUNT) * 100}")
    fi

    if [ -n "$higher" ]; then
        higher_diff=$((higher - AMOUNT))
        higher_pct=$(awk "BEGIN {printf \"%.2f\", ($higher_diff / $AMOUNT) * 100}")
    fi

    # Display options
    echo ""
    echo "Standard denominations blend better in coinjoins."
    echo ""

    local selected
    if [ -n "$exact" ]; then
        echo "Your amount is already a standard denomination."
        selected="$exact"
    else
        echo "Options:"
        echo ""
        if [ -n "$lower" ]; then
            echo "  [L] Send less:  $lower sats  (-$lower_diff sats, -$lower_pct%)"
        fi
        if [ -n "$higher" ]; then
            echo "  [M] Send more:  $higher sats  (+$higher_diff sats, +$higher_pct%)"
        fi
        echo "  [E] Exact amount: $AMOUNT sats  (non-standard)"
        echo ""
        read -p "Choice [L/M/E]: " choice

        case "${choice^^}" in
            L)
                if [ -n "$lower" ]; then
                    selected="$lower"
                else
                    echo "No lower denomination available."
                    return 1
                fi
                ;;
            M)
                if [ -n "$higher" ]; then
                    selected="$higher"
                else
                    echo "No higher denomination available."
                    return 1
                fi
                ;;
            E)
                selected="$AMOUNT"
                ;;
            *)
                echo "Invalid choice."
                return 1
                ;;
        esac
    fi

    # Send payment
    local result error payment_id
    result=$(curl -s -d '{"jsonrpc":"2.0","id":"1","method":"payincoinjoin","params":["'"$ADDRESS"'",'"$selected"']}' "$RPC/$WALLET")
    error=$(echo "$result" | jq -r '.error.message // empty')

    if [ -n "$error" ]; then
        echo "Error: $error"
        return 1
    fi

    payment_id=$(echo "$result" | jq -r '.result')
    echo ""
    echo "Queued: $selected sats -> $ADDRESS"
    echo "Payment ID: $payment_id"
    return 0
}

# Main loop
while true; do
    add_payment
    echo ""
    read -p "Add another payment? [Y/n]: " again
    if [[ "${again^^}" == "N" ]]; then
        break
    fi
done

echo ""
echo "Done. Run wcj.sh to start coinjoin."

wcancel.sh

#!/usr/bin/env bash

# Wasabi Cancel Payments in CoinJoin
# Interactive selection to cancel pending payments

RPC=" http://127.0.0.1:37128"

# Check RPC connection
status=$(curl -s --connect-timeout 3 -d '{"jsonrpc":"2.0","id":"1","method":"getstatus"}' "$RPC" 2>/dev/null)
if [ -z "$status" ]; then
    echo "Error: Cannot connect to Wasabi RPC at $RPC"
    echo "Make sure Wasabi is running and RPC is enabled in Config.json"
    exit 1
fi

# Get wallet list
wallets=$(curl -s -d '{"jsonrpc":"2.0","id":"1","method":"listwallets"}' "$RPC" | jq -r '.result')
wallet_count=$(echo "$wallets" | jq 'length')

if [ "$wallet_count" -eq 0 ]; then
    echo "No wallets found."
    exit 1
fi

# Select wallet
echo "Wallets:"
echo ""
for i in $(seq 0 $((wallet_count - 1))); do
    num=$((i + 1))
    name=$(echo "$wallets" | jq -r ".[$i].walletName")
    echo "  [$num] $name"
done
echo ""
read -p "Select wallet: " wallet_choice

idx=$((wallet_choice - 1))
if [ "$idx" -lt 0 ] || [ "$idx" -ge "$wallet_count" ]; then
    echo "Invalid selection."
    exit 1
fi

WALLET=$(echo "$wallets" | jq -r ".[$idx].walletName")

# Load wallet if not already loaded
echo ""
echo "Loading wallet $WALLET (this may take a moment)..."
load_result=$(curl -s -d '{"jsonrpc":"2.0","id":"1","method":"loadwallet","params":["'"$WALLET"'"]}' "$RPC")
load_error=$(echo "$load_result" | jq -r '.error.message // empty')

if [ -n "$load_error" ] && [[ "$load_error" != *"already"* ]]; then
    echo "Error loading wallet: $load_error"
    exit 1
fi
echo "Wallet ready."
echo ""
echo "=== Pending Payments ==="
echo ""

# Get pending payments
result=$(curl -s -d '{"jsonrpc":"2.0","id":"1","method":"listpaymentsincoinjoin"}' "$RPC/$WALLET")
error=$(echo "$result" | jq -r '.error.message // empty')

if [ -n "$error" ]; then
    echo "Error: $error"
    exit 1
fi

payments=$(echo "$result" | jq -r '.result')
count=$(echo "$payments" | jq 'length')

if [ "$count" -eq 0 ]; then
    echo "No pending payments."
    exit 0
fi

# Display numbered list
for i in $(seq 0 $((count - 1))); do
    num=$((i + 1))
    amount=$(echo "$payments" | jq -r ".[$i].amount")
    address=$(echo "$payments" | jq -r ".[$i].address")
    echo "  [$num] $amount sats -> $address"
done

echo ""
echo "  [A] Cancel all"
echo "  [Q] Quit"
echo ""
read -p "Cancel which? " choice

# Quit
if [[ "${choice^^}" == "Q" ]]; then
    exit 0
fi

# Cancel all
if [[ "${choice^^}" == "A" ]]; then
    echo ""
    ids=$(echo "$payments" | jq -r '.[].id')
    for id in $ids; do
        curl -s -d '{"jsonrpc":"2.0","id":"1","method":"cancelpaymentincoinjoin","params":["'"$id"'"]}' "$RPC/$WALLET" > /dev/null
        amount=$(echo "$payments" | jq -r ".[] | select(.id == \"$id\") | .amount")
        address=$(echo "$payments" | jq -r ".[] | select(.id == \"$id\") | .address")
        echo "Cancelled: $amount sats -> $address"
    done
    exit 0
fi

# Cancel specific numbers (comma or space separated)
selections=$(echo "$choice" | tr ',' ' ')

for sel in $selections; do
    if ! [[ "$sel" =~ ^[0-9]+$ ]]; then
        echo "Invalid selection: $sel"
        continue
    fi
    
    idx=$((sel - 1))
    
    if [ "$idx" -lt 0 ] || [ "$idx" -ge "$count" ]; then
        echo "Invalid selection: $sel"
        continue
    fi
    
    id=$(echo "$payments" | jq -r ".[$idx].id")
    amount=$(echo "$payments" | jq -r ".[$idx].amount")
    address=$(echo "$payments" | jq -r ".[$idx].address")
    
    cancel_result=$(curl -s -d '{"jsonrpc":"2.0","id":"1","method":"cancelpaymentincoinjoin","params":["'"$id"'"]}' "$RPC/$WALLET")
    cancel_error=$(echo "$cancel_result" | jq -r '.error.message // empty')
    
    if [ -n "$cancel_error" ]; then
        echo "Failed to cancel [$sel]: $cancel_error"
    else
        echo "Cancelled: $amount sats -> $address"
    fi
done

wcj.sh

#!/usr/bin/env bash

# Wasabi CoinJoin Payment Runner
# Starts coinjoin and monitors payments, adapting to new/cancelled payments

RPC=" http://127.0.0.1:37128"

# Check RPC connection
status=$(curl -s --connect-timeout 3 -d '{"jsonrpc":"2.0","id":"1","method":"getstatus"}' "$RPC" 2>/dev/null)
if [ -z "$status" ]; then
    echo "Error: Cannot connect to Wasabi RPC at $RPC"
    echo "Make sure Wasabi is running and RPC is enabled in Config.json"
    exit 1
fi

# Get wallet list
wallets=$(curl -s -d '{"jsonrpc":"2.0","id":"1","method":"listwallets"}' "$RPC" | jq -r '.result')
wallet_count=$(echo "$wallets" | jq 'length')

if [ "$wallet_count" -eq 0 ]; then
    echo "No wallets found."
    exit 1
fi

# Select wallet
echo "Wallets:"
echo ""
for i in $(seq 0 $((wallet_count - 1))); do
    num=$((i + 1))
    name=$(echo "$wallets" | jq -r ".[$i].walletName")
    echo "  [$num] $name"
done
echo ""
read -p "Select wallet: " wallet_choice

idx=$((wallet_choice - 1))
if [ "$idx" -lt 0 ] || [ "$idx" -ge "$wallet_count" ]; then
    echo "Invalid selection."
    exit 1
fi

WALLET=$(echo "$wallets" | jq -r ".[$idx].walletName")

# Load wallet if not already loaded
echo ""
echo "Loading wallet $WALLET (this may take a moment)..."
load_result=$(curl -s -d '{"jsonrpc":"2.0","id":"1","method":"loadwallet","params":["'"$WALLET"'"]}' "$RPC")
load_error=$(echo "$load_result" | jq -r '.error.message // empty')

if [ -n "$load_error" ] && [[ "$load_error" != *"already"* ]]; then
    echo "Error loading wallet: $load_error"
    exit 1
fi
echo "Wallet ready."

# Handle Ctrl+C gracefully
cleanup() {
    echo ""
    echo "Stopping coinjoin..."
    curl -s -d '{"jsonrpc":"2.0","id":"1","method":"stopcoinjoin"}' "$RPC/$WALLET" > /dev/null
    echo "CoinJoin stopped."
    exit 0
}
trap cleanup SIGINT

get_pending() {
    curl -s -d '{"jsonrpc":"2.0","id":"1","method":"listpaymentsincoinjoin"}' "$RPC/$WALLET" \
        | jq '[.result[] | select(.state[0].status == "Pending")] | sort_by(.address)'
}

show_pending() {
    local payments="$1"
    local count=$(echo "$payments" | jq 'length')
    if [ "$count" -eq 0 ]; then
        echo "  (none)"
    else
        echo "$payments" | jq -r '.[] | "  \(.amount) sats -> \(.address)"'
    fi
}

# Show initial state
echo ""
echo "=== Wallet: $WALLET ==="
echo ""
echo "Pending payments:"
prev=$(get_pending)
show_pending "$prev"
prev_count=$(echo "$prev" | jq 'length')

if [ "$prev_count" -eq 0 ]; then
    echo ""
    read -p "No pending payments. Start coinjoin anyway? [y/N]: " confirm
    if [[ "${confirm^^}" != "Y" ]]; then
        exit 0
    fi
fi

# Start coinjoin
echo ""
curl -s -d '{"jsonrpc":"2.0","id":"1","method":"startcoinjoin","params":["",false,true]}' "$RPC/$WALLET" > /dev/null
echo "=== CoinJoin started ==="
echo ""

# Track payments
prev_addrs=$(echo "$prev" | jq -r '.[].address' | sort)
ever_had_payments=false
if [ "$prev_count" -gt 0 ]; then
    ever_had_payments=true
fi

while true; do
    curr=$(get_pending)
    curr_count=$(echo "$curr" | jq 'length')
    curr_addrs=$(echo "$curr" | jq -r '.[].address' | sort)
    
    # Track if we ever had payments
    if [ "$curr_count" -gt 0 ]; then
        ever_had_payments=true
    fi
    
    # Check for completed payments
    if [ -n "$prev_addrs" ]; then
        for addr in $prev_addrs; do
            if ! echo "$curr_addrs" | grep -q "^${addr}$"; then
                amount=$(echo "$prev" | jq -r ".[] | select(.address == \"$addr\") | .amount")
                echo "[$(date +%H:%M:%S)] Sent: $amount sats -> $addr"
            fi
        done
    fi
    
    # Check for new payments
    if [ -n "$curr_addrs" ]; then
        for addr in $curr_addrs; do
            if [ -z "$prev_addrs" ] || ! echo "$prev_addrs" | grep -q "^${addr}$"; then
                amount=$(echo "$curr" | jq -r ".[] | select(.address == \"$addr\") | .amount")
                echo "[$(date +%H:%M:%S)] Added: $amount sats -> $addr"
            fi
        done
    fi
    
    # All done? Only exit if we ever had payments and now have none
    if [ "$curr_count" -eq 0 ] && [ "$ever_had_payments" = true ]; then
        break
    fi
    
    # Update state
    prev="$curr"
    prev_count="$curr_count"
    prev_addrs="$curr_addrs"
    
    sleep 30
done

# Final state
echo ""
echo "=== All payments done ==="

curl -s -d '{"jsonrpc":"2.0","id":"1","method":"stopcoinjoin"}' "$RPC/$WALLET" > /dev/null
echo "CoinJoin stopped"