Skip to content

Daemon and CLI

joinstrd runs joinstr in the background against a Bitcoin Core wallet and exposes a REST API on localhost. joinstr-cli is a command line client for that API. Together they let you run coinjoins on a headless server or build joinstr into other software over HTTP.

Requires Python 3, Bitcoin Core with RPC, Tor
Transport Tor only
Source gitlab.com/invincible-privacy/joinstr/-/tree/main/cli

Install

git clone https://gitlab.com/invincible-privacy/joinstr.git
cd joinstr
pip install -r cli/requirements.txt
mkdir -p ~/.joinstr
cp cli/config.example.ini ~/.joinstr/config.ini
chmod 600 ~/.joinstr/config.ini

Configure

~/.joinstr/config.ini holds the node connection, the relay and the Tor proxy. The daemon warns if other users can read the file.

[rpc]
url = http://127.0.0.1:38332
user = <rpc user>
password = <rpc password>
wallet = <wallet name, empty for the default wallet>

[nostr]
relay = wss://nos.lol

[tor]
host = 127.0.0.1
port = 9050

The example uses the signet RPC port. Use 8332 for mainnet.

Tor is required. At start the daemon checks through the proxy that Tor works and exits if it does not. All relay and mempool.space traffic goes through Tor.

Environment variable Default Meaning
JOINSTR_DIR ~/.joinstr Directory with config.ini and state
JOINSTRD_HOST 127.0.0.1 Address the daemon binds
JOINSTRD_PORT 8000 Port the daemon listens on
JOINSTR_API_URL http://127.0.0.1:8000 Daemon URL used by the CLI

Run

python cli/joinstrd.py

The daemon only answers requests whose Host header is a loopback address. Interactive API docs are served at http://127.0.0.1:8000/docs and /redoc, with the schema at /openapi.json.

CLI

python cli/joinstr-cli.py <command> [options]
Command What it does
create --denomination <BTC> --peers <n> [--timeout <seconds>] Create a pool. Timeout defaults to 3600 seconds.
list [--my \| --others] List your pools or open pools on the relay
join [--pool-id <id>] Join a pool
output [--address <addr>] [--pool-id <id>] Register an output if it was not done automatically
input [--pool-id <id>] [--txid <txid> --vout <n>] Pick and sign the coin for the active pool
history Show finished coinjoins
stop Stop the current operation

The CLI signs your input with your node (walletprocesspsbt with ALL|ANYONECANPAY) and hands the signed PSBT to the daemon. It reads the [rpc] section of the same config.ini.

A typical round

# creator
python cli/joinstr-cli.py create --denomination 0.001 --peers 3

# every other peer
python cli/joinstr-cli.py list --others
python cli/joinstr-cli.py join --pool-id <id>

# everyone, once all outputs are registered
python cli/joinstr-cli.py input --txid <txid> --vout <n>

REST API

All endpoints are on http://127.0.0.1:8000. The full schema is at /openapi.json.

Method Path Purpose
GET /status Daemon status
GET /pools/my Pools you created or joined
GET /pools/others Open pools on the relay
POST /pool/create Create a pool
POST /pool/join Join a pool
GET /pool/info Details of the active pool
GET /pool/status Phase of the active pool
POST /pool/register-output Register an output address
GET /pool/my-output Your registered output
GET /pool/outputs All registered outputs
POST /pool/register-input Register a signed input PSBT
GET /pool/my-input Your registered input
GET /pool/inputs All registered inputs
GET /history Finished coinjoins
POST /stop Stop the current operation

/pool/register-input takes a PSBT that is already signed, so the key can live anywhere. A hardware or air gapped signer can sign the registration PSBT with SIGHASH_ALL|ANYONECANPAY and the daemon does the rest.

Fees and broadcast

  • The pool fee rate comes from mempool.space (hourFee) over Tor, with 5 sat/vB as a fallback.
  • A joiner refuses pools with a fee rate above 100 sat/vB and rounds that would cost more than 15000 sats.
  • The final transaction is broadcast through your node. If the node rejects it for a reason other than its own policy, the daemon tries mempool.space over Tor on mainnet and signet.