Configuring the RPC connection

The environment variables the Monero addon reads, how HTTP Digest authentication is negotiated against both daemons, why the backend re-points wallet-rpc at monerod itself, and what XMR_NETWORK actually controls.

8 min readUpdated 3 August 2026rpc, monerod, wallet-rpc, digest-auth, network

The addon reads a short list of environment variables and nothing else. 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 backend restart.

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. Used for fee estimation and sync status, and forwarded to wallet-rpc as its daemon address.
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. If get_version fails at startup the whole chain is disabled.
XMR_RPC_USERtype: secret
Username for HTTP Digest auth against both the daemon and the wallet RPC, and the daemon login forwarded in 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. Selects no endpoint — the real network is whatever monerod runs. Drives withdrawal address-prefix validation.

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.

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 MAINNETXMR_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, but it is still the address wallet-rpc is first pointed at, until a pool daemon answers. 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, it issues set_daemon — the runtime equivalent of relaunching wallet-rpc with --daemon-address — so the wallet syncs against the daemon that is actually up.

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.

The backend owns the wallet RPC's daemon connection

monero-wallet-rpc only syncs against the daemon it was told to use. If it was started without --daemon-address, or pointed somewhere else, wallet refreshes fail with error -38, "no connection to daemon" even though the backend can reach monerod perfectly well on its own.

So the addon takes ownership. At startup it calls set_daemon on the wallet RPC with the host and port parsed out of XMR_DAEMON_RPC_URL, trusted: true, ssl_support: "autodetect", and the same username and password if they are set. It repeats that call, once, whenever a wallet refresh fails with a daemon error (codes -38 or -9), then retries the refresh.

That recovers three common situations without operator intervention: the wallet RPC starting before monerod is listening, a daemon restart, and a wallet RPC launched with no daemon flags at all. It also means the daemon URL in .env is authoritative — editing the systemd unit's --daemon-address without editing .env changes nothing that lasts.

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 address validation now expecting stagenet prefixes.

What it does is decide which address prefixes a withdrawal destination may have:

XMR_NETWORK Accepted first characters
mainnet 4, 8
stagenet 5, 7
testnet 9, A, B

An unrecognised value falls back to the mainnet prefix set. A destination that does not match is refused with "Invalid Monero address" before anything is signed.

Set XMR_NETWORK="testnet" against a mainnet daemon and every real customer withdrawal address is rejected as invalid. Set it to mainnet against a stagenet daemon and stagenet addresses are rejected 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". Run the test after any daemon change.

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 prefix returned by the wallet RPC, falling back to XMR_NETWORK only if the prefix is unrecognised. 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.

  • Ordinary RPC calls: 30 second timeout, up to 3 attempts, one second apart.
  • 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 the wallet was refreshed recently, or 15 minutes if it has not been touched in over six hours.
  • Balance reads refresh with a 120 second budget before returning a number.
  • The withdrawal relay is deliberately 1 attempt, 120 seconds. 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.