Installation
This page covers the minimum path to install JoinMarket NG and run your first commands.
For day-to-day usage, continue with:
Requirements
- Linux or macOS (Windows is supported via a manual install, see Windows section)
- Python 3.11+
- A Bitcoin backend:
descriptor_wallet(Bitcoin Core, recommended), orneutrino(light client)
Recommended Install (Linux/macOS)
curl -sSL https://raw.githubusercontent.com/joinmarket-ng/joinmarket-ng/main/install.sh | bash
source ~/.joinmarket-ng/activate.sh
What this does:
- creates
~/.joinmarket-ng/venv - installs
jmcore,jmwallet,jm-maker, andjm-taker - creates
~/.joinmarket-ng/config.toml - installs/configures Tor unless you pass
--skip-tor - installs static shell completion scripts for bash and zsh (near-instant tab completion)
Common options:
# taker only
curl -sSL https://raw.githubusercontent.com/joinmarket-ng/joinmarket-ng/main/install.sh | bash -s -- --taker
# maker only
curl -sSL https://raw.githubusercontent.com/joinmarket-ng/joinmarket-ng/main/install.sh | bash -s -- --maker
# skip Tor setup
curl -sSL https://raw.githubusercontent.com/joinmarket-ng/joinmarket-ng/main/install.sh | bash -s -- --skip-tor
# update existing installation
curl -sSL https://raw.githubusercontent.com/joinmarket-ng/joinmarket-ng/main/install.sh | bash -s -- --update
Flatpak
The Flatpak package (org.joinmarketng.JamNG) bundles all services
(jmwalletd + JAM web UI, Tor, neutrino light client, orderbook watcher) in a
single sandboxed application.
Build and install
flatpak-builder --user --install --force-clean build-dir flatpak/org.joinmarketng.JamNG.yml
Run
flatpak run org.joinmarketng.JamNG # mainnet
flatpak run org.joinmarketng.JamNG --network signet # signet
flatpak run org.joinmarketng.JamNG --no-gui # headless (opens browser instead of GUI)
Data directory
The Flatpak is sandboxed. All state lives in:
~/.var/app/org.joinmarketng.JamNG/.joinmarket-ng/
This is separate from the standard ~/.joinmarket-ng/ used by a non-Flatpak
install. Network-specific sub-directories are used automatically:
~/.var/app/org.joinmarketng.JamNG/.joinmarket-ng/ # mainnet
~/.var/app/org.joinmarketng.JamNG/.joinmarket-ng/signet/ # signet
~/.var/app/org.joinmarketng.JamNG/.joinmarket-ng/regtest/ # regtest
Migrating an existing wallet
If you have a wallet from a previous (non-Flatpak) install at
~/.joinmarket-ng/wallets/, copy it to the Flatpak data directory:
cp ~/.joinmarket-ng/wallets/default.mnemonic \
~/.var/app/org.joinmarketng.JamNG/.joinmarket-ng/wallets/
The wallet file format (.mnemonic) is the same in both installs.
If the wallet is encrypted you will be prompted for the password when you
select it in the TUI or use jm-wallet CLI commands via
flatpak run --command=sh org.joinmarketng.JamNG -c "jm-wallet info".
Moving wallets between networks
Wallets are stored under the network-specific data directory. If you created a
wallet on mainnet (no --network flag) and want to use it on signet, copy it
to the signet wallets directory:
# Created on mainnet, want to use on signet
cp ~/.var/app/org.joinmarketng.JamNG/.joinmarket-ng/wallets/my-wallet.jmdat \
~/.var/app/org.joinmarketng.JamNG/.joinmarket-ng/signet/wallets/
The same applies when moving a wallet the other way (signet to mainnet).
Note that addresses derived from the same mnemonic are different on mainnet vs
signet because the coin-type in the BIP32 path differs (0' for mainnet,
1' for signet/testnet).
Updating
When you run install.sh --update, the installer:
- Upgrades all installed Python packages to the specified (or latest) version
- Resolves and installs any new or changed dependencies (so a swapped dependency, such as PyNaCl replacing libnacl, is installed rather than leaving the venv missing a module)
- Verifies the core libraries import cleanly after the update and prints actionable remediation if a runtime module is missing
- Checks your config for new settings: compares
config.tomlagainst the latest template and prints any new sections or keys that are available - Refreshes shell completions and Tor configuration
Your existing config is never modified. If new settings are available, the installer prints them so you can add them manually from config.toml.template.
Supply-chain Security
The installer protects the two distinct parts of an install differently.
JoinMarket NG's own code is verified against GPG signatures. The installer resolves the requested version to a commit hash, then refuses to proceed unless a trusted key (committed to signatures/ in the repository) has signed a manifest whose commit: matches that exact hash. The install is then pinned to the verified commit, so a tag that gets repointed afterwards cannot smuggle in different code. You can bypass this with --skip-verify (not recommended); it is auto-enabled for --dev / --version main, which have no release signatures by design.
Third-party Python dependencies use a layered model so they stay both flexible and reproducible:
- Each package's
pyproject.tomldeclares minimal version ranges, so a normalpip installkeeps working across Python versions and resolves conflicts. - Each package ships a lock file (
requirements.txt) generated withpip-compile --generate-hashes. Becausepip-compilerecords hashes for every distribution file of each pinned version (all wheel tags plus the sdist), the locks are portable across Python versions and platforms. - By default the installer fetches the release's
requirements.txtfrom the verified commit and installs dependencies withpip --require-hashes, so any tampered artifact is rejected. The git-based JoinMarket NG packages are then installed with--no-deps(pip cannot hash a git checkout). - Hash verification is a hard requirement: if it cannot be satisfied (for example no pre-built wheel exists for your platform/Python, forcing an un-hashable source build), the install aborts rather than silently weakening integrity. Rerun with
--no-hash-depsto make opting out an explicit, informed choice. --no-hash-depsskips hash verification and only version-pins (still preventing silent upstream upgrades), printing a security warning that hashes are not verified.
# default: hash-checked dependencies (no flag needed)
curl -sSL https://raw.githubusercontent.com/joinmarket-ng/joinmarket-ng/main/install.sh | bash
# opt out of hash verification (version-pinning only)
curl -sSL https://raw.githubusercontent.com/joinmarket-ng/joinmarket-ng/main/install.sh | bash -s -- --no-hash-deps
The requirements.txt lock files are the single source of truth and are regenerated from pyproject.toml by scripts/update-deps.sh, so maintainers never hand-edit pins.
Configure Backend
Edit ~/.joinmarket-ng/config.toml.
If this is a manual/source install and the file does not exist yet:
mkdir -p ~/.joinmarket-ng/wallets
chmod 700 ~/.joinmarket-ng ~/.joinmarket-ng/wallets
curl -fsSL https://raw.githubusercontent.com/joinmarket-ng/joinmarket-ng/main/jmcore/src/jmcore/data/config.toml.template -o ~/.joinmarket-ng/config.toml
Bitcoin Core (descriptor_wallet, recommended)
[bitcoin]
backend_type = "descriptor_wallet"
rpc_url = "http://127.0.0.1:8332"
rpc_user = "your_rpc_user"
rpc_password = "your_rpc_password"
Cookie-based authentication (the default when bitcoind is started without
explicit rpcuser/rpcpassword) works as well and avoids keeping RPC
credentials in config.toml:
[bitcoin]
backend_type = "descriptor_wallet"
rpc_url = "http://127.0.0.1:8332"
rpc_cookie_file = "~/.bitcoin/.cookie"
rpc_cookie_file is mutually exclusive with rpc_user/rpc_password: set
one pair or the other, not both.
Neutrino (light client)
[bitcoin]
backend_type = "neutrino"
neutrino_url = "https://127.0.0.1:8334"
neutrino_tls_cert = "~/.joinmarket-ng/neutrino/tls.cert"
neutrino_auth_token_file = "~/.joinmarket-ng/neutrino/auth_token"
JoinMarket NG does not generate this cert/token itself today. You need to copy them from your neutrino-api instance once, then keep them in:
~/.joinmarket-ng/neutrino/tls.cert~/.joinmarket-ng/neutrino/auth_token
Create the directory:
mkdir -p ~/.joinmarket-ng/neutrino
chmod 700 ~/.joinmarket-ng/neutrino
Neutrino server example (Docker):
On Linux, add your user to the Docker group once (skip if Docker already works without sudo):
sudo usermod -aG docker "$USER"
newgrp docker
docker run -d \
--name neutrino \
--restart unless-stopped \
-p 8334:8334 \
-v neutrino-data:/data/neutrino \
-e NETWORK=mainnet \
ghcr.io/m0wer/neutrino-api
Copy credentials from neutrino-api into JoinMarket NG config directory:
docker cp neutrino:/data/neutrino/tls.cert ~/.joinmarket-ng/neutrino/tls.cert
docker cp neutrino:/data/neutrino/auth_token ~/.joinmarket-ng/neutrino/auth_token
chmod 600 ~/.joinmarket-ng/neutrino/tls.cert ~/.joinmarket-ng/neutrino/auth_token
If you previously used http:// neutrino:
- Switch
neutrino_urltohttps://... - Add
neutrino_tls_cert - Add
neutrino_auth_token_file(orneutrino_auth_token) - Restart JoinMarket NG
On low-power hardware, initial Neutrino sync can take significantly longer (for example, Raspberry Pi 4: ~20 minutes sync plus long prefetch).
First Run
Create a wallet and inspect addresses:
jm-wallet generate
jm-wallet info
Then either:
# mix coins as taker
jm-taker coinjoin --amount 1000000 --destination INTERNAL
# or run maker bot
jm-maker start
Manual Install (from source)
Use this for development or custom environments.
Debian/Ubuntu:
sudo apt update
sudo apt install -y git build-essential libffi-dev libsodium-dev pkg-config python3 python3-venv
macOS:
brew install libsodium pkg-config python3
Install packages:
git clone https://github.com/joinmarket-ng/joinmarket-ng.git
cd joinmarket-ng
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -e ./jmcore
python -m pip install -e ./jmwallet
python -m pip install -e ./maker
python -m pip install -e ./taker
Shell completions are pre-generated and installed automatically by the installer. For editable (development) installs, source the static scripts from the repo:
# bash
source completions/jm-wallet.bash
source completions/jm-maker.bash
source completions/jm-taker.bash
source completions/jmwalletd.bash
# zsh (add to .zshrc)
for f in completions/*.zsh; do source "$f"; done
To regenerate after CLI changes:
python scripts/generate_completions.py
Tor Notes
- Taker and orderbook watcher require Tor SOCKS (
127.0.0.1:9050) - Maker additionally uses Tor control (
127.0.0.1:9051) for ephemeral onion services - If you edit Tor config, restart Tor (
sudo systemctl restart toron Linux,brew services restart toron macOS) - Directory server usually runs as a Tor hidden service in Docker (see Directory Server)
On Debian/Ubuntu maker setups, Tor cookie auth often requires debian-tor group access:
sudo usermod -aG debian-tor "$USER"
newgrp debian-tor
Troubleshooting
jm-wallet: command not found: runsource ~/.joinmarket-ng/activate.sh- build dependency errors on Linux: install
build-essential libffi-dev libsodium-dev pkg-config - Python venv issues: install
python3-venv - RPC failures: verify Bitcoin Core is reachable and credentials in
config.tomlare correct RPC error -32601: Method not foundonlistwallets(or any wallet RPC): your Bitcoin Core node has wallet support disabled. Make surebitcoindis started without-disablewallet=1and thatdisablewallet=1is not set inbitcoin.conf(the descriptor wallet backend cannot work against a wallet-disabled node). Confirm withbitcoin-cli listwalletsreturning a JSON array.
Tracking Wallet Sync Progress
After importing a wallet (especially one with a long history), the
descriptor_wallet backend asks Bitcoin Core to scan the chain for the
imported descriptors. This can take minutes to several hours depending
on the wallet depth and the node hardware -- spinning disks and
Raspberry Pi-class hosts are at the slow end.
jm-wallet info will report only the fidelity bond balance until the
underlying scan finishes. Before suspecting a bug, check that the scan
is actually still running.
The descriptor wallet inside Bitcoin Core is named deterministically from
the mnemonic fingerprint and network, in the form
jm_<fingerprint>_<network> (for example
jm_abc12345_mainnet). List the loaded wallets to find the active one,
then export it as a shell variable:
bitcoin-cli listwallets
WALLET=jm_abc12345_mainnet # replace with your actual name
RPCWALLET="bitcoin-cli -rpcwallet=$WALLET"
Is the node still scanning? getwalletinfo reports a non-null
scanning object while a scan is in flight, with a progress field
between 0.0 and 1.0:
$RPCWALLET getwalletinfo | jq '{scanning: .scanning, txcount: .txcount, balance: .balance}'
When scanning becomes false, the scan is finished -- if balances
are still missing at that point it is a real problem rather than just
slowness.
Are descriptors imported with the expected ranges? A partially
imported wallet shows up here as a missing path or a smaller-than-expected
range:
$RPCWALLET listdescriptors | jq '.descriptors[] | {desc, range, active, internal}'
Each external/internal mixdepth pair adds two descriptors (/0/N/* and
/1/N/*). The range upper bound should be at least the deepest used
address index plus the gap-limit buffer. By default the imported range is
scan_range (1000) and it auto-expands as addresses are used; the buffer
kept ahead of the highest used index is gap_limit (20). If used
addresses sit beyond the imported range (common for wallets migrated from
legacy joinmarket-clientserver), widen it once with
jm-wallet rescan --scan-depth N. See
Wallet Scanning for the full model.
What is the node itself doing? Useful when scanning returns
false but balances still look wrong:
bitcoin-cli getblockchaininfo \
| jq '{blocks, headers, verificationprogress, initialblockdownload, pruned}'
bitcoin-cli getindexinfo # txindex / coinstatsindex / blockfilterindex
bitcoin-cli getmempoolinfo
Force a one-shot rescan when the descriptors look healthy but the
node missed transactions (e.g. after a long downtime or a manual
importdescriptors outside JoinMarket NG):
$RPCWALLET rescanblockchain $START_HEIGHT
Use the wallet creation height as $START_HEIGHT. getwalletinfo
shows birthtime; for an imported BIP39 wallet, set this to the
earliest possible block height that could contain your funds.
Prefer the JoinMarket NG wrappers when possible (they print a before/after coverage snapshot and respect the wallet's recorded creation height as a floor):
jm-wallet info --scan-status # show current Core scan coverage
jm-wallet rescan # kick off rescan and poll until complete
# (Ctrl-C is safe: the scan keeps running
# server-side; re-check with --scan-status)
jm-wallet rescan --scan-depth N # widen the address range to N, then rescan
Cross-check balances and UTXOs without involving JoinMarket NG:
$RPCWALLET getbalances
$RPCWALLET listunspent 0 9999999 '[]' true
If these report the expected funds but jm-wallet info does not, the
issue is on the JoinMarket NG side -- file a bug with the output of
jm-wallet debug-info (which redacts sensitive data).
For Neutrino backends the equivalent diagnostics live on the
neutrino-api server itself rather than via bitcoin-cli. See
Neutrino TLS for credentials and the
neutrino-api project's own /status endpoint for sync state.
Uninstall
There is no automatic uninstaller. Uninstalling has three parts: the Python packages, the JoinMarket NG data directory, and the descriptor wallet inside Bitcoin Core. Pick the steps that apply to your install.
WARNING: the data directory holds your encrypted wallet seeds, the fidelity-bond registry, and other state. Back it up before deleting anything. You will be able to recover the funds with the mnemonic alone.
1) Python packages and virtualenv
If you used install.sh (recommended path), removing the venv removes
all installed JoinMarket NG packages and shell entry points at once:
rm -rf "${JMNG_VENV_DIR:-$HOME/.joinmarket-ng/venv}"
If you installed into a system Python or another venv, uninstall the packages explicitly:
pip uninstall jmcore jmwallet jmwalletd jm-taker jm-maker jm-tumbler \
jm-directory-server joinmarket-orderbook-watcher
Static shell completions installed by install.sh live alongside the
venv and are removed with it. If you also added a manual source ...
line to ~/.bashrc / ~/.zshrc (for example for activate.sh),
remove that line too.
2) Data directory
The data directory defaults to ~/.joinmarket-ng and is configurable
via $JOINMARKET_DATA_DIR. It contains:
config.tomlwallets/(encrypted seeds and the fidelity-bond registry)- Tor state, log files, and per-wallet caches
rm -rf "${JOINMARKET_DATA_DIR:-$HOME/.joinmarket-ng}"
3) Descriptor wallet inside Bitcoin Core
When using the descriptor_wallet backend, JoinMarket NG creates a
watch-only descriptor wallet inside Bitcoin Core named
jm_<fingerprint>_<network> (for example jm_abc12345_mainnet). It is
not removed by uninstalling the Python packages.
List JoinMarket NG wallets currently loaded:
bitcoin-cli listwallets | jq '.[] | select(startswith("jm_"))'
Unload then delete each one. Bitcoin Core has no RPC to remove wallet files, so the directory must be removed manually after unloading:
WALLET="jm_<fingerprint>_<network>"
bitcoin-cli unloadwallet "$WALLET"
# Remove the on-disk wallet directory. Adjust the path if your
# Bitcoin Core datadir is not the default ~/.bitcoin (use
# `bitcoin-cli getwalletinfo` while loaded to confirm the location).
rm -rf "$HOME/.bitcoin/wallets/$WALLET"
On Neutrino-only installs there is no Bitcoin Core wallet to clean up.
Windows (Manual Install)
install.sh is bash-only and targets apt/brew, so Windows users cannot
use it directly. The supported path is a manual pip install of the
JoinMarket NG Python components plus a Tor daemon. CI exercises this exact
path on windows-latest against the public signet directory nodes; if you
follow these steps and the smoke at the end fails, please file a bug.
Prerequisites:
- Windows 10 or later (or Windows Server 2022+).
- Python 3.11+ from python.org (tick "Add Python to PATH" during install).
- PowerShell 7+ (or built-in Windows PowerShell 5).
- Tor (we use the Tor Project Expert Bundle; Tor Browser is not required).
1. Install joinmarket-ng
Clone the repository and install the Python components into a venv:
git clone https://github.com/joinmarket-ng/joinmarket-ng.git
cd joinmarket-ng
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
pip install .\jmcore .\taker .\jmwallet
Add --maker (pip install .\maker) if you want to run as a maker.
2. Install and start Tor
Download the Tor Expert Bundle from
https://www.torproject.org/download/tor/, extract it, and start tor.exe
with the default SOCKS port (9050):
# Example: with the bundle extracted to C:\tor
Start-Process -FilePath C:\tor\tor\tor.exe -ArgumentList "--SocksPort","9050"
Wait until the log reports "Bootstrapped 100%". Keep the window open while you use JoinMarket NG, or install Tor as a service if you prefer.
3. Verify connectivity
Fetch the public signet orderbook over Tor (this proves that Tor is reachable and the installed software can speak the JoinMarket directory protocol):
python scripts\check_signet_orderbook.py --min-offers 1
A successful run prints OK: signet orderbook reachable, N offers. Failures
print the underlying error and exit non-zero.