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/MyWalletAnd 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 -> tb1qyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyySelect 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 stoppedThe 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
- Enable RPC in Wasabi's
Config.json:
"JsonRpcServerEnabled": true- Make scripts executable:
chmod +x wpay.sh wcancel.sh wcj.sh- Requirements:
curl,jq
Typical Workflow
./wpay.sh # Queue your payments
./wcj.sh # Start coinjoin, wait for completionThat'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
donewcj.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"