MMashDiv

Configuring the RPC connection

The environment variables the Monero addon reads, the session lock its wallet RPC URL names, how HTTP Digest authentication is negotiated against both daemons, which daemon the wallet RPC actually syncs against, and what XMR_NETWORK actually controls.

12 min readUpdated 1 October 2026rpc, monerod, wallet-rpc, digest-auth, network, session-lock

The addon reads a short list of environment variables. Everything about how it reaches Monero — which endpoints, which credentials, which network — is in this handful of keys, and every one of them is read once when the service is constructed. A change to any of them needs a restart of every backend process: with the default PM2 file, pm2 restart backend cron.

The core keys

XMR_DAEMON_RPC_URLtype: urldefault: http://127.0.0.1:18081/json_rpc
monerod JSON-RPC URL, including the /json_rpc path. On an install with one daemon it is used for fee estimation, sync status and the /get_transactions check after an ambiguous relay, and it is the daemon the backend points a wallet at when a wallet refresh fails with a daemon error. Once a daemon list is set (XMR_MAINNET_RPC, below), the list's daemons do all of that and this key is only where the backend starts until one of them has answered.
XMR_WALLET_RPC_URLtype: urldefault: http://127.0.0.1:18083/json_rpc
monero-wallet-rpc JSON-RPC URL, including the /json_rpc path. Every wallet operation goes through it, and its host and port name the session lock every backend process shares. If get_version fails when the service starts, the chain is marked inactive in that process, and wallet creation, balance reads and transaction history refuse until it answers. Withdrawals do not check that flag.
XMR_RPC_USERtype: secret
Username for HTTP Digest auth against both the daemon and the wallet RPC, and the daemon login sent with set_daemon. Leave unset only if both services run with --disable-rpc-login.
XMR_RPC_PASSWORDtype: secret
Password paired with XMR_RPC_USER.
XMR_NETWORKtype: stringdefault: mainnet
Declares which Monero network this install operates on — mainnet when unset. Selects no endpoint: the real network is whatever monerod runs. Decides which network's addresses a withdrawal may be sent to.

Both URLs are defaulted, so an install with none of these set will still try 127.0.0.1:18081 and 127.0.0.1:18083. That is usually right, and occasionally the reason a misconfigured install appears to half-work.

One wallet RPC, one lock

monero-wallet-rpc has one wallet open at a time, and the web and cron processes both drive it. So every wallet-rpc session — open a wallet, prove it, read or send, close it — first takes a Redis key named after the wallet RPC:

xmr:wallet-rpc:<host>:<port>:session

<host> and <port> are read from XMR_WALLET_RPC_URL, which has two consequences:

  • Every backend process must spell the URL the same way. localhost:18083 and 127.0.0.1:18083 are the same wallet RPC but two different locks, and two processes holding different locks would open wallets around each other. The processes normally share one .env; keep it that way.
  • Redis is now part of the Monero path. The key lives in the Redis the backend already uses. If Redis is unreachable, no session starts: deposits are not checked and withdrawals are deferred until it is back. That is deliberate: a wallet opened without the lock can be swapped by another process in the middle of the session.

The lock waits, renewals and caps are on the Environment reference, and what each session does under it is on Deposits and confirmations.

A second daemon, and why the wallet is different

XMR_MAINNET_RPCtype: url
A comma-separated list of monerod JSON-RPC URLs forming a failover pool. Takes precedence over XMR_DAEMON_RPC_URL for every daemon query. Leave unset to keep the single-daemon behaviour exactly as it was.
XMR_MAINNET_RPC_FALLBACKtype: url
An optional second list, appended after the first. Same format.

Substitute your network for MAINNET — XMR_STAGENET_RPC on stagenet, and so on. Set neither and nothing changes: a pool of one endpoint is one attempt and the same error you got before, byte for byte.

With two or more, order is not priority. The pool tries an endpoint it has never used before any it has measured, and after that the fastest one, with a 20% margin before it switches. So the second daemon query in a process can go to the second entry whether or not the first answered, and both daemons must be on the same network and healthy — a stale or wrong-chain daemon anywhere in the list will be dialled. XMR_DAEMON_RPC_URL is not consulted for daemon queries once either list key is set. The diagnostics probe every entry in the list and say how many answered.

Only the daemon can fail over, and that is not an oversight. Monero is the one chain here with two RPCs, and they are not alike:

  • monerod answers stateless node queries. Any healthy daemon answers them identically, so a list of them is meaningful.
  • monero-wallet-rpc is a stateful process holding your opened wallet and its keys. A second one would be a second wallet. There is nothing to fail over to, and the backend does not try.

What the backend does instead is move the wallet's daemon. When the pool switches, the backend sends set_daemon — the runtime equivalent of relaunching wallet-rpc with --daemon-address — under the session lock. Inside a session it re-points the wallet that session has open at once. Outside one it tries to take a session without waiting; if another session is running, the move is left for the next daemon read rather than touching a wallet someone else is using. Because set_daemon acts on the wallet that is open, not on the wallet RPC as a whole, the wallets that later sessions open are covered by the refresh repair described below.

If the backend read from a fallback daemon while wallet-rpc stayed on the dead one, the chain would report healthy — the backend can see a daemon — while no wallet could sync and every deposit quietly stopped being credited. That is worse than no failover at all, because the health check agrees with you.

If wallet-rpc refuses the set_daemon call, the daemon read still succeeds and the backend logs a warning naming exactly this consequence. Search your logs for could NOT be repointed.

A daemon that answers — a bad method, a bad parameter — is not treated as a dead endpoint. It stays in the pool and you get its error immediately, rather than the same malformed request being replayed against every daemon you own.

URL rules that are enforced

Three shapes are rejected outright by the diagnostics. The first two for the same reason — a bad URL would otherwise be echoed verbatim inside an error message and end up in a log or a support ticket; the third because it silently dials somewhere else entirely.

  • A URL without a scheme. It must start with http://.
  • A URL with credentials in it. http://user:pass@127.0.0.1:18081/json_rpc is refused with "must not embed user:pass — use XMR_RPC_USER / XMR_RPC_PASSWORD instead".
  • An https:// URL with no port. The addon passes only the hostname, port and path to Node's http module — the scheme is never read and TLS is never negotiated — so https://host/json_rpc is dialled as plain HTTP on port 80, which is never the RPC. With an explicit port (https://host:18081/json_rpc) it does reach the daemon, because a stock monerod runs --rpc-ssl autodetect and accepts plaintext on the same listener; the diagnostics allow that and warn, since the URL says TLS and the wire has none. Write http:// and put a real TLS terminator in front if you need one.

Include the /json_rpc path. The Digest handshake signs the request URI, so a truncated path does not merely 404 — it produces an authentication failure that reads like wrong credentials.

How authentication actually works

Monero's RPC services use HTTP Digest, not Basic. The addon does not send credentials pre-emptively. Every call is made anonymously first, and only when the response is a 401 does it parse the WWW-Authenticate challenge and retry with a Digest header.

Four consequences follow from that.

Unset credentials against an authenticated service produce a specific error. If a 401 comes back and XMR_RPC_USER / XMR_RPC_PASSWORD are empty, the call fails with "Monero daemon/wallet RPC requires authentication but XMR_RPC_USER and XMR_RPC_PASSWORD are not configured", naming which of the two services rejected it. That message is the fastest diagnosis you will get on this chain — read it carefully.

Wrong credentials stop the deposit monitor immediately. A second 401 after the Digest retry raises an authentication failure, and the deposit monitor treats that as permanent: it unmonitors the wallet rather than retrying. Unlike a daemon outage, an auth fault does not heal itself.

Only the first Digest challenge is parsed, and only MD5. If a service offers several challenges the addon uses the first, with algorithm=MD5. Stock monerod and monero-wallet-rpc behave this way; a reverse proxy that rewrites the challenge may not.

The challenge and the answer must travel on the same TCP connection. monerod and monero-wallet-rpc keep the Digest session — the nonce and the request counter — on the connection that issued the challenge, and treat a valid answer arriving on any other connection as stale. The addon reuses its keep-alive socket for the retry, so in normal operation this is invisible. It stops being invisible behind a reverse proxy or load balancer that closes or re-routes connections between the two requests: every call then fails with a 401 that reads like wrong credentials. The diagnostics tell the two apart. A stale challenge is reported as "401 after Digest auth, challenge marked stale — … did not see the challenge and the answer on the same TCP connection"; a real mismatch as "401 after Digest auth — … rejected user …: XMR_RPC_USER / XMR_RPC_PASSWORD do not match its --rpc-login". Earlier builds of the diagnostics answered the challenge on a fresh connection themselves, and so reported bad credentials against a pair the deposit monitor was using without complaint — a false failure of the test, never of the chain.

Because there is one credential pair for two services, both must share the same --rpc-login. There is no way to give the daemon and the wallet RPC different accounts.

Which daemon the wallet RPC syncs against

monero-wallet-rpc only syncs against the daemon it was told to use, and it tells every wallet it opens to use the daemon on its own command line: --daemon-address, plus --daemon-login when monerod requires an RPC login. If a wallet cannot reach a daemon, its refresh fails with error -38, "no connection to daemon", even though the backend can reach monerod perfectly well on its own.

So start the wallet RPC with the right daemon flags — they are what every session's wallet syncs against. The backend no longer points the wallet RPC at a daemon when it starts: set_daemon acts on whichever wallet is open, and at startup that is nothing, or another process's session wallet. Its first daemon read still sends one, under the lock, and with no wallet open there is nothing for it to re-point.

What the backend does is repair a session's wallet when it has to. A wallet refresh that fails with a daemon error (code -38, or an error whose text says the daemon is busy or could not be connected to) makes it call set_daemon on the wallet the session has open — the host and port of XMR_DAEMON_RPC_URL, or of the pool daemon that last answered, with trusted: true, ssl_support: "autodetect", and XMR_RPC_USER / XMR_RPC_PASSWORD as the daemon login when they are set — and retry the refresh once.

That recovers the wallet RPC starting before monerod is listening, a daemon restart, and a wallet RPC launched with no daemon flags at all — one session at a time, because the repair lasts only as long as that wallet stays open. A wallet RPC started without --daemon-login in front of a daemon that requires one therefore hits -38 in every session and depends on this repair each time; if XMR_RPC_USER / XMR_RPC_PASSWORD are unset, or are not the daemon's login, every refresh fails. Put the daemon flags on the wallet RPC's command line and keep them matching .env.

What XMR_NETWORK does and does not do

It does not select an endpoint, and this is the trap that catches people: setting XMR_NETWORK=stagenet does not move you to stagenet. The network you are on is whichever network monerod was started for.

It does name one — XMR_{NETWORK}_RPC above is spelled with it, so changing XMR_NETWORK changes which daemon-list key is read. But that is the key's name, not a switch: point XMR_STAGENET_RPC at a mainnet daemon and you are on mainnet, with withdrawal validation now expecting stagenet addresses.

What it does decide is which network's addresses a withdrawal may be sent to. Every destination is first parsed in full — length, network byte and checksum — and a string that fails is refused as "Invalid Monero address: … It is not a well-formed Monero address". A valid address is then compared with XMR_NETWORK, which this check trims and lower-cases first — MAINNET and Stagenet read as mainnet and stagenet here:

XMR_NETWORK Withdrawals accepted to Addresses start with
unset, or mainnet Mainnet addresses 4 (standard, integrated), 8 (subaddress)
stagenet Stagenet addresses 5, 7
testnet Testnet addresses 9, A, B
any other name — main, live Any well-formed address — not enforced, and an error is logged —

An address for the wrong network is refused before anything is signed, with "This is a Monero … address, but this platform is configured for …. Use a … address, or ask an administrator to check XMR_NETWORK."

Set XMR_NETWORK="testnet" against a mainnet daemon and every real customer withdrawal address is refused as belonging to the wrong network. Set it to mainnet — or leave it unset — against a stagenet daemon and stagenet addresses are refused instead.

The diagnostics catch this: they read nettype out of get_info and fail the check with "XMR_NETWORK=… but the daemon runs … — valid withdrawal addresses will be rejected". The backend says the same when the Monero service starts ("NETWORK MISMATCH: XMR_NETWORK=… but monerod runs …"). Run the test after any daemon change.

Spell it in lowercase, in both places

The deposit side reads the same variable more strictly. Whether XMR is offered for deposit is decided by comparing the XMR token row's network with XMR_NETWORK: surrounding spaces are ignored, but the names must match letter for letter and case for case. An unset variable counts as mainnet, and a row whose network is the chain code itself, XMR, is accepted as mainnet.

So XMR_NETWORK="MAINNET" is a trap. Withdrawals are validated as mainnet, the startup check logs no mismatch, and XMR still disappears from the deposit currency list, because MAINNET is not the mainnet on the token row. Use the exact lowercase name — mainnet, stagenet or testnet — in .env and on the token row, and nothing else.

Deposit addresses are labelled differently and more defensively. When a wallet is created, the network stamped on the address record is derived from the address returned by the wallet RPC, falling back to XMR_NETWORK only if that address cannot be read. The wallet's own output is treated as ground truth, because XMR_NETWORK can be wrong or can change after addresses were issued.

Timeouts, retries and back-off

Worth knowing before you tune anything upstream, because Monero calls are slow by nature and a proxy with a short idle timeout will break them.

  • Daemon calls: 30 second timeout, up to 3 attempts, one second apart.
  • Wallet calls: 30 second timeout by default, and retried only when the connection was refused — the request never reached the wallet RPC. A call that timed out or was reset may still be running inside the wallet RPC, so it is never re-sent; instead the session keeps the lock until the wallet RPC answers get_version again (each probe up to 60 seconds, at most 15 minutes in all) and only then lets the next session in.
  • Wallet refresh: 60 seconds by default. A background refresh of a dormant wallet gets 10 minutes. A withdrawal opens its wallet with 2 minutes if this process refreshed it recently, or 15 minutes if not in the last six hours.
  • Balance reads refresh with a 120 second budget before returning a number.
  • The withdrawal transfer builds and the relay_tx get 120 seconds each. The relay is deliberately 1 attempt: retrying a relay could broadcast a second transaction.

When the daemon is unreachable the monitoring loop applies exponential back-off — 30 s, then 60, 120, capped at five minutes — instead of hammering a wallet RPC that is itself blocked waiting on the daemon. Daemon errors do not count against a wallet's retry budget, so a monitor survives an outage and resumes.

Keys that do nothing

Set these and nothing changes. They circulate in copied .env files and cost people hours during debugging.

Key Reality
XMR_WALLET_PASSWORD Read by no code. Wallets are created and opened with an empty password regardless. The diagnostics raise a warning when it is set, because its presence implies a protection that does not exist
XMR_WALLET_USER Listed in the admin diagnostics, but read by no runtime code

Verifying by hand

curl -s -X POST http://127.0.0.1:18083/json_rpc \
  -H 'Content-Type: application/json' \
  --digest -u "$XMR_RPC_USER:$XMR_RPC_PASSWORD" \
  -d '{"jsonrpc":"2.0","id":"0","method":"get_version"}'
curl -s -X POST http://127.0.0.1:18081/json_rpc \
  -H 'Content-Type: application/json' \
  --digest -u "$XMR_RPC_USER:$XMR_RPC_PASSWORD" \
  -d '{"jsonrpc":"2.0","id":"0","method":"get_info"}'

If get_version answers and get_info reports "synchronized": true on the nettype you configured, the connection layer is correct and any remaining problem is elsewhere. The same two probes are what Admin → Ecosystem → Blockchains → Requirements runs for you.

get_version is the only wallet RPC method that is safe to send to the live wallet RPC by hand, because it reads no wallet. Anything else — above all open_wallet or close_wallet — lands in the middle of whatever session is running. To look inside a wallet, use a copy and a second wallet RPC, as Troubleshooting describes.