Skip to content

JoinMarket Wallet Daemon (jmwalletd)

JAM-compatible HTTP and WebSocket daemon for JoinMarket NG.

jmwalletd exposes wallet, transaction, CoinJoin, maker, and tumbler operations over a FastAPI-based REST API that is compatible with the reference JoinMarket jmwalletd, so JAM (the JoinMarket web UI) can talk to JoinMarket NG without modification.

What It Provides

  • REST API under /api/v1 (wallet, direct send, CoinJoin, maker, tumbler)
  • WebSocket notifications on /ws, /api/v1/ws, and /jmws
  • JWT authentication (access and refresh tokens)
  • Orderbook proxy endpoints under /obwatch/* (also without the /api/v1 prefix, as JAM expects)
  • Optional serving of the JAM web UI static files

Installation

The default complete profile of install.sh installs jmwalletd. On an older installation, first run bash ~/.joinmarket-ng/install.sh --update to refresh the saved installer, then run it again with --update --jmwalletd to add the daemon. Older saved installers do not recognize the new flag. Single-role installations can opt in with --jmwalletd (which also installs maker, taker, and tumbler). To install from a clone in a virtualenv instead:

python -m venv jmvenv
source jmvenv/bin/activate
python -m pip install -e ./jmcore -e ./jmwallet -e ./maker -e ./taker \
    -e ./tumbler -e ./jmwalletd

Alternatively, use the standalone Docker image published by this repository:

docker pull ghcr.io/joinmarket-ng/joinmarket-ng/jmwalletd:latest

For a single-application install that bundles the JAM web UI, jmwalletd, Tor, and neutrino, see the Flatpak page.

Running

Start the daemon:

jmwalletd

Defaults:

  • bind host: 127.0.0.1
  • port: 28183
  • TLS: enabled, with an auto-generated self-signed certificate stored in ~/.joinmarket-ng/ssl/

Common options:

# run plain HTTP on loopback (for a local TLS-terminating reverse proxy)
jmwalletd --no-tls

# listen on all interfaces with TLS enabled
jmwalletd --host 0.0.0.0

# custom data dir and config file
jmwalletd --data-dir /path/to/data --config-file /path/to/config.toml

The wallet API accepts passwords and returns bearer tokens. Never expose it over plain HTTP to an untrusted network. --no-tls should be used only on loopback, or on an isolated container network behind an access-controlled, TLS-terminating reverse proxy. Binding to 0.0.0.0 makes the API reachable on any interface allowed by the firewall; clients must verify the daemon's TLS certificate.

The JMWALLETD_HOST, JMWALLETD_NO_TLS, JOINMARKET_DATA_DIR, and JOINMARKET_CONFIG_FILE environment variables mirror the CLI options.

Connecting JAM

Point JAM at the daemon URL (default https://127.0.0.1:28183). With the default self-signed certificate your browser asks for a one-time exception.

For a combined Docker deployment, use the standalone-ng image maintained by joinmarket-webui/jam-docker. It runs JAM, jmwalletd, Tor, and the orderbook watcher behind plain HTTP nginx on container port 80; jmwalletd remains on its TLS-enabled loopback port inside the container. The JoinMarket NG repository only publishes the standalone jmwalletd image.

If a JAM build is present in <data-dir>/jam (or the system paths /usr/share/jmwalletd/jam and /app/share/jmwalletd/jam used by packaged installs), the daemon serves the UI itself, so opening the daemon URL in a browser is enough.

Configuration

BIP39 Passphrases

Create, recover, and unlock request bodies accept an optional bip39_passphrase string, separate from the file-encryption password. Omission preserves the empty-passphrase behavior of existing clients. The CLI/TUI onboarding setting does not substitute credentials into REST requests. External JAM clients need their own support for entering this field; the daemon API extension alone does not add a web UI prompt.

New containers remember the expected identity, never the passphrase. The active credential is forwarded to direct spending, CoinJoin, maker, and tumbler, then cleared on lock. A mismatch rejects unlock before changing the active session. An actual identity change resets tokens and WebSocket sessions; same-identity reopening preserves existing refresh-token validity.

Upgrade: Existing PBKDF2 and version-1 files remain readable without rewriting or scans caused by missing identity metadata. Their identity remains unconfirmed; supply the intended credential and verify the wallet independently. A deliberate recovery into another named container can establish a binding, but is not a required upgrade step and may scan. New nonempty-passphrase containers use format version 2, which older daemons reject rather than opening the wrong wallet. Keep compatible software with backups.

For API compatibility, failed unlocks still return Invalid credentials. Check both credentials and format support, and consult daemon warnings. See the identity and format details for metadata privacy and downgrade limitations.

jmwalletd uses the shared JoinMarket NG config (~/.joinmarket-ng/config.toml) and the same environment override model as the other components (see Configuration).

The most relevant sections:

  • [network_config] for network selection (network, bitcoin_network, directory_servers)
  • [bitcoin] for the backend (descriptor_wallet or neutrino)
  • [tor] for SOCKS and control settings
  • [taker] policy settings, honored by CoinJoins and tumbles started through the API
  • [tumbler] pacing settings for API-run tumbles

Orderbook proxy target resolution order:

  1. OBWATCH_URL env var
  2. orderbook_watcher.http_host + orderbook_watcher.http_port
  3. fallback http://127.0.0.1:8000

Development

Run the unit tests for this component:

pytest jmwalletd

For Docker-backed integration and JAM-compatibility (reference) coverage, use the root test workflow:

./scripts/run_parallel_tests.sh