Skylight Monero Wallet (MAGIC Grants), monero-lws, TX notifier
Been running Skylight wallet (by MAGIC Grants) pointed at my own monero-lws instance for a few days now.
Other wallets promise “auto sync” but anyone who’s actually used them knows it rarely works. You open the app, stare at a progress bar depending on how long it’s been. Sometimes it just hangs. Sometimes it silently fails.
Skylight + monero-lws eliminates this entirely. Your server scans the chain 24/7 in the background. When you open the wallet, your balance and full tx history are just there. Instantly. No sync. No waiting. No “synchronizing blocks 2847293/3630000.”
The server also pushes webhook notifications on every incoming and outgoing tx the moment it hits a block. I have mine wired to Matrix through n8n. Real-time structured alerts with amount, block height, tx hash, and a link to the block explorer.
The whole stack: monerod (own node) + monero-lws (light wallet server by Lee Clagett ) + Skylight (Android wallet by MAGIC Grants). All open source. All self-hosted. Your view key stays on your infrastructure.
It’s alpha software so expect rough edges (exchange rate display can be broken, enabling tor fixes it). But the core functionality of instant sync and webhook notifications already work faster than anything out there!
If you run your own node already, adding monero-lws takes one docker container. Skylight connects to it over Tor/clearnet/VPN(Tailscale?). The documentation is sparse.
monero-lws: Admin API, Webhooks, and a XMR Transaction Notifier
monero-lws by Lee Clagett powers the new Skylight wallet by MAGIC Grants, supports webhook notifications including zero-conf, and scans the chain continuously so your wallet opens instantly with no sync delay.
The documentation is currently lacking. The README tells you to run --help and figure it out. The admin REST API, webhook registration format, and payload structures are buried in C++ source code with no examples, no curl commands, no JSON schemas.
This article documents everything we reverse-engineered from the source code and live testing. Tested against monero-lws v1.0-alpha (commit fe3f4099), running in Docker.
Docker Compose Setup
monero-lws:
build: ./monero-lws
container_name: monero-lws
restart: unless-stopped
depends_on:
- monerod
ports:
- 8443:8443 # Main REST (wallet connections)
- 8444:8444 # Admin REST (account/webhook management)
volumes:
- /path/to/lws-data:/home/monero-lws/.bitmonero/light_wallet_server
command:
- monero-lws-daemon
- --daemon=tcp://monerod:18082
- --sub=tcp://monerod:18084
- --rest-server=http://0.0.0.0:8443
- --admin-rest-server=http://0.0.0.0:8444
- --confirm-external-bind
- --max-subaddresses=100000
- --disable-admin-auth
- --auto-accept-creation
- --exchange-rate-interval=15
Key flags
--admin-rest-servermust be set explicitly with its own port. Without it, the admin API doesn’t exist.--disable-admin-authmakes the auth field optional. Ideal for local setups unless you want to deal with admin keys.--auto-accept-creationlets wallets register without manual admin approval.--exchange-rate-interval=15fetches fiat rates from cryptocompare every 15 minutes. Without it, Skylight shows permanent loading bars where USD values should be.--untrusted-daemondoes full PoW verification of every block. If you run your own local monerod, skip this flag. It re-verifies the entire chain from block 0 on every restart and will grind for hours/days before the REST servers start accepting requests.
HTTP not HTTPS on the REST server
Despite using port 8443, the rest-server runs http:// not https:// . TLS requires --rest-ssl-key and --rest-ssl-certificate flags with PEM files. If you’re connecting over localhost, HTTP is fine.
The Admin API
All admin endpoints live on the admin port (8444 in this example). Every single request requires a {"params": {...}} wrapper around the actual payload. This is documented nowhere. Without it you get 500 Internal Server Error with zero explanation in the response body.
Account Management
# List all accounts
curl -s -X POST http://127.0.0.1:8444/list_accounts \
-H 'Content-Type: application/json' \
-d '{}'
# List pending creation requests
curl -s -X POST http://127.0.0.1:8444/list_requests \
-H 'Content-Type: application/json' \
-d '{}'
Note: list_accounts and list_requests accept '{}' without the params wrapper. The webhook endpoints require it.
All Admin Endpoints (from rest_server.cpp)
/list_accounts/list_requests/add_account/accept_requests/reject_requests/modify_account_status/rescan/rollback/validate/webhook_add/webhook_delete/webhook_delete_uuid/webhook_list
Webhooks
Valid Webhook Types
From db/data.cpp , using hyphens not underscores:
tx-confirmation-- fires when an incoming transaction is confirmed in a blocktx-spend-- fires when an outgoing transaction is detectedtx-receive-- does NOT exist. Returns blank response, silently fails. Don’t use it.
Registering a Webhook
# Incoming transactions (1 confirmation)
curl -s -X POST http://127.0.0.1:8444/webhook_add \
-H 'Content-Type: application/json' \
-d '{
"params": {
"url": "https://your-webhook-url/endpoint",
"type": "tx-confirmation",
"address": "YOUR_MONERO_ADDRESS",
"confirmations": 1
}
}'
# Outgoing transactions
curl -s -X POST http://127.0.0.1:8444/webhook_add \
-H 'Content-Type: application/json' \
-d '{
"params": {
"url": "https://your-webhook-url/endpoint",
"type": "tx-spend",
"address": "YOUR_MONERO_ADDRESS"
}
}'
Successful registration returns:
{
"payment_id": "0000000000000000",
"event_id": "0b5a32746141445a814a01213f9b8f81",
"token": "",
"confirmations": 1,
"url": "https://your-webhook-url/endpoint"
}
Listing and Deleting Webhooks
# List all registered webhooks
curl -s -X POST http://127.0.0.1:8444/webhook_list \
-H 'Content-Type: application/json' \
-d '{}' | python3 -m json.tool
# Delete webhooks by address
curl -s -X POST http://127.0.0.1:8444/webhook_delete \
-H 'Content-Type: application/json' \
-d '{"params": {"addresses": ["YOUR_MONERO_ADDRESS"]}}'
Real Webhook Payload Structure
This is what monero-lws actually POSTs to your webhook URL when a transaction is detected. Captured from a live tx-confirmation event:
{
"event": "tx-confirmation",
"payment_id": "0000000000000000",
"token": "",
"confirmations": 1,
"event_id": "0b5a32746141445a814a01213f9b8f81",
"tx_info": {
"id": {
"high": 0,
"low": 151142680
},
"block": 3630390,
"index": 0,
"amount": 1350656030000,
"timestamp": 1773535788,
"tx_hash": "3bf9257c40798de8114392fd7bc469df671cfb22ef6656955c81c46d72bafc47",
"tx_prefix_hash": "2b1e331ef2b68e9c5533b090f5b04831f4c1291f23eb7b476d98e9ec524e3436",
"tx_public": "36c2b90f3a30246235c649313a592dd869eff753ef850ccbb71ed9d71ab1facf",
"rct_mask": "7d71dbfdeb340758c9e695d25da83ea706d9928a99ca40963b9e41dbe9ef1a02",
"payment_id": "0000000000000000",
"unlock_time": 0,
"mixin_count": 15,
"coinbase": false,
"fee": 177280000,
"recipient": {
"maj_i": 0,
"min_i": 0
},
"pub": "f10ae4c18f28c27b6e8f9b91a1b3fcab1136c53eef4e1530b08ce12a10b1a526"
}
}
tx-spend has a completely different structure
The tx-spend payload is NOT the same shape as tx-confirmation . Captured from a live tx-spend event:
{
"event": "tx-spend",
"token": "",
"event_id": "75cb712e84564153a20d7476b45cb421",
"tx_info": {
"input": {
"height": 3630435,
"tx_hash": "ef6ebb5dd337afe038681a152ae0d090b83b710de524ef208ef58cae517e70a3",
"image": "c2754e785a57807d9c935e270245e86712a27d5f813b792627474050ceb2c0d3",
"source": {
"high": 0,
"low": 150165702
},
"timestamp": 1773543880,
"unlock_time": 0,
"mixin_count": 15,
"sender": {
"maj_i": 0,
"min_i": 0
}
},
"source": {
"id": {
"high": 0,
"low": 150165702
},
"amount": 4690361647134,
"mixin": 15,
"index": 1,
"tx_public": "bb4dc5dfbfd297dfa759cd7198b11a2e40c4dde566c2813c350326e6de348eac"
}
}
}
Key differences from tx-confirmation :
Amount is at
tx_info.source.amount, nottx_info.amountTx hash is at
tx_info.input.tx_hash, nottx_info.tx_hashBlock height is at
tx_info.input.height, nottx_info.blockTimestamp is at
tx_info.input.timestamp, nottx_info.timestampNo
feefield in the spend payloadNo
payment_idat the top levelIncludes
image(key image) andsource(the output being spent)
amount is in piconero (1 XMR = 1,000,000,000,000 atomic units). Divide by 1e12 to get XMR.
Multiple fires per transaction
Monero transactions create multiple outputs. If you send XMR, the change output returns to your address, triggering a second tx-confirmation webhook for the same tx_hash . Both outputs have different amount and index values but share the same tx_hash . If someone sends you XMR, you only get one fire since the change goes back to their wallet.
A Real-Time Transaction Notifier with n8n
We wired monero-lws webhooks to Matrix/Element notifications through n8n. The full pipeline: monero-lws detects a transaction during block scanning, POSTs a webhook to n8n over the internal Docker network, n8n parses the payload and sends a formatted notification to a Matrix room.
n8n Code Node (incoming payments only)
When you send XMR, monero-lws fires both a tx-spend (outgoing) and a tx-confirmation (change returning to your wallet). Both events arrive as separate webhooks, often in the same millisecond. Rather than attempting to correlate and suppress change outputs across concurrent webhook executions -- which breaks down when n8n processes them in parallel -- we take a simpler approach: drop all tx-spend events and only notify on incoming payments. You already know when you send money. What you want to be notified about is when money arrives.
// monero-lws webhook handler -- incoming payments only
// Drops all tx-spend events (outgoing sends + change outputs)
// ONLY notifies on the FIRST sighting (mempool / 0-conf) to prevent double-firing
const body = $input.first().json.body;
// 1. Drop outgoing spends to prevent change-output spam
if (body.event === 'tx-spend') return [];
// 2. THE NEW FIX: Drop the 'mined' events.
// This gives you the instant alert and ignores the later webhooks.
if (body.event === 'tx-confirmation' && body.confirmations > 0) {
return [];
}
// Extract transaction details from the confirmation payload
const txInfo = body.tx_info;
// Convert from piconero (1 XMR = 1e12 piconero)
const amount = txInfo.amount / 1e12;
// Format timestamp from UNIX epoch
const ts = new Date(txInfo.timestamp * 1000);
const timeStr = ts.toLocaleTimeString('en-US', {
hour: '2-digit', minute: '2-digit', hour12: true
});
const dateStr = ts.toLocaleDateString('en-GB', {
day: '2-digit', month: '2-digit', year: '2-digit'
});
// Pass structured data to the Matrix notification node
return [{
json: {
tx_type: 'in',
readable_amount: amount.toFixed(4),
event_time: `${timeStr} | ${dateStr}`,
tx_hash: txInfo.tx_hash,
confirmations: 0,
block: "Mempool (Unconfirmed)", // Fills the blank space from your screenshot
fee: (txInfo.fee / 1e12).toFixed(6),
coinbase: txInfo.coinbase
}
}];Matrix/Element Notification Template
{{ $json.tx_type == 'in' ? '+' : '-' }}{{ parseFloat($json.readable_amount).toFixed(4) }} XMR
Block {{ $json.block }} | {{ $json.confirmations }} conf
{{ $json.event_time }}
{{ $json.tx_hash.substring(0,8) }}...{{ $json.tx_hash.slice(-8) }}
xmrchain.net/tx/{{ $json.tx_hash }}
Produces notifications like:
+4.8399 XMR
Block 3630395 | 1 conf
01:00 AM | 15/03/26
cf474c9d...267b4d9c
xmrchain.net/tx/cf474c9d30e81ab5d5a83d25add18e691fdb58ec50bdd904b7c5146a267b4d9c
Ad Hoc Findings and Rough Edges (v1.0-alpha, commit fe3f4099)
We ran into several issues over the course of setting things up. Some of this might be incomplete or specific to our environment. Sharing it in case it saves someone time.
Exchange rate display can be broken if you didn’t enable TOR because currently wallet is fetching rates via TOR from Kraken. Server returns rates correctly in logs but the wallet shows loading bars where fiat values should be. Filed as issue #102 on MAGICGrants/skylight-wallet. Confirmed server-side working.
Red warning triangle. Appears below the balance, related to the exchange rate issue. Filed as #100 on the same repo.
REST Server Quirks
Server doesn’t respond during chain verification. If --untrusted-daemon is enabled, both REST servers (main and admin) refuse connections until PoW verification reaches the chain tip. The TCP connection establishes but immediately resets. Remove the flag if you run your own monerod.
Sparse error messages. 500 errors return empty bodies. The only way to debug is docker logs . Set --log-level=4 for full debug output when troubleshooting. Common errors: “Schema missing required field key” (missing params wrapper), “bad method” (wrong HTTP verb, usually GET instead of POST).
PARSE URI regex warnings. You’ll see [PARSE URI] regex not matched for uri in the logs frequently. This fires for webhook URLs and server bind addresses. As far as we can tell, it’s a non-fatal fallback in the URL parser. URLs still get stored and used correctly.
Tips
monero-lws requires monerod with ZMQ enabled (
--zmq-puband--zmq-rpc-bind-port). Some popular Docker images ship with--no-zmqas a default flag. If monero-lws can’t connect to your monerod, check that ZMQ isn’t disabled.Let the initial scan complete fully before doing anything else. Don’t add accounts, webhooks, or change config mid-scan.
Back up the LMDB data directory after the scan reaches the chain tip. This is your clean restore point for everything that follows.
Consider using bind mounts for easy database recovery, not Docker named volumes, for the LMDB data. Named volumes are buried in
/var/lib/docker/volumes/.The
--untrusted-daemonflag re-verifies the entire chain from block 0 on every restart. Only use it if your monerod is a remote untrusted node. If you run your own local monerod, skip it.Webhooks persist across container restarts. They’re stored in the LMDB database, which is why they can affect scanner stability.
The admin port should NOT be exposed publicly. Keep it on localhost or behind VPN or something.
--log-level=4gives full debug output. Useful when troubleshooting webhook or scanner issues. Default is 1.
Support
MAGIC Grants is the nonprofit behind Skylight wallet and the funding source for Lee Clagett’s monero-lws development. They don’t monetize the wallet. From their website:
“How do you make money from this? We don’t. There is currently no monetization feature. Please consider making a donation (which may qualify for a tax deduction) on our campaign website.”
If you’re as stoked about this wallet as I am, consider donating:
https://donate.magicgrants.org/
* Tested on a Raspberry Pi 5 (ARM64 / aarch64 architecture) running Raspberry Pi OS (based on Debian 12 Bookworm).
* This is a field guide, not official documentation. Some details may be incomplete, wrong, or highly specific to our hardware and OS environment.
* Written in collaboration w/ Claude.
#monero #xmr #monerolws #skylight #magicgrants #n8n



There is some minimal documentation in the `docs` folder, particularly for the admin API. The primary API has a fully OpenAPI schema (available at GitHub.com/monero-project/meta), but the admin API OpenAPI schema has not been posted. The schema is complete - I actually prepped a whole site for documentation but never deployed it was only abut 90% complete.