Summary
The default-on wallet spending limit (src/services/spend-limiter.ts, added in #571) meters transfer_stx, transfer_btc, and x402/L402 auto-payments, but not the manual lightning_pay_invoice tool (src/tools/lightning.tools.ts). That tool currently pays a BOLT-11 invoice at its full face value with no amount cap — only the routing fee (maxFeeSats) is bounded.
This was flagged in the security audit (finding F3) and deferred from #571.
What's needed
- Decode the BOLT-11 invoice to get the amount in sats, reusing the existing decoder:
import { decode as decodeBolt11 } from "light-bolt11-decoder";
(same pattern as the L402 path in src/services/x402.service.ts ~line 488 — find the amount section, convert msat → sats).
- Refuse amountless invoices (consistent with the L402 path, which already does this — an unmeterable spend defeats the cap).
await getSpendLimiter().check("sats", amountSats, addr) before provider.payInvoice(...).
await getSpendLimiter().record("sats", amountSats, addr) after a successful pay.
Open question — the ledger key
The sats ledger is keyed by the wallet's Stacks address so BTC L1 + sBTC + L402 spends share one budget. But the Lightning wallet has its own session (~/.aibtc/lightning/keystore.json, unlocked separately via lightning_unlock), so the main STX wallet (getActiveAccount()) may be locked during a Lightning pay.
The L402 path handles this by keying on getWalletManager().getActiveAccount()?.address and skipping the check when it's absent — acceptable for L402 but leaves a gap if we want lightning_pay_invoice reliably metered.
Options to resolve:
- (a) Key by the active Stacks address; fall back to a constant Lightning ledger key (e.g.
__lightning__) when the main wallet is locked, so it's always metered (separate bucket when STX is locked, shared bucket otherwise).
- (b) Derive the Stacks address from the Lightning session mnemonic to always key consistently (more coupling).
- (c) Require the main wallet to be unlocked to meter (UX regression — previously only the Lightning wallet needed unlocking).
(a) is the pragmatic default; pick one and document it.
Acceptance criteria
Out of scope
Contract-call swaps (ALEX/Bitflow/Zest/Jing/Styx) — intentionally not metered (the amount isn't a direct chokepoint param).
Follow-up to #571.
Summary
The default-on wallet spending limit (
src/services/spend-limiter.ts, added in #571) meterstransfer_stx,transfer_btc, and x402/L402 auto-payments, but not the manuallightning_pay_invoicetool (src/tools/lightning.tools.ts). That tool currently pays a BOLT-11 invoice at its full face value with no amount cap — only the routing fee (maxFeeSats) is bounded.This was flagged in the security audit (finding F3) and deferred from #571.
What's needed
src/services/x402.service.ts~line 488 — find theamountsection, convert msat → sats).await getSpendLimiter().check("sats", amountSats, addr)beforeprovider.payInvoice(...).await getSpendLimiter().record("sats", amountSats, addr)after a successful pay.Open question — the ledger key
The
satsledger is keyed by the wallet's Stacks address so BTC L1 + sBTC + L402 spends share one budget. But the Lightning wallet has its own session (~/.aibtc/lightning/keystore.json, unlocked separately vialightning_unlock), so the main STX wallet (getActiveAccount()) may be locked during a Lightning pay.The L402 path handles this by keying on
getWalletManager().getActiveAccount()?.addressand skipping the check when it's absent — acceptable for L402 but leaves a gap if we wantlightning_pay_invoicereliably metered.Options to resolve:
__lightning__) when the main wallet is locked, so it's always metered (separate bucket when STX is locked, shared bucket otherwise).(a) is the pragmatic default; pick one and document it.
Acceptance criteria
lightning_pay_invoicerejects a pay that would exceed the remainingsatsbudget, before paying.satsledger.Out of scope
Contract-call swaps (ALEX/Bitflow/Zest/Jing/Styx) — intentionally not metered (the amount isn't a direct chokepoint param).
Follow-up to #571.