Wallet, NWC, and Payment Services Implementation Plan
Status: in progress; Phases 0-4 complete for unbound interactive NWC connections
Related design: WALLET_NWC_PAYMENTS.md
Goal
Introduce a first-party NIP-47 client, named NWC wallet management, safe monetary types, and core payment service boundaries without adding a third-party payment dependency. Deliver the protocol and wallet infrastructure first, then add interactive payment UX, Cashu integration, and scheduled automation in separate phases.
Delivery Principles
- Keep core in control of credentials, confirmation, execution, and auditing.
- Do not expose a payment operation before its authorization path exists.
- Treat payment rails separately from payer wallet providers.
- Use exact typed amounts and convert to NWC JSON numbers only at the wire.
- Prefer small phases that can be verified without real funds.
- Use mock relay/wallet adapters for deterministic verification where needed.
- Do not add
@getalby/sdkor another payment dependency. - Preserve the existing Cashu behavior while moving its command namespace.
- Keep scheduler v3 and NR zap aggregation out of the infrastructure MVP.
Phase 0: Prompt Context Boundary
Status: complete in the current working tree
- Document
PluginContextas long-lived plugin runtime state. - Document
PluginInvocationContextas request-scoped state. - Remove
promptFnfromPluginContext. - Keep
PluginInvocationContext.promptFnoptional for transports without an interactive answer channel. - Create CLI/Nostr prompt functions per inbound command with the source captured in the closure.
- Keep web prompt functions bound to their websocket request ID.
- Separate pending CLI and Nostr prompts by message source.
- Remove Job, Todo, and Bookmark fallback use of the long-lived prompt.
- Ensure scheduled Job execution receives no prompt function.
- Run TypeScript, targeted ESLint, and affected plugin tests.
Follow-up outside the wallet critical path:
- Consider renaming
PluginContexttoPluginRuntimeContext. - Consider renaming
PluginInvocationContexttoPluginRequestContext. - Review long-lived
sendReplyand interactive bunker-signing services for the same request-scope concerns.
Phase 1: Exact Monetary Types
Status: complete
Proposed files:
src/payments/amount.ts
src/payments/types.ts
- Implement immutable
Satoshibacked bybigint. - Implement immutable
Millisatoshibacked bybigint. - Accept decimal strings and
bigint; reject JavaScriptnumber. - Reject negative, fractional, malformed, and out-of-domain values.
- Reserve positive-amount enforcement for payment-request validation while permitting zero-valued balance representations.
- Add exact satoshi-to-millisatoshi conversion.
- Add exact millisatoshi-to-satoshi conversion with divisibility checks.
- Add explicit floor conversion only where a caller names that behavior.
- Add decimal persistence and display serialization.
- Add an explicitly named checked NWC JSON-number encoder.
- Prevent implicit JavaScript numeric coercion.
- Verify representative parsing, conversion, wire-boundary, and coercion behavior without adding a test file.
Exit criteria:
- Public payment and NWC APIs cannot accidentally accept an untyped number.
- Every conversion is explicit and passes compile, lint, formatting, and runtime smoke verification.
Phase 2: NWC Protocol Foundation
Status: complete
Proposed files:
src/nwc/connection.ts
src/nwc/errors.ts
src/nwc/protocol.ts
src/nwc/schemas.ts
src/nwc/types.ts
- Define strict schemas for kind
13194info events. - Define request and response payload schemas for
get_info,get_balance,pay_invoice, andlookup_invoice. - Define typed NWC error codes and local transport errors.
- Accept only canonical
nostr+walletconnectURIs initially. - Validate 32-byte hex wallet pubkeys and client secrets.
- Parse, validate, normalize, and deduplicate repeated
relayparameters. - Restrict relays to
wss:except explicit local-developmentws:URLs. - Parse optional
lud16without treating it as authorization data. - Redact connection strings from all formatted errors and logs.
- Add a safe connection projection that omits the secret.
- Verify representative URI parsing and redaction behavior without adding a test file.
Exit criteria:
- A connection URI can be validated and safely represented without network access or secret leakage.
Phase 3: First-Party NWC Client
Status: complete
Proposed files:
src/nwc/client.ts
src/nwc/transport.ts
- Use
nostr-toolsevent signing, verification, NIP-44,SimplePool, and event-kind constants. - Fetch and verify kind
13194from the configured wallet pubkey. - Require advertised
nip44_v2; return a typed unsupported-encryption error for NIP-04-only wallets. - Publish to all configured relays and succeed when any relay accepts.
- Subscribe for kind
23195before publishing kind23194. - Include wallet
p, encryption, and bounded expiration tags. - Correlate responses by wallet author, request
e, and clientp. - Verify response signatures before decryption.
- Decrypt with NIP-44 and validate matching
result_type. - Accept and normalize deployed-wallet responses that omit inactive
errororresultfields. - Implement
getInfo(). - Implement
getBalance()with typed amount conversion. - Implement low-level
payInvoice()without exposing it to commands or plugins yet. - Implement
lookupInvoice()for payment reconciliation. - Close subscriptions and timers after success, error, abort, publish failure, and timeout.
- Accept
AbortSignaland use bounded publish/reply deadlines. - Do not log requests, invoices where unnecessary, secrets, decrypted responses, or preimages.
Verification scenarios:
- NIP-44 request encryption and response decryption through an in-memory mock wallet.
- Subscription-before-publication ordering in the mock transport.
- Multiple-relay publish success and complete publish failure.
- Wrong author, wrong
e, wrongp, invalid signature, malformed JSON, wrong result type, and unsupported encryption. - Wallet error mapping including
UNAUTHORIZED,QUOTA_EXCEEDED,INSUFFICIENT_BALANCE,PAYMENT_FAILED, andNOT_FOUND. - Publish timeout, response timeout, abort, duplicate response, and cleanup.
-
pay_invoicefollowed bylookup_invoicein the mock wallet flow. - Real Coinos
get_infoandget_balancerequests through the configured connection.
Exit criteria:
- The client can complete payer-side NWC behavior against deterministic mock verification.
- No interactive or automation consumer can invoke it through public core APIs.
Phase 4: NWC Connection Persistence And Commands
Status: complete for unbound interactive connections; automation metadata is deferred to Phase 10
Proposed files:
src/nwc/state.ts
src/commands/nwc/definition.ts
src/commands/nwc/handler.ts
src/commands/nwc/...renderers...
- Define versioned JSON state for named NWC connections.
- Store the JSON value in the core state database.
- Store connection URIs in plaintext as currently decided.
- Assign stable random connection IDs independent of user labels.
- Classify each connection as
interactiveorautomationin Phase 10. - Require a plugin binding for automation connections in Phase 10.
- Resolve the stable plugin-binding identity before implementing automation access in Phase 10.
- Add safe list projections that never include the secret or complete URI.
- Add create, rename, and remove operations.
- Validate locally before persistence.
- Attempt
get_infoduring Add/Test but allow an unreachable connection to be saved as unverified. - Serialize whole-JSON state updates in database transactions and increment the stored revision.
- Add
/nwc list,/nwc info,/nwc balance,/nwc add,/nwc rename, and/nwc remove. - Make
/nwc addopen an authenticated web form; never accept a connection URI through CLI arguments or a Nostr message. - Keep low-level
/nwc payunavailable until the confirmation flow exists. - Submit the URI in a non-timeline websocket command payload, never a URL.
- Ensure timelines and command results contain only redacted metadata.
- Add confirmation before removing a connection.
Verification scenarios:
- JSON state migration/version rejection and malformed entry recovery.
- Concurrent add/remove without lost updates.
- Redacted state projection and valid WebNode list output.
- Successful Add/verification and Balance command flow with a mock wallet.
- Offline unverified save and later successful Test.
- Interactive versus automation classification rules in Phase 10.
- Automation connection without plugin binding rejection in Phase 10.
Exit criteria:
- Users can manage and inspect several NWC connections without exposing their bearer secrets to the browser after submission.
- Read-only info and balance behavior works end-to-end.
Phase 5: Command Namespace And Aggregate Wallet View
- Move Cashu command implementation from
src/commands/wallet/tosrc/commands/cashu/. - Register the Cashu command as
/cashu. - Intentionally break shipped
/walletCashu invocations at the release boundary so/wallethas unambiguous aggregate-only ownership. - Create a new generic
src/commands/wallet/implementation. - Add
/wallet listas an overview-only wallet widget. - Show safe NWC connection metadata and current availability.
- Show Cashu wallet/mint balances using existing behavior.
- Let the browser append WebLN detection to the server-rendered overview.
- Show WebLN as detected without invoking
enable(). - Add an explicit WebLN Connect action followed by
getInfo(). - Link NWC and Cashu sections to their owner management views.
- Keep mutations out of the aggregate wallet widget.
Exit criteria:
/cashuowns Cashu commands,/nwcowns NWC connections, and/wallet listpresents a coherent multi-wallet overview.
Phase 6: Interactive Payment Contracts And Core Broker
Proposed files:
src/payments/interactive-types.ts
src/payments/validation.ts
src/payments/service.ts
src/payments/lightning-invoice.ts
- Define one Lightning and one Cashu accepted-option type.
- Require at most one option per rail.
- Require the same principal
Satoshiamount across alternatives. - Require non-empty Cashu accepted mints.
- Require deferred Lightning
createInvoiceandcheckSettlementcallbacks. - Require idempotent Cashu
acceptTokenwith a core attempt ID. - Define typed success, rejected, unsupported, and failed results.
- Throw only for invalid API use.
- Define safe unsupported and payment failure codes.
- Implement or isolate the minimum BOLT-11 parser needed to validate network, amount, expiry, and payment hash without adding a dependency.
- Derive app title/icon from registered plugin metadata.
- Add the interactive payment facade to
PluginInvocationContextonly when the transport supports it. - Return unsupported for HTTP/background invocation rather than using a global prompt.
- Enforce one active payment per web client/session and return
PAYMENT_BUSYfor concurrent requests. - Read NWC and Cashu state at request time and require browser discovery at presentation time.
- Keep payment source discovery and execution inside core.
- Do not add a separate provider preflight API.
Exit criteria:
- Plugins can express accepted rails through a transport-safe core contract.
- No plugin can execute an interactive payment without entering the core approval flow.
Phase 7: Core Payment Modal And Provider Adapters
Proposed areas:
src/payments/web-prompt.ts
src/web/ws-schema.ts
src/web/ws.ts
web/src/payments/
- Render concrete source tabs: WebLN, each interactive NWC wallet, Cashu, then Other wallet.
- Make WebLN the first tab when detected.
- Require explicit WebLN Connect before
enable()andgetInfo(). - Never inject an NWC WebLN wrapper into
window.webln. - Invoke named NWC wallets directly through core.
- Always provide QR code, copyable BOLT-11, and
lightning:link. - Add Cashu mint selection when several accepted local mints have funds.
- Show principal separately from provider-specific fees.
- Treat unknown balance as usable.
- Show known or returned insufficient balance without removing other tabs.
- Display a message to fund that wallet or choose another option.
- Defer inline funding workflows.
- Add Pay and Reject with no remembered grant checkbox initially.
- Verify WebLN/NWC preimages against the invoice payment hash.
- Poll the required consumer settlement callback for QR and final merchant confirmation.
- Define bounded modal, invoice, provider, and settlement timeouts.
- Ensure invoice expiry disables Pay and requests a fresh invoice where safe.
- Avoid recording secrets or preimages in browser/server timelines.
Verification scenarios:
- WebLN absent, detected, rejected, connected, paid, and failed.
- Zero, one, and several named NWC wallets.
- NWC offline, unauthorized, quota exceeded, insufficient, timeout, and paid.
- QR settlement callback success, expiry, and timeout.
- Cashu unsupported, mint mismatch, one mint, and several matching mints.
- Equal principal with different provider fees.
- Modal close, Reject, duplicate request, and invalid consumer callback.
- Desktop and mobile layouts.
Exit criteria:
- A plugin can request one payment and the user can complete it through any supported source under core-controlled confirmation.
Phase 8: Migrate Existing Lightning Payments
- Extract roadmap's invoice display and WebLN/QR behavior into the generic core payment flow.
- Make roadmap supply a deferred Lightning option and settlement callback.
- Preserve zap request construction, LNURLP validation, and receipt context.
- Remove roadmap's direct
window.weblninvocation. - Replace or remove the raw-invoice
wallet.payInvoiceclient action. - Add
/nwc payusing the same core confirmation service. - Verify roadmap funding still falls back to QR and BOLT-11.
Exit criteria:
- No shipped feature bypasses the core interactive payment confirmation path.
Phase 9: Cashu Payment Adapter
- Adapt current Cashu balances into payment-source discovery.
- Normalize accepted mint URLs with existing Cashu mint normalization.
- Compute spendability and mint/preparation fees for matching mints.
- Create a token only after Pay approval.
- Deliver through the idempotent consumer callback.
- Record delivery as pending, accepted, failed, or unknown.
- Design token recovery for callback timeout or uncertain acknowledgment.
- Ensure retries cannot transfer two independently spendable tokens for one attempt.
- Add Cashu receipt/audit projections without exposing bearer tokens.
Exit criteria:
- Cashu is a first-class source behind the same interactive contract without changing existing direct Cashu command behavior.
Phase 10: Automation Connections And Scoped Plugin API
- Finalize stable plugin-binding identity.
- Exclude automation connections from interactive modal tabs.
- Add a core-owned scoped automation payment client to
PluginContextfor explicitly authorized background work. - Derive caller identity from plugin registration; do not accept it as a plugin argument.
- Verify connection purpose and binding on every operation.
- Expose
getInfo,getBalance,payInvoice, andlookupInvoicewithout exposing the URI. - Add safe audit rows for attempts and results.
- Add removal confirmation listing dependent automation consumers.
- Disable and notify dependent jobs when a bound connection is removed.
- Document that in-process plugins are trusted and wallet-side NWC budgets remain the hard limit.
Exit criteria:
- One plugin can use only its assigned automation connections through core.
- Credentials never cross the plugin API boundary.
Phase 11: Scheduler V3 Payment Policy
- Add
scheduler:v3rather than changing v2 semantics. - Wrap the plugin-scoped automation service with job/run-specific policy enforcement during scheduled task execution.
- Add an exact
nwcConnectionIdbinding for payment-enabled jobs. - Add optional
maxPerPayment,maxPerRun, lifetimemaxTotal, and rollingmaxPerWindowlimits. - Represent window length as
{ unit, count }. - Require explicit warning confirmation when no local limits are set.
- Add payment attempt persistence keyed by job and run.
- Count settled and unresolved attempts according to conservative policy.
- Use
lookup_invoiceafter an uncertain published payment. - Treat
get_balanceas capacity information, not settlement proof. - Disable and notify on missing, expired, unauthorized, removed, or wallet-budget-exhausted connections.
- Disable and notify when lifetime
maxTotalis exhausted. - Fail only the current run for
maxPerRunexhaustion. - Skip/fail the current run but retain the schedule for
maxPerWindowexhaustion. - Decide terminal behavior after bounded lookup remains unavailable.
- Deep-link payment-related notifications to
/job show <id>. - Show payment attempts, outcomes, and safe reason messages in Job detail.
- Ensure retry and restart recovery cannot silently duplicate a payment.
Exit criteria:
- Scheduled payments have exact wallet selection, layered limits, durable accounting, reconciliation, and actionable notifications.
Phase 12: NR Zap Receipt Support
- Add follower-oriented kind
9735fetching using established NR reaction and relay patterns where appropriate. - Resolve the recipient LNURLP document and authoritative
nostrPubkey. - Validate zap receipt signature, author, zap request, target context, amount, invoice, and payment hash.
- Deduplicate by payment hash.
- Use validated receipts for settlement callbacks and funding totals.
- Keep relay acceptance separate from proof of payment.
Exit criteria:
- NR can discover and count only verified follower zap receipts.
Deferred Work
- NWC
make_invoiceand receiving-wallet UI. - NWC notifications and transaction history.
- NWC keysend and optional extension specifications.
- Payment grants and "Don't ask again" UX.
- Inline funding for WebLN, NWC, and Cashu wallets.
- Credential encryption or OS keychain storage.
- Out-of-process plugin RPC and OS/container/WASI sandboxing.
- Third-party wallet providers through a public capability contract.
- Sub-satoshi interactive payment amounts.
Verification Commands
Run after each applicable phase:
bunx tsc --noEmit
bunx eslint <touched files>
bunx prettier --check <touched files>
git diff --check
Also run each modified installed plugin repository's available verification and
patch checks separately because plugins/ contains independent Git
repositories and is ignored by the AppWeaver core repository.