What changed, and why it matters
This commit is a documentation-only change. It adds a new docs website for the monero-lws project, including Markdown guides, OpenAPI YAML specs, a GitHub Actions workflow to deploy the site, and a MkDocs configuration. No source code, build scripts, or runtime behavior of the application were modified. There is no security-relevant change in the software itself.
No security action required. Review the new GitHub Actions workflow for standard CI hygiene (permissions, token scoping) as part of normal repository maintenance, but the commit itself does not introduce a vulnerability.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit b3ac47705b266d39eb28224efefc494b25fab343 in vtnerd/monero-lws is titled ‘Pushing initial attempt at a docs site’. It deletes docs/administration.md and adds a docs/ tree with API specs (admin.yaml, wallet.yaml), markdown guides, and a GitHub Actions workflow (deploy-pages.yml) that builds and deploys the docs to GitHub Pages using MkDocs. The workflow runs on pushes to master and pull requests to develop when docs/** changes. It uses standard third-party actions (actions/checkout@v4, actions/setup-python@v6, peaceiris/actions-gh-pages@v4) and installs mkdocs plus plugins. No application code, configuration defaults, cryptographic logic, network handling, or database code is changed.
Changed components
docs/ directory.github/workflows/deploy-pages.ymlmkdocs.ymlInspect captured patch +3512 / −1092
diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml
new file mode 100644
index 0000000..640d0b5
--- /dev/null
+++ b/.github/workflows/deploy-pages.yml
@@ -0,0 +1,29 @@
+name: docs
+
+on:
+ push:
+ branches: [ "master" ]
+ paths: [ "docs/**" ]
+ pull_request:
+ branches: [ "develop" ]
+ paths: [ "docs/**" ]
+
+jobs:
+ deploy:
+ steps:
+ - name: Checkout LWS Source
+ uses: actions/checkout@v4
+ - name: Setup Python
+ uses: actions/setup-python@v6
+ - name: Install dependencies
+ run: |
+ python3 -m pip install --upgrade pip
+ python3 -m pip install mkdocs
+ python3 -m pip install mkdocs-swagger-ui-tag
+ python3 -m pip install mkdocs-schema-reader
+ python3 mkdocs build
+ - name: Deploy
+ uses: peaceiris/actions-gh-pages@v4
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_dir: ./site
diff --git a/docs/administration.md b/docs/administration.md
deleted file mode 100644
index c99b28e..0000000
--- a/docs/administration.md
+++ /dev/null
@@ -1,547 +0,0 @@
-# monero-lws Administration
-The `monero-lws-admin` executable or `--admin-rest-server` option in the
-`monero-lws-daemon` executable can be used to administer the database
-used by `monero-lws-daemon`. Any number of `monero-lws-admin` instances can run
-concurrently with a single `monero-lws-daemon` instance on the same database.
-Administration is necessary to authorize new accounts and rescan requests
-submitted from the REST API. The admin executable can also be used to list
-the contents of the LMDB file for debugging purposes.
-
-# monero-lws-admin
-
-The `monero-lws-admin` utility is structured around command-line arguments with
-JSON responses printed to `stdout`. Each administration command takes arguments
-by position. Every available administration command and required+optional
-arguments are listed when the `--help` flag is given to the executable.
-
-The [`jq`](https://stedolan.github.io/jq/) utility is recommended if using
-`monero-lws-admin` in a shell environment. The `jq` program can be used for
-indenting the output to make it more readable, and can be used to
-search+filter the JSON output from the command.
-
-# Admin REST API
-The `monero-lws-daemon` can be started with 1+ `--admin-rest-server` parameters
-that specify a listening location for admin REST clients. By default, there is
-no admin REST server and no available admin accounts.
-
-An admin REST server can be merged with a regular REST server if path prefixes
-are specified, such as
-`--rest-server https://0.0.0.0:8443/basic --admin-rest-server https://0.0.0.0:8443/admin`.
-This will start a server listening on one port, 8443, and requires clients to
-specify `/basic/command` or `/admin/admin_command` when making a
-request.
-
-An admin account account can be created via `monero-lws-admin create_admin`
-_only_ (this command is not available via REST for security purposes). The
-`key` value returned in the `create_admin` JSON object becomes the `auth`
-parameter in the admin REST API. A new admin account is put into the
-`hidden` state - the account is _not_ scanned for transactions and is _not_
-available to the normal REST API, but is available to the admin REST API.
-
-Running `monero-lws-admin list_admin` will display all current admin
-accounts, and their current state ("active", "inactive", or "hidden"). If
-an admin account needs to be revoked, use the `modify_account` command
-to put the account into the "inactive" state. Deleting accounts is not
-currently supported.
-
-Every admin REST request must be a `POST` that contains a JSON object with
-an `auth` field (in default settings) and an optional `params` field:
-
-```json
-{
- "auth":"...",
- "params":{...}
- }
-```
-where the `params` object is specified below. The `auth` field can be omitted
-if `--disable-admin-auth` is specified in the CLI arguments for the REST
-server.
-
-## Commands (of Admin REST API)
-A subset of admin commands are available via admin REST API - the remainder
-are initially omitted for security purposes. The commands available via REST
-are:
- * [**accept_requests**](#accept_requests): `{"type": "import"|"create", "addresses":[...]}`
- * [**add_account**](#add_account): `{"address": ..., "key": ...}`
- * [**list_accounts**](#list_accounts): `{}`
- * [**list_requests**](#list_requests): `{}`
- * [**modify_account_status**](#modify_account_status): `{"status": "active"|"hidden"|"inactive", "addresses":[...]}`
- * [**reject_requests**](#reject_requests): `{"type": "import"|"create", "addresses":[...]}`
- * [**rescan**](#rescan): `{"height":..., "addresses":[...]}`
- * [**validate**](#validate): `{"spend_public_hex":..., "view_public_hex":..., "view_key_hex":...}`
- * [**webhook_add**](#webhook_add): `{"type":"tx-confirmation", "address":"...", "url":"...", ...}` with optional fields:
- * **token**: A string to be returned when the webhook is triggered
- * **payment_id**: 16 hex characters representing a unique identifier for a transaction
- * [**webhook_delete**](#webhook_delete): `{"addresses":[...]}`
- * [**webhook_delete_uuid**](#webhook_delete_uuid): `{"event_ids": [...]}`
- * [**webhook_list**](#webhook_list): `{}`
-
-where the listed object must be the `params` field above.
-
-### accept_requests
-Accepts new account and rescan from block 0 requests in the incoming
-queue.
-
-### add_account
-Add account for view-key scanning. An example of the JSON:
-```json
-{
- "params": {
- "address": "9uTcr6T9GURRt7UADQc2rhjg5oMYBDyoQ5jgx8nAvVvs757WwDkc2vHLPJhwZfCnfVdnWNvuuKzJe8eMVTKwadYzBrYRG5j",
- "key": "deadbeef"
- },
- "auth": "f50922f5fcd186eaa4bd7070b8072b66fea4fd736f06bd82df702e2314187d09"
-}
-```
-
-### list_accounts
-Request a listing of all active accounts in the database. The request
-should look like:
-```bash
-curl -v -H "Content-Type: application/json" -d '{}' http://127.0.0.1:8081/list_accounts
-```
-when auth is disabled, and when enabled:
-```bash
-curl -v -H "Content-Type: application/json" -d '{"auth": "f50922f5fcd186eaa4bd7070b8072b66fea4fd736f06bd82df702e2314187d09"}' http://127.0.0.1:8081/list_accounts
-```
-The response will look something like:
-```json
-{
- "active": [
- {
- "address": "9wRAu3giCtKhSsVnkZJ7LLE6zqzrmMKpPg39S8aoC7T6F6GobeDpz8TcvVfTQT3ucW82oTYKG8v3ZMAeh8SZVXWwMdvwZew",
- "scan_height": 2220875,
- "access_time": 1681244149
- }
- ]
-}
-```
-
-### list_requests
-This is a listing of all pending new account requests and all requests
-to import from genesis block requests. When auth is disabled usage
-looks like:
-```bash
-curl -v -H "Content-Type: application/json" -d '{}' http://127.0.0.1:8081/list_requests
-```
-and with auth enabled looks like:
-```bash
-curl -v -H "Content-Type: application/json" -d '{"auth": "f50922f5fcd186eaa4bd7070b8072b66fea4fd736f06bd82df702e2314187d09"}' http://127.0.0.1:8081/list_requests
-```
-### modify_account_status
-This can change an account status to `active`, `inactive` or `hidden`. The
-`active` state is the normal state - the account is being scanned and
-returned by the API. The `inactive` state is still returned by the API,
-but is no longer being scanned. The `hidden` is the current way to
-"delete" an account - it is not scanned nor returned by the API. Accounts
-cannot currently be deleted due to internal DB requirements.
-
-### reject_requests
-This is the opposite of [`accept_requests`](#accept_requests) above. See
-information from that endpoint on how to use this one.
-
-### rescan
-This tells the scanner to rescan specific account(s) from the specified
-height.
-
-### validate
-This takes the spend_public, view_public, and view key all as hex, and then
-does basic validation for the caller: (1) that each value is 64 hex-ascii
-characters, (2) that the public keys are valid ed25519 points, and that (3)
-the view_key matches the view_public.
-
-The return value is the `address` on success, and `error` object on
-validation failure:
-
-#### Example Request
-```json
-{
- "params": {
- "view_public_hex": "3e77c1ee5a396cd6c3f68ce343882b2a09317649d6501078911fb17a1adac0b6",
- "spend_public_hex": "7028d918af2cc4f1cfa499cca2ef014e57124982b99004e9630caf0b25c13954",
- "view_key_hex": "80..."
- },
- "auth": "f50922f5fcd186eaa4bd7070b8072b66fea4fd736f06bd82df702e2314187d09"
-}
-```
-
-#### Example Failure Return
-```json
-{
- "error": {
- "field": "view_public_hex",
- "details": "Invalid public key format"
- }
-}
-```
-HTTP error codes are still returned if the JSON itself is invalid.
-
-#### Example Success Return
-```json
-{
- "address": "9wRAu3giCtKhSsVnkZJ7LLE6zqzrmMKpPg39S8aoC7T6F6GobeDpz8TcvVfTQT3ucW82oTYKG8v3ZMAeh8SZVXWwMdvwZew"
-}
-```
-
-
-### webhook_add
-This is used to track events happening in the database: (1) a new payment to
-an optional payment_id, or (2) a new account creation. This endpoint always
-requires a URL for callback purposes.
-
-When the event type is `tx-confirmation`, this endpoint requires a web address
-for callback purposes, a primary (not integrated!) address, and finally the
-type ("tx-confirmation"). The event will remain in the database until one of
-the delete commands ([webhook_delete_uuid](#webhook_delete_uuid) or
-[webhook_delete](#webhook_delete)) is used to remove it.
-
-When the event type is `new-account`, this endpoint requires a web address
-for callback purposes, and the type ("new-account"). Spurious information
-will be returned for this endpoint to simplify the server implementation (i.e.
-several fields returned in the initial call are not useful to new account
-creations).
-
-> The provided URL will use SSL/TLS if `https://` is prefixed in the URL and
-will use plaintext if `http://` is prefixed in the URL. If `zmq` is provided
-as the callback, notifications are performed _only_ over the ZMQ pub socket.
-SSL/TLS connections will use the system certificate authority (root-CAs) by
-default, and will ignore all authority checks if
-`--webhook-ssl-verification none` is provided on the command line when
-starting `monero-lws-daemon`. The webhook will fail if there is a mismatch of
-`http` and `https` between the two servers, and will also fail if `https`
-verification is mismatched. The rule is: (1) if the callback server has
-SSL/TLS disabled, the webhook should use `http://`, (2) if the callback server
-has a self-signed certificate, `https://` and `--webhook-ssl-verification none`
-should be used, and (3) if the callback server is using "Let's Encrypt"
-(or similar), then `https://` with no additional command line flag should be
-used.
-
-
-#### `tx-confirmation`
-##### Initial Request to server
-Example where admin authentication is required (`--disable-admin-auth` NOT
-set on start which is the default):
-```json
-{
- "auth": "f50922f5fcd186eaa4bd7070b8072b66fea4fd736f06bd82df702e2314187d09",
- "params": {
- "type": "tx-confirmation",
- "url": "http://127.0.0.1:7000",
- "payment_id": "df034c176eca3296",
- "token": "1234",
- "address": "9uTcr6T9GURRt7UADQc2rhjg5oMYBDyoQ5jgx8nAvVvs757WwDkc2vHLPJhwZfCnfVdnWNvuuKzJe8eMVTKwadYzBrYRG5j"
- }
-}
-```
-
-Example where admin authentication is not required (`--disable-admin-auth` set on start):
-```json
-{
- "params": {
- "type": "tx-confirmation",
- "url": "http://127.0.0.1:7000",
- "payment_id": "df034c176eca3296",
- "token": "1234",
- "address": "9uTcr6T9GURRt7UADQc2rhjg5oMYBDyoQ5jgx8nAvVvs757WwDkc2vHLPJhwZfCnfVdnWNvuuKzJe8eMVTKwadYzBrYRG5j"
- }
-}
-```
-
-As noted above - `payment_id` and `token` are both optional - `token` will
-default to the empty string, and `payment_id` will default to zero.
-##### Initial Response from Server
-The server will replay all values back to the user for confirmation. An
-additional field - `event_id` - is also returned which contains a globally
-unique value (internally this is a 128-bit `UUID`).
-
-Example response:
-```json
-{
- "payment_id": "df034c176eca3296",
- "event_id": "fa10a4db485145f1a24dc09c19a79d43",
- "token": "1234",
- "confirmations": 1,
- "url": "http://127.0.0.1:7000"
-}
-```
-
-If you use the `debug_database` command provided by the `monero-lws-admin`
-executable, the event should be listed in the
-`webhooks_by_account_id,payment_id` field of the returned JSON object. The
-event will remain in the database until an explicit
-[`webhook_delete_uuid`](#webhook_delete_uuid) is invoked.
-
-##### Callback from Server
-When the event "fires" due to a transaction, the provided URL is invoked
-with a JSON payload that looks like the below:
-
-```json
-{
- "event": "tx-confirmation",
- "payment_id": "df034c176eca3296",
- "token": "1234",
- "confirmations": 1,
- "id": "fa10a4db485145f1a24dc09c19a79d43",
- "tx_info": {
- "id": {
- "high": 0,
- "low": 5550229
- },
- "block": 2192100,
- "index": 0,
- "amount": 4949570000,
- "timestamp": 1678324181,
- "tx_hash": "901f9a2a919b6312131537ff6117d56ce2c0dc1f1341b845d7667299e1ef892f",
- "tx_prefix_hash": "89685cb7acb836fde30fae8be5d8b884e92706df086960d0508e146979ef80dc",
- "tx_public": "54c153792e47c1da8ceb3979560c424c1928b7b4a089c1c8b3ce99c563e1d240",
- "rct_mask": "f3449407dc3721299b5309c0c336a17daeebce55165ddd447ba28bbd1f46c201",
- "payment_id": "df034c176eca3296",
- "unlock_time": 0,
- "mixin_count": 15,
- "coinbase": false
- }
-}
-```
-which is the same information provided by the user API. The database will
-contain an entry in the `webhook_events_by_account_id,type,block_id,tx_hash,output_id,payment_id,event_id`
-field of the JSON object provided by the `debug_database` command. The
-entry will be removed when the number of confirmations has been reached.
-
-#### `new-account`
-##### Initial Request to server
-Example where admin authentication is required (`--disable-admin-auth` NOT
-set on start which is the default):
-```json
-{
- "auth": "f50922f5fcd186eaa4bd7070b8072b66fea4fd736f06bd82df702e2314187d09",
- "params": {
- "type": "new-account",
- "url": "http://127.0.0.1:7001",
- "token": "1234"
- }
-}
-```
-
-Example where admin authentication is not required (`--disable-admin-auth` set on start):
-```json
-{
- "params": {
- "type": "new-account",
- "url": "http://127.0.0.1:7001",
- "token": "1234"
- }
-}
-```
-
-As noted above - `token` is optional - it will default to the empty string.
-
-##### Initial Response from Server
-The server will replay all values back to the user for confirmation. An
-additional field - `event_id` - is also returned which contains a globally
-unique value (internally this is a 128-bit `UUID`). The fields
-`confirmations`, and `payment_id` are sent to simplify the backend, and
-can be ignored when the type is `new-account`.
-
-Example response:
-```json
-{
- "payment_id": "0000000000000000",
- "event_id": "c5a735e71b1e4f0a8bfaeff661d0b38a"",
- "token": "1234",
- "confirmations": 1,
- "url": "http://127.0.0.1:7000"
-}
-```
-
-If you use the `debug_database` command provided by the `monero-lws-admin`
-executable, the event should be listed in the
-`webhooks_by_account_id,payment_id` field of the returned JSON object. The
-event will remain in the database until an explicit
-[`webhook_delete_uuid`](#webhook_delete_uuid) is invoked.
-
-##### Callback from Server
-When the event "fires" due to a new account creation, the provided URL is
-invoked with a JSON payload that looks like the below:
-
-```json
-{
- "event_id": "c5a735e71b1e4f0a8bfaeff661d0b38a",
- "token": "",
- "address": "9zGwnfWRMTF9nFVW9DNKp46aJ43CRtQBWNFvPqFVSN3RUKHuc37u2RDi2GXGp1wRdSRo5juS828FqgyxkumDaE4s9qyyi9B"
-}
-```
-
-
-### webhook_delete
-Deletes all webhooks associated with a specific Monero primary address.
-
-### webhook_delete_uuid
-Deletes all references to a specific webhook referenced by its UUID
-(`event_id`)
-
-### webhook_list
-This will list every webhook that is currently "listening" for
-incoming transactions. If the server has auth disabled, the
-request is simply:
-
-```bash
-curl -v -H "Content-Type: application/json" -d '{}' http://127.0.0.1:8081/webhook_list
-```
-and with auth enabled looks like:
-```bash
-curl -v -H "Content-Type: application/json" -d '{"auth": "f50922f5fcd186eaa4bd7070b8072b66fea4fd736f06bd82df702e2314187d09"}' http://127.0.0.1:8081/webhook_list
-```
-
-which returns a JSON object that looks like:
-```json
-{
- "webhooks": [
- {
- "key": {
- "user": 1,
- "type": "tx-confirmation"
- },
- "value": [
- {
- "payment_id": "9bc1a59b34253896",
- "event_id": "4dc201838af54dfe88686bea7e2b599f",
- "token": "12345",
- "confirmations": 5,
- "url": "http://127.0.0.1:8082"
- },
- {
- "payment_id": "9bc1a59b34253896",
- "event_id": "615171e477464401a1a23cdb45b3b433",
- "token": "12345",
- "confirmations": 5,
- "url": "http://127.0.0.1:8082"
- },
- {
- "payment_id": "9bc1a59b34253896",
- "event_id": "e64be3ad6d1647618fbd292be0485901",
- "token": "this is a fresh test",
- "confirmations": 1,
- "url": "http://127.0.0.1:8082/foobar"
- },
- {
- "payment_id": "9bc1a59b34253896",
- "event_id": "fe692cdf7de1453898ad453d8fabce42",
- "token": "12345",
- "confirmations": 5,
- "url": "http://127.0.0.1:8082/foobar"
- }
- ]
- }
- ]
-}
-```
-
-# Examples
-
-## Admin REST API
-
-### Default Settings
-```json
-{
- "auth":"6d732245002a9499b3842c0a7f9fc6b2d657c77bd612dbefa4f7f9357d08530a",
- "params":{
- "status": "inactive",
- "addresses": ["9sAejnQ9EBR1111111111111111111111111111111111AdYmVTw2Tv6L9KYkHjJ2wd737ov8ZL5QU7CJ4zV6basGP9fyno"]
- }
- }
-```
-will put the listed address into the "inactive" state.
-
-### `--disable-admin-auth` Setting
-```json
-{
- "params":{
- "status": "inactive",
- "addresses": ["9sAejnQ9EBR1111111111111111111111111111111111AdYmVTw2Tv6L9KYkHjJ2wd737ov8ZL5QU7CJ4zV6basGP9fyno"]
- }
- }
-```
-
-## monero-lws-admin
-
-**List every active Monero address on a newline:**
- ```bash
- monero-lws-admin list_accounts | jq -r '.active | .[] | .address'
- ```
-
-**Auto-accept every pending account creation request:**
- ```bash
- monero-lws-admin accept_requests create $(monero-lws-admin list_requests | jq -j '.create? | .[]? | .address?+" "')
- ```
-# Debugging
-
-`monero-lws-admin` has a debug mode that dumps everything stored in the
-database, except the blockchain hashes are always truncated and viewkeys are
-omitted by default (a command-line flag can enable viewkey output). Most of
-the array outputs are sorted to accelerate `jq` filtering and search queries.
-
-## Indexes
-
- - **blocks_by_id** - array of objects sorted by block height.
- - **accounts_by_status,id** - A single object where account status names are
- keys. Each value is an array of objects sorted by account id.
- - **accounts_by_address** - A single object where account addresses are keys.
- Each value is an object containing the status and account id for the account
- for lookup in `accounts_by_status,id`. The majority of account lookups should
- be done by this id (an integer).
- - **accounts_by_height,id** - An array of objects sorted by block height. These
- objects contain another array of objects sorted by account id.
- - **outputs_by_account_id,block_id,tx_hash,output_id** - An object where keys
- are account ids. Each value is an array of objects sorted by block height,
- transaction hash, then by output number.
- - **spends_by_account_id,block_id,tx_hash,image** - An object where keys are
- account ids. Each value is an array of objects sorted by block height,
- transaction hash, then by key image.
- - **requests_by_type,address** - An object where keys are request type, and
- each value is an array of objects sorted by address.
-
-## Examples
-
-**List every key-image associated with every account:**
- ```bash
- monenero-lws-admin debug_database | jq '."spends_by_account_id,block_id,tx_hash,output_id" | map_values([.[] | .image])'
- ```
-will output something like:
- ```json
- {"1":["image1", "image2",...],"2":["image1","image2"...],...}
- ```
-
-**List every account that received XMR in a given transaction hash:**
- ```bash
- monenero-lws-admin debug_database | jq '."outputs_by_account_id,block_id,tx_hash,output_id" | map_values(select([.[] | .tx_hash == "hash"] | any)) | keys'
- ```
-will output somethng like:
- ```json
- {"1",...}
- ```
-
-**Add total received XMR for every account**:
- ```bash
- monenero-lws-admin debug_database | jq '."outputs_by_account_id,block_id,tx_hash,output_id" | map_values([.[] | .amount] | add)'
- ```
-will output something like:
- ```json
- {"1":6346,"2":45646}
- ```
-
-# Extending Administration in monero-lws
-
-## JSON via `stdin`
-
-Some commands take sensitive information such as private view keys, and
-therefore reading arguments from `stdin` via JSON array would also be useful for
-those situations. This should be a relatively straightforward adaptation given
-the design of the positional arguments.
-
-## Administration via ZeroMQ
-
-The LMDB database does account lookups by view-public only, so that CurveZMQ
-(which uses curve25519) can be used to authenticate an administration account
-without additional protocol overhead. The parameters to administration commands
-can be sent via JSON or MsgPack array since the functions already use positional
-arguments.
diff --git a/docs/api/admin.md b/docs/api/admin.md
new file mode 100644
index 0000000..00f8dee
--- /dev/null
+++ b/docs/api/admin.md
@@ -0,0 +1 @@
+<swagger-ui src="admin.yaml" />
diff --git a/docs/api/admin.yaml b/docs/api/admin.yaml
new file mode 100644
index 0000000..1f3ad32
--- /dev/null
+++ b/docs/api/admin.yaml
@@ -0,0 +1,941 @@
+openapi: 3.0.4
+info:
+ title: monero-lws Admin API
+ description: |-
+ This document describes the admin API for the [monero-lws](https://github.com/vtnerd/monero-lws) project.
+
+ An admin account can be created running the executable `monero-lws-admin create_admin` only (this command is not available via REST for security purposes). The key value returned in the create_admin JSON object becomes the `auth` parameter in the admin REST API. A new admin account is put into the hidden state - the account is not scanned for transactions and is not available to the normal REST API, but is available to the admin REST API.
+ version: 1.0.0
+tags:
+ - name: account
+ description: Account administration
+ - name: webhook
+ description: Webhook administration
+paths:
+ /accept_requests:
+ post:
+ tags:
+ - account
+ summary: Accept import+create account requests
+ description: |
+ This endpoint can perform two account actions:
+ * Accept new wallets requests created with the `/login` endpoint of the LWS client API.
+ * Accept "import" (from earlier block height) wallet requests created with the `/import_wallet_request` endpoint of the LWS client API.
+ operationId: accept_requests
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ params:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - import
+ - create
+ addresses:
+ type: array
+ items:
+ $ref: '#/components/schemas/base58-address'
+ required:
+ - type
+ - addresses
+ allOf:
+ - $ref: '#/components/schemas/auth'
+ required:
+ - params
+ required: true
+ responses:
+ '200':
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/updated'
+ '400':
+ description: Invalid JSON.
+ '403':
+ description: Invalid `auth` parameter
+ '405':
+ description: Not `POST` method.
+ '500':
+ description: Invalid JSON schema, and all other general errors.
+ '503':
+ description: Unable to read or write from database.
+ /add_account:
+ post:
+ tags:
+ - account
+ summary: Add a new (client) account
+ description: Add a new account to be used by the client API.
+ operationId: add_account
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ params:
+ type: object
+ properties:
+ address:
+ $ref: '#/components/schemas/base58-address'
+ key:
+ description: The view-key corresponding to `address`
+ allOf:
+ - $ref: '#/components/schemas/binary32'
+ required:
+ - address
+ - key
+ allOf:
+ - $ref: '#/components/schemas/auth'
+ required:
+ - params
+ required: true
+ responses:
+ '200':
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/updated'
+ '400':
+ description: Invalid JSON.
+ '403':
+ description: Invalid `auth` parameter
+ '405':
+ description: Not `POST` method.
+ '500':
+ description: Invalid JSON schema, invalid key/address pair, and all other general errors.
+ '503':
+ description: Unable to read or write from database.
+ /list_accounts:
+ post:
+ tags:
+ - account
+ summary: List all accounts
+ operationId: list_accounts
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/auth'
+ required: true
+ responses:
+ '200':
+ description: Success
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ active:
+ description: Accounts that are being scanned and viewable in the client API
+ allOf:
+ - $ref: '#/components/schemas/account'
+ inactive:
+ description: Accounts that are _not_ being scanned, but are viewable in the client API
+ allOf:
+ - $ref: '#/components/schemas/account'
+ hidden:
+ description: Accounts that are neither being scanned nor viewable in the client API. This includes `auth` accounts that access the admin API.
+ allOf:
+ - $ref: '#/components/schemas/account'
+ '400':
+ description: Invalid JSON.
+ '403':
+ description: Invalid `auth` parameter
+ '405':
+ description: Not `POST` method.
+ '500':
+ description: Invalid JSON schema, and all other general errors.
+ '503':
+ description: Unable to read from database.
+ /list_requests:
+ post:
+ tags:
+ - account
+ summary: List all pending create+import requests
+ operationId: list_requests
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/auth'
+ required: true
+ responses:
+ '200':
+ description: Success
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ create:
+ $ref: '#/components/schemas/account-request'
+ import:
+ $ref: '#/components/schemas/account-request'
+ '400':
+ description: Invalid JSON.
+ '403':
+ description: Invalid `auth` parameter
+ '405':
+ description: Not `POST` method.
+ '500':
+ description: Invalid JSON schema, and all other general errors.
+ '503':
+ description: Unable to read from database.
+ /modify_account_status:
+ post:
+ tags:
+ - account
+ summary: Move account(s) to another state.
+ description: |
+ An account can have 3 states:
+ * `active`: Accounts that are being scanned and viewable in the client API
+ * `inactive`: Accounts that are _not_ being scanned, but are viewable in the client API
+ * `hidden`: Accounts that are neither being scanned nor viewable in the client API.
+ operationId: modify_account_status
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ params:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - active
+ - inactive
+ - hidden
+ addresses:
+ type: array
+ items:
+ $ref: '#/components/schemas/base58-address'
+ required:
+ - status
+ - addresses
+ allOf:
+ - $ref: '#/components/schemas/auth'
+ required:
+ - params
+ required: true
+ responses:
+ '200':
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/updated'
+ '400':
+ description: Invalid JSON.
+ '403':
+ description: Invalid `auth` parameter
+ '405':
+ description: Not `POST` method.
+ '500':
+ description: Invalid JSON schema, and all other general errors.
+ '503':
+ description: Unable to read or write from database.
+ /reject_requests:
+ post:
+ tags:
+ - account
+ summary: Reject import+create account requests
+ description: |
+ This endpoint can perform two account actions:
+ * Reject new wallets requests created with the `/login` endpoint of the LWS client API.
+ * Reject "import" (from earlier block height) wallet requests created with the `/import_wallet_request` endpoint of the LWS client API.
+ operationId: reject_requests
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ params:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - import
+ - create
+ addresses:
+ type: array
+ items:
+ $ref: '#/components/schemas/base58-address'
+ required:
+ - type
+ - addresses
+ allOf:
+ - $ref: '#/components/schemas/auth'
+ required:
+ - params
+ required: true
+ responses:
+ '200':
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/updated'
+ '400':
+ description: Invalid JSON.
+ '403':
+ description: Invalid `auth` parameter
+ '405':
+ description: Not `POST` method.
+ '500':
+ description: Invalid JSON schema, and all other general errors.
+ '503':
+ description: Unable to read or write from database.
+ /rescan:
+ post:
+ tags:
+ - account
+ summary: Force account(s) rescan from specific block height
+ operationId: rescan
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ params:
+ type: object
+ properties:
+ height:
+ $ref: '#/components/schemas/uint64'
+ addresses:
+ type: array
+ items:
+ $ref: '#/components/schemas/base58-address'
+ required:
+ - height
+ - addresses
+ allOf:
+ - $ref: '#/components/schemas/auth'
+ required:
+ - params
+ required: true
+ responses:
+ '200':
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/updated'
+ '400':
+ description: Invalid JSON.
+ '403':
+ description: Invalid `auth` parameter
+ '405':
+ description: Not `POST` method.
+ '500':
+ description: Invalid JSON schema, and all other general errors.
+ '503':
+ description: Unable to read or write from database.
+ /validate:
+ post:
+ summary: Validate components of a Monero wallet
+ operationId: validate
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ params:
+ type: object
+ properties:
+ spend_public_hex:
+ $ref: '#/components/schemas/binary32'
+ view_public_hex:
+ $ref: '#/components/schemas/binary32'
+ view_key_hex:
+ $ref: '#/components/schemas/binary32'
+ required:
+ - spend_public_hex
+ - view_public_hex
+ - view_key_hex
+ allOf:
+ - $ref: '#/components/schemas/auth'
+ required:
+ - params
+ required: true
+ responses:
+ '200':
+ description: Success
+ content:
+ application/json:
+ schema:
+ oneOf:
+ - type: object
+ properties:
+ address:
+ $ref: '#/components/schemas/base58-address'
+ required:
+ - address
+ - type: object
+ properties:
+ error:
+ type: object
+ properties:
+ field:
+ type: string
+ enum:
+ - spend_public_hex
+ - view_public_hex
+ - view_key_hex
+ details:
+ type: string
+ required:
+ - field
+ - details
+ required:
+ - error
+ '400':
+ description: Invalid JSON.
+ '403':
+ description: Invalid `auth` parameter
+ '405':
+ description: Not `POST` method.
+ '500':
+ description: Invalid JSON schema, and all other general errors.
+ /webhook_add:
+ post:
+ tags:
+ - webhook
+ summary: List all pending create+import requests
+ operationId: webhook_add
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ params:
+ oneOf:
+ - type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - tx-confirmation
+ - tx-spend
+ url:
+ $ref: '#/components/schemas/webhook-url'
+ address:
+ $ref: '#/components/schemas/base58-address'
+ payment_id:
+ $ref: '#/components/schemas/payment-id'
+ token:
+ type: string
+ minLength: 1
+ confirmations:
+ $ref: '#/components/schemas/uint32'
+ required:
+ - type
+ - url
+ - address
+ - type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - new-account
+ url:
+ $ref: '#/components/schemas/webhook-url'
+ token:
+ type: string
+ minLength: 1
+ required:
+ - type
+ - url
+ allOf:
+ - $ref: '#/components/schemas/auth'
+ required:
+ - params
+ required: true
+ responses:
+ '200':
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/webhook-value'
+ '400':
+ description: Invalid JSON.
+ '403':
+ description: Invalid `auth` parameter
+ '405':
+ description: Not `POST` method.
+ '500':
+ description: Invalid JSON schema, invalid URL, and all other general errors.
+ '503':
+ description: Unable to read or write from database.
+ callbacks:
+ onEvent:
+ '{$requestBody.params.url}':
+ post:
+ requestBody:
+ content:
+ application/json:
+ schema:
+ oneOf:
+ - type: object
+ description: Sent on tx-confirmation events
+ properties:
+ payment_id:
+ $ref: '#/components/schemas/payment-id'
+ token:
+ description: "The same token passed in the initial request"
+ type: string
+ confirmations:
+ $ref: '#/components/schemas/uint32'
+ event_id:
+ $ref: '#/components/schemas/uuid'
+ tx_info:
+ $ref: '#/components/schemas/tx-output'
+ required:
+ - payment_id
+ - token
+ - confirmations
+ - event_id
+ - tx_info
+ - type: object
+ description: Sent on tx-spend events
+ properties:
+ token:
+ description: "The same token passed in the initial request"
+ type: string
+ event_id:
+ $ref: '#/components/schemas/uuid'
+ tx_info:
+ $ref: '#/components/schemas/tx-spend'
+ required:
+ - token
+ - event_id
+ - tx_info
+ - type: object
+ description: Sent on new-account events
+ properties:
+ token:
+ description: "The same token passed in the initial request"
+ type: string
+ event_id:
+ $ref: '#/components/schemas/uuid'
+ address:
+ $ref: '#/components/schemas/base58-address'
+ required:
+ - token
+ - event_id
+ - address
+
+ responses:
+ 201:
+ description: On success
+ /webhook_delete:
+ post:
+ tags:
+ - webhook
+ summary: Delete webhooks associated with Monero address(es)
+ operationId: webhook_delete
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ params:
+ type: object
+ properties:
+ addresses:
+ type: array
+ items:
+ $ref: '#/components/schemas/base58-address'
+ required:
+ - addresses
+ allOf:
+ - $ref: '#/components/schemas/auth'
+ required:
+ - params
+ required: true
+ responses:
+ '200':
+ description: Success
+ content:
+ application/json:
+ schema:
+ type: object
+ '400':
+ description: Invalid JSON.
+ '403':
+ description: Invalid `auth` parameter
+ '405':
+ description: Not `POST` method.
+ '500':
+ description: Invalid JSON schema, and all other general errors.
+ '503':
+ description: Unable to read or write from database.
+ /webhook_delete_uuid:
+ post:
+ tags:
+ - webhook
+ summary: Delete webhooks associated with UUID(s)
+ operationId: webhook_delete_uuid
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ params:
+ type: object
+ properties:
+ event_ids:
+ type: array
+ items:
+ $ref: '#/components/schemas/uuid'
+ required:
+ - event_ids
+ allOf:
+ - $ref: '#/components/schemas/auth'
+ required:
+ - params
+ required: true
+ responses:
+ '200':
+ description: Success
+ content:
+ application/json:
+ schema:
+ type: object
+ '400':
+ description: Invalid JSON.
+ '403':
+ description: Invalid `auth` parameter
+ '405':
+ description: Not `POST` method.
+ '500':
+ description: Invalid JSON schema, and all other general errors.
+ '503':
+ description: Unable to read or write from database.
+ /webhook_list:
+ post:
+ tags:
+ - webhook
+ summary: List all registered webhooks
+ operationId: webhook_list
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ allOf:
+ - $ref: '#/components/schemas/auth'
+ required:
+ - params
+ required: true
+ responses:
+ '200':
+ description: Success
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ webhooks:
+ type: object
+ properties:
+ key:
+ type: object
+ properties:
+ user:
+ $ref: '#/components/schemas/uint32'
+ type:
+ type: string
+ enum:
+ - tx-confirmation
+ - tx-send
+ - new-account
+ value:
+ $ref: '#/components/schemas/webhook-value'
+ required:
+ - key
+ - value
+ required:
+ - webhooks
+ '400':
+ description: Invalid JSON.
+ '403':
+ description: Invalid `auth` parameter
+ '405':
+ description: Not `POST` method.
+ '500':
+ description: Invalid JSON schema, and all other general errors.
+ '503':
+ description: Unable to read from database.
+components:
+ schemas:
+ uint16:
+ type: integer
+ minimum: 0
+ maximum: 65535
+ uint32:
+ type: integer
+ minimum: 0
+ maximum: 4294967295
+ int64:
+ type: integer
+ minimum: -9223372036854775808
+ maximum: 9223372036854775807
+ uint64:
+ type: integer
+ minimum: 0
+ maximum: 18446744073709551615
+ timestamp:
+ description: Seconds since Unix epoch (1970-01-01 00:00:00)
+ allOf:
+ - $ref: '#/components/schemas/int64'
+ base58-address:
+ type: string
+ description: "Standard Monero address in base58"
+ minLength: 95
+ maxLength: 95
+ pattern: "^[0-9A-Za-z]{95}$"
+ example: "47nPhxp2cJeKN2NjamupNUNA13XgzcYPzQBCzzsKcj717s8M2UpFVmmdwSuYwgyy8kPDwU7hpEqTTDvfe5LAb9Aj6nwmEzf"
+ binary:
+ type: string
+ description: "binary data as hex (arbitrary length)"
+ pattern: "^([0-9A-Fa-f]{2})+$"
+ example: "0abcd9dcf0bca63310bb4d7e120918d1"
+ binary8:
+ type: string
+ description: "8-bytes binary data as hex"
+ minLength: 16
+ maxLength: 16
+ pattern: "^[0-9A-Fa-f]{16}$"
+ example: "6d26cac5c9d74f4e"
+ binary16:
+ type: string
+ description: "16-bytes binary data as hex"
+ minLength: 32
+ maxLength: 32
+ pattern: "^[0-9A-Fa-f]{32}$"
+ example: "67adfa30c9504e12955b9ae3ad3cd53b"
+ binary32:
+ type: string
+ description: "32-bytes binary data as hex"
+ minLength: 64
+ maxLength: 64
+ pattern: "^[0-9A-Fa-f]{64}$"
+ example: "ee171270296bbc26d5be4455b6313e1a2086a92080e205f77d6f861f8e5fd205"
+ payment-id:
+ $ref: '#/components/schemas/binary8'
+ uuid:
+ description: "A randomly generated, _unique_, UUID"
+ allOf:
+ - $ref: '#/components/schemas/binary16'
+ webhook-url:
+ description: A HTTP address OR `zmq` to indicate the notification happens over ZMQ PUB/SUB only.
+ type: string
+ pattern: "^(zmq|http://.+|https://.+)$"
+ auth:
+ type: object
+ properties:
+ auth:
+ description: |
+ A key generated via `./monero-lws-admin create_admin`. Not needed if server is started with `--disable-admin-auth`.
+ allOf:
+ - $ref: '#/components/schemas/binary32'
+ address-meta:
+ description: Subaddress index
+ type: object
+ properties:
+ maj_i:
+ $ref: '#/components/schemas/uint32'
+ min_i:
+ $ref: '#/components/schemas/uint32'
+ required:
+ - maj_i
+ - min_i
+ updated:
+ description: List of updated addresses
+ type: object
+ properties:
+ updated:
+ type: array
+ items:
+ $ref: '#/components/schemas/base58-address'
+ required:
+ - updated
+ account:
+ type: object
+ properties:
+ address:
+ $ref: '#/components/schemas/base58-address'
+ scan_height:
+ $ref: '#/components/schemas/uint64'
+ access_time:
+ $ref: '#/components/schemas/timestamp'
+ required:
+ - address
+ - scan_height
+ - access_time
+ account-request:
+ type: object
+ properties:
+ address:
+ $ref: '#/components/schemas/base58-address'
+ start_height:
+ $ref: '#/components/schemas/uint64'
+ required:
+ - address
+ - start_height
+ tx-output-id:
+ description: Blockchain unique identifier for the output. Changes on reorg.
+ type: object
+ properties:
+ high:
+ $ref: '#/components/schemas/uint32'
+ low:
+ $ref: '#/components/schemas/uint32'
+ required:
+ - high
+ - low
+ tx-spend-meta:
+ type: object
+ properties:
+ id:
+ $ref: '#/components/schemas/tx-output-id'
+ amount:
+ $ref: '#/components/schemas/uint64'
+ mixin:
+ $ref: '#/components/schemas/uint32'
+ index:
+ description: Index within vout, required for calculating spend key
+ allOf:
+ - $ref: '#/components/schemas/uint32'
+ tx_public:
+ description: The ephermal key used for the ECDH portion of transaction scanning
+ allOf:
+ - $ref: '#/components/schemas/binary32'
+ required:
+ - id
+ - amount
+ - mixin
+ - index
+ - tx_public
+ tx-output:
+ type: object
+ allOf:
+ - $ref: '#/components/schemas/tx-spend-meta'
+ properties:
+ block:
+ $ref: '#/components/schemas/uint64'
+ timestamp:
+ $ref: '#/components/schemas/timestamp'
+ tx_hash:
+ $ref: '#/components/schemas/binary32'
+ tx_prefix_hash:
+ $ref: '#/components/schemas/binary32'
+ rct_mask:
+ description: The encrypted ringct mask associated with output
+ allOf:
+ - $ref: '#/components/schemas/binary32'
+ payment_id:
+ $ref: '#/components/schemas/payment-id'
+ unlock_time:
+ $ref: '#/components/schemas/uint64'
+ mixin_count:
+ $ref: '#/components/schemas/uint32'
+ coinbase:
+ type: boolean
+ pub:
+ description: The unique public key for the output (which must be used in signature for spending)
+ allOf:
+ - $ref: '#/components/schemas/binary32'
+ recipient:
+ $ref: '#/components/schemas/address-meta'
+ required:
+ - block
+ - timestamp
+ - tx_hash
+ - tx_prefix_hash
+ - unlock_time
+ - mixin_count
+ - coinbase
+ - pub
+ - recipient
+ tx-spend:
+ type: object
+ properties:
+ input:
+ type: object
+ properties:
+ height:
+ $ref: '#/components/schemas/uint64'
+ tx_hash:
+ $ref: '#/components/schemas/binary32'
+ image:
+ $ref: '#/components/schemas/binary32'
+ source:
+ $ref: '#/components/schemas/tx-output-id'
+ timestamp:
+ $ref: '#/components/schemas/timestamp'
+ unlock_time:
+ $ref: '#/components/schemas/uint64'
+ mixin_count:
+ $ref: '#/components/schemas/uint32'
+ payment_id:
+ $ref: '#/components/schemas/payment-id'
+ sender:
+ $ref: '#/components/schemas/address-meta'
+ required:
+ - height
+ - tx_hash
+ - image
+ - source
+ - timestamp
+ - unlock_time
+ - mixin_count
+ - sender
+ output:
+ $ref: '#/components/schemas/tx-spend-meta'
+ required:
+ - input
+ - source
+ webhook-value:
+ type: object
+ properties:
+ event_id:
+ $ref: '#/components/schemas/uuid'
+ payment_id:
+ description: The `payment_id` sent in the request or all zeroes.
+ allOf:
+ - $ref: '#/components/schemas/payment-id'
+ token:
+ description: The token sent in the request or empty string.
+ type: string
+ confirmations:
+ description: The confirmations value sent in the request or `1`.
+ allOf:
+ - $ref: '#/components/schemas/uint32'
+ url:
+ description: The URL sent in the request.
+ allOf:
+ - $ref: '#/components/schemas/webhook-url'
+ required:
+ - event_id
+ - payment_id
+ - token
+ - confirmations
+ - url
diff --git a/docs/api/rmq.md b/docs/api/rmq.md
new file mode 100644
index 0000000..470e7c8
--- /dev/null
+++ b/docs/api/rmq.md
@@ -0,0 +1,55 @@
+# RabbitMQ
+`monero-lws-daemon` uses RabbitMQ to provide JSON notifications of payment_id
+(web)hooks. The feature is optional - by default LWS is _not_ compiled with
+RabbitMQ support. The cmake option -DWITH_RMQ=ON must be specified during the
+configuration stage to enable the feature.
+
+The notification location is specified with `--rmq-address`, the login details
+are specified with `--rmq-credentials` (specified as `user:pass`), the exchange
+is specified with `--rmq-exchange`, and routing name is specified with
+`--rmq-routing`.
+
+## `json-full-payment_hook`
+Only events of this type are sent to RabbitMQ. They are always "simulcast" with
+the webhook URL. If the webhook URL is `zmq` then no simulcast occurs - the
+event is only sent via ZeroMQ and/or RabbitMQ (if both are configured then it
+sent to both, otherwise it is only sent to the one configured).
+
+Example of the "raw" output to RabbitMQ:
+
+```json
+{
+ "index": 2,
+ "event": {
+ "event": "tx-confirmation",
+ "payment_id": "4f695d197f2a3c54",
+ "token": "single zmq wallet",
+ "confirmations": 1,
+ "event_id": "3894f98f5dd54af5857e4f8a961a4e57",
+ "tx_info": {
+ "id": {
+ "high": 0,
+ "low": 5666768
+ },
+ "block": 2265961,
+ "index": 1,
+ "amount": 3117324236131,
+ "timestamp": 1687301600,
+ "tx_hash": "ef3187775584351cc5109de124b877bcc530fb3fdbf77895329dd447902cc566",
+ "tx_prefix_hash": "064884b8a8f903edcfebab830707ed44b633438b47c95a83320f4438b1b28626",
+ "tx_public": "54dce1a6eebafa2fdedcea5e373ef9de1c3d2737ae9f809e80958d1ba4590d74",
+ "rct_mask": "4cdc4c4e340aacb4741ba20f9b0b859242ecdad2fcc251f71d81123a47db3400",
+ "payment_id": "4f695d197f2a3c54",
+ "unlock_time": 0,
+ "mixin_count": 15,
+ "coinbase": false
+ }
+ }
+}
+```
+> `index` is a counter used to detect dropped messages. It is not useful to
+RabbitMQ but is a carry-over from ZeroMQ (the same serialized message is used
+to send to both).
+
+> The `block` and `id` fields in the above example are NOT present when
+`confirmations == 0`.
diff --git a/docs/api/wallet.md b/docs/api/wallet.md
new file mode 100644
index 0000000..52ede48
--- /dev/null
+++ b/docs/api/wallet.md
@@ -0,0 +1 @@
+<swagger-ui src="wallet.yaml" />
diff --git a/docs/api/wallet.yaml b/docs/api/wallet.yaml
new file mode 100644
index 0000000..564fba5
--- /dev/null
+++ b/docs/api/wallet.yaml
@@ -0,0 +1,1210 @@
+openapi: 3.0.4
+info:
+ title: Monero Light-Wallet-Server (LWS) API
+ description: |-
+ This document describes the wallet API provided by the [monero-lws](https://github.com/vtnerd/monero-lws) 1.0 alpha release. This API is fully compatible with the [community API](https://github.com/monero-project/meta/blob/master/api/lightwallet_rest.md), but adds some additional functionality:
+ * The [subaddress provisioning draft](https://github.com/monero-project/meta/pull/647) is implemented
+ * The [subaddress lookahead draft](https://github.com/monero-project/meta/pull/1228) is implemented
+ * A not proposed convenience endpoint `/daemon_status` is provided
+
+ The additional functionality adds features, but does not interfere with the community API.
+
+ version: 2.0.0
+externalDocs:
+ description: A markdown file, if more readable
+ url: https://github.com/monero-project/meta/blob/master/api/lightwallet_rest.md
+tags:
+ - name: account
+ - name: subaddress
+ - name: transaction
+paths:
+ /get_address_info:
+ post:
+ tags:
+ - transaction
+ summary: Calculate account balance
+ description: |
+ Returns the minimal set of information needed to calculate a wallet balance, including the balance of subaddresses. The server cannot calculate when a spend occurs without the spend key, so a list of candidate spends is returned.
+ operationId: get_address_info
+ requestBody:
+ description: Provide `view_key`/`address` pair
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/login'
+ required: true
+ responses:
+ '200':
+ description: Success
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ locked_funds:
+ #description: Sum of unspendable XMR
+ $ref: '#/components/schemas/uint64-string'
+ total_received:
+ #description: Sum of received XMR
+ $ref: '#/components/schemas/uint64-string'
+ total_sent:
+ description: Sum of possibly spent XMR
+ allOf:
+ - $ref: '#/components/schemas/uint64-string'
+ scanned_height:
+ description: Current tx (tx_id) scan progress
+ allOf:
+ - $ref: '#/components/schemas/uint64'
+ scanned_block_height:
+ description: Current block scan progress
+ allOf:
+ - $ref: '#/components/schemas/uint64'
+ start_height:
+ #description: Start height of response
+ $ref: '#/components/schemas/uint64'
+ transaction_height:
+ description: Total transactions sent in Monero
+ allOf:
+ - $ref: '#/components/schemas/uint64'
+ blockchain_height:
+ #description: Current blockchain height
+ $ref: '#/components/schemas/uint64'
+ spent_outputs:
+ description: Possible spend info
+ type: array
+ items:
+ $ref: '#/components/schemas/spend'
+ lookahead:
+ description: The lookahead state, or {0, 0} if not provided
+ allOf:
+ - $ref: '#/components/schemas/address_meta'
+ lookahead_failure:
+ description: If provided, the block number subaddress lookahead failed
+ allOf:
+ - $ref: '#/components/schemas/uint64'
+ rates:
+ $ref: '#/components/schemas/rates'
+ required:
+ - locked_funds
+ - total_received
+ - total_sent
+ - scanned_height
+ - scanned_block_height
+ - start_height
+ - transaction_height
+ - blockchain_height
+ - spent_outputs
+ '400':
+ description: Invalid JSON, or invalid base58 address.
+ '403':
+ description: Account is disabled, or does not exist.
+ '405':
+ description: Not `POST` method.
+ '500':
+ description: Invalid JSON schema, and all other general errors.
+ '503':
+ description: Unable to read from database.
+ /get_address_txs:
+ post:
+ tags:
+ - transaction
+ summary: Get transaction history
+ description: |
+ Returns information needed to show transaction history. The server cannot calculate when a spend occurs without the spend key, so a list of candidate spends is returned.
+ operationId: get_address_txs
+ requestBody:
+ description: |
+ Provide `view_key`/`address` pair, with optional filtering of already seen transactions.
+
+ `since_tx_id` and `since_tx_block_hash` may be omitted, in which case all transactions are returned. If `since_tx_id` is present, `since_tx_block_hash` must be, too. The latter is used to handle the case when a blockchain reorg has rendered the requested `since_tx_id` invalid. Clients must take care to honor the `since_tx_id` returned in the response, which may be different than the value that was in the request.
+ content:
+ application/json:
+ schema:
+ type: object
+ allOf:
+ - $ref: '#/components/schemas/login'
+ properties:
+ since_tx_id:
+ description: Most recent tx already known to client
+ allOf:
+ - $ref: '#/components/schemas/uint64'
+ since_tx_block_hash:
+ description: Block hash of most recent tx
+ allOf:
+ - $ref: '#/components/schemas/binary32'
+ required: true
+ responses:
+ '200':
+ description: |
+ Success
+
+ `since_tx_id` may be omitted, and shall be omitted if omitted in the request. If present, it indicates that this response includes only transactions with an `id` greater than this one. It may be different than the value in the `request`: if a blockchain reorg has rendered the requested id/block_hash pair invalid, then some or all of the prior transaction history must be present. Clients must remove all newer transactions from their local history before appending the ones from this response. If null or omitted, the response contains every transaction.
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ total_received:
+ #description: Sum of received outputs
+ $ref: '#/components/schemas/uint64-string'
+ scanned_height:
+ description: Current tx (txid) scan progress
+ allOf:
+ - $ref: '#/components/schemas/uint64'
+ scanned_block_height:
+ description: Current scan progress
+ allOf:
+ - $ref: '#/components/schemas/uint64'
+ start_height:
+ #description: Start height of response
+ $ref: '#/components/schemas/uint64'
+ blockchain_height:
+ #description: Current blockchain height
+ $ref: '#/components/schemas/uint64'
+ transactions:
+ description: Possible spend info
+ type: array
+ items:
+ $ref: '#/components/schemas/transaction'
+ lookahead:
+ description: The lookahead state, or {0, 0} if not provided
+ allOf:
+ - $ref: '#/components/schemas/address_meta'
+ lookahead_failure:
+ description: If provided, the block number subaddress lookahead failed
+ allOf:
+ - $ref: '#/components/schemas/uint64'
+ since_tx_id:
+ description: Most recent omitted tx
+ allOf:
+ - $ref: '#/components/schemas/uint64'
+ required:
+ - total_received
+ - scanned_height
+ - scanned_block_height
+ - start_height
+ - transaction_height
+ - blockchain_height
+ - transactions
+ '400':
+ description: Invalid JSON, or invalid base58 address.
+ '403':
+ description: Account is disabled or does not exist.
+ '405':
+ description: Not `POST` method.
+ '500':
+ description: Invalid JSON schema, and general errors.
+ '503':
+ description: Unable to read from database.
+ /get_random_outs:
+ post:
+ tags:
+ - transaction
+ summary: Get server constructed rings
+ description: |
+ Selects random outputs to use in a ring signature of a new transaction. If the `amount` is `0` then the monerod RPC get_output_distribution should be used to locally select outputs using a gamma distribution as described in "An Empirical Analysis of Traceability in the Monero Blockchain". If the `amount` is not `0`, then the `monerod` RPC `get_output_histogram` should be used to locally select outputs using a triangular distribution (`uint64_t dummy_out = histogram.total * sqrt(float64(random_uint53) / float64(2^53))`).
+ operationId: get_random_outs
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ count:
+ description: Mixin (name is historical)
+ allOf:
+ - $ref: '#/components/schemas/uint32'
+ amounts:
+ description: XMR amounts that need mixing
+ type: array
+ items:
+ $ref: '#/components/schemas/uint64-string'
+ required:
+ - count
+ - amounts
+ required: true
+ responses:
+ '200':
+ description: Success
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ amount_outs:
+ description: Decoy outputs for each amounts
+ type: array
+ items:
+ $ref: '#/components/schemas/random_outputs'
+ required:
+ - amount_outs
+ '400':
+ description: Invalid JSON.
+ '405':
+ description: Not `POST` method.
+ '500':
+ description: Invalid JSON schema, and general errors.
+ '503':
+ description: Unable to contact monero daemon.
+ /get_subaddrs:
+ post:
+ tags:
+ - subaddress
+ summary: Returns all subaddresses provisioned for a wallet
+ operationId: get_subaddrs
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/login'
+ required: true
+ responses:
+ '200':
+ description: Success
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ all_subaddrs:
+ $ref: '#/components/schemas/subaddrs'
+ max_subaddrs:
+ description: Maximum number of subaddresses permitted by server
+ allOf:
+ - $ref: '#/components/schemas/uint64'
+ required:
+ - all_subaddrs
+ '400':
+ description: Invalid JSON, or invalid base58 address.
+ '403':
+ description: Account is disabled or does not exist.
+ '405':
+ description: Not `POST` method
+ '409':
+ description: Unable to provision subaddresses due to server limits.
+ '500':
+ description: Invalid JSON schema, and general errors.
+ '503':
+ description: Unable to read from database.
+ /get_tree_path:
+ post:
+ tags:
+ - transaction
+ summary: Get all information needed to build a tree cache for fmcp++ signing
+ operationId: get_tree_path
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ output_ids:
+ type: array
+ items:
+ $ref: '#/components/schemas/composite_id'
+ required: [output_ids]
+ required: true
+ responses:
+ '200':
+ description: Success
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ paths:
+ type: array
+ items:
+ $ref: '#/components/schemas/path_spend'
+ last_path:
+ $ref: '#/components/schemas/path'
+ top_block_height:
+ $ref: '#/components/schemas/uint64'
+ n_leaf_tuples:
+ $ref: '#/components/schemas/uint64'
+ top_block_hash:
+ $ref: '#/components/schemas/binary32'
+ required:
+ - paths
+ - last_path
+ - top_block_height
+ - n_leaf_tuples
+ - top_block_hash
+ '400':
+ description: Invalid JSON, invalid base58 address, or invalid composite_id
+ '405':
+ description: Not `POST` method
+ '500':
+ description: Invalid JSON schema, and general errors.
+ '503':
+ description: Unable to read from database.
+ /get_unspent_outs:
+ post:
+ tags:
+ - transaction
+ summary: Get data for needed for sending transaction.
+ description: Returns a list of received outputs. The client must determine when the output was actually spent.
+ operationId: get_unspent_outs
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ allOf:
+ - $ref: '#/components/schemas/login'
+ properties:
+ amount:
+ description: XMR send amount
+ allOf:
+ - $ref: '#/components/schemas/uint64'
+ mixin:
+ description: Minimum mixin for source output
+ allOf:
+ - $ref: '#/components/schemas/uint32'
+ use_dust:
+ description: Return all available outputs
+ type: boolean
+ dust_threshold:
+ description: Ignore outputs below this amount
+ allOf:
+ - $ref: '#/components/schemas/uint64-string'
+ lookahead_failure:
+ description: If provided, the block number subaddress lookahead failed
+ allOf:
+ - $ref: '#/components/schemas/uint64'
+ required:
+ - height
+ - mixin
+ - use_dust
+ required: true
+ responses:
+ '200':
+ description: Success
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ per_byte_fee:
+ description: Estimated network fee (deprecated)
+ allOf:
+ - $ref: '#/components/schemas/uint64-string'
+ fee_mask:
+ description: Fee quantization mask
+ allOf:
+ - $ref: '#/components/schemas/uint64-string'
+ amount:
+ description: The total value in outputs
+ allOf:
+ - $ref: '#/components/schemas/uint64-string'
+ outputs:
+ description: Outputs possibly available for spending
+ type: array
+ items:
+ $ref: '#/components/schemas/output'
+ fees:
+ description: Priority based fee levels, starting with lowest
+ type: array
+ minItems: 1
+ items:
+ $ref: '#/components/schemas/uint64'
+ required:
+ - per_byte_fee
+ - fee_mask
+ - amount
+ - outputs
+ '400':
+ description: Invalid JSON, or invalid base58 address.
+ '403':
+ description: Account is disabled or does not exist.
+ '405':
+ description: Not `POST` method.
+ '500':
+ description: Invalid JSON schema, and general errors.
+ '503':
+ description: Unable to read from database or unable to get monero daemon fee.
+ /get_version:
+ get:
+ summary: Get basic information about Server
+ operationId: get_version
+ responses:
+ '200':
+ description: Succeeded
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ server_type:
+ type: string
+ description: Name of server implementation
+ last_git_commit_hash:
+ description: A hash (of server determined length) corresponding to commit hash
+ allOf:
+ - $ref: '#/components/schemas/binary'
+ last_commit_date:
+ type: string
+ description: Implementation defined commit date
+ monero_version_full:
+ description: Implementation defined version string
+ type: string
+ blockchain_height:
+ description: Current blockchain height (as processed by LWS server)
+ allOf:
+ - $ref: '#/components/schemas/uint64'
+ api:
+ description: Unique number per schema Implementation
+ type: integer
+ max_subaddresses:
+ description: Max subaddresses allowed by server
+ type: integer
+ network:
+ description: Name of network type
+ type: string
+ testnet:
+ type: boolean
+ /import_wallet_request:
+ post:
+ tags:
+ - account
+ summary: Restore from height with optional subaddress lookahead
+ operationId: import_wallet_request
+ requestBody:
+ description: Provide `view_key`/`address` pair
+ content:
+ application/json:
+ schema:
+ description: |
+ If `height` is omitted, then it assumed to be `0`. If lookahead is omitted, it assumed to be `{0, 0}`.
+ type: object
+ allOf:
+ - $ref: '#/components/schemas/login'
+ properties:
+ from_height:
+ $ref: '#/components/schemas/uint64'
+ lookahead:
+ description: "Desired lookahead for (re)scan"
+ allOf:
+ - $ref: '#/components/schemas/address_meta'
+ required: true
+ responses:
+ '200':
+ description: "`payment_id`, `import_fee`, and `payment_address` may be omitted if the client does not need to send XMR to complete the request."
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ payment_address:
+ description: Payment location to enable rescan
+ allOf:
+ - $ref: '#/components/schemas/base58-address'
+ payment_id:
+ description: Bytes for payment_id field to track rescan
+ allOf:
+ - $ref: '#/components/schemas/binary8'
+ import_fee:
+ description: Fee required to complete request
+ allOf:
+ - $ref: '#/components/schemas/uint64-string'
+ new_request:
+ description: "New or existing request"
+ type: boolean
+ request_fulfilled:
+ description: Indicates success
+ type: boolean
+ status:
+ description: Custom message
+ type: string
+ example: "OK"
+ lookahead:
+ description: The lookahead state, or {0, 0} if not provided
+ allOf:
+ - $ref: '#/components/schemas/address_meta'
+ required:
+ - new_request
+ - request_fulfilled
+ - status
+ '400':
+ description: Invalid JSON, or invalid base58 address.
+ '403':
+ description: Account is disabled or does not exist
+ '405':
+ description: Not `POST` method
+ '500':
+ description: Invalid JSON schema, and general errors
+ '503':
+ description: Unable to read from database
+ /login:
+ post:
+ tags:
+ - account
+ summary: Create and check account status
+ description: |
+ The `view_key` bytes are required even if an account is not being created, to prevent metadata leakage.
+ operationId: login
+ requestBody:
+ description: Provide `view_key`/`address` pair
+ content:
+ application/json:
+ schema:
+ allOf:
+ - $ref: '#/components/schemas/login'
+ properties:
+ create_account:
+ type: boolean
+ description: True when account creation should be attempted
+ generated_locally:
+ type: boolean
+ description: True if the account is new (not restored/imported)
+ lookahead:
+ description: "Desired lookahead for new accounts"
+ allOf:
+ - $ref: '#/components/schemas/address_meta'
+ required:
+ - create_account
+ - generated_locally
+ required: true
+ responses:
+ '200':
+ description: Success
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ new_address:
+ description: Whether account was just created
+ type: boolean
+ generated_locally:
+ description: Flag from initial account creation
+ type: boolean
+ start_height:
+ description: Account scanning start block
+ allOf:
+ - $ref: '#/components/schemas/uint64'
+ lookahead:
+ description: The lookahead state, or {0, 0} if not provided
+ allOf:
+ - $ref: '#/components/schemas/address_meta'
+ required:
+ - new_address
+ '400':
+ description: Invalid JSON, or invalid base58 address.
+ '403':
+ description: "`view_key`/`address` pair is invalid"
+ '405':
+ description: Not `POST` method.
+ '500':
+ description: Invalid JSON schema, and general errors.
+ '501':
+ description: Account creation is not allowed.
+ '503':
+ description: Unable to read or write from database.
+ /provision_subaddrs:
+ post:
+ tags:
+ - subaddress
+ summary: Request new sub address ranges
+ description: |
+ Provision subaddresses at specified indexes. No two clients should ever receive the same newly provisioned subaddresses when calling this endpoint; the server should guarantee that newly provisioned subaddresses are fresh.
+ operationId: provision_subaddrs
+ requestBody:
+ content:
+ application/json:
+ schema:
+ description: |
+ The various combinations of `maj_i`, `min_i`, `n_maj`, and `n_min` behave differently, but generally the server provisions subaddresses where it has not already provisioned them, with the indexes specified as lower bounds. If, for example, only `maj_i` is included, then the server should provision a default number of minor subaddresses (e.g. 500) within that `maj_i`, wherever minor subaddresses have not already been provisioned, starting with `min_i` set to 0 as the lower bound.
+
+ If, for example, `maj_i`, `min_i`, `n_maj` and `n_min` are included, then the server should provision `n_maj` majors wherever major subaddresses have not already been provisioned starting with `maj_i` as the lower bound, and for each major, provision `n_min` subaddresses starting with `min_i` as the lower bound.
+
+ If, for example, `maj_i`, `min_i`, `n_maj` and `n_min` are included, then the server should provision `n_maj` majors wherever major subaddresses have not already been provisioned starting with `maj_i` as the lower bound, and for each major, provision `n_min` subaddresses starting with `min_i` as the lower bound.
+
+ All combinations follow the above framework. The server can choose to "pad" counts and provision *more* than a client requests.
+
+ If the server cannot provision a specified number of subaddresses because the server would provision more than the maximum number of subaddresses, HTTP 409 should be returned. Ack that the status code is not perfect here. In this case, the client should make a new request either with a smaller requested number of subaddresses to provision, or at a different major or minor index. If none of `maj_i`, `min_i`, `n_maj`, and `n_min` are included in the request, the server must return a HTTP 400 "Bad Request" error. `get_all` defaults to true if not included in the request.
+ type: object
+ allOf:
+ - $ref: '#/components/schemas/login'
+ properties:
+ maj_i:
+ description: Subaddress major index (defaults to 0)
+ allOf:
+ - $ref: '#/components/schemas/uint32'
+ min_i:
+ description: Subaddress minor index (defaults to 0)
+ allOf:
+ - $ref: '#/components/schemas/uint32'
+ n_maj:
+ description: Number of major subaddresses to provision
+ allOf:
+ - $ref: '#/components/schemas/uint32'
+ n_min:
+ description: Number of minor subaddresses to provision
+ allOf:
+ - $ref: '#/components/schemas/uint32'
+ get_all:
+ description: Whether to include all subaddresses in response. Defaults to true.
+ type: boolean
+ required: true
+ responses:
+ '200':
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/new_subaddrs'
+ '400':
+ description: Invalid JSON, or invalid base58 address.
+ '403':
+ description: Account is disabled or does not exist.
+ '405':
+ description: Not `POST` method.
+ '409':
+ description: Unable to provision subaddresses due to server limits.
+ '500':
+ description: Invalid JSON schema, and general errors.
+ '503':
+ description: Unable to read or write from database
+ /submit_raw_tx:
+ post:
+ tags:
+ - transaction
+ summary: Send a transaction to the Monero network
+ description: |
+ This format is tricky unfortunately, it is custom to the monero daemon. The internal code of monerod must be read to determine this format currently.
+ operationId: submit_raw_tx
+ requestBody:
+ description: Provide `view_key`/`address` pair
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ tx:
+ $ref: '#/components/schemas/binary'
+ required:
+ - tx
+ required: true
+ responses:
+ '200':
+ description: Success
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ status:
+ description: Typically the response by the monero daemon attempting to relay the transaction.
+ type: string
+ example: "OK"
+ required:
+ - status
+ '400':
+ description: Invalid JSON.
+ '405':
+ description: Not `POST` method.
+ '500':
+ description: Invalid JSON schema, and general errors.
+ '503':
+ description: Unable to communicate with monero daemon.
+ /upsert_subaddrs:
+ post:
+ tags:
+ - subaddress
+ summary: Upsert new subaddresses
+ description: |
+ Upsert subaddresses at the specified major and minor indexes. This endpoint is idempotent.
+ operationId: upsert_subaddrs
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ allOf:
+ - $ref: '#/components/schemas/login'
+ properties:
+ subaddrs:
+ description: Subaddresses to upsert
+ type: array
+ minItems: 1
+ items:
+ $ref: '#/components/schemas/subaddrs'
+ get_all:
+ description: Whether to include all subaddresses in response. Defaults to true
+ type: boolean
+ required:
+ - subaddrs
+ required: true
+ responses:
+ '200':
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/new_subaddrs'
+ '400':
+ description: Invalid JSON.
+ '403':
+ description: Account is disabled or does not exist
+ '405':
+ description: Not `POST` method
+ '409':
+ description: Unable to provision subaddresses due to server limits
+ '500':
+ description: Invalid JSON schema, and general errors
+ '503':
+ description: Unable to read from database
+components:
+ schemas:
+ base58-address:
+ type: string
+ description: "Standard Monero address in base58"
+ minLength: 95
+ maxLength: 95
+ pattern: "^[0-9A-Za-z]{95}$"
+ example: "47nPhxp2cJeKN2NjamupNUNA13XgzcYPzQBCzzsKcj717s8M2UpFVmmdwSuYwgyy8kPDwU7hpEqTTDvfe5LAb9Aj6nwmEzf"
+ binary:
+ type: string
+ description: "binary data as hex (arbitrary length)"
+ pattern: "^([0-9A-Fa-f]{2})+$"
+ example: "0abcd9dcf0bca63310bb4d7e120918d1"
+ binary8:
+ type: string
+ description: "8-bytes binary data as hex"
+ minLength: 16
+ maxLength: 16
+ pattern: "^[0-9A-Fa-f]{16}$"
+ example: "6d26cac5c9d74f4e"
+ binary32:
+ type: string
+ description: "32-bytes binary data as hex"
+ minLength: 64
+ maxLength: 64
+ pattern: "^[0-9A-Fa-f]{64}$"
+ example: "ee171270296bbc26d5be4455b6313e1a2086a92080e205f77d6f861f8e5fd205"
+ ringct-packed:
+ type: string
+ description: |
+ A "packed" hex struct containing: commitment|mask|amount. Only the 'mask' is needed, as the amount is already sent, and the commitment can be re-computed from the mask and amount. This format is used for legacy purposes.
+ pattern: "^[0-9A-Fa-f]{192}$"
+ example: "09e732db4d2154dcd79eff818d105184dcf8223ecbfeb64d4673334756ca9433a28dcc2511cb636dc836722f4f54f97e7a84d7aed012798a8f65229004e6dc299500de68879730a32933bd4df0a3a5927149ea8f678e42cbd863c166c73c920d"
+ ringct-random:
+ type: string
+ description: |
+ The first 64-bytes are the hex of the ringct commitment. The next 64-bytes are optionally sent (for legacy purposes), but still required to be hex.
+ pattern: "^[0-9A-Fa-f]{64}([0-9A-Fa-f]{128}){0,1}$"
+ example: "09e732db4d2154dcd79eff818d105184dcf8223ecbfeb64d4673334756ca9433"
+ payment-id:
+ description: 8-Byte encrypted payment-id or 32-byte unencrypted payment-id (deprecated)
+ oneOf:
+ - $ref: '#/components/schemas/binary8'
+ - $ref: '#/components/schemas/binary32'
+ uint64-string:
+ type: string
+ description: An unsigned 64-bit value encoded as a string
+ pattern: "^[1-9][0-9]{0,19}$"
+ minLength: 1
+ maxLength: 20
+ example: "10"
+ uint16:
+ type: integer
+ minimum: 0
+ maximum: 65535
+ uint32:
+ type: integer
+ minimum: 0
+ maximum: 4294967295
+ uint64:
+ type: integer
+ minimum: 0
+ maximum: 18446744073709551615
+ timestamp:
+ description: "'YYYY-MM-DDTHH:MM:SSZ' in UTC/Zulu timezone. No sub-subseconds"
+ type: string
+ pattern: "^[0-9]{4}-[0-9]{2}-[0-9]{2}:[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"
+ example: "2025-06-29T20:44:32Z"
+ login:
+ type: object
+ properties:
+ address:
+ $ref: '#/components/schemas/base58-address'
+ view_key:
+ $ref: '#/components/schemas/binary32'
+ required:
+ - address
+ - view_key
+ address_meta:
+ type: object
+ description: A subaddress meta block (major, minor) index
+ properties:
+ maj_i:
+ $ref: '#/components/schemas/uint32'
+ min_i:
+ $ref: '#/components/schemas/uint32'
+ required:
+ - maj_i
+ - min_i
+ index_range:
+ description: |
+ The first element is the inclusive lower bound of the range. The second element is the inclusive upper bound of the range. The second element is greater than or equal to the first element.
+ type: array
+ minItems: 2
+ maxItems: 2
+ items:
+ $ref: '#/components/schemas/uint32'
+ legacy_id:
+ type: object
+ description: Specify an output using pre-fcmp++ indexing
+ properties:
+ amount:
+ $ref: '#/components/schemas/uint64'
+ index:
+ $ref: '#/components/schemas/uint64'
+ composite_id:
+ type: object
+ description: Specify an output using pre or post fcmp++ indexing
+ oneOf:
+ -
+ type: object
+ properties:
+ legacy:
+ $ref: '#/components/schemas/legacy_id'
+ -
+ type: object
+ properties:
+ unified:
+ $ref: '#/components/schemas/uint64'
+ output_pair:
+ type: object
+ description: The public-keys related to an output
+ properties:
+ output_pubkey:
+ $ref: '#/components/schemas/binary32'
+ commitment:
+ $ref: '#/components/schemas/binary32'
+ leaf:
+ type: object
+ description: Called `OutputContext` in Monero codebase
+ properties:
+ output_id:
+ description: Unified (post fcmp++) output identifier
+ allOf:
+ - $ref: '#/components/schemas/uint64'
+ torsion_checked:
+ type: boolean
+ description: True if checked for (lack of) Torsion point
+ output_pair:
+ $ref: '#/components/schemas/output_pair'
+ chunk:
+ type: object
+ properties:
+ chunk_bytes:
+ description: Curve points in the tree path
+ type: array
+ items:
+ $ref: '#/components/schemas/binary32'
+ path:
+ type: object
+ properties:
+ leaves:
+ type: array
+ items:
+ $ref: '#/components/schemas/leaf'
+ layer_chunks:
+ type: array
+ items:
+ $ref: '#/components/schemas/chunk'
+ path_spend:
+ type: object
+ properties:
+ path:
+ $ref: '#/components/schemas/path'
+ output_id:
+ $ref: '#/components/schemas/composite_id'
+ leaf_idx:
+ $ref: '#/components/schemas/uint64'
+ subaddrs:
+ description: |
+ Key is the major index of Monero subaddresses, values are the minor indexes of subaddresses within that major index.
+ type: object
+ properties:
+ key:
+ $ref: '#/components/schemas/uint32'
+ value:
+ type: array
+ minItems: 1
+ items:
+ $ref: '#/components/schemas/index_range'
+ required:
+ - key
+ - value
+ new_subaddrs:
+ type: object
+ properties:
+ new_subaddrs:
+ #description: All new subaddresses provisioned in the request
+ $ref: '#/components/schemas/subaddrs'
+ all_subaddrs:
+ #descriptiopn: All subaddresses provisioned for the wallet (including new)
+ $ref: '#/components/schemas/subaddrs'
+ required:
+ - new_subaddrs
+ output:
+ type: object
+ properties:
+ tx_id:
+ #description: "Index of tx in blockchain"
+ $ref: '#/components/schemas/uint64'
+ amount:
+ #description: "XMR value of output"
+ $ref: '#/components/schemas/uint64-string'
+ index:
+ description: Index within vout vector
+ allOf:
+ - $ref: '#/components/schemas/uint16'
+ global_index:
+ description: Index within amount (determined by blockchain order)
+ allOf:
+ - $ref: '#/components/schemas/uint64-string'
+ rct:
+ $ref: '#/components/schemas/ringct-packed'
+ tx_hash:
+ #description: "Bytes of tx hash"
+ $ref: '#/components/schemas/binary32'
+ tx_prefix_hash:
+ #description: "Bytes of tx prefix hash"
+ $ref: '#/components/schemas/binary32'
+ public_key:
+ #description: "Bytes of output public key"
+ $ref: '#/components/schemas/binary32'
+ tx_pub_key:
+ description: Bytes of the ephermal tx public key
+ allOf:
+ - $ref: '#/components/schemas/binary32'
+ spend_key_images:
+ description: "Bytes of key images"
+ type: array
+ items:
+ $ref: '#/components/schemas/binary32'
+ timestamp:
+ $ref: '#/components/schemas/timestamp'
+ height:
+ description: Block height
+ allOf:
+ - $ref: '#/components/schemas/uint64'
+ recipient:
+ $ref: '#/components/schemas/address_meta'
+ required:
+ - tx_id
+ - amount
+ - index
+ - global_index
+ - tx_hash
+ - tx_prefix_hash
+ - public_key
+ - tx_pub_key
+ - spend_key_images
+ - timestamp
+ - height
+ rates:
+ description: "Conversion rates into common/popular currencies"
+ type: object
+ properties:
+ AUD:
+ description: "AUD/XMR exchange rate"
+ type: number
+ BRL:
+ description: "BRL/XMR exchange rate"
+ type: number
+ BTC:
+ description: "BTC/XMR exchange rate"
+ type: number
+ CAD:
+ description: "CAD/XMR exchange rate"
+ type: number
+ CHF:
+ description: "CHF/XMR exchange rate"
+ type: number
+ CNY:
+ description: "CNY/XMR exchange rate"
+ type: number
+ EUR:
+ description: "EUR/XMR exchange rate"
+ type: number
+ GBP:
+ description: "GBP/XMR exchange rate"
+ type: number
+ HKD:
+ description: "HKD/XMR exchange rate"
+ type: number
+ INR:
+ description: "INR/XMR exchange rate"
+ type: number
+ JPY:
+ description: "JPY/XMR exchange rate"
+ type: number
+ KRW:
+ description: "KRW/XMR exhcnage rate"
+ type: number
+ MXN:
+ description: "MXN/XMR exchange rate"
+ type: number
+ NOK:
+ description: "NOK/XMR exchange rate"
+ type: number
+ NZD:
+ description: "NZD/XMR exchange rate"
+ type: number
+ SEK:
+ description: "SEK/XMR exchange rate"
+ type: number
+ SGD:
+ description: "SGD/XMR exchange rate"
+ type: number
+ TRY:
+ description: "TRY/XMR exchange rate"
+ type: number
+ USD:
+ description: "USD/XMR exchange rate"
+ type: number
+ RUB:
+ description: "RUB/XMR exchange rate"
+ type: number
+ ZAR:
+ description: "ZAR/XMR exchange rate"
+ type: number
+ spend:
+ description: |
+ This object represents a single _possible_ spend
+
+ `out_index` is a zero-based offset from the original received output. The variable within the monero codebase is the vout array, this is the index within that. It is needed for correct computation of the key_image.
+
+ `mixin` does not include the real spend - this is the number of dummy inputs.
+ type: object
+ properties:
+ amount:
+ #description: "XMR possibly being spent"
+ $ref: '#/components/schemas/uint64-string'
+ key_image:
+ #description: "Bytes of the key image"
+ $ref: '#/components/schemas/binary32'
+ tx_pub_key:
+ description: Bytes of the ephermal tx public key
+ allOf:
+ - $ref: '#/components/schemas/binary32'
+ out_index:
+ description: Index (within vout vector) of source output
+ allOf:
+ - $ref: '#/components/schemas/uint16'
+ mixin:
+ #description: "Mixin of the spend"
+ $ref: '#/components/schemas/uint32'
+ sender:
+ $ref: '#/components/schemas/address_meta'
+ required:
+ - amount
+ - key_image
+ - tx_pub_key
+ - out_index
+ - mixin
+ transaction:
+ description: |
+ This object represents a single Transaction on the blockchain
+
+ `id` is determined by the monero daemon. It is the offset that a transaction appears in the blockchain from the genesis block.
+
+ `timestamp`, `height` and `block_hash` are not sent when `mempool` is true.
+
+ `hash` is determined by how the monero core computes the hash.
+
+ `spent_outputs` is the list of possible spends in this transaction only.
+
+ `payment_id` is omitted if the transaction had none. It is decrypted when the encrypted form is used. The decryption may be incorrect - if the transaction was TO another address, then this will be random bytes. This happens frequently with outgoing payment ids; the received XMR in the transaction is change and the payment id is for the real recipient.
+
+ `mixin` does not include the real spend - this is the number of decoy inputs.
+ properties:
+ id:
+ description: "Index of tx in blockchain"
+ allOf:
+ - $ref: '#/components/schemas/uint64'
+ hash:
+ #description: "Bytes of tx hash"
+ $ref: '#/components/schemas/binary32'
+ timestamp:
+ $ref: '#/components/schemas/timestamp'
+ total_received:
+ #description: "Total XMR received"
+ $ref: '#/components/schemas/uint64-string'
+ total_sent:
+ #description: "XMR possibly being spent"
+ $ref: '#/components/schemas/uint64-string'
+ unlock_time:
+ #description: "Tx unlock time field"
+ $ref: '#/components/schemas/uint64'
+ block_hash:
+ $ref: '#/components/schemas/binary32'
+ height:
+ description: Block height
+ allOf:
+ - $ref: '#/components/schemas/uint64'
+ spent_outputs:
+ description: List of possible spends
+ type: array
+ items:
+ $ref: '#/components/schemas/spend'
+ payment_id:
+ $ref: '#/components/schemas/payment-id'
+ coinbase:
+ type: boolean
+ mempool:
+ type: boolean
+ mixin:
+ #description: "Mixin of the receive"
+ $ref: '#/components/schemas/uint32'
+ required:
+ - id
+ - hash
+ - total_received
+ - total_sent
+ - unlock_time
+ - spent_outputs
+ - coinbase
+ - mempool
+ - mixin
+ random_output:
+ description: |
+ A single random output, used for generating ring signatures.
+
+ `global_index` is determined by the monero daemon. It is the offset from the first time the amount appeared in the blockchain. After ringct, this is the order of outputs as they appear in the blockchain.
+ type: object
+ properties:
+ global_index:
+ description: Index within amount
+ allOf:
+ - $ref: '#/components/schemas/uint64-string'
+ public_key:
+ #description: "Bytes of output public key"
+ $ref: '#/components/schemas/binary32'
+ rct:
+ #description: "Bytes containing ringct commitment"
+ $ref: '#/components/schemas/ringct-random'
+ required:
+ - global_index
+ - public_key
+ random_outputs:
+ description: |
+ `outputs` is omitted by the server if the amount does not have enough mixable outputs.
+ type: object
+ properties:
+ amount:
+ $ref: '#/components/schemas/uint64-string'
+ outputs:
+ description: Server selected ring
+ type: array
+ items:
+ $ref: '#/components/schemas/random_output'
+ required:
+ - amount
+ - outputs
+
diff --git a/docs/api/zmq.md b/docs/api/zmq.md
new file mode 100644
index 0000000..4d717a2
--- /dev/null
+++ b/docs/api/zmq.md
@@ -0,0 +1,154 @@
+# Webhooks / ZeroMQ
+monero-lws can report scanner events via HTTP/1.1 webhooks or ZeroMQ PUB/SUB.
+The ZeroMQ PUB is distinct from the `monerod` PUB socket;
+`monero-lws-daemon --zmq-pub` creates a socket just for `monero-lws-daemon`
+scanner updates. Webhooks can be "http" or "https" - the latter performing
+standard system certificate authority checks (self-signed certificates NOT
+allowed).
+
+Events are ALWAYS published over ZMQ-PUB, whereas webhooks are only used when
+a specific URL is given to `/webhook_add`. If the provided URL is `zmq` then
+only ZMQ-PUB is used to push the event. All other URLs must start with
+`http://` or `https://` as per [admin api spec](/api/admin).
+
+## Registration
+Registering for events requires `/webhook_add` from the
+[admin api](/api/admin). Examples and full schema are provided on that page.
+
+## Events
+Every callback for an event has this structure:
+```json
+{
+ "index": 0,
+ "event": {...}
+}
+```
+The `index` field is per event type, and tracks the number of events in that
+type. This is useful only for ZeroMQ, where events can be dropped.
+
+### Payments
+A payment event occurs from 0-confirmations up to the user requested number of
+confirmations. A payment event, even when using ZeroMQ, only occurs when one is
+registered via `/webhook_add`. Payment registerations can only be done on
+primary addresses and optionally a payment id. Registering a primary address
+automatically registers all subaddresses; specifying a payment id only
+registers that payment id. Leaving the payment id field blank during
+registeration enables hooks from all payment ids.
+
+The relevant topics for ZeroMQ-PUB are: `json-full-payment_hook`, and
+`msgpack-full-payment_hook`.
+
+Example payload of a webhook callback:
+```json
+{
+ "index": 2,
+ "event": {
+ "event": "tx-confirmation",
+ "payment_id": "4f695d197f2a3c54",
+ "token": "single zmq wallet",
+ "confirmations": 1,
+ "event_id": "3894f98f5dd54af5857e4f8a961a4e57",
+ "tx_info": {
+ "id": {
+ "high": 0,
+ "low": 5666768
+ },
+ "block": 2265961,
+ "index": 1,
+ "amount": 3117324236131,
+ "timestamp": 1687301600,
+ "tx_hash": "ef3187775584351cc5109de124b877bcc530fb3fdbf77895329dd447902cc566",
+ "tx_prefix_hash": "064884b8a8f903edcfebab830707ed44b633438b47c95a83320f4438b1b28626",
+ "tx_public": "54dce1a6eebafa2fdedcea5e373ef9de1c3d2737ae9f809e80958d1ba4590d74",
+ "rct_mask": "4cdc4c4e340aacb4741ba20f9b0b859242ecdad2fcc251f71d81123a47db3400",
+ "payment_id": "4f695d197f2a3c54",
+ "unlock_time": 0,
+ "mixin_count": 15,
+ "coinbase": false
+ }
+ }
+}
+
+```
+
+### Spends
+All of the information in the [payments](#payments) section is relevant to
+here, except instead of payment confirmations, spend confirmations are
+published. The relevant ZeroMQ-PUB topics are: `json-full-spend_hook` and
+`msgpack-full_spend_hook`. Example payload of a webhook callback:
+
+```json
+{
+ "index": 0,
+ "event": {
+ "event": "tx-spend",
+ "token": "spend-xmr",
+ "event_id": "7ff047aa74e14f4aa978469bc0eec8ec",
+ "tx_info": {
+ "input": {
+ "height": 2464207,
+ "tx_hash": "97d4e66c4968b16fec7662adc9f8562c49108d3c5e7030c4d6dd32d97fb62540",
+ "image": "b0fe7acd9e17bb8b9ac2daae36d4cb607ac60ed8a101cc9b2e1f74016cf80b24",
+ "source": {
+ "high": 0,
+ "low": 6246316
+ },
+ "timestamp": 1711902214,
+ "unlock_time": 0,
+ "mixin_count": 15,
+ "sender": {
+ "maj_i": 0,
+ "min_i": 0
+ }
+ },
+ "source": {
+ "id": {
+ "high": 0,
+ "low": 6246316
+ },
+ "amount": 10000000000,
+ "mixin": 15,
+ "index": 0,
+ "tx_public": "426ccd6d39535a1ee8636d14978581e580fcea35c8d3843ceb32eb688a0197f7"
+ }
+ }
+ }
+}
+```
+
+### New Accounts
+When a new account is registered, regardless of the `--auto-accept-creation`
+flag for `monero-lws-daemon`, the system will publish events. Registration
+occurs in the [admin api](/api/admin). The ZeroMQ-PUB topics are
+`json_full-new_account` and `msgpack-full-new_account`. Example of callback:
+
+```json
+{
+ "index": 2,
+ "event": {
+ "event": "new-account",
+ "event_id": "c5a735e71b1e4f0a8bfaeff661d0b38a",
+ "token": "",
+ "address": "9zGwnfWRMTF9nFVW9DNKp46aJ43CRtQBWNFvPqFVSN3RUKHuc37u2RDi2GXGp1wRdSRo5juS828FqgyxkumDaE4s9qyyi9B"
+ }
+}
+```
+
+### Updated Accounts
+You can register to be notified when the scanner has updated _any_ account,
+and the event will batch the notifications per-thread. The ZeroMQ-PUB topics
+are `json-minimal-scanned` and `msgpack-minimal-scanned`. Example of
+callback:
+
+```json
+{
+ "index": 13,
+ "event": {
+ "height": 2438536,
+ "id": "9197e1c6f3de28a98dfc579325903e5416ef1ba2681043c54b5fff0d39645a7f",
+ "addresses": [
+ "9xkhhJSa7ZhS5sAcTix6ozL14RwdgxbV7JZVFW4rCghN7GidutaykfxDHfgW45UPiCTXncuvZ91GNSGgxs3b2Cin9TU8nP3"
+ ]
+ }
+}
+```
diff --git a/docs/apps/admin.md b/docs/apps/admin.md
new file mode 100644
index 0000000..60252b5
--- /dev/null
+++ b/docs/apps/admin.md
@@ -0,0 +1,548 @@
+# Admin
+The `monero-lws-admin` executable or `--admin-rest-server` option in the
+`monero-lws-daemon` executable can be used to administer the database
+used by `monero-lws-daemon`. Any number of `monero-lws-admin` instances can run
+concurrently with a single `monero-lws-daemon` instance on the same database.
+Administration is necessary to authorize new accounts and rescan requests
+submitted from the [wallet API](/api/wallet). The admin executable can also be
+used to list the contents of the LMDB file for debugging purposes.
+
+# monero-lws-admin
+
+The `monero-lws-admin` utility is structured around command-line arguments with
+JSON responses printed to `stdout`. Each administration command takes arguments
+by position. Every available administration command and required+optional
+arguments are listed when the `--help` flag is given to the executable.
+
+The [`jq`](https://stedolan.github.io/jq/) utility is recommended if using
+`monero-lws-admin` in a shell environment. The `jq` program can be used for
+indenting the output to make it more readable, and can be used to
+search+filter the JSON output from the command.
+
+# Admin REST API
+The `monero-lws-daemon` can be started with 1+ `--admin-rest-server` parameters
+that specify a listening location for [admin API](/api/admin) clients. By
+default, there is no admin REST server and no available admin accounts.
+
+An admin REST server can be merged with a regular REST server if path prefixes
+are specified, such as
+`--rest-server https://0.0.0.0:8443/basic --admin-rest-server https://0.0.0.0:8443/admin`.
+This will start a server listening on one port, 8443, and requires clients to
+specify `/basic/command` or `/admin/admin/command` when making a
+request.
+
+An admin account account can be created via `monero-lws-admin create_admin`
+_only_ (this command is not available via REST for security purposes). The
+`key` value returned in the `create_admin` JSON object becomes the `auth`
+parameter in the admin REST API. A new admin account is put into the
+`hidden` state - the account is _not_ scanned for transactions and is _not_
+available to the normal REST API, but is available to the admin REST API.
+
+Running `monero-lws-admin list_admin` will display all current admin
+accounts, and their current state ("active", "inactive", or "hidden"). If
+an admin account needs to be revoked, use the `modify_account` command
+to put the account into the "inactive" state. Deleting accounts is not
+currently supported.
+
+Every admin REST request must be a `POST` that contains a JSON object with
+an `auth` field (in default settings) and an optional `params` field:
+
+```json
+{
+ "auth":"...",
+ "params":{...}
+ }
+```
+where the `params` object is specified below. The `auth` field can be omitted
+if `--disable-admin-auth` is specified in the CLI arguments for the REST
+server.
+
+## Commands (of Admin REST API)
+A subset of admin commands are available via [admin REST API](/api/admin) - the
+remainder are initially omitted for security purposes. The commands available
+via REST are:
+
+ * [**accept_requests**](#accept_requests): `{"type": "import"|"create", "addresses":[...]}`
+ * [**add_account**](#add_account): `{"address": ..., "key": ...}`
+ * [**list_accounts**](#list_accounts): `{}`
+ * [**list_requests**](#list_requests): `{}`
+ * [**modify_account_status**](#modify_account_status): `{"status": "active"|"hidden"|"inactive", "addresses":[...]}`
+ * [**reject_requests**](#reject_requests): `{"type": "import"|"create", "addresses":[...]}`
+ * [**rescan**](#rescan): `{"height":..., "addresses":[...]}`
+ * [**validate**](#validate): `{"spend_public_hex":..., "view_public_hex":..., "view_key_hex":...}`
+ * [**webhook_add**](#webhook_add): `{"type":"tx-confirmation", "address":"...", "url":"...", ...}` with optional fields:
+ * **token**: A string to be returned when the webhook is triggered
+ * **payment_id**: 16 hex characters representing a unique identifier for a transaction
+ * [**webhook_delete**](#webhook_delete): `{"addresses":[...]}`
+ * [**webhook_delete_uuid**](#webhook_delete_uuid): `{"event_ids": [...]}`
+ * [**webhook_list**](#webhook_list): `{}`
+
+where the listed object must be the `params` field above.
+
+### accept_requests
+Accepts new account and rescan from block 0 requests in the incoming
+queue.
+
+### add_account
+Add account for view-key scanning. An example of the JSON:
+```json
+{
+ "params": {
+ "address": "9uTcr6T9GURRt7UADQc2rhjg5oMYBDyoQ5jgx8nAvVvs757WwDkc2vHLPJhwZfCnfVdnWNvuuKzJe8eMVTKwadYzBrYRG5j",
+ "key": "deadbeef"
+ },
+ "auth": "f50922f5fcd186eaa4bd7070b8072b66fea4fd736f06bd82df702e2314187d09"
+}
+```
+
+### list_accounts
+Request a listing of all active accounts in the database. The request
+should look like:
+```bash
+curl -v -H "Content-Type: application/json" -d '{}' http://127.0.0.1:8081/list_accounts
+```
+when auth is disabled, and when enabled:
+```bash
+curl -v -H "Content-Type: application/json" -d '{"auth": "f50922f5fcd186eaa4bd7070b8072b66fea4fd736f06bd82df702e2314187d09"}' http://127.0.0.1:8081/list_accounts
+```
+The response will look something like:
+```json
+{
+ "active": [
+ {
+ "address": "9wRAu3giCtKhSsVnkZJ7LLE6zqzrmMKpPg39S8aoC7T6F6GobeDpz8TcvVfTQT3ucW82oTYKG8v3ZMAeh8SZVXWwMdvwZew",
+ "scan_height": 2220875,
+ "access_time": 1681244149
+ }
+ ]
+}
+```
+
+### list_requests
+This is a listing of all pending new account requests and all requests
+to import from genesis block requests. When auth is disabled usage
+looks like:
+```bash
+curl -v -H "Content-Type: application/json" -d '{}' http://127.0.0.1:8081/list_requests
+```
+and with auth enabled looks like:
+```bash
+curl -v -H "Content-Type: application/json" -d '{"auth": "f50922f5fcd186eaa4bd7070b8072b66fea4fd736f06bd82df702e2314187d09"}' http://127.0.0.1:8081/list_requests
+```
+### modify_account_status
+This can change an account status to `active`, `inactive` or `hidden`. The
+`active` state is the normal state - the account is being scanned and
+returned by the API. The `inactive` state is still returned by the API,
+but is no longer being scanned. The `hidden` is the current way to
+"delete" an account - it is not scanned nor returned by the API. Accounts
+cannot currently be deleted due to internal DB requirements.
+
+### reject_requests
+This is the opposite of [`accept_requests`](#accept_requests) above. See
+information from that endpoint on how to use this one.
+
+### rescan
+This tells the scanner to rescan specific account(s) from the specified
+height.
+
+### validate
+This takes the spend_public, view_public, and view key all as hex, and then
+does basic validation for the caller: (1) that each value is 64 hex-ascii
+characters, (2) that the public keys are valid ed25519 points, and that (3)
+the view_key matches the view_public.
+
+The return value is the `address` on success, and `error` object on
+validation failure:
+
+#### Example Request
+```json
+{
+ "params": {
+ "view_public_hex": "3e77c1ee5a396cd6c3f68ce343882b2a09317649d6501078911fb17a1adac0b6",
+ "spend_public_hex": "7028d918af2cc4f1cfa499cca2ef014e57124982b99004e9630caf0b25c13954",
+ "view_key_hex": "80..."
+ },
+ "auth": "f50922f5fcd186eaa4bd7070b8072b66fea4fd736f06bd82df702e2314187d09"
+}
+```
+
+#### Example Failure Return
+```json
+{
+ "error": {
+ "field": "view_public_hex",
+ "details": "Invalid public key format"
+ }
+}
+```
+HTTP error codes are still returned if the JSON itself is invalid.
+
+#### Example Success Return
+```json
+{
+ "address": "9wRAu3giCtKhSsVnkZJ7LLE6zqzrmMKpPg39S8aoC7T6F6GobeDpz8TcvVfTQT3ucW82oTYKG8v3ZMAeh8SZVXWwMdvwZew"
+}
+```
+
+
+### webhook_add
+This is used to track events happening in the database: (1) a new payment to
+an optional payment_id, or (2) a new account creation. This endpoint always
+requires a URL for callback purposes.
+
+When the event type is `tx-confirmation`, this endpoint requires a web address
+for callback purposes, a primary (not integrated!) address, and finally the
+type ("tx-confirmation"). The event will remain in the database until one of
+the delete commands ([webhook_delete_uuid](#webhook_delete_uuid) or
+[webhook_delete](#webhook_delete)) is used to remove it.
+
+When the event type is `new-account`, this endpoint requires a web address
+for callback purposes, and the type ("new-account"). Spurious information
+will be returned for this endpoint to simplify the server implementation (i.e.
+several fields returned in the initial call are not useful to new account
+creations).
+
+> The provided URL will use SSL/TLS if `https://` is prefixed in the URL and
+will use plaintext if `http://` is prefixed in the URL. If `zmq` is provided
+as the callback, notifications are performed _only_ over the ZMQ pub socket.
+SSL/TLS connections will use the system certificate authority (root-CAs) by
+default, and will ignore all authority checks if
+`--webhook-ssl-verification none` is provided on the command line when
+starting `monero-lws-daemon`. The webhook will fail if there is a mismatch of
+`http` and `https` between the two servers, and will also fail if `https`
+verification is mismatched. The rule is: (1) if the callback server has
+SSL/TLS disabled, the webhook should use `http://`, (2) if the callback server
+has a self-signed certificate, `https://` and `--webhook-ssl-verification none`
+should be used, and (3) if the callback server is using "Let's Encrypt"
+(or similar), then `https://` with no additional command line flag should be
+used.
+
+
+#### `tx-confirmation`
+##### Initial Request to server
+Example where admin authentication is required (`--disable-admin-auth` NOT
+set on start which is the default):
+```json
+{
+ "auth": "f50922f5fcd186eaa4bd7070b8072b66fea4fd736f06bd82df702e2314187d09",
+ "params": {
+ "type": "tx-confirmation",
+ "url": "http://127.0.0.1:7000",
+ "payment_id": "df034c176eca3296",
+ "token": "1234",
+ "address": "9uTcr6T9GURRt7UADQc2rhjg5oMYBDyoQ5jgx8nAvVvs757WwDkc2vHLPJhwZfCnfVdnWNvuuKzJe8eMVTKwadYzBrYRG5j"
+ }
+}
+```
+
+Example where admin authentication is not required (`--disable-admin-auth` set on start):
+```json
+{
+ "params": {
+ "type": "tx-confirmation",
+ "url": "http://127.0.0.1:7000",
+ "payment_id": "df034c176eca3296",
+ "token": "1234",
+ "address": "9uTcr6T9GURRt7UADQc2rhjg5oMYBDyoQ5jgx8nAvVvs757WwDkc2vHLPJhwZfCnfVdnWNvuuKzJe8eMVTKwadYzBrYRG5j"
+ }
+}
+```
+
+As noted above - `payment_id` and `token` are both optional - `token` will
+default to the empty string, and `payment_id` will default to zero.
+##### Initial Response from Server
+The server will replay all values back to the user for confirmation. An
+additional field - `event_id` - is also returned which contains a globally
+unique value (internally this is a 128-bit `UUID`).
+
+Example response:
+```json
+{
+ "payment_id": "df034c176eca3296",
+ "event_id": "fa10a4db485145f1a24dc09c19a79d43",
+ "token": "1234",
+ "confirmations": 1,
+ "url": "http://127.0.0.1:7000"
+}
+```
+
+If you use the `debug_database` command provided by the `monero-lws-admin`
+executable, the event should be listed in the
+`webhooks_by_account_id,payment_id` field of the returned JSON object. The
+event will remain in the database until an explicit
+[`webhook_delete_uuid`](#webhook_delete_uuid) is invoked.
+
+##### Callback from Server
+When the event "fires" due to a transaction, the provided URL is invoked
+with a JSON payload that looks like the below:
+
+```json
+{
+ "event": "tx-confirmation",
+ "payment_id": "df034c176eca3296",
+ "token": "1234",
+ "confirmations": 1,
+ "id": "fa10a4db485145f1a24dc09c19a79d43",
+ "tx_info": {
+ "id": {
+ "high": 0,
+ "low": 5550229
+ },
+ "block": 2192100,
+ "index": 0,
+ "amount": 4949570000,
+ "timestamp": 1678324181,
+ "tx_hash": "901f9a2a919b6312131537ff6117d56ce2c0dc1f1341b845d7667299e1ef892f",
+ "tx_prefix_hash": "89685cb7acb836fde30fae8be5d8b884e92706df086960d0508e146979ef80dc",
+ "tx_public": "54c153792e47c1da8ceb3979560c424c1928b7b4a089c1c8b3ce99c563e1d240",
+ "rct_mask": "f3449407dc3721299b5309c0c336a17daeebce55165ddd447ba28bbd1f46c201",
+ "payment_id": "df034c176eca3296",
+ "unlock_time": 0,
+ "mixin_count": 15,
+ "coinbase": false
+ }
+}
+```
+which is the same information provided by the user API. The database will
+contain an entry in the `webhook_events_by_account_id,type,block_id,tx_hash,output_id,payment_id,event_id`
+field of the JSON object provided by the `debug_database` command. The
+entry will be removed when the number of confirmations has been reached.
+
+#### `new-account`
+##### Initial Request to server
+Example where admin authentication is required (`--disable-admin-auth` NOT
+set on start which is the default):
+```json
+{
+ "auth": "f50922f5fcd186eaa4bd7070b8072b66fea4fd736f06bd82df702e2314187d09",
+ "params": {
+ "type": "new-account",
+ "url": "http://127.0.0.1:7001",
+ "token": "1234"
+ }
+}
+```
+
+Example where admin authentication is not required (`--disable-admin-auth` set on start):
+```json
+{
+ "params": {
+ "type": "new-account",
+ "url": "http://127.0.0.1:7001",
+ "token": "1234"
+ }
+}
+```
+
+As noted above - `token` is optional - it will default to the empty string.
+
+##### Initial Response from Server
+The server will replay all values back to the user for confirmation. An
+additional field - `event_id` - is also returned which contains a globally
+unique value (internally this is a 128-bit `UUID`). The fields
+`confirmations`, and `payment_id` are sent to simplify the backend, and
+can be ignored when the type is `new-account`.
+
+Example response:
+```json
+{
+ "payment_id": "0000000000000000",
+ "event_id": "c5a735e71b1e4f0a8bfaeff661d0b38a"",
+ "token": "1234",
+ "confirmations": 1,
+ "url": "http://127.0.0.1:7000"
+}
+```
+
+If you use the `debug_database` command provided by the `monero-lws-admin`
+executable, the event should be listed in the
+`webhooks_by_account_id,payment_id` field of the returned JSON object. The
+event will remain in the database until an explicit
+[`webhook_delete_uuid`](#webhook_delete_uuid) is invoked.
+
+##### Callback from Server
+When the event "fires" due to a new account creation, the provided URL is
+invoked with a JSON payload that looks like the below:
+
+```json
+{
+ "event_id": "c5a735e71b1e4f0a8bfaeff661d0b38a",
+ "token": "",
+ "address": "9zGwnfWRMTF9nFVW9DNKp46aJ43CRtQBWNFvPqFVSN3RUKHuc37u2RDi2GXGp1wRdSRo5juS828FqgyxkumDaE4s9qyyi9B"
+}
+```
+
+
+### webhook_delete
+Deletes all webhooks associated with a specific Monero primary address.
+
+### webhook_delete_uuid
+Deletes all references to a specific webhook referenced by its UUID
+(`event_id`)
+
+### webhook_list
+This will list every webhook that is currently "listening" for
+incoming transactions. If the server has auth disabled, the
+request is simply:
+
+```bash
+curl -v -H "Content-Type: application/json" -d '{}' http://127.0.0.1:8081/webhook_list
+```
+and with auth enabled looks like:
+```bash
+curl -v -H "Content-Type: application/json" -d '{"auth": "f50922f5fcd186eaa4bd7070b8072b66fea4fd736f06bd82df702e2314187d09"}' http://127.0.0.1:8081/webhook_list
+```
+
+which returns a JSON object that looks like:
+```json
+{
+ "webhooks": [
+ {
+ "key": {
+ "user": 1,
+ "type": "tx-confirmation"
+ },
+ "value": [
+ {
+ "payment_id": "9bc1a59b34253896",
+ "event_id": "4dc201838af54dfe88686bea7e2b599f",
+ "token": "12345",
+ "confirmations": 5,
+ "url": "http://127.0.0.1:8082"
+ },
+ {
+ "payment_id": "9bc1a59b34253896",
+ "event_id": "615171e477464401a1a23cdb45b3b433",
+ "token": "12345",
+ "confirmations": 5,
+ "url": "http://127.0.0.1:8082"
+ },
+ {
+ "payment_id": "9bc1a59b34253896",
+ "event_id": "e64be3ad6d1647618fbd292be0485901",
+ "token": "this is a fresh test",
+ "confirmations": 1,
+ "url": "http://127.0.0.1:8082/foobar"
+ },
+ {
+ "payment_id": "9bc1a59b34253896",
+ "event_id": "fe692cdf7de1453898ad453d8fabce42",
+ "token": "12345",
+ "confirmations": 5,
+ "url": "http://127.0.0.1:8082/foobar"
+ }
+ ]
+ }
+ ]
+}
+```
+
+# Examples
+
+## Admin REST API
+
+### Default Settings
+```json
+{
+ "auth":"6d732245002a9499b3842c0a7f9fc6b2d657c77bd612dbefa4f7f9357d08530a",
+ "params":{
+ "status": "inactive",
+ "addresses": ["9sAejnQ9EBR1111111111111111111111111111111111AdYmVTw2Tv6L9KYkHjJ2wd737ov8ZL5QU7CJ4zV6basGP9fyno"]
+ }
+ }
+```
+will put the listed address into the "inactive" state.
+
+### `--disable-admin-auth` Setting
+```json
+{
+ "params":{
+ "status": "inactive",
+ "addresses": ["9sAejnQ9EBR1111111111111111111111111111111111AdYmVTw2Tv6L9KYkHjJ2wd737ov8ZL5QU7CJ4zV6basGP9fyno"]
+ }
+ }
+```
+
+## monero-lws-admin
+
+**List every active Monero address on a newline:**
+ ```bash
+ monero-lws-admin list_accounts | jq -r '.active | .[] | .address'
+ ```
+
+**Auto-accept every pending account creation request:**
+ ```bash
+ monero-lws-admin accept_requests create $(monero-lws-admin list_requests | jq -j '.create? | .[]? | .address?+" "')
+ ```
+# Debugging
+
+`monero-lws-admin` has a debug mode that dumps everything stored in the
+database, except the blockchain hashes are always truncated and viewkeys are
+omitted by default (a command-line flag can enable viewkey output). Most of
+the array outputs are sorted to accelerate `jq` filtering and search queries.
+
+## Indexes
+
+ - **blocks_by_id** - array of objects sorted by block height.
+ - **accounts_by_status,id** - A single object where account status names are
+ keys. Each value is an array of objects sorted by account id.
+ - **accounts_by_address** - A single object where account addresses are keys.
+ Each value is an object containing the status and account id for the account
+ for lookup in `accounts_by_status,id`. The majority of account lookups should
+ be done by this id (an integer).
+ - **accounts_by_height,id** - An array of objects sorted by block height. These
+ objects contain another array of objects sorted by account id.
+ - **outputs_by_account_id,block_id,tx_hash,output_id** - An object where keys
+ are account ids. Each value is an array of objects sorted by block height,
+ transaction hash, then by output number.
+ - **spends_by_account_id,block_id,tx_hash,image** - An object where keys are
+ account ids. Each value is an array of objects sorted by block height,
+ transaction hash, then by key image.
+ - **requests_by_type,address** - An object where keys are request type, and
+ each value is an array of objects sorted by address.
+
+## Examples
+
+**List every key-image associated with every account:**
+ ```bash
+ monenero-lws-admin debug_database | jq '."spends_by_account_id,block_id,tx_hash,output_id" | map_values([.[] | .image])'
+ ```
+will output something like:
+ ```json
+ {"1":["image1", "image2",...],"2":["image1","image2"...],...}
+ ```
+
+**List every account that received XMR in a given transaction hash:**
+ ```bash
+ monenero-lws-admin debug_database | jq '."outputs_by_account_id,block_id,tx_hash,output_id" | map_values(select([.[] | .tx_hash == "hash"] | any)) | keys'
+ ```
+will output somethng like:
+ ```json
+ {"1",...}
+ ```
+
+**Add total received XMR for every account**:
+ ```bash
+ monenero-lws-admin debug_database | jq '."outputs_by_account_id,block_id,tx_hash,output_id" | map_values([.[] | .amount] | add)'
+ ```
+will output something like:
+ ```json
+ {"1":6346,"2":45646}
+ ```
+
+# Extending Administration in monero-lws
+
+## JSON via `stdin`
+
+Some commands take sensitive information such as private view keys, and
+therefore reading arguments from `stdin` via JSON array would also be useful for
+those situations. This should be a relatively straightforward adaptation given
+the design of the positional arguments.
+
+## Administration via ZeroMQ
+
+The LMDB database does account lookups by view-public only, so that CurveZMQ
+(which uses curve25519) can be used to authenticate an administration account
+without additional protocol overhead. The parameters to administration commands
+can be sent via JSON or MsgPack array since the functions already use positional
+arguments.
diff --git a/docs/apps/client.md b/docs/apps/client.md
new file mode 100644
index 0000000..e69de29
diff --git a/docs/apps/daemon.md b/docs/apps/daemon.md
new file mode 100644
index 0000000..2d59d38
--- /dev/null
+++ b/docs/apps/daemon.md
@@ -0,0 +1,81 @@
+# monero-lws-daemon
+
+`monero-lws-daemon` hosts the HTTP/1.1 server components of the
+[wallet](/api/wallet) and [admin](/api/admin) API.
+The wallet API is enabled by default on `http://127.0.0.1:8443`, and the admin
+API is disabled by default. The daemon will listen for wallet API requests on
+every `--rest-server` argument (multiple can be supplied). The same properties
+apply to `--admin-rest-server` with the admin API.
+
+The wallet API and admin API can be merged into one server instance through a
+shared prefix: `--rest-server http://127.0.0.1:8443/wallet` combined with
+` --admin-rest-server http://127.0.0.1:8443/admin` will listen on one port for
+both APIs, and the client software must use `/wallet` or `/admin` to access
+the correct API.
+
+## SSL/TLS
+
+Every REST endpoint that starts with `https://` will only communicate via
+SSL/TLS. By default, a new certificate is created each time the lws daemon
+starts. The commands `--rest-ssl-key` and `--rest-ssl-certificate` allow for
+explicit server keys to be used, and they are applied to _every_ `https://`
+REST argument (both wallet API and admin API).
+
+## Subaddresses
+
+Subaddresses are disabled by default in the lws daemon - all relevant endpoints
+will fail. The option `--max-subaddresses`, when specified with a non-zero
+value, will enable subaddress endpoints and impose restrictions on their
+creation. If the value is lowered in the future, existing subaddresses are not
+retroactively removed, so the limit is enforced purely at creation time.
+
+If `--max-subaddresses` is zero or left unspecified, it will always disable
+subaddresses, even if a user previously created some. So it is generally
+recommended that once enabled, they always be enabled, even if the value
+is lowered to `1` to discourage their use.
+
+## ZeroMQ Sub
+
+The daemon supports ZeroMQ SUB from `monerod` for instant mempool and block
+notification. The `monerod` instance must use the option `--zmq-pub` and
+`monero-lws-daemon` must use the `--sub` option. Both executables support
+`tcp://` and `ipc://` for the rendevous location.
+
+When a new block is processed by `monerod`, the lws daemon will immediately
+begin processing as a result of the notification. Without proper `--sub`
+usage, the lws daemon will poll the `monerod` instance at 20 second intervals.
+
+The wallet API will **NOT** report mempool transactions due to a limitation
+of the design. Webhooks **will** report mempool ("0-conf") transactions.
+
+## Webhooks
+
+The [admin API](/api/admin) has an endpoint, `/webhook_add`, with an `url`
+field that must start with `http://`, `https://`, or `zmq`. See the link to
+the API for specs/schema.
+
+Every webhook is "simulcast" over an optional ZeroZQ PUB socket specified with
+`--zmq-pub` (which is different from `monerod --zmq-pub`). If the webhook was
+created with `params.url == zmq` then the notification occurs only over ZeroMQ.
+
+The `params.address` field of `/webhook_add`, when needed as per spec, must
+be a primary address. Subaddresses registered with the wallet API are
+automatically reported to any primary address webhook. Omit `payment_id` to
+report on _every_ transaction to that account.
+
+See [webhooks documentation](/usage/webhooks) for more information.
+
+## Remote Scanning
+
+The view-key scanning process can be spread to multiple machines with the
+`--lws-server-addr` option. The remote side should use the
+[`monero-lws-client`](#monero-lws-client) executable, and connect back to the
+`monero-lws-daemon` location. Usage of `--lws-server-pass` is highly
+recommended when doing remote scanning, as it will prevent rogue processes
+from hijacking the scan process. Also consider specifying the password in a
+ `--config-file`, otherwise the process list will leak the password.
+
+The protocol is a custom unencrypted binary format, so `ssh -L ...` should be
+used on the `monero-lws-client` side to provide proper encryption, and
+additional authentication).
+
diff --git a/docs/balance_new_addresses.md b/docs/balance_new_addresses.md
deleted file mode 100644
index a1bb991..0000000
--- a/docs/balance_new_addresses.md
+++ /dev/null
@@ -1,122 +0,0 @@
-# Balance New Addresses
-
-## Overview
-
-The `--balance-new-addresses` option changes how new addresses are assigned to scanner threads while the scanner is actively running. Instead of using round-robin distribution, addresses are assigned to threads based on their current scanning progress, ensuring better workload distribution and faster synchronization.
-
-## Motivation
-
-The default round-robin algorithm assigns new addresses sequentially to threads without considering their current state. This can lead to:
-- New addresses being assigned to threads that are far behind, causing unnecessary delays
-- Imbalanced workload when addresses with different scan heights are added
-- Inefficient thread utilization when threads finish at different times
-
-The balance-new-addresses algorithm addresses these issues by intelligently matching new addresses to threads based on their scanning progress.
-
-## Configuration
-
-- `--balance-new-addresses`: Enable balanced assignment of new addresses to threads (default: false)
-
-## Usage
-
-Enable balanced new address assignment:
-```bash
-monero-lws-daemon --balance-new-addresses [other options]
-```
-
-Or in config file:
-```
-balance-new-addresses=true
-```
-
-## Algorithm
-
-### Thread Selection Criteria
-
-When a new address is added (e.g., an inactive account becomes active), the algorithm selects a thread using the following priority:
-
-1. **Primary preference**: Thread with the highest scan height that is **not above** the account's scan height
- - If multiple threads meet this criteria, choose the one with the fewest addresses
-
-2. **Fallback**: If all threads are above the account's scan height, choose the thread with the **lowest** scan height
- - This minimizes backtracking when all threads have already passed the account's starting point
- - If multiple threads have the same lowest height, choose the one with the fewest addresses
-
-### Scan Height Tracking
-
-Each thread maintains a `current_min_height` value representing the minimum scan height of all accounts currently assigned to that thread. This value is:
-- Updated when new accounts are added to the thread (if the new account has a lower scan height)
-- Updated periodically as the thread processes blocks and advances its scanning progress
-- Used by the algorithm to determine which thread is best suited for a new account
-
-### Scope
-
-- **Only affects local threads**: The algorithm only applies to threads running on the local daemon. When enabled and local threads are available, **all new addresses are assigned exclusively to local threads**. Remote scanner clients will not receive any new addresses unless there are zero local threads available.
-- **Only affects new addresses**: This algorithm is used when addresses are added dynamically (e.g., inactive accounts becoming active). Initial thread assignment at startup uses the standard block-depth-threading or round-robin algorithms.
-- **Works with other threading options**: Can be used alongside `--block-depth-threading` and `--split-synced` options.
-
-## Example
-
-Consider a scenario with 3 local threads and a new account that needs to be assigned:
-
-**Thread states:**
-- Thread 0: scanning at height 3,000,000 (5 accounts)
-- Thread 1: scanning at height 3,100,000 (3 accounts)
-- Thread 2: scanning at height 2,900,000 (7 accounts)
-
-**Scenario 1: New account at height 3,050,000**
-- Thread 0: height 3,000,000 ≤ 3,050,000 ✓
-- Thread 1: height 3,100,000 > 3,050,000 ✗
-- Thread 2: height 2,900,000 ≤ 3,050,000 ✓
-- **Result**: Thread 0 is selected (highest height ≤ account height, and fewer accounts than Thread 2)
-
-**Scenario 2: New account at height 2,800,000 (all threads are above)**
-- Thread 0: height 3,000,000 > 2,800,000 ✗
-- Thread 1: height 3,100,000 > 2,800,000 ✗
-- Thread 2: height 2,900,000 > 2,800,000 ✗
-- **Result**: Thread 2 is selected (lowest height among all threads, minimizing backtracking)
-
-**Scenario 3: New account at height 3,200,000 (all threads are below)**
-- Thread 0: height 3,000,000 ≤ 3,200,000 ✓
-- Thread 1: height 3,100,000 ≤ 3,200,000 ✓
-- Thread 2: height 2,900,000 ≤ 3,200,000 ✓
-- **Result**: Thread 1 is selected (highest height ≤ account height, and fewer accounts than Thread 0)
-
-## Comparison with Round-Robin
-
-**Round-Robin Algorithm** (default):
-- Assigns addresses sequentially: Thread 0, Thread 1, Thread 2, Thread 0, ...
-- Does not consider thread state or account scan height
-- Simple and predictable, but can lead to suboptimal assignments
-
-**Balance New Addresses Algorithm**:
-- Considers each thread's current scanning progress
-- Matches accounts to threads based on scan height compatibility
-- More complex, but provides better workload distribution and faster synchronization
-
-## When New Addresses Are Added
-
-The balance-new-addresses algorithm is triggered when:
-- An inactive account becomes active (status changes from inactive to active)
-- New accounts are detected during periodic checks (every 10 seconds)
-
-It is **not** triggered by:
-- Initial thread assignment at startup (uses block-depth-threading or round-robin)
-- Full account reassignment (e.g., after rescan operations)
-- Accounts assigned to remote scanner clients
-
-## Benefits
-
-- **Faster synchronization**: New addresses are assigned to threads that are already at or near their scan height
-- **Reduced backtracking**: Minimizes cases where threads need to scan backwards to process new accounts
-- **Better workload distribution**: Accounts are distributed based on actual thread progress rather than arbitrary rotation
-- **Improved thread utilization**: Threads that are ahead receive new work more efficiently
-
-## Limitations
-
-- **Remote threads excluded**: When enabled and local threads are available, remote scanner clients will not receive any new addresses. Remote threads only receive work if there are zero local threads available.
-- Does not affect initial thread assignment at startup
-- Requires tracking thread scan heights, which adds minimal overhead
-- May not provide benefits if all threads are at similar heights
-- **Important**: If you have remote scanner clients and want them to receive new addresses, do not enable this option, or ensure you have zero local scanner threads configured
-
diff --git a/docs/block_depth_threading.md b/docs/block_depth_threading.md
deleted file mode 100644
index 9cdea0d..0000000
--- a/docs/block_depth_threading.md
+++ /dev/null
@@ -1,106 +0,0 @@
-# Block Depth Threading
-
-## Overview
-
-Block depth threading is a work distribution algorithm that balances scanner thread workload based on the amount of blockchain data each address needs to process, rather than simply distributing addresses evenly across threads. This approach addresses the performance inefficiency where threads with fewer blocks to scan finish early and become idle while other threads continue processing.
-
-## Motivation
-
-The default threading algorithm distributes addresses evenly across threads, treating each address as an equal unit of work. However, addresses have vastly different synchronization requirements:
-- A fully synced address may only need to process a few recent blocks
-- A newly added address may need to scan hundreds of thousands of blocks
-
-This imbalance causes threads with mostly synced addresses to finish quickly and remain idle while threads with unsynced addresses continue working, resulting in poor CPU utilization and longer overall sync times.
-
-## Configuration
-
-- `--block-depth-threading`: Enable block depth threading algorithm (default: false)
-- `--min-block-depth`: Minimum block depth value for workload calculations (default: 16)
-
-## Usage
-
-Enable block depth threading with default settings:
-```bash
-monero-lws-daemon --block-depth-threading [other options]
-```
-
-Enable with custom minimum block depth:
-```bash
-monero-lws-daemon --block-depth-threading --min-block-depth=32 [other options]
-```
-
-Or in config file:
-```
-block-depth-threading=true
-min-block-depth=32
-```
-
-## Algorithm
-
-### Block Depth Calculation
-
-For each address, the **block depth** is calculated as the number of blocks remaining to be scanned:
-
-```
-blockdepth = max(current_blockchain_height - address_scan_height, min_block_depth)
-```
-
-Where `min_block_depth` is the value specified by `--min-block-depth` (default: 16).
-
-Addresses with blockdepth less than the minimum are assigned the minimum value. This prevents edge cases where fully synced addresses would have zero blockdepth, which could cause:
-- Division by zero or near-zero values in workload calculations
-- Degenerate cases where many fully-synced accounts get assigned together
-- Poor workload distribution when most accounts are fully synced
-
-### Minimum Block Depth
-
-The `--min-block-depth` flag sets the minimum block depth value used in workload calculations. This ensures that even fully synced accounts contribute meaningfully to workload balancing.
-
-**Default value**: 16 blocks
-
-**Example**: With `--min-block-depth=16`, an account at the current blockchain height (0 blocks remaining) is treated as having 16 blocks remaining for workload distribution purposes.
-
-**When to adjust**: You may want to increase this value if you have many fully synced accounts and want them to contribute more to workload balancing, or decrease it if you want more precise distribution for nearly-synced accounts.
-
-### Thread Assignment
-
-1. **Calculate total work**: Sum all address blockdepths to get `total_blockdepth`
-2. **Calculate target per thread**: `blockdepth_per_thread = total_blockdepth / thread_count`
-3. **Sort addresses**: Order by blockdepth (smallest first)
-4. **Distribute to threads**:
- - Addresses are assigned sequentially to threads
- - Accounts are added to the current thread until the cumulative depth reaches or exceeds the target
- - When target is reached, move to the next thread
- - Final thread receives any remaining addresses
-
-This overallocation strategy ensures more balanced workload distribution and better thread utilization throughout the scanning process.
-
-## Example
-
-With 4 threads and 20 accounts with varying sync states:
-- Accounts A-H: 16 blocks each (synced, at minimum) = 128 blocks
-- Accounts I-L: 100 blocks each = 400 blocks
-- Accounts M-P: 300 blocks each = 1,200 blocks
-- Accounts Q-T: 500 blocks each = 2,000 blocks
-
-**Old Algorithm** (by count, evenly distributed - 5 accounts per thread):
-- Thread 0: A, B, C, D, E (80 blocks) ✓ finishes immediately
-- Thread 1: F, G, H, I, J (228 blocks) ⏱
-- Thread 2: K, L, M, N, O (1,028 blocks) ⏱⏱
-- Thread 3: P, Q, R, S, T (2,100 blocks) ⏱⏱⏱⏱ takes much longer
-- **Problem**: Despite equal account count (5 per thread), massive workload imbalance - thread 3 has 26x more work than thread 0
-
-**New Algorithm** (by depth, balanced workload with alternating over/under allocation):
-- Total: 3,728 blocks, target: 932 blocks/thread
-- Thread 0: (even, over-allocate): A, B, C, D, E, F, G, H, I, J, K, L, M, N (1,128 blocks)
-- Thread 1: (odd, under-allocate): O, P (600 blocks)
-- Thread 2: (even, over-allocate): Q, R (1,000 blocks)
-- Thread 3: (odd, under-allocate): S, T (1,000 blocks)
-- **Result**: All 4 threads utilized with better balance (600-1,128 vs 80-2,100 blocks), synced accounts efficiently grouped
-
-## Benefits
-
-- **Improved parallelization**: All threads remain active longer
-- **Reduced sync time**: More efficient CPU utilization
-- **Better resource usage**: Eliminates idle threads waiting for others to complete
-- **Predictable performance**: Workload is distributed based on actual work required
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..8fec412
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,53 @@
+# monero-lws Overview
+
+[monero-lws](https://github.com/vtnerd/monero-lws) is a Monero light-wallet
+server that adheres to the community ratified
+[LWS spec](https://github.com/monero-project/meta/blob/master/api/lightwallet_rest.md).
+The server does all "cryptographic scanning" that is required of a Monero
+wallet, and is non-custodial (the "spend" keys are never sent to server). It is
+compatible with [Skylight wallet](https://skylight.magicgrants.org), and
+[lwcli](https://github.com/cifro-codes/lwcli).
+
+## Background
+
+Monero wallets require complex scanning/decryption against _every_ transaction
+to determine if funds were sent to that wallet. This process can be offloaded
+to an outside process **without** giving up the "spend" keys of the wallet. The
+LWS API provides a standard mechanism for splitting the "scanning" process from
+the "spend" process.
+
+The only negative of splitting this process is privacy - the "LWS server" knows
+all incoming funds and can typically infer outgoing funds as well. This privacy
+issue can be mitigated by self-hosting a LWS server. `monero-lws` and
+[OpenMonero](https://github.com/moneroexamples/openmonero)
+are the two options for self-hosting.
+
+## Clients
+
+The two available wallets are [lwcli](/lwcli/) and
+[Skylight wallet](https://skylight.magicgrants.org). `lwcli` is a TUI wallet
+aimed at advanced users, whereas `Skylight` is a beginner wallet.
+
+Programmers wishing to create a new LWS wallet should investigate the
+[lwsf](https://github.com/vtnerd/lwsf) project which provides a simple C++
+interface of typical wallet functions while performing all of the heavy lifting
+of communicating to the server and creating transactions.
+
+## Versions
+
+The `develop` branch is considered "nightly", and the `master` branch is
+considered the "alpha" branch. Only developers should use `develop` because
+DB changes could be incompatible from commit to commit.
+
+After changes are well tested, they are moved to the `master` branch which
+should rarely see incompatible DB changes. Users of this branch should
+check the `src/lws_version.h.in` file - a major (first) number change indicates
+that a incompatible DB change has been made. You will be unable to "roll-back"
+to a prior `master` commit unless you save your DB and re-use it with that
+older commited.
+
+The `release-v` branches indicate beta/stable versions of the software. The
+major (first) number indicates the DB version. Upgrades from lower numbers
+to higher numbers is possible, but the reverse is never true. The minor
+(second) number refers to stability: new features are never imported into a
+release branch, instead a new release with higher major/minor number are used.
diff --git a/docs/installation/docker.md b/docs/installation/docker.md
new file mode 100644
index 0000000..d4bad7c
--- /dev/null
+++ b/docs/installation/docker.md
@@ -0,0 +1,15 @@
+# Docker
+Docker is the easiest way to get a recent copy of `monero-lws`. The paths are:
+```bash
+docker pull vtnerd/monero-lws
+docker pull ghcr.io/vtnerd/monero-lws
+```
+which will download the latest stable release. A bleeding edge version can be
+retrieved with:
+```bash
+docker pull vtnerd/monero-lws:master
+docker pull ghcr.io/vtnerd/monero-lws:master
+```
+which is considered "alpha" software. The docker build comes with
+`monero-lws-daemon` and `monero-lws-admin`.
+
diff --git a/docs/installation/source.md b/docs/installation/source.md
new file mode 100644
index 0000000..2e694b5
--- /dev/null
+++ b/docs/installation/source.md
@@ -0,0 +1,80 @@
+# From Source
+
+## Branches
+Building from source requires deciding on a branch to compile:
+
+ * `develop` is where all merges go first. So this the "nightly" branch, and
+ is generally only recommended for developers. This should compile against
+ the `master` branch of `monerod`.
+ * `master` is where tested merges go - making this the "alpha" branch.
+ Recommended only if some new feature(s) are needed. This should compile
+ against the `master` branch of `monerod`.
+ * `release-v...` - The first number specifies the `monero-lws` version and
+ the second number specifies the `monerod` version. Example:
+ `release-v0.3_0.18` is the v0.3 version of `monero-lws` to be compiled
+ against the v0.18 version of `monerod`. The most recent release branch is
+ recommended for the bulk of users. The supported release branches are:
+ * release-v0.3_0.18
+
+## Dependencies
+After deciding on a branch, the next step is dependency installation. The only
+dependencies are those required by `monerod` - go to
+the [`monerod` dependency section](https://github.com/monero-project/monero/?tab=readme-ov-file#dependencies)
+and follow those instructions for installing dependencies. Then come back to
+this guide.
+
+## Building
+### Beginner
+
+The "beginner" method for compilation lets the git submodules do all of the
+hard work, but is only available on `develop` and `master` branches. The
+process is simply:
+
+```bash
+git clone https://github.com/vtnerd/monero-lws.git
+git checkout master
+git submodule update --init --recursive
+mkdir monero-lws/build && cd monero-lws/build
+cmake -DCMAKE_BUILD_TYPE=Release ..
+make -j$(nproc)
+```
+
+If on macOS, replace `-j$(nproc)` with `-j8` (or the number of cores on your
+system). This should produce three binaries in the `monero/build/src` folder:
+`monero-lws-daemon`, `monero-lws-admin`, and `monero-lws-client`. You can now
+move onto [usage](/monero-lws/usage/).
+
+### Advanced
+
+If you already have the `monerod` source tree on your system, you can save some
+space by re-using that copy. You need to manually ensure that the `monerod`
+branch is the one needed by `monero-lws`. The
+[first instruction](#building-from-source) specifies the expected `monerod`
+branch foreach `monero-lws` branch.
+
+The process skips the submodule init and specifies the monero tree manually:
+
+```bash
+git clone https://github.com/vtnerd/monero-lws.git
+git checkout origin/release-v0.3_0.18
+mkdir monero-lws/build && cd monero-lws/build
+cmake -DMONERO_SOURCE_DIR=/path/to/monero_0.18/source -DCMAKE_BUILD_TYPE=Release ..
+make -j$(nproc)
+```
+
+Again, replace `-j$(nproc)` on macOS with appropriate core count. This should
+produce three executables in the `monero-lws/build/src` directory. Move onto
+[usage](/monero-lws/usage/) portion.
+
+### Other Build Options
+
+Advanced users/developers may wish to specify additional build-time options
+supported by the project. The easiest is specifying `-DSTATIC=ON` at the
+cmake stage, which tries to use static libraries where possible.
+
+The other available option is `-DBUILD_TESTS=ON` which can also be specified at
+the cmake stage. This creates an executable `tests/unit/monero-lws-unit`.
+
+The last option is `-DSANITIZER=address` which can be specified at the cmake
+stage. This builds both monero source tree and lws source tree with the address
+sanitizer.
diff --git a/docs/rmq.md b/docs/rmq.md
deleted file mode 100644
index 66bbd14..0000000
--- a/docs/rmq.md
+++ /dev/null
@@ -1,55 +0,0 @@
-# monero-lws RabbitMQ Usage
-Monero-lws uses RabbitMQ to provide JSON notifications of payment_id
-(web)hooks. The feature is optional - by default LWS is _not_ compiled with
-RabbitMQ support. The cmake option -DWITH_RMQ=ON must be specified during the
-configuration stage to enable the feature.
-
-The notification location is specified with `--rmq-address`, the login details
-are specified with `--rmq-credentials` (specified as `user:pass`), the exchange
-is specified with `--rmq-exchange`, and routing name is specified with
-`--rmq-routing`.
-
-## `json-full-payment_hook`
-Only events of this type are sent to RabbitMQ. They are always "simulcast" with
-the webhook URL. If the webhook URL is `zmq` then no simulcast occurs - the
-event is only sent via ZeroMQ and/or RabbitMQ (if both are configured then it
-sent to both, otherwise it is only sent to the one configured).
-
-Example of the "raw" output to RabbitMQ:
-
-```json
-{
- "index": 2,
- "event": {
- "event": "tx-confirmation",
- "payment_id": "4f695d197f2a3c54",
- "token": "single zmq wallet",
- "confirmations": 1,
- "event_id": "3894f98f5dd54af5857e4f8a961a4e57",
- "tx_info": {
- "id": {
- "high": 0,
- "low": 5666768
- },
- "block": 2265961,
- "index": 1,
- "amount": 3117324236131,
- "timestamp": 1687301600,
- "tx_hash": "ef3187775584351cc5109de124b877bcc530fb3fdbf77895329dd447902cc566",
- "tx_prefix_hash": "064884b8a8f903edcfebab830707ed44b633438b47c95a83320f4438b1b28626",
- "tx_public": "54dce1a6eebafa2fdedcea5e373ef9de1c3d2737ae9f809e80958d1ba4590d74",
- "rct_mask": "4cdc4c4e340aacb4741ba20f9b0b859242ecdad2fcc251f71d81123a47db3400",
- "payment_id": "4f695d197f2a3c54",
- "unlock_time": 0,
- "mixin_count": 15,
- "coinbase": false
- }
- }
-}
-```
-> `index` is a counter used to detect dropped messages. It is not useful to
-RabbitMQ but is a carry-over from ZeroMQ (the same serialized message is used
-to send to both).
-
-> The `block` and `id` fields in the above example are NOT present when
-`confirmations == 0`.
diff --git a/docs/scan/balance_new_addresses.md b/docs/scan/balance_new_addresses.md
new file mode 100644
index 0000000..a1bb991
--- /dev/null
+++ b/docs/scan/balance_new_addresses.md
@@ -0,0 +1,122 @@
+# Balance New Addresses
+
+## Overview
+
+The `--balance-new-addresses` option changes how new addresses are assigned to scanner threads while the scanner is actively running. Instead of using round-robin distribution, addresses are assigned to threads based on their current scanning progress, ensuring better workload distribution and faster synchronization.
+
+## Motivation
+
+The default round-robin algorithm assigns new addresses sequentially to threads without considering their current state. This can lead to:
+- New addresses being assigned to threads that are far behind, causing unnecessary delays
+- Imbalanced workload when addresses with different scan heights are added
+- Inefficient thread utilization when threads finish at different times
+
+The balance-new-addresses algorithm addresses these issues by intelligently matching new addresses to threads based on their scanning progress.
+
+## Configuration
+
+- `--balance-new-addresses`: Enable balanced assignment of new addresses to threads (default: false)
+
+## Usage
+
+Enable balanced new address assignment:
+```bash
+monero-lws-daemon --balance-new-addresses [other options]
+```
+
+Or in config file:
+```
+balance-new-addresses=true
+```
+
+## Algorithm
+
+### Thread Selection Criteria
+
+When a new address is added (e.g., an inactive account becomes active), the algorithm selects a thread using the following priority:
+
+1. **Primary preference**: Thread with the highest scan height that is **not above** the account's scan height
+ - If multiple threads meet this criteria, choose the one with the fewest addresses
+
+2. **Fallback**: If all threads are above the account's scan height, choose the thread with the **lowest** scan height
+ - This minimizes backtracking when all threads have already passed the account's starting point
+ - If multiple threads have the same lowest height, choose the one with the fewest addresses
+
+### Scan Height Tracking
+
+Each thread maintains a `current_min_height` value representing the minimum scan height of all accounts currently assigned to that thread. This value is:
+- Updated when new accounts are added to the thread (if the new account has a lower scan height)
+- Updated periodically as the thread processes blocks and advances its scanning progress
+- Used by the algorithm to determine which thread is best suited for a new account
+
+### Scope
+
+- **Only affects local threads**: The algorithm only applies to threads running on the local daemon. When enabled and local threads are available, **all new addresses are assigned exclusively to local threads**. Remote scanner clients will not receive any new addresses unless there are zero local threads available.
+- **Only affects new addresses**: This algorithm is used when addresses are added dynamically (e.g., inactive accounts becoming active). Initial thread assignment at startup uses the standard block-depth-threading or round-robin algorithms.
+- **Works with other threading options**: Can be used alongside `--block-depth-threading` and `--split-synced` options.
+
+## Example
+
+Consider a scenario with 3 local threads and a new account that needs to be assigned:
+
+**Thread states:**
+- Thread 0: scanning at height 3,000,000 (5 accounts)
+- Thread 1: scanning at height 3,100,000 (3 accounts)
+- Thread 2: scanning at height 2,900,000 (7 accounts)
+
+**Scenario 1: New account at height 3,050,000**
+- Thread 0: height 3,000,000 ≤ 3,050,000 ✓
+- Thread 1: height 3,100,000 > 3,050,000 ✗
+- Thread 2: height 2,900,000 ≤ 3,050,000 ✓
+- **Result**: Thread 0 is selected (highest height ≤ account height, and fewer accounts than Thread 2)
+
+**Scenario 2: New account at height 2,800,000 (all threads are above)**
+- Thread 0: height 3,000,000 > 2,800,000 ✗
+- Thread 1: height 3,100,000 > 2,800,000 ✗
+- Thread 2: height 2,900,000 > 2,800,000 ✗
+- **Result**: Thread 2 is selected (lowest height among all threads, minimizing backtracking)
+
+**Scenario 3: New account at height 3,200,000 (all threads are below)**
+- Thread 0: height 3,000,000 ≤ 3,200,000 ✓
+- Thread 1: height 3,100,000 ≤ 3,200,000 ✓
+- Thread 2: height 2,900,000 ≤ 3,200,000 ✓
+- **Result**: Thread 1 is selected (highest height ≤ account height, and fewer accounts than Thread 0)
+
+## Comparison with Round-Robin
+
+**Round-Robin Algorithm** (default):
+- Assigns addresses sequentially: Thread 0, Thread 1, Thread 2, Thread 0, ...
+- Does not consider thread state or account scan height
+- Simple and predictable, but can lead to suboptimal assignments
+
+**Balance New Addresses Algorithm**:
+- Considers each thread's current scanning progress
+- Matches accounts to threads based on scan height compatibility
+- More complex, but provides better workload distribution and faster synchronization
+
+## When New Addresses Are Added
+
+The balance-new-addresses algorithm is triggered when:
+- An inactive account becomes active (status changes from inactive to active)
+- New accounts are detected during periodic checks (every 10 seconds)
+
+It is **not** triggered by:
+- Initial thread assignment at startup (uses block-depth-threading or round-robin)
+- Full account reassignment (e.g., after rescan operations)
+- Accounts assigned to remote scanner clients
+
+## Benefits
+
+- **Faster synchronization**: New addresses are assigned to threads that are already at or near their scan height
+- **Reduced backtracking**: Minimizes cases where threads need to scan backwards to process new accounts
+- **Better workload distribution**: Accounts are distributed based on actual thread progress rather than arbitrary rotation
+- **Improved thread utilization**: Threads that are ahead receive new work more efficiently
+
+## Limitations
+
+- **Remote threads excluded**: When enabled and local threads are available, remote scanner clients will not receive any new addresses. Remote threads only receive work if there are zero local threads available.
+- Does not affect initial thread assignment at startup
+- Requires tracking thread scan heights, which adds minimal overhead
+- May not provide benefits if all threads are at similar heights
+- **Important**: If you have remote scanner clients and want them to receive new addresses, do not enable this option, or ensure you have zero local scanner threads configured
+
diff --git a/docs/scan/block_depth_threading.md b/docs/scan/block_depth_threading.md
new file mode 100644
index 0000000..9cdea0d
--- /dev/null
+++ b/docs/scan/block_depth_threading.md
@@ -0,0 +1,106 @@
+# Block Depth Threading
+
+## Overview
+
+Block depth threading is a work distribution algorithm that balances scanner thread workload based on the amount of blockchain data each address needs to process, rather than simply distributing addresses evenly across threads. This approach addresses the performance inefficiency where threads with fewer blocks to scan finish early and become idle while other threads continue processing.
+
+## Motivation
+
+The default threading algorithm distributes addresses evenly across threads, treating each address as an equal unit of work. However, addresses have vastly different synchronization requirements:
+- A fully synced address may only need to process a few recent blocks
+- A newly added address may need to scan hundreds of thousands of blocks
+
+This imbalance causes threads with mostly synced addresses to finish quickly and remain idle while threads with unsynced addresses continue working, resulting in poor CPU utilization and longer overall sync times.
+
+## Configuration
+
+- `--block-depth-threading`: Enable block depth threading algorithm (default: false)
+- `--min-block-depth`: Minimum block depth value for workload calculations (default: 16)
+
+## Usage
+
+Enable block depth threading with default settings:
+```bash
+monero-lws-daemon --block-depth-threading [other options]
+```
+
+Enable with custom minimum block depth:
+```bash
+monero-lws-daemon --block-depth-threading --min-block-depth=32 [other options]
+```
+
+Or in config file:
+```
+block-depth-threading=true
+min-block-depth=32
+```
+
+## Algorithm
+
+### Block Depth Calculation
+
+For each address, the **block depth** is calculated as the number of blocks remaining to be scanned:
+
+```
+blockdepth = max(current_blockchain_height - address_scan_height, min_block_depth)
+```
+
+Where `min_block_depth` is the value specified by `--min-block-depth` (default: 16).
+
+Addresses with blockdepth less than the minimum are assigned the minimum value. This prevents edge cases where fully synced addresses would have zero blockdepth, which could cause:
+- Division by zero or near-zero values in workload calculations
+- Degenerate cases where many fully-synced accounts get assigned together
+- Poor workload distribution when most accounts are fully synced
+
+### Minimum Block Depth
+
+The `--min-block-depth` flag sets the minimum block depth value used in workload calculations. This ensures that even fully synced accounts contribute meaningfully to workload balancing.
+
+**Default value**: 16 blocks
+
+**Example**: With `--min-block-depth=16`, an account at the current blockchain height (0 blocks remaining) is treated as having 16 blocks remaining for workload distribution purposes.
+
+**When to adjust**: You may want to increase this value if you have many fully synced accounts and want them to contribute more to workload balancing, or decrease it if you want more precise distribution for nearly-synced accounts.
+
+### Thread Assignment
+
+1. **Calculate total work**: Sum all address blockdepths to get `total_blockdepth`
+2. **Calculate target per thread**: `blockdepth_per_thread = total_blockdepth / thread_count`
+3. **Sort addresses**: Order by blockdepth (smallest first)
+4. **Distribute to threads**:
+ - Addresses are assigned sequentially to threads
+ - Accounts are added to the current thread until the cumulative depth reaches or exceeds the target
+ - When target is reached, move to the next thread
+ - Final thread receives any remaining addresses
+
+This overallocation strategy ensures more balanced workload distribution and better thread utilization throughout the scanning process.
+
+## Example
+
+With 4 threads and 20 accounts with varying sync states:
+- Accounts A-H: 16 blocks each (synced, at minimum) = 128 blocks
+- Accounts I-L: 100 blocks each = 400 blocks
+- Accounts M-P: 300 blocks each = 1,200 blocks
+- Accounts Q-T: 500 blocks each = 2,000 blocks
+
+**Old Algorithm** (by count, evenly distributed - 5 accounts per thread):
+- Thread 0: A, B, C, D, E (80 blocks) ✓ finishes immediately
+- Thread 1: F, G, H, I, J (228 blocks) ⏱
+- Thread 2: K, L, M, N, O (1,028 blocks) ⏱⏱
+- Thread 3: P, Q, R, S, T (2,100 blocks) ⏱⏱⏱⏱ takes much longer
+- **Problem**: Despite equal account count (5 per thread), massive workload imbalance - thread 3 has 26x more work than thread 0
+
+**New Algorithm** (by depth, balanced workload with alternating over/under allocation):
+- Total: 3,728 blocks, target: 932 blocks/thread
+- Thread 0: (even, over-allocate): A, B, C, D, E, F, G, H, I, J, K, L, M, N (1,128 blocks)
+- Thread 1: (odd, under-allocate): O, P (600 blocks)
+- Thread 2: (even, over-allocate): Q, R (1,000 blocks)
+- Thread 3: (odd, under-allocate): S, T (1,000 blocks)
+- **Result**: All 4 threads utilized with better balance (600-1,128 vs 80-2,100 blocks), synced accounts efficiently grouped
+
+## Benefits
+
+- **Improved parallelization**: All threads remain active longer
+- **Reduced sync time**: More efficient CPU utilization
+- **Better resource usage**: Eliminates idle threads waiting for others to complete
+- **Predictable performance**: Workload is distributed based on actual work required
diff --git a/docs/scan/round_robin.md b/docs/scan/round_robin.md
new file mode 100644
index 0000000..bdf4750
--- /dev/null
+++ b/docs/scan/round_robin.md
@@ -0,0 +1,5 @@
+# Round-Robin
+This is the default scan algorithm used by `monero-lws-daemon`. Each account
+is sorted by height, and then distributed evenly across threads. Older accounts
+will typically end up on the first thread, however, some accounts are stuck
+"waiting" for the old accounts to "catch-up".
diff --git a/docs/scan/split_synced_threads.md b/docs/scan/split_synced_threads.md
new file mode 100644
index 0000000..b3b30c7
--- /dev/null
+++ b/docs/scan/split_synced_threads.md
@@ -0,0 +1,84 @@
+# Split Synced Threading
+
+## Overview
+
+The split-sync threading feature is an enhancement to the block-depth-threading algorithm that isolates synced addresses from unsynced addresses across different scanner threads. This prevents fully or near-fully synced addresses from being blocked by addresses that are still catching up to the blockchain.
+
+Split-sync is enabled by setting `--split-sync-threads` to a value greater than 0, which specifies the percentage of threads to allocate for synced accounts.
+
+## Requirements
+
+- Requires `--block-depth-threading`
+- Only affects initial thread assignment at startup
+- `--split-sync-threads` accepts a value from 0-1 representing the percentage of threads for synced accounts (e.g., 0.25 = 25%)
+- `--split-sync-depth` accepts a numeric value representing the maximum block depth for an address to be considered synced (defaults to 10)
+- If `--split-sync-threads` is not provided or set to 0, split-sync is disabled
+
+## Arguments
+
+- `--split-sync-threads=[numthreadspercent]`: Specified as a number from 0-1, where 0.25 = 25%. Defaults to 0 (disabled) if not specified. Split-sync is enabled when this value is > 0.
+- `--split-sync-depth=[synced depth]`: The maximum block depth for an account to be considered synced. Defaults to 10 if not specified. Accounts with `raw_blockdepth ≤ split-sync-depth` are considered synced.
+
+## Behavior
+
+### Address Classification
+
+Addresses are classified as either **synced** or **unsynced** based on their block depth:
+- **Synced address**: `raw_blockdepth ≤ --split-sync-depth` value (default 10)
+- **Unsynced address**: `raw_blockdepth > --split-sync-depth` value
+
+Note: The `--min-block-depth` parameter is only used for assigning a minimum block depth value to addresses for workload calculations, and is not used to determine if an address is synced.
+
+### Thread Allocation Algorithm
+
+When `--split-sync-threads > 0`:
+
+1. **Calculate synced thread count**: The number of threads allocated for synced accounts is calculated as `ceil(split-sync-threads × total_threads)`, rounded up. The remaining threads are used for unsynced accounts.
+
+2. **Separate accounts**: Accounts are classified as synced or unsynced based on `--split-sync-depth`.
+
+3. **Distribute synced accounts**: Synced accounts are distributed across the allocated synced threads using **round-robin assignment** (account 0 → thread 0, account 1 → thread 1, ..., account N → thread (N % num_synced_threads)). Accounts are already sorted by block depth (smallest to largest) from the block-depth-threading preparation.
+
+4. **Distribute unsynced accounts**: Unsynced accounts are distributed across the remaining threads using the **standard block-depth-threading algorithm** (alternating over/under-allocation strategy) starting from the first unsynced thread.
+
+### Edge Cases
+
+- **No synced accounts**: If there are no synced accounts, all threads are used for unsynced accounts with standard block-depth-threading.
+- **Fewer synced accounts than threads**: If there are fewer synced accounts than threads allocated for synced accounts, only as many synced threads are used as there are synced accounts (one account per thread). The remaining threads are used for unsynced accounts.
+- **Split-sync disabled**: If `--split-sync-threads` is 0 or not set (default), split-sync is disabled and all accounts use standard block-depth-threading.
+
+## Example
+
+With 4 threads, `--split-sync-threads=0.25`, and `--split-sync-depth=16`, and 20 accounts with varying sync states:
+- Accounts A-H: 16 blocks each (synced) = 128 blocks
+- Accounts I-L: 100 blocks each (unsynced) = 400 blocks
+- Accounts M-P: 300 blocks each (unsynced) = 1,200 blocks
+- Accounts Q-T: 500 blocks each (unsynced) = 2,000 blocks
+
+**Thread allocation**:
+- Synced threads: `ceil(0.25 × 4) = 1` thread
+- Unsynced threads: `4 - 1 = 3` threads
+
+**With split-sync enabled**:
+- Thread 0 (synced, round-robin): A, B, C, D, E, F, G, H (8 accounts)
+ - Contains: 8 synced (A-H) only - **synced thread**
+- Thread 1 (unsynced, block-depth-threading): I, J, K, L, M (800 blocks)
+ - Contains: 5 unsynced (I-M) only - **unsynced thread**
+- Thread 2 (unsynced, block-depth-threading): N, O, P, Q (1,400 blocks)
+ - Contains: 4 unsynced - **unsynced thread**
+- Thread 3 (unsynced, block-depth-threading): R, S, T (1,500 blocks)
+ - Contains: 3 unsynced - **unsynced thread**
+- **Result**: Synced addresses (A-H) are isolated in Thread 0 and can process updates quickly without waiting for unsynced addresses. All unsynced addresses are separated into threads 1-3 using block-depth-threading.
+
+**Without split-sync** (standard block-depth-threading):
+- Total: 3,728 blocks, target: 932 blocks/thread
+- Thread 0 (even, over-allocate): A, B, C, D, E, F, G, H, I, J, K, L, M, N (1,128 blocks)
+ - Contains: 8 synced (A-H) + 6 unsynced (I-N) - **mixed**
+- Thread 1 (odd, under-allocate): O, P (600 blocks)
+- Thread 2 (even, over-allocate): Q, R (1,000 blocks)
+- Thread 3 (odd, under-allocate): S, T (1,000 blocks)
+- **Problem**: Synced addresses (A-H) are mixed with unsynced addresses (I-N) in Thread 0, causing synced addresses to wait for unsynced ones to catch up
+
+## Benefits
+
+This approach ensures that synced addresses receive timely updates without being delayed by the potentially lengthy synchronization process of newly added or far-behind addresses. By pre-allocating threads and using round-robin distribution for synced accounts, the algorithm provides predictable thread allocation and better isolation between synced and unsynced workloads.
diff --git a/docs/split_synced_threads.md b/docs/split_synced_threads.md
deleted file mode 100644
index b3b30c7..0000000
--- a/docs/split_synced_threads.md
+++ /dev/null
@@ -1,84 +0,0 @@
-# Split Synced Threading
-
-## Overview
-
-The split-sync threading feature is an enhancement to the block-depth-threading algorithm that isolates synced addresses from unsynced addresses across different scanner threads. This prevents fully or near-fully synced addresses from being blocked by addresses that are still catching up to the blockchain.
-
-Split-sync is enabled by setting `--split-sync-threads` to a value greater than 0, which specifies the percentage of threads to allocate for synced accounts.
-
-## Requirements
-
-- Requires `--block-depth-threading`
-- Only affects initial thread assignment at startup
-- `--split-sync-threads` accepts a value from 0-1 representing the percentage of threads for synced accounts (e.g., 0.25 = 25%)
-- `--split-sync-depth` accepts a numeric value representing the maximum block depth for an address to be considered synced (defaults to 10)
-- If `--split-sync-threads` is not provided or set to 0, split-sync is disabled
-
-## Arguments
-
-- `--split-sync-threads=[numthreadspercent]`: Specified as a number from 0-1, where 0.25 = 25%. Defaults to 0 (disabled) if not specified. Split-sync is enabled when this value is > 0.
-- `--split-sync-depth=[synced depth]`: The maximum block depth for an account to be considered synced. Defaults to 10 if not specified. Accounts with `raw_blockdepth ≤ split-sync-depth` are considered synced.
-
-## Behavior
-
-### Address Classification
-
-Addresses are classified as either **synced** or **unsynced** based on their block depth:
-- **Synced address**: `raw_blockdepth ≤ --split-sync-depth` value (default 10)
-- **Unsynced address**: `raw_blockdepth > --split-sync-depth` value
-
-Note: The `--min-block-depth` parameter is only used for assigning a minimum block depth value to addresses for workload calculations, and is not used to determine if an address is synced.
-
-### Thread Allocation Algorithm
-
-When `--split-sync-threads > 0`:
-
-1. **Calculate synced thread count**: The number of threads allocated for synced accounts is calculated as `ceil(split-sync-threads × total_threads)`, rounded up. The remaining threads are used for unsynced accounts.
-
-2. **Separate accounts**: Accounts are classified as synced or unsynced based on `--split-sync-depth`.
-
-3. **Distribute synced accounts**: Synced accounts are distributed across the allocated synced threads using **round-robin assignment** (account 0 → thread 0, account 1 → thread 1, ..., account N → thread (N % num_synced_threads)). Accounts are already sorted by block depth (smallest to largest) from the block-depth-threading preparation.
-
-4. **Distribute unsynced accounts**: Unsynced accounts are distributed across the remaining threads using the **standard block-depth-threading algorithm** (alternating over/under-allocation strategy) starting from the first unsynced thread.
-
-### Edge Cases
-
-- **No synced accounts**: If there are no synced accounts, all threads are used for unsynced accounts with standard block-depth-threading.
-- **Fewer synced accounts than threads**: If there are fewer synced accounts than threads allocated for synced accounts, only as many synced threads are used as there are synced accounts (one account per thread). The remaining threads are used for unsynced accounts.
-- **Split-sync disabled**: If `--split-sync-threads` is 0 or not set (default), split-sync is disabled and all accounts use standard block-depth-threading.
-
-## Example
-
-With 4 threads, `--split-sync-threads=0.25`, and `--split-sync-depth=16`, and 20 accounts with varying sync states:
-- Accounts A-H: 16 blocks each (synced) = 128 blocks
-- Accounts I-L: 100 blocks each (unsynced) = 400 blocks
-- Accounts M-P: 300 blocks each (unsynced) = 1,200 blocks
-- Accounts Q-T: 500 blocks each (unsynced) = 2,000 blocks
-
-**Thread allocation**:
-- Synced threads: `ceil(0.25 × 4) = 1` thread
-- Unsynced threads: `4 - 1 = 3` threads
-
-**With split-sync enabled**:
-- Thread 0 (synced, round-robin): A, B, C, D, E, F, G, H (8 accounts)
- - Contains: 8 synced (A-H) only - **synced thread**
-- Thread 1 (unsynced, block-depth-threading): I, J, K, L, M (800 blocks)
- - Contains: 5 unsynced (I-M) only - **unsynced thread**
-- Thread 2 (unsynced, block-depth-threading): N, O, P, Q (1,400 blocks)
- - Contains: 4 unsynced - **unsynced thread**
-- Thread 3 (unsynced, block-depth-threading): R, S, T (1,500 blocks)
- - Contains: 3 unsynced - **unsynced thread**
-- **Result**: Synced addresses (A-H) are isolated in Thread 0 and can process updates quickly without waiting for unsynced addresses. All unsynced addresses are separated into threads 1-3 using block-depth-threading.
-
-**Without split-sync** (standard block-depth-threading):
-- Total: 3,728 blocks, target: 932 blocks/thread
-- Thread 0 (even, over-allocate): A, B, C, D, E, F, G, H, I, J, K, L, M, N (1,128 blocks)
- - Contains: 8 synced (A-H) + 6 unsynced (I-N) - **mixed**
-- Thread 1 (odd, under-allocate): O, P (600 blocks)
-- Thread 2 (even, over-allocate): Q, R (1,000 blocks)
-- Thread 3 (odd, under-allocate): S, T (1,000 blocks)
-- **Problem**: Synced addresses (A-H) are mixed with unsynced addresses (I-N) in Thread 0, causing synced addresses to wait for unsynced ones to catch up
-
-## Benefits
-
-This approach ensures that synced addresses receive timely updates without being delayed by the potentially lengthy synchronization process of newly added or far-behind addresses. By pre-allocating threads and using round-robin distribution for synced accounts, the algorithm provides predictable thread allocation and better isolation between synced and unsynced workloads.
diff --git a/docs/zmq.md b/docs/zmq.md
deleted file mode 100644
index 4c22a7c..0000000
--- a/docs/zmq.md
+++ /dev/null
@@ -1,178 +0,0 @@
-# monero-lws ZeroMQ Usage
-Monero-lws uses ZeroMQ-RPC to retrieve information from a Monero daemon,
-ZeroMQ-SUB to get immediate notifications of blocks and transactions from a
-Monero daemon, and ZeroMQ-PUB to notify external applications of payment_id
-and new account (web)hooks.
-
-## External "pub" socket
-The bind location of the ZMQ-PUB socket is specified with the `--zmq-pub`
-option. Users are still required to "subscribe" to topics:
- * `json-full-payment_hook`: A JSON object of a single webhook payment event
- that has recently triggered (identical output as webhook).
- * `msgpack-full-payment_hook`: A msgpack object of a webhook payment events
- that have recently triggered (identical output as webhook).
- * `json-full-new_account_hook`: A JSON object of a single new account
- creation that has recently triggered (identical output as webhook).
- * `msgpack-full-new_account_hook`: A msgpack object of a single new account
- creation that has recently triggered (identical output as webhook).
- * `json-minimal-scanned`: A JSON object of a list of user primary addresses,
- with their new height and block hash.
- * `msgpack-minimal-scanned:` A msgpack object of a list of user primary
- addresses with their new height and block hash.
- * `json-full-spend_hook': A JSON object of a webhook spend event that has
- recently triggerd (identical output as webhook).
- * `msgpack-full-spend_hook`: A msgpack object of a single new account
- creation that has recently triggered (identical output as webhook).
-
-
-### `json-full-payment_hook`/`msgpack-full-payment_hook`
-These topics receive PUB messages when a webhook ([`webhook_add`](administration.md)),
-event is triggered for a payment (`tx-confirmation`). If the specified URL is
-`zmq`, then notifications are only done over the ZMQ-PUB socket, otherwise the
-notification is sent over ZMQ-PUB socket AND the specified URL. Invoking
-`webhook_add` with a `payment_id` of zeroes (the field is optional in
-`webhook_add`), will match on all transactions that lack a `payment_id`.
-
-Example of the "raw" output from ZMQ-SUB side:
-
-```json
-json-full-payment_hook:{
- "index": 2,
- "event": {
- "event": "tx-confirmation",
- "payment_id": "4f695d197f2a3c54",
- "token": "single zmq wallet",
- "confirmations": 1,
- "event_id": "3894f98f5dd54af5857e4f8a961a4e57",
- "tx_info": {
- "id": {
- "high": 0,
- "low": 5666768
- },
- "block": 2265961,
- "index": 1,
- "amount": 3117324236131,
- "timestamp": 1687301600,
- "tx_hash": "ef3187775584351cc5109de124b877bcc530fb3fdbf77895329dd447902cc566",
- "tx_prefix_hash": "064884b8a8f903edcfebab830707ed44b633438b47c95a83320f4438b1b28626",
- "tx_public": "54dce1a6eebafa2fdedcea5e373ef9de1c3d2737ae9f809e80958d1ba4590d74",
- "rct_mask": "4cdc4c4e340aacb4741ba20f9b0b859242ecdad2fcc251f71d81123a47db3400",
- "payment_id": "4f695d197f2a3c54",
- "unlock_time": 0,
- "mixin_count": 15,
- "coinbase": false
- }
- }
-}
-
-```
-
-Notice the `json-full-payment_hook:` prefix - this is required for the ZMQ PUB/SUB
-subscription model. The subscriber requests data from a certain "topic" where
-matching is done by string prefixes.
-
-> `index` is a counter used to detect dropped messages.
-
-> The `block` and `id` fields in the above example are NOT present when
-`confirmations == 0`.
-
-### `json-full-new_account_hook`/`msgpack-full-new_account_hook`
-These topics receive PUB messages when a webhook ([`webhook_add`](administration.md)),
-event is triggered for a new account (`new-account`). If the specified URL is
-`zmq`, then notifications are only done over the ZMQ-PUB socket, otherwise the
-notification is sent over ZMQ-PUB socket AND the specified URL. Invoking
-`webhook_add` with a `payment_id` of zeroes (the field is optional in
-`webhook_add`), will match on all transactions that lack a `payment_id`.
-
-Example of the "raw" output from ZMQ-SUB side:
-
-```json
-json-full-new_account_hook:{
- "index": 2,
- "event": {
- "event": "new-account",
- "event_id": "c5a735e71b1e4f0a8bfaeff661d0b38a",
- "token": "",
- "address": "9zGwnfWRMTF9nFVW9DNKp46aJ43CRtQBWNFvPqFVSN3RUKHuc37u2RDi2GXGp1wRdSRo5juS828FqgyxkumDaE4s9qyyi9B"
- }
-}
-```
-
-Notice the `json-full-new_account_hook:` prefix - this is required for the ZMQ
-PUB/SUB subscription model. The subscriber requests data from a certain "topic"
-where matching is done by string prefixes.
-
-> `index` is a counter used to detect dropped messages.
-
-### `json-minimal-scanned`/`msgpack-minimal-scanned`
-These topics receive PUB messages when a thread has finished scanning 1+
-accounts. The last block height and hash is sent.
-
-Example of the "raw" output from ZMQ-SUB side:
-```json
-json-minimal-scanned:{
- "index": 13,
- "event": {
- "height": 2438536,
- "id": "9197e1c6f3de28a98dfc579325903e5416ef1ba2681043c54b5fff0d39645a7f",
- "addresses": [
- "9xkhhJSa7ZhS5sAcTix6ozL14RwdgxbV7JZVFW4rCghN7GidutaykfxDHfgW45UPiCTXncuvZ91GNSGgxs3b2Cin9TU8nP3"
- ]
-
-> `index` is a counter used to detect dropped messages.
-
-### `json-full-spend_hook`/`msgpack-full-spend_hook`
-These topics receive PUB messages when a webhook ([`webhook_add`](administration.md)),
-event is triggered for a spend (`tx-spend`). If the specified URL is
-`zmq`, then notifications are only done over the ZMQ-PUB socket, otherwise the
-notification is sent over ZMQ-PUB socket AND the specified URL. Invoking
-`webhook_add` with a `payment_id` or `confirmation` results in a NOP because
-both fields are unused for spends. This event is only triggered on
-confirmation==1 (`confirmation` field on `webhook_add`s have no effect, and
-mempool spends are not scanned). The intent is to notify the user of unexpected
-spend operations. The end user will need to use `tx_info.input.image`,
-`tx_info.source.index`, and `tx_info.source.tx_public` to determine if the
-output was actually spent or being used as a decoy.
-
-Example of the "raw" output from ZMQ-SUB side:
-
-```json
-json-full-spend_hook:{
- "index": 0,
- "event": {
- "event": "tx-spend",
- "token": "spend-xmr",
- "event_id": "7ff047aa74e14f4aa978469bc0eec8ec",
- "tx_info": {
- "input": {
- "height": 2464207,
- "tx_hash": "97d4e66c4968b16fec7662adc9f8562c49108d3c5e7030c4d6dd32d97fb62540",
- "image": "b0fe7acd9e17bb8b9ac2daae36d4cb607ac60ed8a101cc9b2e1f74016cf80b24",
- "source": {
- "high": 0,
- "low": 6246316
- },
- "timestamp": 1711902214,
- "unlock_time": 0,
- "mixin_count": 15,
- "sender": {
- "maj_i": 0,
- "min_i": 0
- }
- },
- "source": {
- "id": {
- "high": 0,
- "low": 6246316
- },
- "amount": 10000000000,
- "mixin": 15,
- "index": 0,
- "tx_public": "426ccd6d39535a1ee8636d14978581e580fcea35c8d3843ceb32eb688a0197f7"
- }
- }
- }
-}
-```
-
-> `index` is a counter used to detect dropped messages
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 0000000..65bad70
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,27 @@
+site_name: docs.monerolws.com
+site_url: https://docs.monerolws.com
+site_description: Documentation for Monero-LWS
+nav:
+ - Overview: index.md
+ - Installation:
+ - From Source: installation/source.md
+ - Docker: installation/docker.md
+ - Processes:
+ - daemon: apps/daemon.md
+ - admin: apps/admin.md
+ - client: apps/client.md
+ - Scan Algorithms:
+ - Round Robin: scan/round_robin.md
+ - Balance New Addresses: scan/balance_new_addresses.md
+ - Block Depth Threading: scan/block_depth_threading.md
+ - Split Synced Threads: scan/split_synced_threads.md
+ - API:
+ - Wallet: api/wallet.md
+ - Admin: api/admin.md
+ - Webhooks/ZeroMQ: api/zmq.md
+ - RabbitMQ: api/rmq.md
+plugins:
+ - swagger-ui-tag
+theme: readthedocs
+repo_url: https://github.com/vtnerd/monero-lws
+edit_uri: edit/develop/docs
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.