Every article about light wallets eventually reaches the same conclusion: the privacy trade-off disappears if you run the server yourself. Very few explain how, and none of them mention the throughput characteristics that determine whether your setup will actually be usable.
This is a practical guide to monero-lws, the open-source light wallet server. We run one in production behind our browser wallet, and the numbers and gotchas below come from operating it rather than from reading the README.
Version note: `monero-lws` is alpha software under active development, and flags change between revisions. The instance we run reports 1.0-alpha against Monero v0.18.4.6. Always check monero-lws-daemon --help for your build rather than trusting any guide, including this one.
Why bother
Running your own gives you three things:
Nobody else holds your view key. This is the whole point. A self-hosted server sees your balance because you are the server. The privacy cost described in our light wallet explainer drops to zero.
Instant sync on every device. Once the server has scanned, any browser or phone you point at it gets balances immediately. No 40-minute wait per device.
You can serve other people. Family, a small community, or the public. This is what we do.
The cost is a machine that stays online, several hundred gigabytes of disk, and a willingness to operate infrastructure.
What you actually need
Disk is the binding constraint. A pruned mainnet node is substantially smaller than an archival one, but "smaller" is relative — our pruned node's database is around 140 GB as of mid-2026 and grows continuously. Budget 250 GB minimum and plan to expand. An unpruned node is far larger and unnecessary for this purpose.
RAM: 8 GB is workable, 16 GB comfortable. monerod will use whatever you give it for caching.
CPU matters more than people expect, and matters in a specific way covered under threading below. Four cores is a functional floor; more cores directly buys scanning throughput.
An SSD is not optional. The initial sync on spinning rust is measured in days rather than hours.
Step 1 — monerod with ZMQ enabled
monero-lws does not talk to monerod over the normal JSON-RPC port. It uses the ZMQ-RPC interface, which is off by default in many setups. This is the single most common thing people get wrong.
A working configuration looks like this:
monerod \
--non-interactive \
--prune-blockchain \
--rpc-bind-ip 127.0.0.1 \
--rpc-bind-port 18081 \
--zmq-rpc-bind-ip 127.0.0.1 \
--zmq-rpc-bind-port 18082 \
--restricted-rpc \
--no-igd \
--db-sync-mode fast:async:250000000bytes \
--data-dir /home/monero/.bitmonero
The parts that matter:
--zmq-rpc-bind-port 18082is whatmonero-lwsconnects to. Without it, nothing works.- Bind both RPC interfaces to
127.0.0.1. There is no reason to expose them to the internet; the light wallet server runs on the same host. --restricted-rpclimits what the RPC can be asked to do, which is good practice even on localhost.--prune-blockchainsaves very substantial disk and is fine for this workload.--db-sync-mode fast:async:...meaningfully speeds up initial sync. It trades some crash-durability for throughput — acceptable here, since a corrupted node can simply resync.
Let the node fully sync before continuing. Everything downstream depends on it.
Step 2 — build monero-lws
There are no official binaries, so you compile it. The project lives at github.com/vtnerd/monero-lws and builds against the Monero source tree. It needs a C++ toolchain, CMake, Boost, and the usual Monero build dependencies; the repository's README is authoritative and stays current in a way this paragraph will not.
Two things worth knowing before you start:
- The build pulls in Monero itself, so it is not quick — expect a substantial compile on modest hardware.
- Match your
monero-lwsrevision to a compatible Monero version. Mismatches produce confusing runtime failures rather than clean errors.
You end up with two binaries: monero-lws-daemon (the server) and monero-lws-admin (account management).
Step 3 — run the daemon
A minimal, sane invocation:
monero-lws-daemon \
--db-path /home/monero/.monero-lws \
--daemon tcp://127.0.0.1:18082 \
--rest-server http://127.0.0.1:8443 \
--admin-rest-server http://127.0.0.1:8444 \
--scan-threads 12 \
--rest-threads 4
Breaking that down:
--daemon tcp://127.0.0.1:18082— the ZMQ endpoint from step 1, not the JSON-RPC port.--rest-server— the client-facing API. Bind it to localhost and put a reverse proxy in front; see step 4.--admin-rest-server— account administration. This must never be publicly reachable.--scan-threads— the blockchain scanner pool. See the threading section; this number matters more than any other.
Additional tuning flags exist for scanner behaviour and sync strategy, and they vary by revision — run --help against your build. We also run a couple of local modifications to the scanner's thread-assignment logic that are not upstream, so treat our exact production flag set as ours rather than as a recommendation.
Run it under systemd with Restart=always and a raised LimitNOFILE (16384 is a reasonable value). The scanner holds a lot of file handles.
On --disable-admin-auth: this flag turns off authentication on the admin API. It is defensible only when the admin server is bound to 127.0.0.1 and firewalled, which is how we run it. If you bind the admin interface to 0.0.0.0 with auth disabled, you have handed account control to the internet. Do not copy that flag without understanding the binding it depends on.
Step 4 — put a reverse proxy in front
Do not expose monero-lws directly. Terminate TLS and apply rate limits with nginx (or Caddy) in front of it. At minimum you want:
- TLS, via Let's Encrypt.
- Rate limiting on account creation. This is the endpoint that gets abused. Each new account costs you a scanner assignment and real CPU; without a limit, one script can saturate your server. We apply a strict per-IP limit here and a looser one elsewhere.
- Rate limiting on login and import. Historical rescans are the most expensive operation the server performs.
- CORS headers, if a browser wallet is calling the API.
- The admin port never proxied. Ever.
This layer is not optional polish. A light wallet server with an open, unlimited account-creation endpoint will be found and abused.
The threading reality nobody documents
Here is the operational detail that determines whether your server feels fast, and which we learned the hard way.
The scanner assigns each wallet to a thread, and one wallet's catch-up scan is handled by one thread. A wallet that needs to scan two years of history gets exactly one thread's worth of throughput. Adding more --scan-threads lets you scan more wallets concurrently — it does not make any single wallet's historical scan faster.
This has consequences worth planning around:
- Catch-up is slow and cannot be parallelised per wallet. In the denser regions of the chain, a single wallet's scan proceeds at a few hundred blocks per second. Years of history genuinely takes a while, and no amount of hardware changes that for one wallet.
--scan-threadsshould track your core count and your concurrent-user expectations, not your desire for one wallet to sync faster.- Once caught up, everything is instant. The pain is strictly one-time per wallet. Steady-state operation is cheap, since it only scans new blocks.
If you are serving only yourself and your family, this is a non-issue — you will do a handful of initial scans and then never think about it again. If you are serving the public, it is the main capacity planning input.
Practical consequence: a good restore height matters far more on a light wallet server than most people realise, because it directly determines how much single-threaded scanning work you are asking for. See restore heights explained.
Account management
New wallets arrive as account requests. You either accept them manually with monero-lws-admin, or enable automatic acceptance for creation and import if you are running an open service.
monero-lws-admin is also where you list accounts, inspect scan heights, and deactivate wallets. If you run a public instance, expect to want some automation around inactive accounts — every registered wallet consumes scanner attention indefinitely, and dormant ones are pure overhead.
Treat the LWS database as production state. It contains view keys and per-account scan positions. Back it up, and be extremely careful with bulk administrative operations against a live instance — mass status changes combined with rescans can leave the account index inconsistent with the record store, which is a genuinely unpleasant thing to recover from. Snapshot before you experiment.
Verifying it works
Once running, point a light wallet client at your REST endpoint. Reasonable checks:
- A brand-new wallet with a recent restore height should show a balance within seconds.
- An old wallet should begin scanning and report steadily advancing progress.
- The admin API should be unreachable from anywhere but localhost. Test this from outside rather than assuming.
monerodshould show your LWS instance as a local connection and nothing unexpected as an external one.
Is it worth it?
If you hold meaningful amounts of Monero and want light wallet convenience without giving anyone your view key, yes — unambiguously. The setup is an afternoon, and the ongoing burden is that of any small server: updates, monitoring, backups.
If you just want to check a small balance occasionally, running a full node and a light wallet server is disproportionate. Use someone else's instance with Tor and wallet separation, per the mitigations we outlined.
What we would push back on is the idea that self-hosting is only for experts. If you have ever run a home server, you can run this. The Monero ecosystem is better when more people do — a light wallet ecosystem with many small independent servers is dramatically healthier than one with a few large ones, ours included.
We publish full instructions for self-hosting our entire stack — site, node and light wallet server — precisely so that our instance is a convenience rather than a dependency. If we ever became the single point everyone relied on, that would be a failure of the thing we are trying to build.