lightning-terminal/docs/accounts.md
Viktor Torstensson 6bca24793c
docs: document account operator controls
Document the configurable maximum account payment size and optional
channel balance reservation check, including their defaults, fee
handling, and operational limitations.
2026-08-07 12:27:24 +02:00

8.3 KiB

Custodial accounts

This document describes how the "accounts" feature of LiT can be used to create custodial user accounts with their own balances on an existing lnd node.

What is an account?

An account in the LiT context is a virtual construct that provides restricted access to an existing lnd/LiT node. An account has a virtual (off-chain only) balance in satoshis and an optional expiration. That allows a node operator to give someone else (or some client application, see Use cases) restricted access to their node with the ability to only spend up to a certain amount of the node's channel balance.

NOTE: An account's balance is purely virtual. If an account is created with an initial balance higher than the node's actual overall channel balance, that is equivalent to fractional reserve banking. Therefore, the user accepting an account-restricted access enters a trust relationship with the node operator that the promised balance of the account is actually spendable.

How do accounts work?

The accounts system is made possible thanks to the power of macaroons and the RPC middleware interceptor of lnd.

What does that mean?

It means a node operator can give another user or client application access to their node through the default gRPC interface of lnd, which makes this access mode fully compatible with any remote (or local) user interfaces or apps/browser plugins, as well as any LNC (Lightning Node Connect) connection.

What features and balance the restricted user has access to is solely controlled by the macaroon that is given to the user. So a user that wants to get restricted access to a node will receive a macaroon that is bound to an account that is defined in LiT. Because of the cryptographic setup of macaroons, that restriction cannot be removed from the macaroon by the user without invalidating the macaroon itself. Therefore, any user/application using such a restricted macaroon will trigger special rules in the RPC middleware interceptor mentioned above. See the features section below to find out what those rules are.

Features

When an account-restricted macaroon is used, the RPC middleware interceptor enforces the following rules on the RPC interface:

  • Any payment made by a custodial/restricted user account is deducted from an account's virtual balance (the full amount, including off-chain routing fees).
  • If a payment (or the sum of multiple in-flight payments) exceeds the account's virtual balance, it is denied.
  • The on-chain balance of any RPC responses such as the WalletBalance RPC is always shown as 0. A custodial/restricted user shouldn't be able to see what on-chain balance is available to the node operator as an account can only spend off-chain balances anyway.
  • The off-chain balance (e.g. the response returned by the ChannelBalance RPC) always reflects the account's virtual balance and not the node's overall channel balance (and any remote balances are always shown as 0).
  • The list of active/pending/closed channels is always returned empty. The custodial/restricted user should not need to care (or even know) about channels and their internal workings.
  • The list of payments and invoices is filtered to only return payments/invoices created or paid by the account.
  • Invoices created by an account are mapped to that account. If/when such a mapped invoice is paid, the amount is credited to that account's virtual balance.

Operator controls

LiT provides optional account safeguards through its configuration:

  • accounts.max-payment-size-msat limits the total amount a single account payment may debit, including fees. For SendPaymentV2, the configured fee limit is included; for SendToRouteV2, the route's stated fee is included. The default value of 0 disables this limit.

  • accounts.check-channel-balance rejects new accounts, administrative credits and administrative balance increases that would make the sum of all account balances exceed the node's current local channel balance. It is disabled by default. When enabled, allocations fail if LiT cannot query the Lightning client for the channel balance.

    Note: The channel-balance check only applies when a balance is allocated. It cannot prevent the node's local balance from subsequently falling below the allocated account balance if the operator sends non-account Lightning payments or other non-account activity.

Use cases

The following (definitely non-exhaustive) list of use cases is made possible by the accounts system:

  • The "Uncle Jim" model: The tech-savvy person of the family (e.g. "Uncle Jim") operates a Lightning node. He manages the liquidity of the node and provides the capital for the channels. He can onboard his family members by creating an account, locking a macaroon to that account and then scanning a QR code with an app like Zeus on the family member's smartphone.
  • The "spend up to a certain amount automatically" model: A web user has a browser extension like Alby installed and wants to allow that extension to pay invoices for paywalls automatically up to a certain amount per month. That amount could be enforced by the account so the browser extension doesn't have to keep track of its spending actions. And an account can be shared between extensions installed in different browsers.
  • The "allowance" model: A parent wants to give their child their allowance in Lightning satoshis. They create an account over the allowance amount and top up the account each week/month.

HOWTO

This section describes how an account can be created and used.

Create the account

The first thing that needs to be done is to create the account with its initial balance (and an optional expiry). This needs to be done by the node operator, meaning access to the lit.macaroon is required.

Example:

$ litcli accounts create 50000 --save_to /tmp/accounts.macaroon

{
        "account": {
                "id": "d64dbc31b28edf66",
                "initial_balance": "50000",
                "current_balance": "50000",
                "last_update": "1652353332",
                "expiration_date": "0"
        },
        "macaroon": "020103........."
}
Account macaroon saved to /tmp/accounts.macaroon

This created a new account (ID d64dbc31b28edf66) with an initial balance of 50k satoshis and no expiration. A new macaroon was baked that contains the correct permissions and is locked to that account. The macaroon file was stored under /tmp/accounts.macaroon in this example.

Use the macaroon

This step is done by the user/app that should be given the restricted access. An example could be to create a QR code with a tool like lndconnect that can be scanned by mobile apps to connect to the node. Or some browser extensions require the user to upload the macaroon to the browser.

It's absolutely crucial to use the macaroon generated in the previous step here to make sure the restrictions are applied.

The permissions and restrictions of a macaroon can always be inspected by:

$ lncli printmacaroon --macaroon_file /tmp/accounts.macaroon

{
        "version": 2,
        "location": "lnd",
        "root_key_id": "0",
        "permissions": [
                "info:read",
                "invoices:read",
                "invoices:write",
                "offchain:read",
                "offchain:write",
                "onchain:read"
        ],
        "caveats": [
                "lnd-custom account d64dbc31b28edf66"
        ]
}

The important part is the lnd-custom account ... part in the caveats section.

Example of using lncli to check the account balance (assuming integrated lnd mode, adjust RPC server/port and TLS cert for remote mode):

$ lncli --macaroonpath=/tmp/accounts.macaroon channelbalance

{
    "balance": "5000",
    "pending_open_balance": "0",
    "local_balance": {
        "sat": "5000",
        "msat": "5000000"
    },
    "remote_balance": {
        "sat": "0",
        "msat": "0"
    }
    ...
}