Skip to content

For LLMs and agents

This page is for language models, coding agents and the people who use them. It explains how to load these docs into an agent and gives a compact brief with the facts an agent needs most often.

Machine readable docs

URL Contents
/llms.txt Index of every page with a one line summary, in the llms.txt format
/llms-full.txt Every page as markdown in one file, links made absolute
/<page>.md The markdown source of any page, for example /clients/electrum.md

Every HTML page also links its markdown source with <link rel="alternate" type="text/markdown"> and a link at the bottom of the page.

To give an agent the whole documentation, point it at https://docs.joinstr.xyz/llms-full.txt. To let it pick pages, start from https://docs.joinstr.xyz/llms.txt.

Brief

joinstr is a coinjoin implementation for bitcoin. Peers coordinate over a nostr relay with no central coordinator. A pool has one denomination, a fixed number of peers and one relay. Each peer contributes one input and receives one equal output. There is no change output. Inputs are signed with SIGHASH_ALL|ANYONECANPAY after all outputs are known, then every peer combines, checks and broadcasts the same transaction.

Components

Component Source Language Backend Latest
Electrum plugin (reference) gitlab.com/invincible-privacy/joinstr, plugin/ Python Electrum 4.6.0 or newer, any build 0.4.2
Web UI and Umbrel app gitlab.com/invincible-privacy/joinstr, gui/ Python, Flask Bitcoin Core RPC Umbrel 0.1.2
Daemon joinstrd and joinstr-cli gitlab.com/invincible-privacy/joinstr, cli/ Python, FastAPI Bitcoin Core RPC main
Android app gitlab.com/invincible-privacy/joinstr-kmp Kotlin Multiplatform Bitcoin Core RPC 0.3.0
Sparrow fork github.com/QcMrHyde/sparrow-joinstr Java Sparrow server 0.2.0
Rust library and Dart bindings github.com/rust-joinstr/joinstr Rust, Dart Electrum server master

Constants

Name Value
Pool announcement nostr kind 2022, JSON content, no tags, signed by the pool key
Pool messages nostr kind 4, NIP-04 encrypted, tag ["p", pubkey]
Message types new_pool, join_pool, credentials (no type), reject, output, input, reregister, resign
Default relay wss://nos.lol
Default Tor proxy 127.0.0.1:9050
Sighash for registration 0x81 (ALL\|ANYONECANPAY)
Sighash for recovery 0x01 (ALL)
Output order BIP69, by amount then scriptPubKey bytes
Output amount round(denom × 1e8) − floor(fee_rate × 100 × peers) ÷ peers
Allowed input denomination + 500 to denomination + 5000 sats
Maximum cost per peer 15000 sats
Fee bounds at finalization 100 to 10000 sats per peer
Pool id 10 hex characters followed by unix time
Poll budget 360 polls every 5 seconds
Recovery phase timeout 120 seconds each
Electrum GUI pool timeout 600 seconds
Release signing key 9253062A4F92D63459085CA62D230520212A5901
Android APK certificate SHA-256 9F:D6:8F:FB:5E:B3:2F:D6:FC:2B:5D:FB:C9:7B:23:46:C7:54:C4:62:6F:9F:44:41:19:7B:D5:73:AB:16:18:E8

Common tasks

Install and verify the Electrum plugin.

gpg --keyserver hkps://keys.openpgp.org --recv-keys 9253062A4F92D63459085CA62D230520212A5901
gpg --verify joinstr.zip.asc joinstr.zip
# then Electrum > Tools > Plugins > Add > joinstr.zip

Run a pool from the Electrum daemon.

electrum --offline setconfig plugins.joinstr.enabled true
electrum --offline setconfig proxy "socks5:127.0.0.1:9050::"
electrum daemon -d
electrum load_wallet
electrum joinstr_list_pools
electrum joinstr_create_pool 0.001 3 <txid:vout>
electrum joinstr_join_pool <pool_id> <txid:vout>

Run the daemon and CLI against Bitcoin Core.

pip install -r cli/requirements.txt
cp cli/config.example.ini ~/.joinstr/config.ini   # set [rpc], [nostr], [tor]
python cli/joinstrd.py                              # REST API on 127.0.0.1:8000, docs at /docs
python cli/joinstr-cli.py create --denomination 0.001 --peers 3
python cli/joinstr-cli.py join --pool-id <id>
python cli/joinstr-cli.py input --txid <txid> --vout <n>

Run the test suites of the main repository.

python -m pytest plugin/tests/ cli/tests/ gui/tests/

Invariants

Code that builds or changes a joinstr client must keep these, or peers will reject its messages or lose coins.

  1. Sign the announcement with the pool key it advertises.
  2. Accept credentials only if their private_key derives the announced public_key and every term matches the announcement.
  3. Send every pool message to the relay named in the pool, not a configured default.
  4. Order outputs by BIP69 before signing. Sorting by address string is wrong.
  5. Sign registration inputs with 0x81 and finalize them with an empty final scriptSig for native segwit.
  6. Never sign a registration PSBT whose single input covers every output, since anyone could broadcast it alone.
  7. Refuse a round that would cost more than 15000 sats or leave an output below dust.
  8. Keep Python json.dumps spacing in message bodies, because some peers match "type": "<name>" as text.
  9. Route relay traffic through Tor with a new circuit per operation. Fail closed if Tor is down.
  10. Use a fresh address for every output and never reuse an address across pools.

Sources of truth

  • The protocol page is the reference for the wire format. It follows plugin/joinstr.py in the main repository. NIP.md in the same repository is the original design and is older.
  • Releases lists the current versions.
  • The security policy explains how to report vulnerabilities. Do not file them as public issues.